2 // @name          Reddit Enhancement Suite
 
   3 // @namespace     http://reddit.honestbleeps.com/
 
   4 // @description   A suite of tools to enhance reddit...
 
   5 // @copyright     2010-2013, Steve Sobel (http://redditenhancementsuite.com/)
 
   6 // @license       GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
 
   7 // @author        honestbleeps
 
   8 // @include       http://redditenhancementsuite.com/*
 
   9 // @include       http://reddit.honestbleeps.com/*
 
  10 // @include       http://reddit.com/*
 
  11 // @include       https://reddit.com/*
 
  12 // @include       http://*.reddit.com/*
 
  13 // @include       https://*.reddit.com/*
 
  15 // @updateURL     http://redditenhancementsuite.com/latest/reddit_enhancement_suite.meta.js
 
  16 // @downloadURL   http://redditenhancementsuite.com/latest/reddit_enhancement_suite.user.js
 
  17 // @require       https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
 
  20 /*jshint undef: true, unused: true, strict: false, laxbreak: true, multistr: true, smarttabs: true, sub: true, browser: true */
 
  22 var RESVersion = "4.3.0.4";
 
  24 var jQuery, $, guiders, Tinycon, SnuOwnd;
 
  27         Reddit Enhancement Suite - a suite of tools to enhance Reddit
 
  28         Copyright (C) 2010-2012 - honestbleeps (steve@honestbleeps.com)
 
  30         RES is released under the GPL. However, I do ask a favor (obviously I don't/can't require it, I ask out of courtesy):
 
  32         Because RES auto updates and is hosted from a central server, I humbly request that if you intend to distribute your own
 
  33         modified Reddit Enhancement Suite, you name it something else and make it very clear to your users that it's your own
 
  34         branch and isn't related to mine.
 
  36         RES is updated very frequently, and I get lots of tech support questions/requests from people on outdated versions. If 
 
  37         you're distributing RES via your own means, those recipients won't always be on the latest and greatest, which makes 
 
  38         it harder for me to debug things and understand (at least with browsers that auto-update) whether or not people are on 
 
  39         a current version of RES.
 
  41         I can't legally hold you to any of this - I'm just asking out of courtesy.
 
  43         Thanks, I appreciate your consideration.  Without further ado, the all-important GPL Statement:
 
  45         This program is free software: you can redistribute it and/or modify
 
  46         it under the terms of the GNU General Public License as published by
 
  47         the Free Software Foundation, either version 3 of the License, or
 
  48         (at your option) any later version.
 
  50         This program is distributed in the hope that it will be useful,
 
  51         but WITHOUT ANY WARRANTY; without even the implied warranty of
 
  52         MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
  53         GNU General Public License for more details.
 
  55         You should have received a copy of the GNU General Public License
 
  56         along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
  60 var tokenizeCSS = 'ul.token-input-list-facebook { overflow: hidden; height: auto !important; height: 1%; width: 400px; border: 1px solid #96bfe8; cursor: text; font-size: 12px; font-family: Verdana; min-height: 1px; z-index: 1010; margin: 0; padding: 0; background-color: #fff; list-style-type: none; float: left; margin-right: 12px; }';
 
  61 tokenizeCSS += '.optionsTable ul.token-input-list-facebook  {clear: left; float: none; margin-right: 0; }';
 
  62 tokenizeCSS += 'ul.token-input-list-facebook li input { border: 0; width: 100px; padding: 3px 8px; background-color: white; margin: 2px 0; -webkit-appearance: caret; }';
 
  63 tokenizeCSS += 'li.token-input-token-facebook { overflow: hidden;  height: auto !important;  height: 15px; margin: 3px; padding: 1px 3px; background-color: #eff2f7; color: #000; cursor: default; border: 1px solid #ccd5e4; font-size: 11px; border-radius: 5px; float: left; white-space: nowrap; }';
 
  64 tokenizeCSS += 'li.token-input-token-facebook p { display: inline; padding: 0; margin: 0;}';
 
  65 tokenizeCSS += 'li.token-input-token-facebook span { color: #a6b3cf; margin-left: 5px; font-weight: bold; cursor: pointer;}';
 
  66 tokenizeCSS += 'li.token-input-selected-token-facebook { background-color: #5670a6; border: 1px solid #3b5998; color: #fff;}';
 
  67 tokenizeCSS += 'li.token-input-input-token-facebook { float: left; margin: 0; padding: 0; list-style-type: none;}';
 
  68 tokenizeCSS += 'div.token-input-dropdown-facebook { position: absolute; width: 400px; background-color: #fff; overflow: hidden; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; cursor: default; font-size: 11px; font-family: Verdana; z-index: 100000100; }';
 
  69 tokenizeCSS += 'div.token-input-dropdown-facebook p { margin: 0; padding: 5px; font-weight: bold; color: #777;}';
 
  70 tokenizeCSS += 'div.token-input-dropdown-facebook ul { margin: 0; padding: 0;}';
 
  71 tokenizeCSS += 'div.token-input-dropdown-facebook ul li { background-color: #fff; padding: 3px; margin: 0; list-style-type: none;}';
 
  72 tokenizeCSS += 'div.token-input-dropdown-facebook ul li.token-input-dropdown-item-facebook { background-color: #fff;}';
 
  73 tokenizeCSS += 'div.token-input-dropdown-facebook ul li.token-input-dropdown-item2-facebook { background-color: #fff;}';
 
  74 tokenizeCSS += 'div.token-input-dropdown-facebook ul li em { font-weight: bold; font-style: normal;}';
 
  75 tokenizeCSS += 'div.token-input-dropdown-facebook ul li.token-input-selected-dropdown-item-facebook { background-color: #3b5998; color: #fff;}';
 
  78 var guidersCSS = '.guider { background: #FFF; border: 1px solid #666; font-family: arial; position: absolute; outline: none; z-index: 100000005 !important; padding: 4px 12px; width: 500px; z-index: 100; -moz-box-shadow: 0 0 8px #111; -webkit-box-shadow: 0 0 8px #111; box-shadow: 0 0 8px #111; border-radius: 4px;}';
 
  79 guidersCSS += '.guider_buttons { height: 36px; position: relative; width: 100%; }';
 
  80 guidersCSS += '.guider_content { position: relative; }';
 
  81 guidersCSS += '.guider_description { margin-bottom: 10px; }';
 
  82 guidersCSS += '.guider_content h1 { color: #1054AA; float: left; font-size: 21px; }';
 
  83 guidersCSS += '.guider_close { float: right; padding: 10px 0 0; }';
 
  84 // guidersCSS += '.x_button { background-image: url(\'x_close_button.jpg\'); cursor: pointer; height: 13px; width: 13px; }';
 
  85 guidersCSS += '.x_button { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAIAAAD9iXMrAAAACXBIWXMAAAsTAAALEwEAmpwYAAABe0lEQVQoFT2RT0/CQBDFd7tbqIULiAaMgK1ngSqFCyHwyaFANS1+BEDigRgIFORAQv/5lhI32eR1duY3b6Z0vvg+Ho+M0TAMGWPx5VBKCSH/OpPJ8OVyqaqKqqqcsSgKophKkpTUIBX6dDptNhuON13X7wq3F4RgkEQJnpCr1c9sNgOFhUHgebvpdNrpdNAiiiIwt1vPdd1er8c5D4IAvAjm8vl8LpebTCZ4SKXSu91+Mh7XajX1Jv17OMCs4PlBBCutVgvIwcBqGIZt269GvVqtwAalMUD8OiYhKGo2Tfv9w7Isw3jTNA3FOGiIHA6FLcByTIjn7dfrdaFwv1gsKuWSoigXntgAxxXGKYHx8WjUqL9gfMdxhta43+/LMpeYHITxled5h5FIquv6Exjw6rifw+Gw2+2ez2eMLOZF62w2a5pm+fEhaYRIu23O51+Kkkpyrn1lmRWLxcQrKmEG+vlZAwQaxRwXa0NUHHzE4i/7vh8TCSTEoEul0h9jNtRZlgw9UAAAAABJRU5ErkJggg==); cursor: pointer; height: 13px; width: 13px; }';
 
  86 guidersCSS += '.guider_content p { clear: both; color: #333; font-size: 13px; }';
 
  87 guidersCSS += '.guider_button { background: -moz-linear-gradient(top, #5CA9FF 0%, #3D79C3 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5CA9FF), color-stop(100%, #3D79C3)); background-color: #4A95E0; border: solid 1px #4B5D7E; color: #FFF; cursor: pointer; display: inline-block; float: right; font-size: 75%; font-weight: bold; margin-left: 6px; min-width: 40px; padding: 3px 5px; text-align: center; text-decoration: none;border-radius: 2px; }';
 
  88 guidersCSS += '#guider_overlay { background-color: #000; width: 100%; height: 100%; position: fixed; top: 0; left: 0; opacity: 0.5; filter: alpha(opacity=50); z-index: 1000; }';
 
  90    * For optimization, the arrows image is inlined in the css below.
 
  92    * To use your own arrows image, replace this background-image with your own arrows.
 
  93    * It should have four arrows, top, right, left, and down.
 
  95 guidersCSS += '.guider_arrow { width: 42px; height: 42px; position: absolute; display: none; background-repeat: no-repeat; z-index: 100000006 !important; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAACoCAMAAAChZYy6AAABelBMVEX///8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzN+fn4zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzPe3t4zMzMzMzN2dnYzMzMzMzPV1dUzMzMzMzMzMzMzMzMzMzN8fHwzMzOEhIQzMzPa2tozMzMzMzNtbW0zMzNzc3MzMzMzMzPR0dFtbW1xcXHS0tIzMzMzMzNxcXHh4eEzMzMzMzPi4uIzMzMzMzMzMzMzMzMzMzMzMzOEhITPz8+MjIwzMzMzMzMzMzOHh4czMzMzMzPZ2dkzMzMzMzPPz88zMzPW1tYzMzMzMzPV1dVvb28zMzMzMzMzMzOBgYEzMzPY2NiBgYHh4eHd3d3Nzc3o6OikpKQzMzPe3t7R0dEzMzPZ2dna2tqHh4czMzMzMzP///+41NW+AAAAfXRSTlMAAAECBA0HCBUfDhYsYy4gPU07CyF4PFCLjnVhA3ZMYC1PuxSMOmJ0jSscivBOGbY+eepVDDZ3Hrk/vgbuKn+yNLUJKOmxs+pZU7TwMgXcEhcmHRNun+SzNw+HvnJe6Eok50vsZArrpHFSNbyJ137v7ufmkyXv6F/s6qFWWFA9DhkAAAWkSURBVHhe7djpU+LKHsfh3ARiQBE0ioIIKoSwCKiAiAguuG+Ay7jv+z4uozNq/vebDub8OImdpOq8O3W+L62nPkVZZTc24aEokiQMjWUnaBNlCEcicb+5zhAeSy/mLYwhPDebPHY2iJjWxcvrBb4jZggLwn4i0NnRZAAL4k4S7SJu1sOCtHNtDBRtYByLgepjoIDn290+PEYENoPFQAFPR90+b3PY4kcYQ+VNSpgLZ/wp2kPhqIxHFRioeq+jUf7Ny+VXVtkcRcoUs6OpW37vIF0pm3MUUMwep7bXks4Kc2YCit2Pi8ur64x1iQSK39199rCUM0SfnrMvJdoAfdjgk5uWLf0PsLAT2P3MFsusSYcuDLv6bTGuGLfS2r+sXhGetvU12pkU7SFlioE2EbbUW2tPiG/gjatVDYEC7EHQARBHfykhjv7sCSkgho4g2I2BQAHaZYijI0OhwWC3owsghv4eCmhBoH+0IWz5veAOxpwiNAPEHcUfXoB6B3zYzmCg/rXxzy8juOL0KRsZm1sWNLc8NxZhPQTFRtKz79p0fTYdYSmCmogvfhT+aMn9QnIxPkERJO0Pe91DWjTBH+f9NEmQJrPdGQz8xsuTQIfTYkZ/4VQd0xUb1MgmOmMNTB1FEHJ2BCfP2+UoytZ3dUNWHW2qRr+yjmAIkx34KwrZnu/pOIrSKIrNQrT5KwrZ1h79KMpa7Q5b6KdWVDc7L0eV2V9KOQPR2mwjZCHqlqPKrKtXGfXJUUW2TZmdlqPKbIsyOxmFqDp7ox+FrF4UssNAR91eiGpmX3FROds/XBMNQ1Sd7bO5FqryKOrjIKqZneK9YT+K4rOn1ezj7RuXgSgmu6MXhU8beBCEH9t7XCaFolrZ2O6GIFysHeQhisnauU/+6e4ymV5JeRDQyDLFbPL5/spZWYUoJpuKFzez2etKmYUoJktby5aXwwwDX9qwWQ/NbpVK1jOQeEuZlujckrGbhBSncP+NJCnKYwhSJnqCZQ3BOrM/HokYgow9vJgeMwDr7V1O78fsnCEYC7oL78va0CpCR3dwMDAkXu3GoHRZ68PQ0NeFpgEbHd221lAPgtJwsAWgPCxsQ/Bvlx4eunoU1xge9gqKqWEfgjcAgaphv2sYoJqSHjrF2EV4ClA5OFTiRS6GigsI4ClpYsvF7OduYAcgji5tWTaT/MYDACylSy/Z5ycAGjRXOsze3xmiS9bM9dXlxQ99SpjOmIozubY99ahLqZy5XEkf7PG3U0c6lKRy7OpKnvO+8dHRVxyVux465c+EOa/PHR2dxFI1np7EURmbaLPfEm6W8IwmJUiEGUtDc5PP3T4PGCgWDygoFnd0to8DBorHQHVx4hyoPj4BqoWdIg4k9oFq4lgHX1hfBqCJj5Ozc8YOeEseDniD14bxy8j4Ffcv3f/UI9HUP1ZR9OUpR6MvT7qUyp1ZS6UtlvaQGArSzGQOXyxlK03pUA9brlxns5vFeMpEfkfhJKBXK86r++dktoj+IZX3HfWkVtLJy7sn/pOzW00klqKoP3+wdiEIG7uxxnrIqilpSmW4vW3xQH4I2PpaUFZFIRr28lOCuJ3+NsgqKYqaM9zb7SOiC65TyKooRNGGIauiKGrhfNEjBFEWPq2KUjQT9rpHJQhZJZWjzb7oq0x7XTYHyqopijZAFGVbpayCQnQSKGQVFEWb3NNCzW6qWSWFqDqroBCtWY+craXVaPuMjCBrR9laWo3Oyway3ShbS6VohxyF/QpVszVUinaiqDrbJWa/KEQH1PRnTZaA6DgCGlkCE5U3Ego67GYpS2CikB2Us0Q16oQoLosoVYeiCQGzITlLyNFzHB0JBJ1SlpCiMRTFZ2NdjJglqtHACZ7+/sqix7/8MY+i+KwbPQCQ0pNisrCvRf8UPtCTYvWhcl3Q3Hv1odL48+f/AZYqoYKwlk9uAAAAAElFTkSuQmCC); }  ';
 
  96 guidersCSS += '.guider_arrow_right { display: block; background-position: 0 0; right: -42px; }';
 
  97 guidersCSS += '.guider_arrowdown { display: block; background-position: 0 -42px; bottom: -42px; }';
 
  98 guidersCSS += '.guider_arrow_up { display: block; background-position: 0 -126px; top: -42px; }';
 
  99 guidersCSS += '.guider_arrow_left { display: block; background-position: 0 -84px; left: -42px;}';
 
 100 guidersCSS += '.guider_content h2 { margin-top: 1em; }';
 
 101 guidersCSS += '.guider_content td  { vertical-align: top; }';
 
 102 guidersCSS += '.guider_content td + td { padding-left: 1em; }';
 
 103 guidersCSS += '.guider_content td code { white-space: nowrap; }';
 
 107 // DOM utility functions
 
 108 var escapeLookups = { "&": "&", '"': """, "<": "<", ">": ">" };
 
 109 function escapeHTML(str) {
 
 110         return (typeof str === 'undefined' || str === null) ?
 
 112                 str.toString().replace(/[&"<>]/g, function(m) { return escapeLookups[m]; });
 
 115 function insertAfter( referenceNode, newNode ) {
 
 116         if ((typeof referenceNode === 'undefined') || (referenceNode === null)) {
 
 117                 console.log(arguments.callee.caller);
 
 118         } else if ((typeof referenceNode.parentNode !== 'undefined') && (typeof referenceNode.nextSibling !== 'undefined')) {
 
 119                 if (referenceNode.parentNode === null) {
 
 120                         console.log(arguments.callee.caller);
 
 122                         referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling );
 
 126 function createElementWithID(elementType, id, classname) {
 
 127         var obj = document.createElement(elementType);
 
 129                 obj.setAttribute('id', id);
 
 131         if ((typeof classname !== 'undefined') && (classname !== '')) {
 
 132                 obj.setAttribute('class', classname);
 
 137 // this alias is to account for opera having different behavior...
 
 138 if (typeof navigator === 'undefined') navigator = window.navigator;
 
 140 //Because Safari 5.1 doesn't have Function.bind
 
 141 if (typeof Function.prototype.bind === 'undefined') {
 
 142         Function.prototype.bind = function(context) {
 
 145                         return oldRef.apply(context || null, Array.prototype.slice.call(arguments));
 
 150 var BrowserDetect = {
 
 152                 this.browser = this.searchString(this.dataBrowser) || "An unknown browser";
 
 153                 this.version = this.searchVersion(navigator.userAgent) ||
 
 154                                                 this.searchVersion(navigator.appVersion) ||
 
 155                                                 "an unknown version";
 
 156                 this.OS = this.searchString(this.dataOS) || "an unknown OS";
 
 158                 // set up MutationObserver variable to take whichever is supported / existing...
 
 159                 // unfortunately, this doesn't (currently) exist in Opera.
 
 160                 // this.MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver || null;
 
 161                 // At the time of writing WebKit's mutation observer leaks entire pages on refresh so it needs to be disabled.
 
 162                 this.MutationObserver = window.MutationObserver || window.WebKitMutationObserver ||  window.MozMutationObserver || null;
 
 164                 // null out MutationObserver to test legacy DOMNodeInserted
 
 165                 // this.MutationObserver = null;
 
 167         searchString: function (data) {
 
 168                 for (var i=0;i<data.length;i++) {
 
 169                         var dataString = data[i].string;
 
 170                         var dataProp = data[i].prop;
 
 171                         this.versionSearchString = data[i].versionSearch || data[i].identity;
 
 173                                 if (dataString.indexOf(data[i].subString) !== -1)
 
 174                                         return data[i].identity;
 
 177                                 return data[i].identity;
 
 180         searchVersion: function (dataString) {
 
 181                 var index = dataString.indexOf(this.versionSearchString);
 
 182                 if (index === -1) return;
 
 183                 return parseFloat(dataString.substring(index+this.versionSearchString.length+1));
 
 185         isChrome: function() { return typeof chrome !== 'undefined'; },
 
 186         isFirefox: function() { return typeof self.on === 'function'; },
 
 187         isOperaBlink: function() { return typeof chrome !== 'undefined' && BrowserDetect.browser === "Opera"; },
 
 188         isOpera: function() { return typeof opera !== 'undefined'; },
 
 189         isSafari: function() { return typeof safari !== 'undefined'; },
 
 192                         string: navigator.userAgent,
 
 197                         string: navigator.userAgent,
 
 201                 {       string: navigator.userAgent,
 
 202                         subString: "OmniWeb",
 
 203                         versionSearch: "OmniWeb/",
 
 207                         string: navigator.vendor,
 
 210                         versionSearch: "Version"
 
 215                         versionSearch: "Version"
 
 218                         string: navigator.vendor,
 
 223                         string: navigator.vendor,
 
 225                         identity: "Konqueror"
 
 228                         string: navigator.userAgent,
 
 229                         subString: "Firefox",
 
 233                         string: navigator.vendor,
 
 237                 {               // for newer Netscapes (6+)
 
 238                         string: navigator.userAgent,
 
 239                         subString: "Netscape",
 
 243                         string: navigator.userAgent,
 
 245                         identity: "Explorer",
 
 246                         versionSearch: "MSIE"
 
 249                         string: navigator.userAgent,
 
 255                         // for older Netscapes (4-)
 
 256                         string: navigator.userAgent,
 
 257                         subString: "Mozilla",
 
 258                         identity: "Netscape",
 
 259                         versionSearch: "Mozilla"
 
 264                         string: navigator.platform,
 
 269                         string: navigator.platform,
 
 274                         string: navigator.userAgent,
 
 276                         identity: "iPhone/iPod"
 
 279                         string: navigator.platform,
 
 286 BrowserDetect.init();
 
 289         // safely parses JSON and won't kill the whole script if JSON.parse fails
 
 290         // if localStorageSource is specified, will offer the user the ability to delete that localStorageSource to stop further errors.
 
 291         // if silent is specified, it will fail silently...
 
 292         parse: function(data, localStorageSource, silent) {
 
 294                         if (BrowserDetect.isSafari()) {
 
 295                                 if (data.substring(0,2) === 's{') {
 
 296                                         data = data.substring(1,data.length);
 
 299                         return JSON.parse(data);
 
 301                         if (silent) return {};
 
 302                         if (localStorageSource) {
 
 303                                 var msg = 'Error caught: JSON parse failure on the following data from "'+localStorageSource+'": <textarea rows="5" cols="50">' + data + '</textarea><br>RES can delete this data to stop errors from happening, but you might want to copy/paste it to a text file so you can more easily re-enter any lost information.';
 
 304                                 alert(msg, function() {
 
 305                                         // back up a copy of the corrupt data
 
 306                                         localStorage.setItem(localStorageSource + '.error', data);
 
 307                                         // delete the corrupt data
 
 308                                         RESStorage.removeItem(localStorageSource);
 
 311                                 alert('Error caught: JSON parse failure on the following data: ' + data);
 
 318 // array compare utility function for keyCode arrays
 
 319 function keyArrayCompare(fromArr, toArr) {
 
 320         // if we've passed in a number, fix that and make it an array with alt, shift and ctrl set to false.
 
 321         if (typeof toArr === 'number') {
 
 322                 toArr = [toArr, false, false, false];
 
 323         } else if (toArr.length === 4) {
 
 326         if (fromArr.length !== toArr.length) return false;
 
 327         for (var i = 0; i < toArr.length; i++) {
 
 328                 if (fromArr[i].compare) {
 
 329                         if (!fromArr[i].compare(toArr[i])) return false;
 
 331                 if (fromArr[i] !== toArr[i]) return false;
 
 336 // utility function for checking events against keyCode arrays
 
 337 function checkKeysForEvent(event, keyArray) {
 
 338         //[keycode, alt, ctrl, shift, meta]
 
 339         // if we've passed in a number, fix that and make it an array with alt, shift and ctrl set to false.
 
 340         if (typeof keyArray === 'number') {
 
 341                 keyArray = [keyArray, false, false, false, false];
 
 342         } else if (keyArray.length === 4) {
 
 343                 keyArray.push(false);
 
 345         if (event.keyCode != keyArray[0]) return false;
 
 346         else if (event.altKey != keyArray[1]) return false;
 
 347         else if (event.ctrlKey != keyArray[2]) return false;
 
 348         else if (event.shiftKey != keyArray[3]) return false;
 
 349         else if (event.metaKey != keyArray[4]) return false;
 
 353 function operaUpdateCallback(obj) {
 
 354         RESUtils.compareVersion(obj);
 
 356 function operaForcedUpdateCallback(obj) {
 
 357         RESUtils.compareVersion(obj, true);
 
 360 // This object will store xmlHTTPRequest callbacks for Safari because Safari's extension architecture seems stupid.
 
 361 // This really shouldn't be necessary, but I can't seem to hold on to an onload function that I pass to the background page...
 
 362 xhrQueue = { count: 0, onloads: [] };
 
 365 // if this is a jetpack addon, add an event listener like Safari's message handler...
 
 366 if (BrowserDetect.isFirefox()) {
 
 367         self.on('message', function(msgEvent) {
 
 368                 switch (msgEvent.name) {
 
 369                         case 'GM_xmlhttpRequest':
 
 370                                 // Fire the appropriate onload function for this xmlhttprequest.
 
 371                                 xhrQueue.onloads[msgEvent.XHRID](msgEvent.response);
 
 373                         case 'compareVersion':
 
 374                                 var forceUpdate = false;
 
 375                                 if (typeof msgEvent.message.forceUpdate !== 'undefined') forceUpdate = true;
 
 376                                 RESUtils.compareVersion(msgEvent.message, forceUpdate);
 
 379                                 var tweet = msgEvent.response;
 
 380                                 var thisExpando = modules['styleTweaks'].tweetExpando;
 
 381                                 $(thisExpando).html(tweet.html);
 
 382                                 thisExpando.style.display = 'block';
 
 383                                 thisExpando.classList.add('twitterLoaded');
 
 385                                 // for now, commenting out the old way of handling tweets as AMO will not approve.
 
 387                                 var tweet = msgEvent.response;
 
 388                                 var thisExpando = modules['styleTweaks'].tweetExpando;
 
 389                                 thisExpando.innerHTML = '';
 
 390                                 // the iframe is to sandbox this remote javascript from accessing reddit's javascript, etc.
 
 391                                 // this is done this way as requested by the AMO review team.
 
 392                                 var sandboxFrame = document.createElement('iframe');
 
 393                                 var seamless = document.createAttribute('seamless');
 
 394                                 sandboxFrame.setAttribute('sandbox','allow-scripts allow-same-origin');
 
 395                                 sandboxFrame.setAttributeNode(seamless);
 
 396                                 sandboxFrame.setAttribute('style','border: none;');
 
 397                                 sandboxFrame.setAttribute('width','480');
 
 398                                 sandboxFrame.setAttribute('height','260');
 
 399                                 sandboxFrame.setAttribute('src','data:text/html,<html><head><base href="https://platform.twitter.com"></head><body>'+encodeURIComponent(tweet.html)+"</body></html>");
 
 400                                 $(thisExpando).append(sandboxFrame);
 
 401                                 // $(thisExpando).html(tweet.html);
 
 402                                 thisExpando.style.display = 'block';
 
 403                                 thisExpando.classList.add('twitterLoaded');
 
 405                         case 'getLocalStorage':
 
 406                                 // Does RESStorage have actual data in it?  If it doesn't, they're a legacy user, we need to copy 
 
 407                                 // old school localStorage from the foreground page to the background page to keep their settings...
 
 408                                 if (typeof msgEvent.message.importedFromForeground === 'undefined') {
 
 409                                         // it doesn't exist.. copy it over...
 
 411                                                 requestType: 'saveLocalStorage',
 
 414                                         self.postMessage(thisJSON);
 
 416                                         setUpRESStorage(msgEvent.message);
 
 420                         case 'saveLocalStorage':
 
 421                                 // Okay, we just copied localStorage from foreground to background, let's set it up...
 
 422                                 setUpRESStorage(msgEvent.message);
 
 425                                 RESStorage.setItem(msgEvent.itemName, msgEvent.itemValue, true);
 
 428                                 // console.log('unknown event type in self.on');
 
 429                                 // console.log(msgEvent.toSource());
 
 435 // This is the message handler for Safari - the background page calls this function with return data...
 
 436 function safariMessageHandler(msgEvent) {
 
 437         switch (msgEvent.name) {
 
 438                 case 'GM_xmlhttpRequest':
 
 439                         // Fire the appropriate onload function for this xmlhttprequest.
 
 440                         xhrQueue.onloads[msgEvent.message.XHRID](msgEvent.message);
 
 442                 case 'compareVersion':
 
 443                         var forceUpdate = false;
 
 444                         if (typeof msgEvent.message.forceUpdate !== 'undefined') forceUpdate = true;
 
 445                         RESUtils.compareVersion(msgEvent.message, forceUpdate);
 
 448                         var tweet = msgEvent.message;
 
 449                         var thisExpando = modules['styleTweaks'].tweetExpando;
 
 450                         $(thisExpando).html(tweet.html);
 
 451                         thisExpando.style.display = 'block';
 
 452                         thisExpando.classList.add('twitterLoaded');
 
 454                 case 'getLocalStorage':
 
 455                         // Does RESStorage have actual data in it?  If it doesn't, they're a legacy user, we need to copy 
 
 456                         // old schol localStorage from the foreground page to the background page to keep their settings...
 
 457                         if (typeof msgEvent.message.importedFromForeground === 'undefined') {
 
 458                                 // it doesn't exist.. copy it over...
 
 460                                         requestType: 'saveLocalStorage',
 
 463                                 safari.self.tab.dispatchMessage('saveLocalStorage', thisJSON);
 
 465                                 setUpRESStorage(msgEvent.message);
 
 469                 case 'saveLocalStorage':
 
 470                         // Okay, we just copied localStorage from foreground to background, let's set it up...
 
 471                         setUpRESStorage(msgEvent.message);
 
 474                 case 'addURLToHistory':
 
 475                         var url = msgEvent.message.url;
 
 476                         modules['showImages'].imageTrackFrame.contentWindow.location.replace(url);
 
 479                         RESStorage.setItem(msgEvent.message.itemName, msgEvent.message.itemValue, true);
 
 482                         // console.log('unknown event type in safariMessageHandler');
 
 487 // This is the message handler for Opera - the background page calls this function with return data...
 
 488 function operaMessageHandler(msgEvent) {
 
 489         var eventData = msgEvent.data;
 
 490         switch (eventData.msgType) {
 
 491                 case 'GM_xmlhttpRequest':
 
 492                         // Fire the appropriate onload function for this xmlhttprequest.
 
 493                         xhrQueue.onloads[eventData.XHRID](eventData.data);
 
 495                 case 'compareVersion':
 
 496                         var forceUpdate = false;
 
 497                         if (typeof eventData.data.forceUpdate !== 'undefined') forceUpdate = true;
 
 498                         RESUtils.compareVersion(eventData.data, forceUpdate);
 
 501                         var tweet = eventData.data;
 
 502                         var thisExpando = modules['styleTweaks'].tweetExpando;
 
 503                         $(thisExpando).html(tweet.html);
 
 504                         thisExpando.style.display = 'block';
 
 505                         thisExpando.classList.add('twitterLoaded');
 
 507                 case 'getLocalStorage':
 
 508                         // Does RESStorage have actual data in it?  If it doesn't, they're a legacy user, we need to copy 
 
 509                         // old schol localStorage from the foreground page to the background page to keep their settings...
 
 510                         if (typeof eventData.data.importedFromForeground === 'undefined') {
 
 511                                 // it doesn't exist.. copy it over...
 
 513                                         requestType: 'saveLocalStorage',
 
 516                                 opera.extension.postMessage(JSON.stringify(thisJSON));
 
 518                                 if (location.hostname.match('reddit')) {
 
 519                                         setUpRESStorage(eventData.data);
 
 524                 case 'saveLocalStorage':
 
 525                         // Okay, we just copied localStorage from foreground to background, let's set it up...
 
 526                         setUpRESStorage(eventData.data);
 
 527                         if (location.hostname.match('reddit')) {
 
 532                         if ((typeof RESStorage !== 'undefined') && (typeof RESStorage.setItem === 'function')) {
 
 533                                 RESStorage.setItem(eventData.itemName, eventData.itemValue, true);
 
 535                                 // a change in opera requires this wait/timeout for the RESStorage grab to work...
 
 536                                 var waitForRESStorage = function(eData) {
 
 537                                         if ((typeof RESStorage !== 'undefined') && (typeof RESStorage.setItem === 'function')) {
 
 538                                                 RESStorage.setItem(eData.itemName, eData.itemValue, true);
 
 540                                                 setTimeout(function() { waitForRESStorage(eData); }, 200);
 
 543                                 var savedEventData = {
 
 544                                         itemName: eventData.itemName,
 
 545                                         itemValue: eventData.itemValue
 
 547                                 waitForRESStorage(savedEventData);
 
 550                 case 'addURLToHistory':
 
 551                         var url = eventData.url;
 
 552                         if (! eventData.isPrivate) {
 
 553                                 modules['showImages'].imageTrackFrame.contentWindow.location.replace(url);
 
 557                         // console.log('unknown event type in operaMessageHandler');
 
 562 // listen for messages from chrome background page
 
 563 if (BrowserDetect.isChrome()) {
 
 564         chrome.extension.onMessage.addListener(
 
 565                 function(request, sender, sendResponse) {
 
 566                         switch(request.requestType) {
 
 568                                         RESStorage.setItem(request.itemName, request.itemValue, true);
 
 571                                         // sendResponse({status: "unrecognized request type"});
 
 578 if (BrowserDetect.isSafari()) {
 
 579         // Safari has a ridiculous bug that causes it to lose access to safari.self.tab if you click the back button.
 
 580         // this stupid one liner fixes that.
 
 581         window.onunload = function(){};
 
 582         safari.self.addEventListener("message", safariMessageHandler, false);
 
 584 // we can't do this check for opera here because we need to wait until DOMContentLoaded is triggered, I think.  Putting this in RESinit();
 
 586 // opera compatibility
 
 587 if (BrowserDetect.isOpera()) {
 
 588         // removing this line for new localStorage methodology (store in extension localstorage)
 
 589         sessionStorage = window.sessionStorage;
 
 590         localStorage = window.localStorage;
 
 591         location = window.location;
 
 592         XMLHttpRequest = window.XMLHttpRequest;
 
 595 // Firebug stopped showing console.log for some reason. Need to use unsafeWindow if available. Not sure if this was due to a Firebug version update or what.
 
 596 if (typeof unsafeWindow !== 'undefined') {
 
 597         if ((typeof unsafeWindow.console !== 'undefined') && (!BrowserDetect.isFirefox())) {
 
 598                 console = unsafeWindow.console;
 
 599         } else if (typeof console === 'undefined') {
 
 610 // GreaseMonkey API compatibility for non-GM browsers (Chrome, Safari, Firefox)
 
 611 // @copyright      2009, 2010 James Campos
 
 612 // @modified            2010 Steve Sobel - added some missing gm_* functions
 
 613 // @license        cc-by-3.0; http://creativecommons.org/licenses/by/3.0/
 
 614 if ((typeof GM_deleteValue === 'undefined') || (typeof GM_addStyle === 'undefined')) {
 
 615         GM_addStyle = function(css) {
 
 616                 var style = document.createElement('style');
 
 617                 style.textContent = css;
 
 618                 var head = document.getElementsByTagName('head')[0];
 
 620                         head.appendChild(style);
 
 624         GM_deleteValue = function(name) {
 
 625                 localStorage.removeItem(name);
 
 628         GM_getValue = function(name, defaultValue) {
 
 629                 var value = localStorage.getItem(name);
 
 633                 value = value.substring(1);
 
 636                                 return value === 'true';
 
 638                                 return Number(value);
 
 644         GM_log = function(message) {
 
 645                 console.log(message);
 
 648         GM_registerMenuCommand = function(name, funk) {
 
 652         GM_setValue = function(name, value) {
 
 653                 value = (typeof value)[0] + value;
 
 654                 localStorage.setItem(name, value);
 
 657         if (BrowserDetect.browser === "Explorer") {
 
 658                 GM_xmlhttpRequest = function(obj) {
 
 660                             crossDomain = (obj.url.indexOf(location.hostname) === -1);
 
 661                         if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
 
 662                                 obj.requestType = 'GM_xmlhttpRequest';
 
 663                                 request = new XDomainRequest();
 
 664                                 request.onload = function() {obj.onload(request);};
 
 665                                 request.onerror = function() {if (obj.onerror) {obj.onerror(request);}};
 
 666                                 request.open(obj.method,obj.url);
 
 667                                 request.send(obj.data);
 
 670                                 request = new XMLHttpRequest();
 
 671                                 request.onreadystatechange=function() {
 
 672                                         if (obj.onreadystatechange) {
 
 673                                                 obj.onreadystatechange(request);
 
 675                                         if (request.readyState === 4 && obj.onload) {
 
 679                                 request.onerror = function() {
 
 681                                                 obj.onerror(request);
 
 685                                         request.open(obj.method,obj.url,true);
 
 694                                                         statusText:'Forbidden'
 
 700                                         for (var name in obj.headers) {
 
 701                                                 request.setRequestHeader(name,obj.headers[name]);
 
 704                                 request.send(obj.data);
 
 709         if (BrowserDetect.isChrome()) {
 
 710                 GM_xmlhttpRequest = function(obj) {
 
 711                         var crossDomain = (obj.url.indexOf(location.hostname) === -1);
 
 713                         if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
 
 714                                 obj.requestType = 'GM_xmlhttpRequest';
 
 715                                 if (typeof obj.onload !== 'undefined') {
 
 716                                         chrome.extension.sendMessage(obj, function(response) {
 
 717                                                 obj.onload(response);
 
 721                                 var request=new XMLHttpRequest();
 
 722                                 request.onreadystatechange = function() {
 
 723                                         if (obj.onreadystatechange) {
 
 724                                                 obj.onreadystatechange(request);
 
 726                                         if(request.readyState === 4 && obj.onload) {
 
 730                                 request.onerror = function() {
 
 732                                                 obj.onerror(request);
 
 736                                         request.open(obj.method,obj.url,true);
 
 745                                                         statusText:'Forbidden'
 
 750                                 if(obj.headers) { for(var name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
 
 751                                 request.send(obj.data); return request;
 
 754         } else if (BrowserDetect.isSafari())  {
 
 755                 GM_xmlhttpRequest = function(obj) {
 
 756                         obj.requestType = 'GM_xmlhttpRequest';
 
 757                         // Since Safari doesn't provide legitimate callbacks, I have to store the onload function here in the main
 
 758                         // userscript in a queue (see xhrQueue), wait for data to come back from the background page, then call the onload.
 
 760                         // oy vey... another problem. When Safari sends xmlhttpRequests from the background page, it loses the cookies etc that it'd have 
 
 761                         // had from the foreground page... so we need to write a bit of a hack here, and call different functions based on whether or 
 
 762                         // not the request is cross domain... For same-domain requests, we'll call from the foreground...
 
 763                         var crossDomain = (obj.url.indexOf(location.hostname) === -1);
 
 765                         if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
 
 766                                 obj.XHRID = xhrQueue.count;
 
 767                                 xhrQueue.onloads[xhrQueue.count] = obj.onload;
 
 768                                 safari.self.tab.dispatchMessage("GM_xmlhttpRequest", obj);
 
 771                                 var request=new XMLHttpRequest();
 
 772                                 request.onreadystatechange = function() {
 
 773                                         if (obj.onreadystatechange) {
 
 774                                                 obj.onreadystatechange(request);
 
 776                                         if (request.readyState === 4 && obj.onload) {
 
 780                                 request.onerror = function() {
 
 782                                                 obj.onerror(request);
 
 786                                         request.open(obj.method,obj.url,true);
 
 795                                                         statusText:'Forbidden'
 
 800                                 if(obj.headers) { for(var name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
 
 801                                 request.send(obj.data); return request;
 
 804         } else if (BrowserDetect.isOpera()) {
 
 805                 GM_xmlhttpRequest = function(obj) {
 
 806                         obj.requestType = 'GM_xmlhttpRequest';
 
 807                         // Turns out, Opera works this way too, but I'll forgive them since their extensions are so young and they're awesome people...
 
 809                         // oy vey... cross domain same issue with Opera.
 
 810                         var crossDomain = (obj.url.indexOf(location.hostname) === -1);
 
 812                         if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
 
 813                                 obj.XHRID = xhrQueue.count;
 
 814                                 xhrQueue.onloads[xhrQueue.count] = obj.onload;
 
 815                                 opera.extension.postMessage(JSON.stringify(obj));
 
 818                                 var request=new XMLHttpRequest();
 
 819                                 request.onreadystatechange = function() {
 
 820                                         if (obj.onreadystatechange) {
 
 821                                                 obj.onreadystatechange(request);
 
 823                                         if (request.readyState === 4 && obj.onload) {
 
 827                                 request.onerror = function() {
 
 829                                                 obj.onerror(request);
 
 833                                         request.open(obj.method,obj.url,true);
 
 842                                                         statusText:'Forbidden'
 
 848                                         for (var name in obj.headers) {
 
 849                                                 request.setRequestHeader(name,obj.headers[name]);
 
 852                                 request.send(obj.data); return request;
 
 855         } else if (BrowserDetect.isFirefox()) {
 
 856                 // we must be in a Firefox / jetpack addon...
 
 857                 GM_xmlhttpRequest = function(obj) {
 
 858                         var crossDomain = (obj.url.indexOf(location.hostname) === -1);
 
 860                         if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
 
 861                                 obj.requestType = 'GM_xmlhttpRequest';
 
 862                                 // okay, firefox's jetpack addon does this same stuff... le sigh..
 
 863                                 if (typeof obj.onload !== 'undefined') {
 
 864                                         obj.XHRID = xhrQueue.count;
 
 865                                         xhrQueue.onloads[xhrQueue.count] = obj.onload;
 
 866                                         self.postMessage(obj);
 
 870                                 var request=new XMLHttpRequest();
 
 871                                 request.onreadystatechange = function() {
 
 872                                         if (obj.onreadystatechange) {
 
 873                                                 obj.onreadystatechange(request);
 
 875                                         if(request.readyState === 4 && obj.onload) {
 
 879                                 request.onerror = function() {
 
 881                                                 obj.onerror(request);
 
 885                                         request.open(obj.method,obj.url,true);
 
 894                                                         statusText:'Forbidden'
 
 899                                 if(obj.headers) { for(var name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
 
 900                                 request.send(obj.data); return request;
 
 905         // this hack is to avoid an unsafeWindow error message if a gm_xhr is ever called as a result of a jQuery-induced ajax call.
 
 906         // yes, it's ugly, but it's necessary if we're using Greasemonkey together with jQuery this way.
 
 907         var oldgmx = GM_xmlhttpRequest;
 
 908         GM_xmlhttpRequest = function(params) {
 
 909                 setTimeout(function() {
 
 918 // define common RESUtils - reddit related functions and data that may need to be accessed...
 
 920         preInit: function() {
 
 921                 // we store a localStorage key because the async call is too slow to add classes to
 
 922                 // the document prior to page load, thus the flash of unstyled content.
 
 923                 RESUtils.getDocHTML();
 
 925         // to avoid the flash of unstyled content, the very first thing we should do is get a hold
 
 926         // of the document object and add necessary classes...
 
 927         getDocHTML: function() {
 
 929                         document.html = document.documentElement;
 
 930                         if (localStorage.getItem('RES_nightMode')) {
 
 931                                 // no need to check the background - we're in night mode for sure.
 
 932                                 modules['styleTweaks'].redditDark();
 
 935                         setTimeout(RESUtils.getDocHTML, 1);
 
 938         // A cache variable to store CSS that will be applied at the end of execution...
 
 939         randomHash: function(len) {
 
 940                 var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
 
 941                 var numChars = len || 5;
 
 942                 var randomString = '';
 
 943                 for (var i=0; i<numChars; i++) {
 
 944                         var rnum = Math.floor(Math.random() * chars.length);
 
 945                         randomString += chars.substring(rnum,rnum+1);
 
 951         addCSS: function(css) {
 
 952                 if (RESUtils.postLoad) {
 
 953                         var style = $('<style />').html(css).appendTo('head');
 
 955                                 remove: function() { style.remove(); }
 
 961         insertParam: function(href, key, value) {
 
 963                 if (href.indexOf('?') === -1) pre = '?';
 
 964                 return href + pre + key + '=' + value;
 
 966         // checks if script should run on current URL using exclude / include.
 
 967         isMatchURL: function (moduleID) {
 
 969                 var currURL = location.href;
 
 970                 // get includes and excludes...
 
 971                 var excludes = modules[moduleID].exclude;
 
 972                 var includes = modules[moduleID].include;
 
 973                 // first check excludes...
 
 974                 if (typeof excludes !== 'undefined') {
 
 975                         for (i=0, len = excludes.length; i<len; i++) {
 
 976                                 // console.log(moduleID + ' -- ' + excludes[i] + ' - excl test - ' + currURL + ' - result: ' + excludes[i].test(currURL));
 
 977                                 if (excludes[i].test(currURL)) {
 
 982                 // then check includes...
 
 983                 for (i=0, len=includes.length; i<len; i++) {
 
 984                         // console.log(moduleID + ' -- ' + includes[i] + ' - incl test - ' + currURL + ' - result: ' + includes[i].test(currURL));
 
 985                         if (includes[i].test(currURL)) {
 
 991         // gets options for a module...
 
 992         getOptionsFirstRun: [],
 
 993         getOptions: function(moduleID) {
 
 994                 if (this.getOptionsFirstRun[moduleID]) {
 
 995                         // we've already grabbed these out of localstorage, so modifications should be done in memory. just return that object.
 
 996                         return modules[moduleID].options;
 
 998                 var thisOptions = RESStorage.getItem('RESoptions.' + moduleID);
 
 999                 if ((thisOptions) && (thisOptions !== 'undefined') && (thisOptions !== null)) {
 
1000                         // merge options (in case new ones were added via code) and if anything has changed, update to localStorage
 
1001                         var storedOptions = safeJSON.parse(thisOptions, 'RESoptions.' + moduleID);
 
1002                         var codeOptions = modules[moduleID].options;
 
1003                         var newOption = false;
 
1004                         for (var attrname in codeOptions) {
 
1005                                 if (typeof storedOptions[attrname] === 'undefined') {
 
1007                                         storedOptions[attrname] = codeOptions[attrname];
 
1009                                         codeOptions[attrname].value = storedOptions[attrname].value;
 
1012                         modules[moduleID].options = codeOptions;
 
1014                                 RESStorage.setItem('RESoptions.' + moduleID, JSON.stringify(modules[moduleID].options));
 
1017                         // nothing in localStorage, let's set the defaults...
 
1018                         RESStorage.setItem('RESoptions.' + moduleID, JSON.stringify(modules[moduleID].options));
 
1020                 this.getOptionsFirstRun[moduleID] = true;
 
1021                 return modules[moduleID].options;
 
1023         getUrlParams: function () {
 
1024                 var result = {}, queryString = location.search.substring(1),
 
1025                     re = /([^&=]+)=([^&]*)/g, m;
 
1026                 while ((m = re.exec(queryString))) {
 
1027                         result[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
 
1031         setOption: function(moduleID, optionName, optionValue) {
 
1032                 if (optionName.match(/_[\d]+$/)) {
 
1033                         optionName = optionName.replace(/_[\d]+$/,'');
 
1035                 var thisOptions = this.getOptions(moduleID);
 
1036                 var saveOptionValue;
 
1037                 if (optionValue === '') {
 
1038                         saveOptionValue = '';
 
1039                 } else if ((isNaN(optionValue)) || (typeof optionValue === 'boolean') || (typeof optionValue === 'object')) {
 
1040                         saveOptionValue = optionValue;
 
1041                 } else if (optionValue.indexOf('.') !== -1) {
 
1042                         saveOptionValue = parseFloat(optionValue);
 
1044                         saveOptionValue = parseInt(optionValue, 10);
 
1046                 thisOptions[optionName].value = saveOptionValue;
 
1047                 // save it to the object...
 
1048                 modules[moduleID].options = thisOptions;
 
1049                 // save it to RESStorage...
 
1050                 RESStorage.setItem('RESoptions.' + moduleID, JSON.stringify(modules[moduleID].options));
 
1053         click: function(obj, button) {
 
1054                 var evt = document.createEvent('MouseEvents');
 
1055                 button = button || 0;
 
1056                 evt.initMouseEvent('click', true, true, window.wrappedJSObject, 0, 1, 1, 1, 1, false, false, false, false, button, null);
 
1057                 obj.dispatchEvent(evt);
 
1059         mousedown: function(obj, button) {
 
1060                 var evt = document.createEvent('MouseEvents');
 
1061                 button = button || 0;
 
1062                 evt.initMouseEvent('mousedown', true, true, window.wrappedJSObject, 0, 1, 1, 1, 1, false, false, false, false, button, null);
 
1063                 obj.dispatchEvent(evt);
 
1065         loggedInUser: function(tryingEarly) {
 
1066                 if (typeof this.loggedInUserCached === 'undefined') {
 
1067                         var userLink = document.querySelector('#header-bottom-right > span.user > a');
 
1068                         if ((userLink !== null) && (!userLink.classList.contains('login-required'))) {
 
1069                                 this.loggedInUserCached = userLink.innerHTML;
 
1070                                 this.loggedInUserHashCached = document.querySelector('[name=uh]').value;
 
1073                                         // trying early means we're trying before DOM load may be complete, so if we fail here
 
1074                                         // we don't want to null this, we want to allow another try.
 
1075                                         // currently the only place this is really used is username hider, which tries (if possible)
 
1076                                         // to hide the username as early/fast as possible.
 
1077                                         delete this.loggedInUserCached;
 
1078                                         delete this.loggedInUserHashCached;
 
1080                                         this.loggedInUserCached = null;
 
1084                 return this.loggedInUserCached;
 
1086         loggedInUserHash: function() {
 
1087                 this.loggedInUser();
 
1088                 return this.loggedInUserHashCached;
 
1090         getUserInfo: function(callback, username, live) {
 
1091                 // Default to currently logged-in user, for backwards compatibility
 
1092                 username = (typeof username !== "undefined" ? username : RESUtils.loggedInUser());
 
1093                 if (username === null) return false;
 
1095                 // Default to getting live data (i.e. from reddit's server)
 
1096                 live = (typeof live === "boolean" ? live : true);
 
1098                 if (!(username in RESUtils.userInfoCallbacks)) {
 
1099                         RESUtils.userInfoCallbacks[username] = [];
 
1101                 RESUtils.userInfoCallbacks[username].push(callback);
 
1102                 var cacheData = RESStorage.getItem('RESUtils.userInfoCache.' + username) || '{}';
 
1103                 var userInfoCache = safeJSON.parse(cacheData);
 
1104                 var lastCheck = (userInfoCache !== null) ? parseInt(userInfoCache.lastCheck, 10) || 0 : 0;
 
1105                 var now = new Date();
 
1106                 // 300000 = 5 minutes
 
1107                 if (live && (now.getTime() - lastCheck) > 300000) {
 
1108                         if (!RESUtils.userInfoRunning) {
 
1109                                 RESUtils.userInfoRunning = true;
 
1112                                         url:    location.protocol + "//" + location.hostname + "/user/" + encodeURIComponent(username) + "/about.json?app=res",
 
1113                                         onload: function(response) {
 
1114                                                 var thisResponse = JSON.parse(response.responseText);
 
1115                                                 var userInfoCache = {
 
1116                                                         lastCheck: now.getTime(),
 
1117                                                         userInfo: thisResponse
 
1119                                                 RESStorage.setItem('RESUtils.userInfoCache.' + username, JSON.stringify(userInfoCache));
 
1120                                                 while (RESUtils.userInfoCallbacks[username].length > 0) {
 
1121                                                         var thisCallback = RESUtils.userInfoCallbacks[username].pop();
 
1122                                                         thisCallback(userInfoCache.userInfo);
 
1124                                                 RESUtils.userInfoRunning = false;
 
1129                         while (RESUtils.userInfoCallbacks[username].length > 0) {
 
1130                                 var thisCallback = RESUtils.userInfoCallbacks[username].pop();
 
1131                                 thisCallback(userInfoCache.userInfo);
 
1135         userInfoCallbacks: {},
 
1136         commentsRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*comments\/?[-\w\.\/]*/i,
 
1137         friendsCommentsRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/r\/friends\/*comments\/?/i,
 
1138         inboxRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/message\/[-\w\.\/]*/i,
 
1139         profileRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.#=]*\/?(comments)?\/?(\?([a-z]+=[a-zA-Z0-9_%]*&?)*)?$/i, // fix to regex contributed by s_quark
 
1140         submitRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/([-\w\.\/]*\/)?submit\/?(\?.*)?$/i,
 
1141         prefsRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/prefs\/?/i,
 
1142         wikiRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\.]+\/wiki?/i,
 
1143         pageType: function() {
 
1144                 if (typeof this.pageTypeSaved === 'undefined') {
 
1146                         var currURL = location.href.split('#')[0];
 
1147                         if (RESUtils.profileRegex.test(currURL)) {
 
1148                                 pageType = 'profile';
 
1149                         } else if ((RESUtils.commentsRegex.test(currURL)) || (RESUtils.friendsCommentsRegex.test(currURL))) {
 
1150                                 pageType = 'comments';
 
1151                         } else if (RESUtils.inboxRegex.test(currURL)) {
 
1153                         } else if (RESUtils.submitRegex.test(currURL)) {
 
1154                                 pageType = 'submit';
 
1155                         } else if (RESUtils.prefsRegex.test(currURL)) {
 
1157                         } else if (RESUtils.wikiRegex.test(currURL)) {
 
1160                                 pageType = 'linklist';
 
1162                         this.pageTypeSaved = pageType;
 
1164                 return this.pageTypeSaved;
 
1166         commentPermalinkRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*comments\/[a-z0-9]+\/[^\/]+\/[a-z0-9]+\/?$/i,
 
1167         isCommentPermalinkPage: function() {
 
1168                 if (typeof this.isCommentPermalinkSaved === 'undefined') {
 
1169                         var currURL = location.href.split('#')[0];
 
1170                         if (RESUtils.commentPermalinkRegex.test(currURL)) {
 
1171                                 this.isCommentPermalinkSaved = true;
 
1173                                 this.isCommentPermalinkSaved = false;
 
1177                 return this.isCommentPermalinkSaved;
 
1179         matchRE: /^https?:\/\/(?:[a-z]+)\.reddit\.com\/r\/([\w\.\+]+).*/i,
 
1180         matchDOM: /^https?:\/\/(?:[a-z]+)\.reddit\.com\/domain\/([\w\.\+]+).*/i,
 
1181         currentSubreddit: function(check) {
 
1182                 if (typeof this.curSub === 'undefined') {
 
1183                         var match = location.href.match(RESUtils.matchRE);
 
1184                         if (match !== null) {
 
1185                                 this.curSub = match[1];
 
1186                                 if (check) return (match[1].toLowerCase() === check.toLowerCase());
 
1189                                 if (check) return false;
 
1193                         if (check) return (this.curSub.toLowerCase() === check.toLowerCase());
 
1197         currentDomain: function(check) {
 
1198                 if (typeof this.curDom === 'undefined') {
 
1199                         var match = location.href.match(RESUtils.matchDOM);
 
1200                         if (match !== null) {
 
1201                                 this.curDom = match[1];
 
1202                                 if (check) return (match[1].toLowerCase() === check.toLowerCase());
 
1205                                 if (check) return false;
 
1209                         if (check) return (this.curDom.toLowerCase() === check.toLowerCase());
 
1213         currentUserProfile: function() {
 
1214                 if (typeof this.curUserProfile === 'undefined') {
 
1215                         var match = location.href.match(/^https?:\/\/(?:[a-z]+)\.reddit\.com\/user\/([\w\.]+).*/i);
 
1216                         if (match !== null) {
 
1217                                 this.curUserProfile = match[1];
 
1223                         return this.curUserProfile;
 
1226         getXYpos: function (obj) {
 
1227                 var topValue= 0,leftValue= 0;
 
1229                         leftValue += obj.offsetLeft;
 
1230                         topValue += obj.offsetTop;
 
1231                         obj = obj.offsetParent;
 
1233                 return { 'x': leftValue, 'y': topValue };
 
1235         elementInViewport: function (obj) {
 
1236                 // check the headerOffset - if we've pinned the subreddit bar, we need to add some pixels so the "visible" stuff is lower down the page.
 
1237                 var headerOffset = this.getHeaderOffset();
 
1238                 var top = obj.offsetTop - headerOffset;
 
1239                 var left = obj.offsetLeft;
 
1240                 var width = obj.offsetWidth;
 
1241                 var height = obj.offsetHeight;
 
1242                 while(obj.offsetParent) {
 
1243                         obj = obj.offsetParent;
 
1244                         top += obj.offsetTop;
 
1245                         left += obj.offsetLeft;
 
1248                         top >= window.pageYOffset &&
 
1249                         left >= window.pageXOffset &&
 
1250                         (top + height) <= (window.pageYOffset + window.innerHeight - headerOffset) &&
 
1251                         (left + width) <= (window.pageXOffset + window.innerWidth)
 
1254         setMouseXY: function(e) {
 
1255                 e = e || window.event;
 
1256                 var cursor = {x:0, y:0};
 
1257                 if (e.pageX || e.pageY) {
 
1261                         cursor.x = e.clientX +
 
1262                                 (document.documentElement.scrollLeft ||
 
1263                                 document.body.scrollLeft) -
 
1264                                 document.documentElement.clientLeft;
 
1265                         cursor.y = e.clientY +
 
1266                                 (document.documentElement.scrollTop ||
 
1267                                 document.body.scrollTop) -
 
1268                                 document.documentElement.clientTop;
 
1270                 RESUtils.mouseX = cursor.x;
 
1271                 RESUtils.mouseY = cursor.y;
 
1273         elementUnderMouse: function ( obj ) {
 
1275                     top = $obj.offset().top,
 
1276                     left = $obj.offset().left,
 
1277                     width = $obj.outerWidth(),
 
1278                     height = $obj.outerHeight(),
 
1279                     right = left + width,
 
1280                     bottom = top + height;
 
1281                 if ((RESUtils.mouseX >= left) && (RESUtils.mouseX <= right) && (RESUtils.mouseY >= top) && (RESUtils.mouseY <= bottom)) {
 
1287         doElementsCollide: function (ele1, ele2, margin) {
 
1288                 margin = margin || 0;
 
1292                 var dims1 = ele1.offset();
 
1293                 dims1.right = dims1.left + ele1.width();
 
1294                 dims1.bottom = dims1.top + ele1.height();
 
1296                 dims1.left -= margin;
 
1297                 dims1.top -= margin;
 
1298                 dims1.right += margin;
 
1299                 dims1.bottom += margin;
 
1302                 var dims2 = ele2.offset();
 
1303                 dims2.right = dims2.left + ele2.width();
 
1304                 dims2.bottom = dims2.top + ele2.height();
 
1308                                 (dims1.left < dims2.left  && dims2.left < dims1.right) ||
 
1309                                 (dims1.left < dims2.right && dims2.right < dims1.right) ||
 
1310                                 (dims2.left < dims1.left  && dims1.left < dims2.right) ||
 
1311                                 (dims2.left < dims1.right && dims1.right < dims2.right)
 
1314                                 (dims1.top  < dims2.top    && dims2.top    < dims1.bottom) ||
 
1315                                 (dims1.top  < dims2.bottom && dims2.bottom < dims1.bottom) ||
 
1316                                 (dims2.top  < dims1.top    && dims1.top    < dims2.bottom) ||
 
1317                                 (dims2.top  < dims1.bottom && dims1.bottom < dims2.bottom))
 
1320                         // In layman's terms:
 
1321                         // If one of the box's left/right borders is between the other box's left/right
 
1322                         // and same with top/bottom,
 
1323                         // then they collide.  
 
1324                         // This could probably be logicked into a more compact form.
 
1331         scrollTo: function(x,y) {
 
1332                 var headerOffset = this.getHeaderOffset();
 
1333                 window.scrollTo(x,y-headerOffset);
 
1335         getHeaderOffset: function() {
 
1336                 if (typeof this.headerOffset === 'undefined') {
 
1337                         this.headerOffset = 0;
 
1338                         switch (modules['betteReddit'].options.pinHeader.value) {
 
1342                                         this.theHeader = document.querySelector('#sr-header-area');
 
1345                                         this.theHeader = document.querySelector('#sr-header-area');
 
1348                                         this.theHeader = document.querySelector('#header');
 
1351                         if (this.theHeader) {
 
1352                                 this.headerOffset = this.theHeader.offsetHeight + 6;
 
1355                 return this.headerOffset;
 
1357         setSelectValue: function(obj, value) {
 
1358                 for (var i=0, len=obj.length; i < len; i++) {
 
1359                         // for some reason in firefox, obj[0] is undefined... weird. adding a test for existence of obj[i]...
 
1360                         // okay, now as of ff8, it's even barfing here unless we console.log out a check - nonsensical.
 
1361                         // a bug has been filed to bugzilla at:
 
1362                         // https://bugzilla.mozilla.org/show_bug.cgi?id=702847
 
1363                         if ((obj[i]) && (obj[i].value == value)) {
 
1364                                 obj[i].selected = true;
 
1368         stripHTML: function(str) {
 
1369                 var regExp = /<\/?[^>]+>/gi;
 
1370                 str = str.replace(regExp, "");
 
1373         sanitizeHTML: function(htmlStr) {
 
1374                 if (!this.sanitizer) {
 
1375                         var SnuOwnd = window.SnuOwnd;
 
1376                         var redditCallbacks = SnuOwnd.getRedditCallbacks();
 
1377                         var callbacks = SnuOwnd.createCustomCallbacks({
 
1378                                 paragraph: function(out, text, options){
 
1379                                         if (text) out.s += text.s;
 
1381                                 autolink: redditCallbacks.autolink,
 
1382                                 raw_html_tag: redditCallbacks.raw_html_tag
 
1384                         var rendererConfig = SnuOwnd.defaultRenderState();
 
1385                         rendererConfig.flags = SnuOwnd.DEFAULT_WIKI_FLAGS;
 
1386                         rendererConfig.html_element_whitelist = [
 
1387                                 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'div', 'code',
 
1388                                 'br', 'hr', 'p', 'a', 'img', 'pre', 'blockquote', 'table',
 
1389                                 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'strong', 'em',
 
1390                                 'i', 'b', 'u', 'ul', 'ol', 'li', 'dl', 'dt', 'dd',
 
1391                                 'font', 'center', 'small', 's', 'q', 'sub', 'sup', 'del'
 
1393                         rendererConfig.html_attr_whitelist = [
 
1394                                 'href', 'title', 'src', 'alt', 'colspan',
 
1395                                 'rowspan', 'cellspacing', 'cellpadding', 'scope',
 
1396                                 'face', 'color', 'size', 'bgcolor', 'align'
 
1398                         this.sanitizer = SnuOwnd.getParser({
 
1399                                 callbacks: callbacks,
 
1400                                 context: rendererConfig
 
1403                 return this.sanitizer.render(htmlStr);
 
1405         fadeElementOut: function(obj, speed, callback) {
 
1406                 if (obj.getAttribute('isfading') === 'in') {
 
1409                 obj.setAttribute('isfading','out');
 
1410                 speed = speed || 0.1;
 
1411                 if (obj.style.opacity === '') obj.style.opacity = '1';
 
1412                 if (obj.style.opacity <= 0) {
 
1413                         obj.style.display = 'none';
 
1414                         obj.setAttribute('isfading',false);
 
1415                         if (callback) callback();
 
1418                         var newOpacity = parseFloat(obj.style.opacity) - speed;
 
1419                         if (newOpacity < speed) newOpacity = 0;
 
1420                         obj.style.opacity = newOpacity;
 
1421                         setTimeout(function() { RESUtils.fadeElementOut(obj, speed, callback); }, 100);
 
1424         fadeElementIn: function(obj, speed, finalOpacity) {
 
1425                 finalOpacity = finalOpacity || 1;
 
1426                 if (obj.getAttribute('isfading') === 'out') {
 
1429                 obj.setAttribute('isfading','in');
 
1430                 speed = speed || 0.1;
 
1431                 if ((obj.style.display === 'none') || (obj.style.display === '')) {
 
1432                         obj.style.opacity = 0;
 
1433                         obj.style.display = 'block';
 
1435                 if (obj.style.opacity >= finalOpacity) {
 
1436                         obj.setAttribute('isfading',false);
 
1437                         obj.style.opacity = finalOpacity;
 
1440                         var newOpacity = parseFloat(obj.style.opacity) + parseFloat(speed);
 
1441                         if (newOpacity > finalOpacity) newOpacity = finalOpacity;
 
1442                         obj.style.opacity = newOpacity;
 
1443                         setTimeout(function() { RESUtils.fadeElementIn(obj, speed, finalOpacity); }, 100);
 
1446         setCursorPosition: function(form, pos) {
 
1450                 if (elem.setSelectionRange) {
 
1451                         elem.setSelectionRange(pos, pos);
 
1452                 } else if (elem.createTextRange) {
 
1453                         var range = elem.createTextRange();
 
1454                         range.collapse(true);
 
1455                         range.moveEnd('character', pos);
 
1456                         range.moveStart('character', pos);
 
1462         setNewNotification: function() {
 
1463                 $('#RESSettingsButton, #RESMainGearOverlay .gearIcon').addClass('newNotification').click(function() {
 
1464                         location.href = '/r/RESAnnouncements';
 
1467         createMultiLock: function() {
 
1472                         lock: function(lockname, value) {
 
1473                                 if (typeof lockname === "undefined") return;
 
1474                                 if (locks[lockname]) return;
 
1476                                 locks[lockname] = value || true;
 
1480                         unlock: function(lockname) {
 
1481                                 if (typeof lockname === "undefined") return;
 
1482                                 if (!locks[lockname]) return;
 
1484                                 locks[lockname] = false;
 
1488                         locked: function(lockname) {
 
1489                                 if (typeof lockname !== "undefined") {
 
1490                                         // Is this lock set?
 
1491                                         return locks[lockname];
 
1499         indexOptionTable: function(moduleID, optionKey, keyFieldIndex) {
 
1500                 var source = modules[moduleID].options[optionKey].value;
 
1502                         modules[moduleID].options[optionKey].fields[keyFieldIndex].type === 'list' ?
 
1505                 return RESUtils.indexArrayByProperty(source, keyFieldIndex, keyIsList);
 
1507         indexArrayByProperty: function(source, keyIndex, keyValueSeparator) {
 
1508                 if (!source || !source.length) {
 
1509                         return function() { };
 
1512                 var index = createIndex();
 
1515                 function createIndex() {
 
1518                         for (var i = 0, length = source.length; i < length; i++) {
 
1519                                 var item = source[i];
 
1520                                 var key = item && item[keyIndex];
 
1523                                 if (keyValueSeparator) {
 
1524                                         var keys = key.toLowerCase().split(keyValueSeparator);
 
1525                                         for (var ki = 0, klength = keys.length; ki < klength; ki++) {
 
1537                 function getItem(key) {
 
1538                         key = key && key.toLowerCase();
 
1539                         var item = index[key];
 
1543         inList: function(needle, haystack, separator, isCaseSensitive) {
 
1544                 if (!needle || !haystack) return false;
 
1546                 separator = separator || ',';
 
1548                 if (haystack.indexOf(separator) !== -1) {
 
1549                         var haystacks = haystack.split(separator);
 
1550                         if (RESUtils.inArray(needle, haystacks, isCaseSensitive)) {
 
1554                         if (caseSensitive) {
 
1555                                 return (needle == haystack);
 
1557                                 return (needle.toLowerCase() == haystack.toLowerCase());
 
1561         inArray: function(needle, haystacks, isCaseSensitive) {
 
1562                 if (!isCaseSensitive) needle = needle.toLowerCase();
 
1564                 for (var i = 0, length = haystacks.length; i < length; i++) {
 
1565                         if (isCaseSensitive) {
 
1566                                 if (needle == haystacks[i])  {
 
1570                                 if (needle == haystacks[i].toLowerCase())  {
 
1576         firstRun: function() {
 
1577                 // if this is the first time this version has been run, pop open the what's new tab, background focused.
 
1578                 if (RESStorage.getItem('RES.firstRun.'+RESVersion) === null) {
 
1579                         RESStorage.setItem('RES.firstRun.'+RESVersion,'true');
 
1580                         RESUtils.openLinkInNewTab('http://redditenhancementsuite.com/whatsnew.html?v='+RESVersion, false);
 
1583         // checkForUpdate: function(forceUpdate) {
 
1584         checkForUpdate: function() {
 
1585                 if (RESUtils.currentSubreddit('RESAnnouncements')) {
 
1586                         RESStorage.removeItem('RES.newAnnouncement','true');
 
1588                 var now = new Date();
 
1589                 var lastCheck = parseInt(RESStorage.getItem('RESLastUpdateCheck'), 10) || 0;
 
1590                 // if we haven't checked for an update in 24 hours, check for one now!
 
1591                 // if (((now.getTime() - lastCheck) > 86400000) || (RESVersion > RESStorage.getItem('RESlatestVersion')) || ((RESStorage.getItem('RESoutdated') === 'true') && (RESVersion == RESStorage.getItem('RESlatestVersion'))) || forceUpdate) {
 
1592                 if ((now.getTime() - lastCheck) > 86400000) {
 
1593                         // now we're just going to check /r/RESAnnouncements for new posts, we're not checking version numbers...
 
1594                         var lastID = RESStorage.getItem('RES.lastAnnouncementID');
 
1595                         $.getJSON('/r/RESAnnouncements/.json?limit=1&app=res', function(data) {
 
1596                                 RESStorage.setItem('RESLastUpdateCheck',now.getTime());
 
1597                                 var thisID = data.data.children[0].data.id;
 
1598                                 if (thisID != lastID) {
 
1599                                         RESStorage.setItem('RES.newAnnouncement','true');
 
1600                                         RESUtils.setNewNotification();
 
1602                                 RESStorage.setItem('RES.lastAnnouncementID', thisID);
 
1605                         var jsonURL = 'http://reddit.honestbleeps.com/update.json?v=' + RESVersion;
 
1606                         // mark off that we've checked for an update...
 
1607                         RESStorage.setItem('RESLastUpdateCheck',now.getTime());
 
1608                         var outdated = false;
 
1609                         if (BrowserDetect.isChrome()) {
 
1610                                 // we've got chrome, so we need to hit up the background page to do cross domain XHR
 
1612                                         requestType: 'compareVersion',
 
1615                                 chrome.extension.sendMessage(thisJSON, function(response) {
 
1616                                         // send message to background.html to open new tabs...
 
1617                                         outdated = RESUtils.compareVersion(response, forceUpdate);
 
1619                         } else if (BrowserDetect.isSafari()) {
 
1620                                 // we've got safari, so we need to hit up the background page to do cross domain XHR
 
1622                                         requestType: 'compareVersion',
 
1624                                         forceUpdate: forceUpdate
 
1626                                 safari.self.tab.dispatchMessage("compareVersion", thisJSON);
 
1627                         } else if (BrowserDetect.isOpera()) {
 
1628                                 // we've got opera, so we need to hit up the background page to do cross domain XHR
 
1630                                         requestType: 'compareVersion',
 
1632                                         forceUpdate: forceUpdate
 
1634                                 opera.extension.postMessage(JSON.stringify(thisJSON));
 
1636                                 // we've got greasemonkey, so we can do cross domain XHR.
 
1640                                         onload: function(response) {
 
1641                                                 outdated = RESUtils.compareVersion(JSON.parse(response.responseText), forceUpdate);
 
1649         compareVersion: function(response, forceUpdate) {
 
1650                 if (RESVersion < response.latestVersion) {
 
1651                         RESStorage.setItem('RESoutdated','true');
 
1652                         RESStorage.setItem('RESlatestVersion',response.latestVersion);
 
1653                         RESStorage.setItem('RESmessage',response.message);
 
1655                                 $(RESConsole.RESCheckUpdateButton).html('You are out of date! <a target="_blank" href="http://reddit.honestbleeps.com/download">[click to update]</a>');
 
1659                         RESStorage.setItem('RESlatestVersion',response.latestVersion);
 
1660                         RESStorage.setItem('RESoutdated','false');
 
1662                                 $(RESConsole.RESCheckUpdateButton).html('You are up to date!');
 
1668         proEnabled: function() {
 
1669                 return ((typeof modules['RESPro'] !== 'undefined') && (modules['RESPro'].isEnabled()));
 
1671         niceKeyCode: function(charCode) {
 
1672                 var keyComboString = '';
 
1673                 var testCode, niceString;
 
1674                 if (typeof charCode === 'string') {
 
1675                         var tempArray = charCode.split(',');
 
1676                         if (tempArray.length) {
 
1677                                 if (tempArray[1] === 'true') keyComboString += 'alt-';
 
1678                                 if (tempArray[2] === 'true') keyComboString += 'ctrl-';
 
1679                                 if (tempArray[3] === 'true') keyComboString += 'shift-';
 
1680                                 if (tempArray[4] === 'true') keyComboString += 'command-';
 
1682                         testCode = parseInt(charCode, 10);
 
1683                 } else if (typeof charCode === 'object') {
 
1684                         testCode = parseInt(charCode[0], 10);
 
1685                         if (charCode[1]) keyComboString += 'alt-';
 
1686                         if (charCode[2]) keyComboString += 'ctrl-';
 
1687                         if (charCode[3]) keyComboString += 'shift-';
 
1688                         if (charCode[4]) keyComboString += 'command-';
 
1692                                 niceString = "backspace"; //  backspace
 
1695                                 niceString = "tab"; //  tab
 
1698                                 niceString = "enter"; //  enter
 
1701                                 niceString = "shift"; //  shift
 
1704                                 niceString = "ctrl"; //  ctrl
 
1707                                 niceString = "alt"; //  alt
 
1710                                 niceString = "pause/break"; //  pause/break
 
1713                                 niceString = "caps lock"; //  caps lock
 
1716                                 niceString = "escape"; //  escape
 
1719                                 niceString = "page up"; // page up, to avoid displaying alternate character and confusing people
 
1722                                 niceString = "page down"; // page down
 
1725                                 niceString = "end"; // end
 
1728                                 niceString = "home"; // home
 
1731                                 niceString = "left arrow"; // left arrow
 
1734                                 niceString = "up arrow"; // up arrow
 
1737                                 niceString = "right arrow"; // right arrow
 
1740                                 niceString = "down arrow"; // down arrow
 
1743                                 niceString = "insert"; // insert
 
1746                                 niceString = "delete"; // delete
 
1749                                 niceString = "left window"; // left window
 
1752                                 niceString = "right window"; // right window
 
1755                                 niceString = "select key"; // select key
 
1758                                 niceString = "numpad 0"; // numpad 0
 
1761                                 niceString = "numpad 1"; // numpad 1
 
1764                                 niceString = "numpad 2"; // numpad 2
 
1767                                 niceString = "numpad 3"; // numpad 3
 
1770                                 niceString = "numpad 4"; // numpad 4
 
1773                                 niceString = "numpad 5"; // numpad 5
 
1776                                 niceString = "numpad 6"; // numpad 6
 
1779                                 niceString = "numpad 7"; // numpad 7
 
1782                                 niceString = "numpad 8"; // numpad 8
 
1785                                 niceString = "numpad 9"; // numpad 9
 
1788                                 niceString = "multiply"; // multiply
 
1791                                 niceString = "add"; // add
 
1794                                 niceString = "subtract"; // subtract
 
1797                                 niceString = "decimal point"; // decimal point
 
1800                                 niceString = "divide"; // divide
 
1803                                 niceString = "F1"; // F1
 
1806                                 niceString = "F2"; // F2
 
1809                                 niceString = "F3"; // F3
 
1812                                 niceString = "F4"; // F4
 
1815                                 niceString = "F5"; // F5
 
1818                                 niceString = "F6"; // F6
 
1821                                 niceString = "F7"; // F7
 
1824                                 niceString = "F8"; // F8
 
1827                                 niceString = "F9"; // F9
 
1830                                 niceString = "F10"; // F10
 
1833                                 niceString = "F11"; // F11
 
1836                                 niceString = "F12"; // F12
 
1839                                 niceString = "num lock"; // num lock
 
1842                                 niceString = "scroll lock"; // scroll lock
 
1845                                 niceString = ";"; // semi-colon
 
1848                                 niceString = "="; // equal-sign
 
1851                                 niceString = ","; // comma
 
1854                                 niceString = "-"; // dash
 
1857                                 niceString = "."; // period
 
1860                                 niceString = "/"; // forward slash
 
1863                                 niceString = "`"; // grave accent
 
1866                                 niceString = "["; // open bracket
 
1869                                 niceString = "\\"; // back slash
 
1872                                 niceString = "]"; // close bracket
 
1875                                 niceString = "'"; // single quote
 
1878                                 niceString = String.fromCharCode(testCode);
 
1881                 return keyComboString + niceString;
 
1883         niceDate: function(d, usformat) {
 
1884                 d = d || new Date();
 
1885                 var year = d.getFullYear();
 
1886                 var month = (d.getMonth() + 1);
 
1887                 month = (month < 10) ? '0'+month : month;
 
1888                 var day = d.getDate();
 
1889                 day = (day < 10) ? '0'+day : day;
 
1890                 var fullString = year+'-'+month+'-'+day;
 
1892                         fullString = month+'-'+day+'-'+year;
 
1896         niceDateTime: function(d, usformat) {
 
1897                 d = d || new Date();
 
1898                 var dateString = RESUtils.niceDate(d);
 
1899                 var hours = d.getHours();
 
1900                 hours = (hours < 10) ? '0'+hours : hours;
 
1901                 var minutes = d.getMinutes();
 
1902                 minutes = (minutes < 10) ? '0'+minutes : minutes;
 
1903                 var seconds = d.getSeconds();
 
1904                 seconds = (seconds < 10) ? '0'+seconds : seconds;
 
1905                 var fullString = dateString + ' ' + hours + ':'+minutes+':'+seconds;
 
1908         niceDateDiff: function(origdate, newdate) {
 
1909                 // Enter the month, day, and year below you want to use as
 
1910                 // the starting point for the date calculation
 
1912                         newdate = new Date();
 
1915                 var amonth = origdate.getUTCMonth() + 1;
 
1916                 var aday = origdate.getUTCDate();
 
1917                 var ayear = origdate.getUTCFullYear();
 
1919                 var tyear = newdate.getUTCFullYear();
 
1920                 var tmonth = newdate.getUTCMonth() + 1;
 
1921                 var tday = newdate.getUTCDate();
 
1930                 if (((tyear % 4 === 0) && (tyear % 100 !== 0)) || (tyear % 400 === 0)) {
 
1934                 var m = [31, f, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
 
1936                 var dyear = tyear - ayear;
 
1938                 var dmonth = tmonth - amonth;
 
1939                 if (dmonth < 0 && dyear > 0) {
 
1940                         dmonth = dmonth + 12;
 
1944                 var dday = tday - aday;
 
1947                                 var ma = amonth + tmonth;
 
1949                                 if (ma >= 12) { ma = ma - 12; }
 
1950                                 if (ma < 0) { ma = ma + 12; }
 
1951                                 dday = dday + m[ma];
 
1955                                         dmonth = dmonth + 12;
 
1962                 var returnString = '';
 
1964                 if (dyear === 0) { y = 0; }
 
1965                 if (dmonth === 0) { mm = 0; }
 
1966                 if (dday === 0) { d = 0; }
 
1967                 if ((y === 1) && (mm === 1)) { a1 = 1; }
 
1968                 if ((y === 1) && (d === 1)) { a1 = 1; }
 
1969                 if ((mm === 1) && (d === 1)) { a2 = 1; }
 
1972                                 returnString += dyear + " year";
 
1974                                 returnString += dyear + " years";
 
1977                 if ((a1 === 1) && (a2 === 0)) { returnString += " and "; }
 
1978                 if ((a1 === 1) && (a2 === 1)) { returnString += ", "; }
 
1981                                 returnString += dmonth + " month";
 
1983                                 returnString += dmonth + " months";
 
1986                 if (a2 === 1) { returnString += " and "; }
 
1989                                 returnString += dday + " day";
 
1991                                 returnString += dday + " days";
 
1994                 if (returnString === '') {
 
1995                         returnString = '0 days';
 
1997                 return returnString;
 
1999         checkIfSubmitting: function() {
 
2000                 this.checkedIfSubmitting = true;
 
2001                 if ((location.href.match(/\/r\/[\w]+\/submit\/?/i)) || (location.href.match(/reddit\.com\/submit\/?/i))) {
 
2002                         var thisSubRedditInput = document.getElementById('sr-autocomplete');
 
2003                         if (thisSubRedditInput) {
 
2004                                 var thisSubReddit = thisSubRedditInput.value;
 
2005                                 var title = document.querySelector('textarea[name=title]');
 
2006                                 if (typeof this.thisSubRedditInputListener === 'undefined') {
 
2007                                         this.thisSubRedditInputListener = true;
 
2008                                         thisSubRedditInput.addEventListener('change', function(e) {
 
2009                                                 RESUtils.checkIfSubmitting();
 
2012                                 if ((thisSubReddit.toLowerCase() === 'enhancement') || (thisSubReddit.toLowerCase() === 'resissues')) {
 
2013                                         RESUtils.addCSS('#submittingToEnhancement { display: none; min-height: 300px; font-size: 14px; line-height: 15px; margin-top: 10px; width: 518px; position: absolute; z-index: 999; } #submittingToEnhancement ol { margin-left: 10px; margin-top: 15px; list-style-type: decimal; } #submittingToEnhancement li { margin-left: 25px; }');
 
2014                                         RESUtils.addCSS('.submittingToEnhancementButton { border: 1px solid #444; border-radius: 2px; padding: 3px 6px; cursor: pointer; display: inline-block; margin-top: 12px; }');
 
2015                                         RESUtils.addCSS('#RESBugReport, #RESFeatureRequest { display: none; }');
 
2016                                         RESUtils.addCSS('#RESSubmitOptions .submittingToEnhancementButton { margin-top: 30px; }');
 
2017                                         var textDesc = document.getElementById('text-desc');
 
2018                                         this.submittingToEnhancement = createElementWithID('div','submittingToEnhancement','RESDialogSmall');
 
2019                                         /*jshint multistr: true */
 
2020                                         var submittingHTML = " \
 
2021                                         <h3>Submitting to r/Enhancement</h3> \
 
2022                                         <div class=\"RESDialogContents\"> \
 
2023                                                 <div id=\"RESSubmitOptions\"> \
 
2024                                                         What kind of a post do you want to submit to r/Enhancement? So that we can better support you, please choose from the options below, and please take care to read the instructions, thanks!<br> \
 
2025                                                         <div id=\"RESSubmitBug\" class=\"submittingToEnhancementButton\">I want to submit a bug report</div><br> \
 
2026                                                         <div id=\"RESSubmitFeatureRequest\" class=\"submittingToEnhancementButton\">I want to submit a feature request</div><br> \
 
2027                                                         <div id=\"RESSubmitOther\" class=\"submittingToEnhancementButton\">I want to submit a general question or other item</div> \
 
2029                                                 <div id=\"RESBugReport\"> \
 
2030                                                         Are you sure you want to submit a bug report? We get a lot of duplicates and it would really help if you took a moment to read the following: <br> \
 
2032                                                                 <li>Have you searched /r/RESIssues to see if someone else has reported it?</li> \
 
2033                                                                 <li>Have you checked the <a target=\"_blank\" href=\"http://www.reddit.com/r/Enhancement/wiki/faq\">RES FAQ?</a></li> \
 
2034                                                                 <li>Are you sure it's a bug with RES specifically? Do you have any other userscripts/extensions running?  How about addons like BetterPrivacy, Ghostery, CCleaner, etc?</li> \
 
2037                                                         Please also check out the latest known / popular bugs first:<br> \
 
2038                                                         <ul id=\"RESKnownBugs\"><li style=\"color: red;\">Loading...</li></ul> \
 
2039                                                         <span id=\"submittingBug\" class=\"submittingToEnhancementButton\">I still want to submit a bug!</span> \
 
2041                                                 <div id=\"RESFeatureRequest\"> \
 
2042                                                         So you want to request a feature, great!  Please just consider the following, first:<br> \
 
2044                                                                 <li>Have you searched /r/Enhancement to see if someone else has requested it?</li> \
 
2045                                                                 <li>Is it something that would appeal to Reddit as a whole?  Personal or subreddit specific requests usually aren't added to RES.</li> \
 
2048                                                         Please also check out the latest known popular feature requests first:<br> \
 
2049                                                         <ul id=\"RESKnownFeatureRequests\"><li style=\"color: red;\">Loading...</li></ul> \
 
2050                                                         <span id=\"submittingFeature\" class=\"submittingToEnhancementButton\">I still want to submit a feature request!<span> \
 
2053                                         $(this.submittingToEnhancement).html(submittingHTML);
 
2054                                         insertAfter(textDesc, this.submittingToEnhancement);
 
2055                                         setTimeout(function() {
 
2056                                                 $('#RESSubmitBug').click(
 
2058                                                                 $('#RESSubmitOptions').fadeOut(
 
2060                                                                                 $('#RESBugReport').fadeIn();
 
2063                                                                                         url:    'http://redditenhancementsuite.com/knownbugs.json',
 
2064                                                                                         onload: function(response) {
 
2065                                                                                                 $('#RESKnownBugs').html('');
 
2066                                                                                                 var data = safeJSON.parse(response.responseText);
 
2067                                                                                                 $.each(data, function(key, val) {
 
2068                                                                                                         $('#RESKnownBugs').append('<li><a target="_blank" href="'+val.url+'">'+val.description+'</a></li>');
 
2076                                                 $('#RESSubmitFeatureRequest').click(
 
2078                                                                 $('#RESSubmitOptions').fadeOut(
 
2080                                                                                 $('#RESFeatureRequest').fadeIn();
 
2081                                                                                 $.getJSON('http://redditenhancementsuite.com/knownfeaturerequests.json', function(data) {
 
2082                                                                                         $('#RESKnownFeatureRequests').html('');
 
2083                                                                                         $.each(data, function(key, val) {
 
2084                                                                                                 $('#RESKnownFeatureRequests').append('<li><a target="_blank" href="'+val.url+'">'+val.description+'</a></li>');
 
2091                                                 $('#submittingBug').click(
 
2093                                                                 $('#sr-autocomplete').val('RESIssues');
 
2094                                                                 $('li a.text-button').click();
 
2095                                                                 $('#submittingToEnhancement').fadeOut();
 
2097                                                                 var txt = "- RES Version: " + RESVersion + "\n";
 
2098                                                                 txt += "- Browser: " + BrowserDetect.browser + "\n";
 
2099                                                                 if (typeof navigator === 'undefined') navigator = window.navigator;
 
2100                                                                 txt+= "- Browser Version: " + BrowserDetect.version + "\n";
 
2101                                                                 txt+= "- Cookies Enabled: " + navigator.cookieEnabled + "\n";
 
2102                                                                 txt+= "- Platform: " + BrowserDetect.OS + "\n";
 
2103                                                                 txt+= "- Did you search /r/RESIssues before submitting this: No. That, or I didn't notice this text here and edit it!\n\n";
 
2104                                                                 $('.usertext-edit textarea').val(txt);
 
2105                                                                 title.value = '[bug] Please describe your bug here. If you have screenshots, please link them in the selftext.';
 
2108                                                 $('#submittingFeature').click(
 
2110                                                                 $('#sr-autocomplete').val('Enhancement');
 
2111                                                                 $('#submittingToEnhancement').fadeOut();
 
2112                                                                 title.value = '[feature request] Please summarize your feature request here, and elaborate in the selftext.';
 
2115                                                 $('#RESSubmitOther').click(
 
2117                                                                 $('#sr-autocomplete').val('Enhancement');
 
2118                                                                 $('#submittingToEnhancement').fadeOut();
 
2122                                                 $('#submittingToEnhancement').fadeIn();
 
2124                                 } else if (typeof this.submittingToEnhancement !== 'undefined') {
 
2125                                         this.submittingToEnhancement.parentNode.removeChild(this.submittingToEnhancement);
 
2126                                         if (title.value === 'Submitting a bug? Please read the box above...') {
 
2133         isEmpty: function(obj) {
 
2134                 for(var prop in obj) {
 
2135                         if(obj.hasOwnProperty(prop))
 
2140         deleteCookie: function(cookieName) {
 
2142                                 requestType: 'deleteCookie',
 
2146                 if (BrowserDetect.isChrome()) {
 
2147                         chrome.extension.sendMessage(requestJSON);
 
2148                 } else if (BrowserDetect.isSafari())  {
 
2149                         document.cookie = cookieName + '=null;expires=' + new Date() +'; path=/;domain=reddit.com';
 
2150                 } else if (BrowserDetect.isOpera()) {
 
2151                         document.cookie = cookieName + '=null;expires=' + new Date() +'; path=/;domain=reddit.com';
 
2152                 } else if (BrowserDetect.isFirefox()) {
 
2153                         self.postMessage(requestJSON);
 
2156         openLinkInNewTab: function(url, focus) {
 
2158                 if (BrowserDetect.isChrome()) {
 
2160                                 requestType: 'openLinkInNewTab',
 
2164                         // send message to background.html to open new tabs...
 
2165                         chrome.extension.sendMessage(thisJSON);
 
2166                 } else if (BrowserDetect.isSafari()) {
 
2168                                 requestType: 'openLinkInNewTab',
 
2172                         safari.self.tab.dispatchMessage("openLinkInNewTab", thisJSON);
 
2173                 } else if (BrowserDetect.isOpera()) {
 
2175                                 requestType: 'openLinkInNewTab',
 
2179                         opera.extension.postMessage(JSON.stringify(thisJSON));
 
2180                 } else if (BrowserDetect.isFirefox()) {
 
2182                                 requestType: 'openLinkInNewTab',
 
2186                         self.postMessage(thisJSON);
 
2191         notification: function(contentObj, delay) {
 
2193                 if (typeof contentObj.message === 'undefined') {
 
2194                         if (typeof contentObj === 'string') {
 
2195                                 content = contentObj;
 
2200                         content = contentObj.message;
 
2204                 if (contentObj.header) {
 
2205                         header = contentObj.header;
 
2209                         if (contentObj.moduleID && modules[contentObj.moduleID]) {
 
2210                                 header.push(modules[contentObj.moduleID].moduleName);
 
2213                         if (contentObj.type === 'error') {
 
2214                                 header.push('Error');
 
2216                                 header.push('Notification');
 
2220                         header = header.join(' ');
 
2223                 if (contentObj.moduleID && modules[contentObj.moduleID]) {
 
2224                         header += modules['settingsNavigation'].makeUrlHashLink(contentObj.moduleID, contentObj.optionKey, ' ', 'gearIcon');
 
2228                 if (typeof this.notificationCount === 'undefined') {
 
2229                         this.adFrame = document.body.querySelector('#ad-frame');
 
2231                                 this.adFrame.style.display = 'none';
 
2233                         this.notificationCount = 0;
 
2234                         this.notificationTimers = [];
 
2235                         this.RESNotifications = createElementWithID('div','RESNotifications');
 
2236                         document.body.appendChild(this.RESNotifications);
 
2238                 var thisNotification = document.createElement('div');
 
2239                 thisNotification.classList.add('RESNotification');
 
2240                 thisNotification.setAttribute('id','RESNotification-'+this.notificationCount);
 
2241                 $(thisNotification).html('<div class="RESNotificationHeader"><h3>'+header+'</h3><div class="RESNotificationClose RESCloseButton">×</div></div><div class="RESNotificationContent">'+content+'</div>');
 
2242                 var thisNotificationCloseButton = thisNotification.querySelector('.RESNotificationClose');
 
2243                 thisNotificationCloseButton.addEventListener('click', function(e) {
 
2244                         var thisNotification = e.target.parentNode.parentNode;
 
2245                         RESUtils.closeNotification(thisNotification);
 
2247                 this.setCloseNotificationTimer(thisNotification, delay);
 
2248                 this.RESNotifications.style.display = 'block';
 
2249                 this.RESNotifications.appendChild(thisNotification);
 
2250                 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'notification');
 
2251                 RESUtils.fadeElementIn(thisNotification, 0.2, 1);
 
2252                 this.notificationCount++;
 
2254         setCloseNotificationTimer: function(e, delay) {
 
2255                 delay = delay || 3000;
 
2256                 var thisNotification = (typeof e.currentTarget !== 'undefined') ? e.currentTarget : e;
 
2257                 var thisNotificationID = thisNotification.getAttribute('id').split('-')[1];
 
2258                 thisNotification.classList.add('timerOn');
 
2259                 clearTimeout(RESUtils.notificationTimers[thisNotificationID]);
 
2260                 var thisTimer = setTimeout(function() {
 
2261                         RESUtils.closeNotification(thisNotification);
 
2263                 RESUtils.notificationTimers[thisNotificationID] = thisTimer;
 
2264                 thisNotification.addEventListener('mouseover',RESUtils.cancelCloseNotificationTimer, false);
 
2265                 thisNotification.removeEventListener('mouseout',RESUtils.setCloseNotification,false);
 
2267         cancelCloseNotificationTimer: function(e) {
 
2268                 var thisNotificationID = e.currentTarget.getAttribute('id').split('-')[1];
 
2269                 e.currentTarget.classList.remove('timerOn');
 
2270                 clearTimeout(RESUtils.notificationTimers[thisNotificationID]);
 
2271                 e.target.removeEventListener('mouseover',RESUtils.cancelCloseNotification,false);
 
2272                 e.currentTarget.addEventListener('mouseout',RESUtils.setCloseNotificationTimer, false);
 
2274         closeNotification: function(ele) {
 
2275                 RESUtils.fadeElementOut(ele, 0.1, RESUtils.notificationClosed);
 
2277         notificationClosed: function(ele) {
 
2278                 var notifications = RESUtils.RESNotifications.querySelectorAll('.RESNotification');
 
2280                 for (var i=0, len=notifications.length; i<len; i++) {
 
2281                         if (notifications[i].style.opacity === '0') {
 
2282                                 notifications[i].parentNode.removeChild(notifications[i]);
 
2286                 if (destroyed == notifications.length) {
 
2287                         RESUtils.RESNotifications.style.display = 'none';
 
2288                         if (RESUtils.adFrame) RESUtils.adFrame.style.display = 'block';
 
2291                 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'notification');
 
2293         toggleButton: function(fieldID, enabled, onText, offText, isTable) {
 
2294                 enabled = enabled || false;
 
2295                 var checked = (enabled) ? 'CHECKED' : '';
 
2296                 onText = onText || 'on';
 
2297                 offText = offText || 'off';
 
2298                 var thisToggle = document.createElement('div');
 
2299                 thisToggle.setAttribute('class','toggleButton');
 
2300                 thisToggle.setAttribute('id',fieldID+'Container');
 
2303                         tableAttr = ' tableOption="true"';
 
2305                 $(thisToggle).html('<span class="toggleOn">'+onText+'</span><span class="toggleOff">'+offText+'</span><input id="'+fieldID+'" type="checkbox" '+tableAttr+checked+'>');
 
2306                 thisToggle.addEventListener('click', function(e) {
 
2307                         var thisCheckbox = this.querySelector('input[type=checkbox]');
 
2308                         var enabled = thisCheckbox.checked;
 
2309                         thisCheckbox.checked = !enabled;
 
2311                                 this.classList.remove('enabled');
 
2313                                 this.classList.add('enabled');
 
2316                 if (enabled) thisToggle.classList.add('enabled');
 
2319         addCommas: function(nStr) {
 
2321                 var x = nStr.split('.');
 
2323                 var x2 = x.length > 1 ? '.' + x[1] : '';
 
2324                 var rgx = /(\d+)(\d{3})/;
 
2325                 while (rgx.test(x1)) {
 
2326                         x1 = x1.replace(rgx, '$1' + ',' + '$2');
 
2330         generateTable: function(items, call, context) {
 
2331                 if (!items || !call) return;
 
2332                 // Sanitize single item into items array
 
2333                 if (!(items.length && typeof items !== "string")) items = [ items ];
 
2335                 var description = [];
 
2336                 description.push('<table>');
 
2338                 for (var i = 0; i < items.length; i++) {
 
2339                         var item = call(items[i], i, items, context);
 
2340                         if (typeof item === "string") {
 
2341                                 description.push(item);
 
2342                         } else if (item.length) {
 
2343                                 description = description.concat(item);
 
2346                 description.push('</table>');
 
2347                 description = description.join('\n');
 
2351         xhrCache: function(operation) {
 
2353                         requestType: 'XHRCache',
 
2354                         operation: operation
 
2356                 if (BrowserDetect.isChrome()) {
 
2357                         chrome.extension.sendMessage(thisJSON);
 
2358                 } else if (BrowserDetect.isSafari()) {
 
2359                         safari.self.tab.dispatchMessage('XHRCache', thisJSON);
 
2360                 } else if (BrowserDetect.isOpera()) {
 
2361                         opera.extension.postMessage(JSON.stringify(thisJSON));
 
2362                 } else if (BrowserDetect.isFirefox()) {
 
2363                         self.postMessage(thisJSON);
 
2366         initObservers: function() {
 
2367                 var siteTable, observer;
 
2368                 if (RESUtils.pageType() !== 'comments') {
 
2369                         // initialize sitetable observer...
 
2370                         siteTable = document.querySelector('#siteTable');
 
2371                         var stMultiCheck = document.querySelectorAll('#siteTable');
 
2372                         if (stMultiCheck.length === 2) {
 
2373                                 siteTable = stMultiCheck[1];
 
2376                         if (BrowserDetect.MutationObserver && siteTable) {
 
2377                                 observer = new BrowserDetect.MutationObserver(function(mutations) {
 
2378                                         mutations.forEach(function(mutation) {
 
2379                                                 if (mutation.addedNodes[0].id.indexOf('siteTable') !== -1) {
 
2380                                                         // when a new sitetable is loaded, we need to add new observers for selftexts within that sitetable...
 
2381                                                         $(mutation.addedNodes[0]).find('.entry div.expando').each(function() {
 
2382                                                                 RESUtils.addSelfTextObserver(this);
 
2384                                                         RESUtils.watchers.siteTable.forEach(function(callback) {
 
2385                                                                 if (callback) callback(mutation.addedNodes[0]);
 
2391                                 observer.observe(siteTable, {
 
2394                                         characterData: false
 
2397                                 // Opera doesn't support MutationObserver - so we need this for Opera support.
 
2399                                         siteTable.addEventListener('DOMNodeInserted', function(event) {
 
2400                                                 if ((event.target.tagName === 'DIV') && (event.target.getAttribute('id') && event.target.getAttribute('id').indexOf('siteTable') !== -1)) {
 
2401                                                         RESUtils.watchers.siteTable.forEach(function(callback) {
 
2402                                                                 if (callback) callback(event.target);
 
2409                         // initialize sitetable observer...
 
2410                         siteTable = document.querySelector('.commentarea > .sitetable');
 
2412                         if (BrowserDetect.MutationObserver && siteTable) {
 
2413                                 observer = new BrowserDetect.MutationObserver(function(mutations) {
 
2414                                         mutations.forEach(function(mutation) {
 
2415                                                 if (mutation.addedNodes.length > 0 && mutation.addedNodes[0].classList.contains('thing')) {
 
2416                                                         var thing = mutation.addedNodes[0];
 
2417                                                         var newCommentEntry = thing.querySelector('.entry');
 
2418                                                         if (!$(newCommentEntry).data('alreadyDetected')) {
 
2419                                                                 $(newCommentEntry).data('alreadyDetected', true);
 
2420                                                                 $(thing).find('.child').each(function() {
 
2421                                                                         RESUtils.addNewCommentFormObserver(this);
 
2423                                                                 RESUtils.watchers.newComments.forEach(function(callback) {
 
2424                                                                         if (callback) callback(newCommentEntry);
 
2431                                 observer.observe(siteTable, {
 
2434                                         characterData: false
 
2437                                 // Opera doesn't support MutationObserver - so we need this for Opera support.
 
2439                                         siteTable.addEventListener('DOMNodeInserted', RESUtils.mutationEventCommentHandler, false);
 
2444                 $('.entry div.expando').each(function() {
 
2445                         RESUtils.addSelfTextObserver(this);
 
2448                 // initialize new comments observers on demand, by first wiring up click listeners to "load more comments" buttons.
 
2449                 // on click, we'll add a mutation observer...
 
2450                 $('.morecomments a').click(RESUtils.addNewCommentObserverToTarget);
 
2452                 // initialize new comments forms observers on demand, by first wiring up click listeners to reply buttons.
 
2453                 // on click, we'll add a mutation observer...
 
2454                 // $('body').delegate('ul.flat-list li a[onclick*=reply]', 'click', RESUtils.addNewCommentFormObserver);
 
2455                 $('.thing .child').each(function() {
 
2456                         RESUtils.addNewCommentFormObserver(this);
 
2460         // Opera doesn't support MutationObserver - so we need this for Opera support.
 
2461         mutationEventCommentHandler: function (event) {
 
2462                 if ((event.target.tagName === 'DIV') && (event.target.classList.contains('thing'))) {
 
2463                         // we've found a matching element - stop propagation.
 
2464                         event.stopPropagation();
 
2465                         // because nested DOMNodeInserted events are an absolute CLUSTER to manage,
 
2466                         // only send individual comments through to the callback.
 
2467                         // Otherwise, we end up calling functions on a parent, then its child (which
 
2468                         // already got scanned when we passed in the parent), etc.
 
2469                         var thisComment = event.target.querySelector('.entry');
 
2470                         if (! $(thisComment).data('alreadyDetected')) {
 
2471                                 $(thisComment).data('alreadyDetected', true);
 
2472                                 // wire up listeners for new "more comments" links...
 
2473                                 $(event.target).find('.morecomments a').click(RESUtils.addNewCommentObserverToTarget);
 
2474                                 RESUtils.watchers.newComments.forEach(function(callback) {
 
2475                                         RESUtils.addNewCommentFormObserver(event.target);
 
2476                                         if (callback) callback(thisComment);
 
2481         addNewCommentObserverToTarget: function (e) {
 
2482                 var ele = $(e.currentTarget).closest('.sitetable')[0];
 
2483                 // mark this as having an observer so we don't add multiples...
 
2484                 if (! $(ele).hasClass('hasObserver')) {
 
2485                         $(ele).addClass('hasObserver');
 
2486                         RESUtils.addNewCommentObserver(ele);
 
2489         addNewCommentObserver: function(ele) {
 
2490                 var mutationNodeToObserve = ele;
 
2491                 if (BrowserDetect.MutationObserver) {
 
2492                         var observer = new BrowserDetect.MutationObserver(function(mutations) {
 
2493                                 // we need to get ONLY the nodes that are new...
 
2494                                 // get the nodeList from each mutation, find comments within it,
 
2495                                 // then call our callback on it.
 
2496                                 for (var i=0, len=mutations.length; i<len; i++) {
 
2497                                         var thisMutation = mutations[i];
 
2498                                         var nodeList = thisMutation.addedNodes;
 
2499                                         // look at the added nodes, and find comment containers.
 
2500                                         for (var j=0, jLen=nodeList.length; j<jLen; j++) {
 
2501                                                 if (nodeList[j].classList.contains('thing')) {
 
2502                                                         $(nodeList[j]).find('.child').each(function() {
 
2503                                                                 RESUtils.addNewCommentFormObserver(this);
 
2506                                                         // check for "load new comments" links within this group as well...
 
2507                                                         $(nodeList[j]).find('.morecomments a').click(RESUtils.addNewCommentObserverToTarget);
 
2509                                                         var subComments = nodeList[j].querySelectorAll('.entry');
 
2510                                                         // look at the comment containers and find actual comments...
 
2511                                                         for (var k=0, kLen=subComments.length; k<kLen; k++) {
 
2512                                                                 var thisComment = subComments[k];
 
2513                                                                 if (! $(thisComment).data('alreadyDetected')) {
 
2514                                                                         $(thisComment).data('alreadyDetected', true);
 
2515                                                                         RESUtils.watchers.newComments.forEach(function(callback) {
 
2516                                                                                 if (callback) callback(thisComment);
 
2524                                 // RESUtils.watchers.newComments.forEach(function(callback) {
 
2525                                 // // add form observers to these new comments we've found...
 
2526                                 //      $(mutations[0].target).find('.thing .child').each(function() {
 
2527                                 //              RESUtils.addNewCommentFormObserver(this);
 
2529                                 //      // check for "load new comments" links within this group as well...
 
2530                                 //      $(mutations[0].target).find('.morecomments a').click(RESUtils.addNewCommentObserverToTarget);
 
2531                                 //      callback(mutations[0].target);
 
2534                                 // disconnect this observer once all callbacks have been run.
 
2535                                 // unless we have the nestedlisting class, in which case don't disconnect because that's a
 
2536                                 // bottom level load more comments where even more can be loaded after, so they all drop into this
 
2537                                 // same .sitetable div.
 
2538                                 if (! $(ele).hasClass('nestedlisting')) {
 
2539                                         observer.disconnect();
 
2543                         observer.observe(mutationNodeToObserve, {
 
2546                                 characterData: false
 
2549                         mutationNodeToObserve.addEventListener('DOMNodeInserted', RESUtils.mutationEventCommentHandler, false);
 
2552         addNewCommentFormObserver: function(ele) {
 
2553                 var commentsFormParent = ele;
 
2554                 if (BrowserDetect.MutationObserver) {
 
2555                         // var mutationNodeToObserve = moreCommentsParent.parentNode.parentNode.parentNode.parentNode;
 
2556                         var observer = new BrowserDetect.MutationObserver(function(mutations) {
 
2557                                 var form = $(mutations[0].target).children('form');
 
2558                                 if ((form) && (form.length === 1)) {
 
2559                                         RESUtils.watchers.newCommentsForms.forEach(function(callback) {
 
2563                                         var newOwnComment = $(mutations[0].target).find(' > div.sitetable > .thing:first-child'); // assumes new comment will be prepended to sitetable's children
 
2564                                         if ((newOwnComment) && (newOwnComment.length === 1)) {
 
2565                                                 // new comment detected from the current user...
 
2566                                                 RESUtils.watchers.newComments.forEach(function(callback) {
 
2567                                                         callback(newOwnComment[0]);
 
2573                         observer.observe(commentsFormParent, {
 
2576                                 characterData: false
 
2579                         // Opera doesn't support MutationObserver - so we need this for Opera support.
 
2580                         commentsFormParent.addEventListener('DOMNodeInserted', function(event) {
 
2581                                 // TODO: proper tag filtering here, it's currently all wrong.
 
2582                                 if (event.target.tagName === 'FORM') {
 
2583                                         RESUtils.watchers.newCommentsForms.forEach(function(callback) {
 
2584                                                 if (callback) callback(event.target);
 
2587                                         var newOwnComment = $(event.target).find(' > div.sitetable > .thing:first-child'); // assumes new comment will be prepended to sitetable's children
 
2588                                         if ((newOwnComment) && (newOwnComment.length === 1)) {
 
2589                                                 // new comment detected from the current user...
 
2590                                                 RESUtils.watchers.newComments.forEach(function(callback) {
 
2591                                                         callback(newOwnComment[0]);
 
2598         addSelfTextObserver: function(ele) {
 
2599                 var selfTextParent = ele;
 
2600                 if (BrowserDetect.MutationObserver) {
 
2601                         // var mutationNodeToObserve = moreCommentsParent.parentNode.parentNode.parentNode.parentNode;
 
2602                         var observer = new BrowserDetect.MutationObserver(function(mutations) {
 
2603                                 var form = $(mutations[0].target).find('form');
 
2604                                 if ((form) && (form.length > 0)) {
 
2605                                         RESUtils.watchers.selfText.forEach(function(callback) {
 
2611                         observer.observe(selfTextParent, {
 
2614                                 characterData: false
 
2617                         // Opera doesn't support MutationObserver - so we need this for Opera support.
 
2618                         selfTextParent.addEventListener('DOMNodeInserted', function(event) {
 
2619                                 // TODO: proper tag filtering here, it's currently all wrong.
 
2620                                 if (event.target.tagName === 'FORM') {
 
2621                                         RESUtils.watchers.selfText.forEach(function(callback) {
 
2622                                                 if (callback) callback(event.target);
 
2628         watchForElement: function(type, callback) {
 
2631                                 RESUtils.watchers.siteTable.push(callback);
 
2634                                 RESUtils.watchers.newComments.push(callback);
 
2637                                 RESUtils.watchers.selfText.push(callback);
 
2639                         case 'newCommentsForms':
 
2640                                 RESUtils.watchers.newCommentsForms.push(callback);
 
2648                 newCommentsForms: []
 
2650         // A link is a comment code if all these conditions are true:
 
2651         // * It has no content (i.e. content.length === 0)
 
2652         // * Its href is of the form "/code"
 
2654         // In case it's not clear, here is a list of some common comment
 
2655         // codes on a specific subreddit:
 
2656         // http://www.reddit.com/r/metarage/comments/p3eqe/full_updated_list_of_comment_faces_wcodes/
 
2657         COMMENT_CODE_REGEX: /^\/\w+$/,
 
2658         isCommentCode: function (link) {
 
2659                 var content = link.innerHTML;
 
2661                 // Note that link.href will return the full href (which includes the
 
2662                 // reddit.com domain). We don't want that.
 
2663                 var href = link.getAttribute("href");
 
2665                 return !content && this.COMMENT_CODE_REGEX.test(href);
 
2668         Starts a unique named timeout.
 
2669         If there is a running timeout with the same name cancel the old one in favor of the new.
 
2670         Call with no time/call parameter (null/undefined/missing) to and existing one with the given name.
 
2671         Used to derfer an action until a series of events has stopped.
 
2672         e.g. wait until a user a stopped typing to update a comment preview.
 
2673         (name based on similar function in underscore.js)
 
2675         debounceTimeouts: {},
 
2676         debounce: function(name, time, call, data) {
 
2677                 if (name == null) return;
 
2678                 if (RESUtils.debounceTimeouts[name] !== undefined) {
 
2679                         window.clearTimeout(RESUtils.debounceTimeouts[name]);
 
2680                         delete RESUtils.debounceTimeouts[name];
 
2682                 if (time !== null && call !== null) {
 
2683                         RESUtils.debounceTimeouts[name] = window.setTimeout(function() {
 
2684                                 delete RESUtils.debounceTimeouts[name];
 
2691         Iterate through an array in chunks, executing a callback on each element.
 
2692         Each chunk is handled asynchronously from the others with a delay betwen each batch.
 
2693         If the provided callback returns false iteration will be halted.
 
2695         forEachChunked: function(array, chunkSize, delay, call) {
 
2696                 if (typeof array === 'undefined' || array === null) return;
 
2697                 if (typeof chunkSize === 'undefined' || chunkSize === null || chunkSize < 1) return;
 
2698                 if (typeof delay === 'undefined' || delay === null || delay < 0) return;
 
2699                 if (typeof call === 'undefined' || call === null) return;
 
2701                 var length = array.length;
 
2702                 function doChunk() {
 
2703                         for (var end = Math.min(array.length, counter+chunkSize); counter < end; counter++) {
 
2704                                 var ret = call(array[counter], counter, array);
 
2705                                 if (ret === false) return;
 
2707                         if (counter < array.length) {
 
2708                                 window.setTimeout(doChunk, delay);
 
2711                 window.setTimeout(doChunk, delay);
 
2713         getComputedStyle: function(elem, property){
 
2714                 if (elem.constructor === String) {
 
2715                         elem = document.querySelector(elem);
 
2716                 } else if (!(elem instanceof Node)) {
 
2720                 if(document.defaultView && document.defaultView.getComputedStyle) {
 
2721                         strValue = document.defaultView.getComputedStyle(elem, "").getPropertyValue(property);
 
2722                 } else if(elem.currentStyle){
 
2723                         property = property.replace(/\-(\w)/g, function(strMatch, p1){
 
2724                                 return p1.toUpperCase();
 
2726                         strValue = oElm.currentStyle[property];
 
2736                         closeOnMouseOut: true
 
2740                 The contents of state are as follows:
 
2742                         //The DOM element that triggered the hover popup.
 
2744                         //Resolved values for timing, etc.
 
2746                         //Usecase specific object
 
2753                 begin: function(onElement, conf, callback, context) {
 
2754                         var hover = RESUtils.hover;
 
2755                         if (hover.container === null) hover.create();
 
2756                         if (hover.state !== null) {
 
2759                         var state = hover.state = {
 
2761                                 options: $.extend({}, hover.defaults, conf),
 
2765                         hover.showTimer = setTimeout(function() {
 
2766                                 hover.cancelShowTimer();
 
2767                                 hover.clearShowListeners();
 
2770                                 hover.state.element.addEventListener('mouseout', hover.startHideTimer, false);
 
2771                         }, state.options.openDelay);
 
2773                         state.element.addEventListener('click', hover.cancelShow, false);
 
2774                         state.element.addEventListener('mouseout', hover.cancelShow, false);
 
2777                 create: function() {
 
2778                         var container = $('<div id="RESHoverContainer" class="RESDialogSmall"> \
 
2779                                 <h3 id="RESHoverTitle"></h3> \
 
2780                                 <div class="RESCloseButton">x</div> \
 
2781                                 <div id="RESHoverBody" class="RESDialogContents"> \
 
2784                         document.body.appendChild(container);
 
2786                         $(container).hover(function() {
 
2787                                 if (RESUtils.hover.state !== null) {
 
2788                                         RESUtils.hover.cancelHideTimer();
 
2791                                 if (RESUtils.hover.state !== null) {
 
2792                                         RESUtils.hover.cancelHideTimer();
 
2793                                         if (RESUtils.hover.state.options.closeOnMouseOut === true) {
 
2794                                                 RESUtils.hover.startHideTimer();
 
2799                         $(container).on('click', '.RESCloseButton', function() {
 
2800                                 RESUtils.hover.close(true);
 
2802                         RESUtils.hover.container = container;
 
2806                         css += '#RESHoverContainer { display: none; position: absolute; z-index: 10001; }';
 
2807                         css += '#RESHoverContainer:before { content: ""; position: absolute; top: 10px; left: -26px; border-style: solid; border-width: 10px 29px 10px 0; border-color: transparent #c7c7c7; display: block; width: 0; z-index: 1; }';
 
2808                         css += '#RESHoverContainer:after { content: ""; position: absolute; top: 10px; left: -24px; border-style: solid; border-width: 10px 29px 10px 0; border-color: transparent #f0f3fc; display: block; width: 0; z-index: 1; }';
 
2809                         css += '#RESHoverContainer.right:before { content: ""; position: absolute; top: 10px; right: -26px; left: auto; border-style: solid; border-width: 10px 0 10px 29px; border-color: transparent #c7c7c7; display: block; width: 0; z-index: 1; }';
 
2810                         css += '#RESHoverContainer.right:after { content: ""; position: absolute; top: 10px; right: -24px; left: auto; border-style: solid; border-width: 10px 0 10px 29px; border-color: transparent #f0f3fc; display: block; width: 0; z-index: 1; }';
 
2812                         RESUtils.addCSS(css);
 
2815                         var hover = RESUtils.hover;
 
2816                         var def = $.Deferred();
 
2818                                 .progress(hover.set)
 
2821                         hover.state.callback(def, hover.state.element, hover.state.context);
 
2823                 set: function(header, body) {
 
2824                         var hover = RESUtils.hover;
 
2825                         var container = hover.container;
 
2826                         if (header != null) $('#RESHoverTitle').empty().append(header);
 
2827                         if (body != null) $('#RESHoverBody').empty().append(body);
 
2829                         var XY=RESUtils.getXYpos(hover.state.element);
 
2831                         var width = $(hover.state.element).width();
 
2832                         var tooltipWidth = $(container).width();
 
2833                         tooltipWidth = hover.state.options.width;
 
2835                         RESUtils.fadeElementIn(hover.container, hover.state.options.fadeSpeed);
 
2836                         if((window.innerWidth-XY.x-width)<=tooltipWidth){
 
2837                                 // tooltip would go off right edge - reverse it.
 
2838                                 container.classList.add('right');
 
2841                                         left: XY.x - tooltipWidth - 30,
 
2845                                 container.classList.remove('right');
 
2848                                         left: XY.x + width + 25,
 
2853                 cancelShow: function(e) {
 
2854                         RESUtils.hover.close(true);
 
2856                 clearShowListeners: function() {
 
2857                         if (RESUtils.hover.state === null) return;
 
2858                         var element = RESUtils.hover.state.element;
 
2859                         var func = RESUtils.hover.cancelShow;
 
2861                         element.removeEventListener('click', func, false);
 
2862                         element.removeEventListener('mouseout', func, false);
 
2864                 cancelShowTimer: function() {
 
2865                         if (RESUtils.hover.showTimer === null) return;
 
2866                         clearTimeout(RESUtils.hover.showTimer);
 
2867                         RESUtils.hover.showTimer = null;
 
2869                 startHideTimer: function() {
 
2870                         if (RESUtils.hover.state !== null) {
 
2871                                 RESUtils.hover.hideTimer = setTimeout(function() {
 
2872                                         RESUtils.hover.cancelHideTimer();
 
2873                                         RESUtils.hover.close(true);
 
2874                                 }, RESUtils.hover.state.options.fadeDelay);
 
2877                 cancelHideTimer: function() {
 
2878                         var hover = RESUtils.hover;
 
2879                         if (RESUtils.hover.state !== null) {
 
2880                                 hover.state.element.removeEventListener('mouseout', hover.startHideTimer, false);
 
2882                         if (hover.hideTimer === null) return;
 
2883                         clearTimeout(hover.hideTimer);
 
2884                         hover.hideTimer = null;
 
2886                 close: function(fade) {
 
2887                         var hover = RESUtils.hover;
 
2888                         function afterHide() {
 
2889                                 $('#RESHoverTitle, #RESHoverBody').empty();
 
2890                                 hover.clearShowListeners();
 
2891                                 hover.cancelShowTimer();
 
2892                                 hover.cancelHideTimer();
 
2895                         if (fade && hover.state !== null) {
 
2896                                 RESUtils.fadeElementOut(hover.container, hover.state.options.fadeSpeed, afterHide);
 
2898                                 $(hover.container).hide(afterHide);
 
2905 // Create a nice alert function...
 
2910         init: function(callback) {
 
2912                 var alertCSS = '#alert_message { ' +
 
2915                         'background-color: #EFEFEF;' +
 
2916                         'border: 1px solid black;' +
 
2918                         'font-size: 10px;' +
 
2920                         'padding-left: 60px;' +
 
2921                         'padding-right: 60px;' +
 
2922                         'position: fixed!important;' +
 
2923                         'position: absolute;' +
 
2926                         'z-index: 1000000201;' +
 
2927                         'text-align: left;' +
 
2931                 '#alert_message .button {' +
 
2932                         'border: 1px solid black;' +
 
2933                         'font-weight: bold;' +
 
2934                         'font-size: 10px;' +
 
2936                         'padding-left: 7px;' +
 
2937                         'padding-right: 7px;' +
 
2939                         'background-color: #DFDFDF;' +
 
2940                         'cursor: pointer;' +
 
2942                 '#alert_message span {' +
 
2944                         'margin-bottom: 15px;   ' +
 
2946                 '#alert_message_background {' +
 
2947                         'position: fixed; top: 0; left: 0; bottom: 0; right: 0;' +
 
2948                         'background-color: #333333; z-index: 100000200;' +
 
2951                 GM_addStyle(alertCSS);
 
2953                 gdAlert.populateContainer(callback);
 
2957         populateContainer: function(callback) {
 
2958                 gdAlert.container = createElementWithID('div','alert_message');
 
2959                 gdAlert.container.appendChild(document.createElement('span'));
 
2960                 if (typeof callback === 'function') {
 
2961                         this.okButton = document.createElement('input');
 
2962                         this.okButton.setAttribute('type','button');
 
2963                         this.okButton.setAttribute('value','confirm');
 
2964                         this.okButton.addEventListener('click',callback, false);
 
2965                         this.okButton.addEventListener('click',gdAlert.close, false);
 
2966                         var closeButton = document.createElement('input');
 
2967                         closeButton.setAttribute('type','button');
 
2968                         closeButton.setAttribute('value','cancel');
 
2969                         closeButton.addEventListener('click',gdAlert.close, false);
 
2970                         gdAlert.container.appendChild(this.okButton);
 
2971                         gdAlert.container.appendChild(closeButton);
 
2973                         /* if (this.okButton) {
 
2974                                 gdAlert.container.removeChild(this.okButton);
 
2975                                 delete this.okButton;
 
2977                         var closeButton = document.createElement('input');
 
2978                         closeButton.setAttribute('type','button');
 
2979                         closeButton.setAttribute('value','ok');
 
2980                         closeButton.addEventListener('click',gdAlert.close, false);
 
2981                         gdAlert.container.appendChild(closeButton);
 
2983                 var br = document.createElement('br');
 
2984                 br.setAttribute('style','clear: both');
 
2985                 gdAlert.container.appendChild(br);
 
2986                 document.body.appendChild(gdAlert.container);
 
2988         open: function(text, callback) {
 
2989                 if (gdAlert.isOpen) {
 
2992                 gdAlert.isOpen = true;
 
2993                 gdAlert.populateContainer(callback);
 
2996                 // gdAlert.container.getElementsByTagName("SPAN")[0].innerHTML = text;
 
2997                 $(gdAlert.container.getElementsByTagName("SPAN")[0]).html(text);
 
2998                 gdAlert.container.getElementsByTagName("INPUT")[0].focus();
 
2999                 gdAlert.container.getElementsByTagName("INPUT")[0].focus();
 
3001                 //create site overlay
 
3002                 gdAlert.overlay = createElementWithID("div", "alert_message_background");
 
3003                 document.body.appendChild(gdAlert.overlay);
 
3005                 // center messagebox (requires prototype functions we don't have, so we'll redefine...)
 
3006                 // var arrayPageScroll = document.viewport.getScrollOffsets();
 
3007                 // var winH = arrayPageScroll[1] + (document.viewport.getHeight());
 
3008                 // var lightboxLeft = arrayPageScroll[0];
 
3009                 var arrayPageScroll = [ document.documentElement.scrollLeft , document.documentElement.scrollTop ];
 
3010                 var winH = arrayPageScroll[1] + (window.innerHeight);
 
3011                 var lightboxLeft = arrayPageScroll[0];
 
3013                 gdAlert.container.style.top = ((winH / 2) - 90) + "px";
 
3014                 gdAlert.container.style.left = ((gdAlert.getPageSize()[0] / 2) - 155) + "px";
 
3017                 new Effect.Appear(gdAlert.container, {duration: 0.2});
 
3018                 new Effect.Opacity(gdAlert.overlay, {duration: 0.2, to: 0.8});
 
3020                 RESUtils.fadeElementIn(gdAlert.container, 0.3);
 
3021                 RESUtils.fadeElementIn(gdAlert.overlay, 0.3);
 
3022                 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'gdAlert');
 
3026                 gdAlert.isOpen = false;
 
3028                 new Effect.Fade(gdAlert.container, {duration: 0.3});
 
3029                 new Effect.Fade(gdAlert.overlay, {duration: 0.3, afterFinish: function() {
 
3030                         document.body.removeChild(gdAlert.overlay);
 
3033                 RESUtils.fadeElementOut(gdAlert.container, 0.3);
 
3034                 RESUtils.fadeElementOut(gdAlert.overlay, 0.3);
 
3035                 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'gdAlert');
 
3038         getPageSize: function() {
 
3039                 var xScroll, yScroll;
 
3040                 if (window.innerHeight && window.scrollMaxY) {
 
3041                         xScroll = window.innerWidth + window.scrollMaxX;
 
3042                         yScroll = window.innerHeight + window.scrollMaxY;
 
3043                 } else if (document.body.scrollHeight > document.body.offsetHeight){ // all but Explorer Mac
 
3044                         xScroll = document.body.scrollWidth;
 
3045                         yScroll = document.body.scrollHeight;
 
3046                 } else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari
 
3047                         xScroll = document.body.offsetWidth;
 
3048                         yScroll = document.body.offsetHeight;
 
3051                 var windowWidth, windowHeight;
 
3053                 if (self.innerHeight) { // all except Explorer
 
3054                         if(document.documentElement.clientWidth){
 
3055                                 windowWidth = document.documentElement.clientWidth;
 
3057                                 windowWidth = self.innerWidth;
 
3059                         windowHeight = self.innerHeight;
 
3060                 } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode
 
3061                         windowWidth = document.documentElement.clientWidth;
 
3062                         windowHeight = document.documentElement.clientHeight;
 
3063                 } else if (document.body) { // other Explorers
 
3064                         windowWidth = document.body.clientWidth;
 
3065                         windowHeight = document.body.clientHeight;
 
3068                 // for small pages with total height less then height of the viewport
 
3069                 if(yScroll < windowHeight){
 
3070                         pageHeight = windowHeight;
 
3072                         pageHeight = yScroll;
 
3075                 // for small pages with total width less then width of the viewport
 
3076                 if(xScroll < windowWidth){
 
3077                         pageWidth = xScroll;
 
3079                         pageWidth = windowWidth;
 
3081                 return [pageWidth,pageHeight];
 
3085 //overwrite the alert function
 
3086 var alert = function(text, callback) {
 
3087         if (gdAlert.container === false) {
 
3088                 gdAlert.init(callback);
 
3090         gdAlert.open(text, callback);
 
3093 // this function copies localStorage (from the GM import script) to FF addon simplestorage...
 
3094 function GMSVtoFFSS() {
 
3095         var console = unsafeWindow.console;
 
3096         for (var key in localStorage) {
 
3097                 RESStorage.setItem(key, localStorage[key]);
 
3099         localStorage.setItem('copyComplete','true');
 
3100         localStorage.removeItem('RES.lsTest');
 
3101         RESUtils.notification('Data transfer complete. You may now uninstall the Greasemonkey script');
 
3104 // jquery plugin CSS
 
3105 RESUtils.addCSS(tokenizeCSS);
 
3106 RESUtils.addCSS(guidersCSS);
 
3108 // define the RESConsole class
 
3111         RESConsoleContainer: '',
 
3113         RESConfigPanelOptions: null,
 
3114         // make the modules panel accessible to this class for updating (i.e. when preferences change, so we can redraw it)
 
3115         RESConsoleConfigPanel: createElementWithID('div', 'RESConsoleConfigPanel', 'RESPanel'),
 
3116         RESConsoleAboutPanel: createElementWithID('div', 'RESConsoleAboutPanel', 'RESPanel'),
 
3117         RESConsoleProPanel: createElementWithID('div', 'RESConsoleProPanel', 'RESPanel'),
 
3118         addConsoleLink: function() {
 
3119                 this.userMenu = document.querySelector('#header-bottom-right');
 
3120                 if (this.userMenu) {
 
3121                         var RESPrefsLink = $("<span id='openRESPrefs'><span id='RESSettingsButton' title='RES Settings' class='gearIcon'></span>")
 
3122                                                                         .mouseenter(RESConsole.showPrefsDropdown);
 
3123                         $(this.userMenu).find("ul").after(RESPrefsLink).after("<span class='separator'>|</span>");
 
3124                         this.RESPrefsLink = RESPrefsLink[0];
 
3127         addConsoleDropdown: function() {
 
3128                 this.gearOverlay = createElementWithID('div','RESMainGearOverlay');
 
3129                 this.gearOverlay.setAttribute('class','RESGearOverlay');
 
3130                 $(this.gearOverlay).html('<div class="gearIcon"></div>');
 
3132                 this.prefsDropdown = createElementWithID('div','RESPrefsDropdown','RESDropdownList');
 
3133                 $(this.prefsDropdown).html('<ul id="RESDropdownOptions"><li id="SettingsConsole">settings console</li><li id="RES-donate">donate to RES</li></ul>');
 
3134                 var thisSettingsButton = this.prefsDropdown.querySelector('#SettingsConsole');
 
3135                 this.settingsButton = thisSettingsButton;
 
3136                 thisSettingsButton.addEventListener('click', function() {
 
3137                         RESConsole.hidePrefsDropdown();
 
3140                 var thisDonateButton = this.prefsDropdown.querySelector('#RES-donate');
 
3141                 thisDonateButton.addEventListener('click', function() {
 
3142                         RESUtils.openLinkInNewTab('http://redditenhancementsuite.com/contribute.html', true);
 
3144                 $(this.prefsDropdown).mouseleave(function() {
 
3145                         RESConsole.hidePrefsDropdown();
 
3147                 $(this.prefsDropdown).mouseenter(function() {
 
3148                         clearTimeout(RESConsole.prefsTimer);
 
3150                 $(this.gearOverlay).mouseleave(function() {
 
3151                         RESConsole.prefsTimer = setTimeout(function() {
 
3152                                 RESConsole.hidePrefsDropdown();
 
3155                 document.body.appendChild(this.gearOverlay);
 
3156                 document.body.appendChild(this.prefsDropdown);
 
3157                 if (RESStorage.getItem('RES.newAnnouncement','true')) {
 
3158                         RESUtils.setNewNotification();
 
3161         showPrefsDropdown: function(e) {
 
3162                 var thisTop = parseInt($(RESConsole.userMenu).offset().top + 1, 10);
 
3163                 // var thisRight = parseInt($(window).width() - $(RESConsole.RESPrefsLink).offset().left);
 
3164                 // thisRight = 175-thisRight;
 
3165                 var thisLeft = parseInt($(RESConsole.RESPrefsLink).offset().left - 6, 10);
 
3166                 // $('#RESMainGearOverlay').css('left',thisRight+'px');
 
3167                 $('#RESMainGearOverlay').css('height',$('#header-bottom-right').outerHeight()+'px');
 
3168                 $('#RESMainGearOverlay').css('left',thisLeft+'px');
 
3169                 $('#RESMainGearOverlay').css('top',thisTop+'px');
 
3170                 RESConsole.prefsDropdown.style.top = parseInt(thisTop+$(RESConsole.userMenu).outerHeight(), 10)+'px';
 
3171                 RESConsole.prefsDropdown.style.right = '0px';
 
3172                 RESConsole.prefsDropdown.style.display = 'block';
 
3173                 $('#RESMainGearOverlay').show();
 
3174                 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'prefsDropdown');
 
3176         hidePrefsDropdown: function(e) {
 
3177                 RESConsole.RESPrefsLink.classList.remove('open');
 
3178                 $('#RESMainGearOverlay').hide();
 
3179                 RESConsole.prefsDropdown.style.display = 'none';
 
3180                 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'prefsDropdown');
 
3182         resetModulePrefs: function() {
 
3185                         'betteReddit': true,
 
3186                         'singleClick': true,
 
3187                         'subRedditTagger': true,
 
3188                         'uppersAndDowners': true,
 
3189                         'keyboardNav': true,
 
3190                         'commentPreview': true,
 
3193                         'usernameHider': false,
 
3194                         'accountSwitcher': true,
 
3195                         'styleTweaks': true,
 
3196                         'filteReddit': true,
 
3197                         'spamButton': false,
 
3198                         'bitcointip': false,
 
3201                 this.setModulePrefs(prefs);
 
3204         getAllModulePrefs: function(force) {
 
3206                 // if we've done this before, just return the cached version
 
3207                 if ((!force) && (typeof this.getAllModulePrefsCached !== 'undefined')) return this.getAllModulePrefsCached;
 
3208                 // get the stored preferences out first.
 
3209                 if (RESStorage.getItem('RES.modulePrefs') !== null) {
 
3210                         storedPrefs = safeJSON.parse(RESStorage.getItem('RES.modulePrefs'), 'RES.modulePrefs');
 
3211                 } else if (RESStorage.getItem('modulePrefs') !== null) {
 
3212                         // Clean up old moduleprefs.
 
3213                         storedPrefs = safeJSON.parse(RESStorage.getItem('modulePrefs'), 'modulePrefs');
 
3214                         RESStorage.removeItem('modulePrefs');
 
3215                         this.setModulePrefs(storedPrefs);
 
3217                         // looks like this is the first time RES has been run - set prefs to defaults...
 
3218                         storedPrefs = this.resetModulePrefs();
 
3220                 if (storedPrefs === null) {
 
3223                 // create a new JSON object that we'll use to return all preferences. This is just in case we add a module, and there's no pref stored for it.
 
3225                 // for any stored prefs, drop them in our prefs JSON object.
 
3226                 for (var module in modules) {
 
3227                         if (storedPrefs[module]) {
 
3228                                 prefs[module] = storedPrefs[module];
 
3229                         } else if ((! modules[module].disabledByDefault) && ((storedPrefs[module] == null) || (module === 'dashboard'))) {
 
3230                                 // looks like a new module, or no preferences. We'll default it to on.
 
3231                                 // we also default dashboard to on. It's not really supposed to be disabled.
 
3232                                 prefs[module] = true;
 
3234                                 prefs[module] = false;
 
3237                 if ((typeof prefs !== 'undefined') && (prefs !== 'undefined') && (prefs)) {
 
3238                         this.getAllModulePrefsCached = prefs;
 
3242         getModulePrefs: function(moduleID) {
 
3244                         var prefs = this.getAllModulePrefs();
 
3245                         return prefs[moduleID];
 
3247                         alert('no module name specified for getModulePrefs');
 
3250         setModulePrefs: function(prefs) {
 
3251                 if (prefs !== null) {
 
3252                         RESStorage.setItem('RES.modulePrefs', JSON.stringify(prefs));
 
3255                         alert('error - no prefs specified');
 
3258         create: function() {
 
3259                 // create the console container
 
3260                 this.RESConsoleContainer = createElementWithID('div', 'RESConsole');
 
3261                 // hide it by default...
 
3262                 // this.RESConsoleContainer.style.display = 'none';
 
3263                 // create a modal overlay
 
3264                 this.modalOverlay = createElementWithID('div', 'modalOverlay');
 
3265                 this.modalOverlay.addEventListener('click', function(e) {
 
3269                 document.body.appendChild(this.modalOverlay);
 
3270                 // create the header
 
3271                 var RESConsoleHeader = createElementWithID('div', 'RESConsoleHeader');
 
3272                 // create the top bar and place it in the header
 
3273                 var RESConsoleTopBar = createElementWithID('div', 'RESConsoleTopBar');
 
3274                 this.logo = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAAeCAMAAABHRo19AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACxFBMVEXw8/wAAAD+//8EBAQSEhIPDw/w8/v+/v4JCQkHBwcCAgKSk5W8vLz9SADz8/MtLS0iIiIcHBz/VAAYGBmRkZFkZGUkJCQVFhZiYmOZmp2QkpfQ09r9/f3n6vA5OTkvLy//TAAxMTEUFRTl5eVqa2zu8fnt7/fV19ydnqCen6Lt8Pj/TwDk5ORaWlrg4ug1NTUpKSrX19cgICDp6/J6enrFxcW1trpDQ0M7OzwnJyenp6f6TQAXFxj/WACFhojr6+uNjpBHR0cfHx+vr7GSkpJMTEwYGBg+Pj5cXF3CwsJISEj29vYQEBDe3t7+SwBmZmixsbH19fXo6OhQUFAgICJgYWXHyM3q7PTs7vW3uLvb3eKqq650dXbS09js7/aTlJY5OjmUlJeenp7r7vWWl5n8/Px4eHihoqWEhYfO0NTj5euDg4Pa3OGRkpTJy8/g4ODe4Obc3Nzv8vqjo6O1tbW3uLyrq6t1dXX5ya5/f3/5xqxZWVqKiopra2v4uJb99vLCw8fFxsouLS6Oj5Hs7OzY2t+jpKZ4eXv2tY8NDQ35WQny8vJkZGT2lWGQkJB8fHzi5OrLzNFAQUPm6O/3f0W7u7v3oXP4dTb2nXH62MX3pHb87+bn5+dWV1dvb3E0NDT4lWP3jFP4vJn2cS79+vaJioxNTU376d72f0H4Wwf2fT7759z9+fX1lmH4XAv2bSb40bheX2A6Ojr9+vj76t/9+vf76+H5XxVGRkZxcnPQ0te+vr52dnaztLfExMT2tZFYWFhSUlLV1dVwcXL52MS4uLiysrKam5rW1tZPT1CVlZWYmJiUlJRHR0ipqq0qKiqzs7P39/fq6urj4+P89fH09PT6+vo4ODjq7PNsbW4oKCh0dHTv7++3t7fk5u2IiYtFRUU3NzdPT0/Kysru7u6NjY1tbW1gYGBfX19sbGyHh4fh4eEzPXfuAAACPElEQVR4Xq3SQ9fkQBTH4bpVSdru17Zt28bYtm3btm3btm37S8yk0oteTKc7c+a3uf/Nc3JyEvT/48KF69Uhu7dk3AfaZ48PRiHgUwLdpGLdtFbecrkPOxvjuSRcmp2vaIsQt6gdLME4UtlGGs6NFW7+GIw7Qidp2BAq3KaQWg650mwC9LSs6JpRfZG03PTo32reMrmzIW3IlGaSZY/W+aCcoY/xq1SCKXAC5xAaGObkFoSmZoK3uaxqlgzL6vol3UohjIpDLWq6J4jaaNZUnsb4syMCsHU5o10q4015sZAshp2LuuCu4DSZFzJrrh0GURj3Ai8BNHrQ08TdyvZXDsDzYBD+W4OJK5bFh9nGIaRuKKTTxw5fOtJTUCtWjh3H31NQiCdOso2DiVlXSsXGDN+M6XRdnlmtmUNXYrGaLPhD3IFvoQfQrH4KkMdRsjgiK2IZXcurs4zHVvFrdSasQTaeTFu7DtPWa4yaDXSd0xh9N22mMyUVieItWwW8bfuOnbvo2r1n7779mOZ6QByHHsRChw4fsXwsz6OPsdDxE0i0kyQA20rLFIhjzuW0TVxIgpB4Z+AsBRXn1RZTdeEivXFyFbLXJTaJvmkDNJgLrly95iR3juTt9eIbyH6ucJPq2hJGQQiru63lbbriDocc6C7cu1/BgwcPH9U/4cdT9TNQIcd6/oK8fFWbg4Vev0n0I6VvkcO9A38Fq495X5T3wZkhLvAROZ6KYT59Lvvy9VvU9x8/1fW/DEygHfEbNdeCkgdk4HMAAAAASUVORK5CYII=';
 
3275                 // this string is split because a specific sequence of characters screws up some git clients into thinking this file is binary.
 
3276                 this.loader = 'data:image/gif;base64,R0lGODlhHQAWANUAAESatESetEyetEyitEyivFSivFSmvFymvFyqvGSqvGSqxGSuxGyuxGyyxHSyxHS2xHS2zHy2zHy6zIS6zIS+zIy+zIzCzIzC1JTG1JzK1JzK3JzO3KTO3KTS3KzS3KzW3LTW3LTW5LTa5Lza5Lze5MTe5MTi5MTi7Mzi7Mzm7NTm7NTq7Nzq7Nzq9Nzu9OTu9OTy9Ozy9Oz29Oz2/PT2/PT6/Pz6/Pz+/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH';
 
3277                 this.loader += '/C05FVFNDQVBFMi4wAwEAAAAh/h1CdWlsdCB3aXRoIEdJRiBNb3ZpZSBHZWFyIDQuMAAh+QQIBgAAACwAAAAAHQAWAAAG/sCbcEgs3myyEIzjQr2MUGjrgpFMrJIMhxTtei4SbPhKwXCeXaLren00GIuHlSLxzNJDD4NOWST8CwsUgxEjeEIcDYN0ICkjFA4UFYMcRXckIS8XKysTCJKSGCMkHBUXpwwXRC8UGheLpgsMDBKmF6YWF7kODYY3LmawoKcXCxIKFMSnkBIELDczIxODk2SmpoMFbg8XDg4SAAoTNTUY1BcTDQsKCw2nGGAMBAUJDQcCDZ8yNzESya8NFDCAEFAChoO6GGSowEDDggsq0HhIZisVixkwQFDBkIHCARQ1XICosSIGEYe5MFjAsE8IigwcYWa402VEyoNmRozgkEFDbs8MBRS0jJJCwAOcMn1u4MBTA4UHNdLIgIAOg08NGphqZWAggohDHBIEqMCRqZYMEjZMMPBgaJcYcDAcQMBhwgMOGOg9AOHrUIkQ8hJQQKDgQaQFEQ4ZuRABxSwREtqWcKHYiIwaWm6UGBG18o0gACH5BAgGAAAALAAAAAAdABYAAAb';
 
3278                 this.loader += '+wJtwSCwKXabWBjaS2YxQowqDkUysEg4GFe1+LtgrVkKddYsvCRbSYCwcEgpl4jGfhR3GnLJILP4JchQQJXdCHhCCEiApIxUNFZESGkUzNCsaMBwjMRQFE3IVGCMkHBYXFBcQGEM1NhRUexWqCRAQsxcWuBcXEQgkQjEXGYIUFanIDxENEry5F48SByo3MCWCx1fGzlcHCxKQEggUAgYWrqjGcg0LCguQuVUNBwUJbgIKDBFmMKi4DfnYKCBDhUqDCRgWYFDmAoYQDs2cMcCwYkaMEBYKUjiAAsaMDzFgxCDiocEpDBcwjBSSIkMGDRkwWHDYJUSqghg2jBjB4eVzSwwKINA4Y0JAhIIuYcLkoKFnAwc1zsyYYCFC0pccsmZNcNCDoQ4FCmAQ1TPr2A4JClCIeufFggcUAkDg8ECCBwkF4F4YYYhlCAQFHEwwwECCAwcINDzpK2QGBQ4gFEwAsSDDDA4vGBOxUaMfFw5cNN8IAgAh+QQIBgAAACwAAAAAHQAWAAAG/sCbcEgsClcqlAc2qtWMUCOKc5FYrZyK6xmFhizWiURMxmBm3SIMMp48GoyFQ0Kpc9BpIcchpiz+';
 
3279                 this.loader += 'gHUUESd5Qh4QghIhKCMUDhQVFBIYRTMvMxgtIxw1GAJ0khkiJRwUF6gRGUNOGRUYghQYEQgSEBcWFBa7uGAEIUI1p7GSFRUXg3MRqKgWFwoRCSs3LiPIkhRkyKgSDggFj3UHEwcEFk8ZoXUNCn8OqBjIDQj0Cg0CCA8PMTctsMcX4jBwwI6SGQsZAnJYcKrBCn43ODxgFvBCixkwvpjJQIGBChU3RqioAVFIiAjOMFjAIGNICgwZNGTA4ABGmhATzZjhMIJTacyYNClwiVLCgKyNP2VyWIqhgIOhUGQkwyBT6VIOGRSA4WCIg4AGHDNgZYrBawEMUKO0aCCBAYALGRiUZVCLwoMRhoS80IDgQIQGBuY0SJDgRMm8MCiguJAgZgIUL23mlcLyBQbJk28EAQAh+QQIBgAAACwAAAAAHQAWAAAG/sCbcEgsClWwEElFstWMUGPpM5FUJxTMBUaLRkcUq2QsplwwXS8R5hBDGoxFm0LXyNRDj4OCXSQWgAl0FBEpeEIce3QSISlgDhUUFRAXRTQqNRwlKhgzGgUQgxkjJRxmFxcTHEMzLyRmgxQaFIIQFReRqBcWFxIDH0MYsZKSu2MMhLoWtwzNKjctHsJ0FWPFqBMLCAIXDxEXBw4MARhPHhKSkXCADbdnFA4KfggNBaASMDecxBcN8g7+JGAYiArEggwOHHRogOLODQ8NdF1YgKHFjCRnBlqQ0MKEjRRN8g0JcWoghhhDUmTIoCEDBQUio3hQYMEkhg0jRnBgyTMLcEovJhbUHLiypQYNOzlIABDhiZcYLx/wbMmh6k4IGbAe0jBgQi+kGapi4FABAAIOP9WsiCDBnksHHDAceEABAgMTh4TMqIBggYQDCCREWHBgAYxneYW0wPCiwQIQEh686FAusREQHmyE4FDDhuUbQQAAIfkECAYAAAAsAAAAAB0AFgAABv7Am3BILN5sqhlHVUrVaMaosSSSUCTYygUTm0mlKKxkIiZTKJrat/hqkCcPhrxhpVQw3rXwA6FMKAoLgoJnVyl6QhwMhRIfKCQUDhV2EBdFNSc0IhwvGiocCH12GSMlHBQXqRIcQzMoKhMWhRQZFwwSERd2uhcWvRQFHkMef4UVkxcVVgtXqRYYWg4HDSs3LRgYs2apvRMGCgJjDxcKoQIYNjcjEWe6DQyBDVpbFg8JDAsGDAcCDxQuN1DwSgVvwYMGCiRgyyYBxQILExR8iBBCzY0QDXz5YoChxQwYIZ5hyAANRokYLkQ8IfJhHoZnMYagyEBTA4QDMNZwMCAS23aGESM6ZNAwlGaFPGByLaRZMwMHDRwaBKCQ7osMCQUk1NQAlYPXlxoUaECE4QCGCKuccqDpwUEABh5eIFoRKUCCqBKIJbgg4V4LREJmPFAQ4UGBRQ0QIJjgggTgISpGmFDwwAODCy0mbHhshIaHQxdG3KhRFXAQACH5BAgGAAAALAAAAAAdABYAAAb+wJtwSCzeaiwYxwVyxWrGqBEVklAkksmFspxJpalHdoydZDu0b7HlME8ejAVDTKFULlC1MAShTCgLCguDC3V+J182QxmFdRIeKSMUDnYUEBhGJy4rGDAeJRwMlHYZI6B3FxcPHUM0ISwVlXUYGA0QWhRbFhe7FhUIHkI1JVaGsbEXERILf6mpuxEDDCs3LncWdRVYuc4WBgsCDxUNFA8CEAUXNzYnVrEUDXEKDXcYFxURB3IICgoCDRhY3EDRLFUDQRAOSqCFAV4KZRgQcMDAYQiJB7xSMcCwggaMEBVoZaAlA0XHEDBqKBLSAZU9DDGGoNCAIYMGBwdiftFQwAJ1Q4ojRnDIYLOoBC9fVORiOFKDTQ0coi44oE7NjAYCKBB1CnVD1JoVDlTUcwEgAy4Zog7lcMDAQhd6qmFIAEBCBgUWODhokKHBgQY648Jg0CCCvwgUEhxIwCFoXCIqXGRIUFOBBxINSDyO4mnGCgoubMDYLCQIACH5BAgGAAAALAAAAAAdABYAAAb+wJtwSCzeaq+W59WZuWrGqFHFkVAkkolFMkrRpFIUZJLFlsmiGLi4gmApjwaD0ZhQ7hfbejhyUOwLCQuDC3d3JWB6QhoIhhEgKCMUfhUVEBlGKCcwFyonHhwOEHcVGCMkHBUXFxUNHEM1HigZFBWGpRENFKsXFr2/FA0hQjAtdoa1uxcSDwyjqr4XfwIKLDcxyYZktau+CgkGDRcPERQBDo1HJ8fSDQsKCw2qGNIQBQsMCQcMAggaLTdQlOPFQIGzBgokYFhIYQGIDA0yFAqR4csNExC6XWBwgcUMGCFKLVwYo0WJGiVW2FB0Q4OWVQtlDJmFQUOGCAlgrOFw4MJ9SAwcRozokEGDhg0cLDiYsWbFlpEZMBQtyoFDBgYOLkABM+NAAQsZpmqoWjUDhwYFPuy5sYwCgppmrVot8EBCBRdrX2AoIADDhAVhGZQ6YEDC1rUrGEwyUIBChAUIFpAwtZaIixkQHEpYUOKqC5aVh7AoYcNDhRozXoQWEgQAIfkECAYAAAAsAAAAAB0AFgAABv7Am3BILN5ostNo5ZmtbMaosZWhUCQTSUVSItWk0hIES5aQJ6UXuLgyZyONBcMhsVIw37VwBJlYFwmACwt2FCNgUEIZCFZZICkjFA4UFRQRG0YuITIaIi0eGBARdhohJRwXqRcLGUQeIRx+dn4SCxWptxYXt1sRIUIuK5V2FZWpEw0OCxYUqbpWBgYsR8NWW3W4FxYOCIMWEg4XAggMFDY1IpW3FHEKCw23GBeSAgoNDAINBQcbLTcqD5rNY6CAAQSCEjAopMAAg4cFGBw0QJFhhpATE1StwrBiRgwQdzBkwEABBo0QNFacKILhgSqFMYak0JAhg4YIEGKC8cDggnZChRxGjOBQk6aGWjLWrKDw4OdIoxqIcnBgwUIeKTEMKFBo0yaHr0Q1GCBwSA9JBwe6fs3AwcKBC+Bc6LkRg0IBBBrmcGDHoYKAtDrnomhwAd8yBggUPAjxoMRcIjFgJJAAYgEEE2NqWHzMpkWNCx5usFDD+UYQACH5BAgGAAAALAAAAAAdABYAAAb+wJtwSCzeajWRqjSKqYxQ6OuCkVgnFMlpVItGR1fJxCrJUkYvb3EliYwfjLijPN501cKQw7zo+ymAEyJqNkIaCYBZICgjFHsVFRIcRjQcMCEbMSESD1gVFBkiJRwWFxQXCxhEIRkeiaeOEgqnFRcVpbUXViBCLSUYr5+fpgsQCqYXyaYUCQQsR8CAn2MUuRcWEgcOC4ALFgcEDBI2NRymtRQNfg25GBMNAQgMDQUJCAUZaS4OFsMMfQ4aKJCAoaAFCBJGLPiEoIQHGEJInFKWqsUMTRQKZrjg4IUNES1klCiCgYGygjGGoMigIUOGahC9bLJQsOCGESM6tGSpYYFwgRlqUgSs6ZKlSw4tQU24EyXGAQgYXGpoqYGDVXMCDozEA+yAggwYrlqV0CBDgwZp8MyQUOABBgMUODiI0MGBgAQhVuAZUqKaAgEQKCBI0CAjA717h9QogaBqggshEnCwkTYxkRU0VkxQYcNETMtBAAAh+QQIBgAAACwAAAAAHQAWAAAG/sCbcEgs3mo0kAuEaq2MUOiLgpFYKZLLaBTthrATSViMrYRe3WILLHk0GAuHhILt1NLDDyNMWSgWCQsLFBNYXHg3HIN0EiApIxQOFBWEHEU1Nh4oKRgvJREMk5MYIyUclBcXCxdEKBcedIUXFAwPCpOpFhSpqQ8Qhy0dHHR0lKgXChIIu7kYWA4DLUcchaJ8vLoUBhELEhYMEg0A4DY1GbMVsw2CCg3pGFUMAgftBgcLBxcyNzEQzBQNFDBwEFACPAwXJjTwEOEBhgQeSMAQIoKChXQXGGBYMSOGiAoHLSxQcePECRsoZhDBoCAVQgwxhqDAoCGDBngqu0A6CI/DdJYONoMaKLCvS4oDDQ5moGlzA4cNSzNEuNNFhoIKFjAE1eCUg9cIARaUQMTBgQAIN716lZr1gIOJeGY0yBehgFaNHBAMYEBiLKIbJDg8KGBgwgMECRxUgNAg5l8hNjQwgAQRw4IUMKQ9JuLiRsUaMEYUfRwEADs=';
 
3280                 RESConsoleTopBar.setAttribute('class','RESDialogTopBar');
 
3281                 $(RESConsoleTopBar).html('<img id="RESLogo" src="'+this.logo+'"><h1>reddit enhancement suite</h1>');
 
3282                 RESConsoleHeader.appendChild(RESConsoleTopBar);
 
3283                 this.RESConsoleVersion = createElementWithID('div','RESConsoleVersion');
 
3284                 $(this.RESConsoleVersion).text('v' + RESVersion);
 
3285                 RESConsoleTopBar.appendChild(this.RESConsoleVersion);
 
3287                 // Create the search bar and place it in the top bar
 
3288                 var RESSearchContainer = modules['settingsNavigation'].renderSearchForm();
 
3289                 RESConsoleTopBar.appendChild(RESSearchContainer);
 
3291                 var RESSubredditLink = createElementWithID('a','RESConsoleSubredditLink');
 
3292                 $(RESSubredditLink).text('/r/Enhancement');
 
3293                 RESSubredditLink.setAttribute('href','http://reddit.com/r/Enhancement');
 
3294                 RESSubredditLink.setAttribute('alt','The RES Subreddit');
 
3295                 RESConsoleTopBar.appendChild(RESSubredditLink);
 
3296                 // create the close button and place it in the header
 
3297                 var RESClose = createElementWithID('span', 'RESClose', 'RESCloseButton');
 
3298                 $(RESClose).text('×');
 
3299                 RESClose.addEventListener('click', function(e) {
 
3303                 RESConsoleTopBar.appendChild(RESClose);
 
3304                 this.categories = [];
 
3305                 for (var module in modules) {
 
3306                         if ((typeof modules[module].category !== 'undefined') && (this.categories.indexOf(modules[module].category) === -1)) {
 
3307                                 this.categories.push(modules[module].category);
 
3310                 this.categories.sort();
 
3312                 // var menuItems = this.categories.concat(['RES Pro','About RES'));
 
3313                 var menuItems = this.categories.concat(['About RES']);
 
3314                 var RESMenu = createElementWithID('ul', 'RESMenu');
 
3315                 for (var item = 0; item < menuItems.length; item++) {
 
3316                         var thisMenuItem = document.createElement('li');
 
3317                         $(thisMenuItem).text(menuItems[item]);
 
3318                         thisMenuItem.setAttribute('id', 'Menu-' + menuItems[item]);
 
3319                         thisMenuItem.addEventListener('click', function(e) {
 
3321                                 RESConsole.menuClick(this);
 
3323                         RESMenu.appendChild(thisMenuItem);
 
3325                 RESConsoleHeader.appendChild(RESMenu);
 
3326                 this.RESConsoleContainer.appendChild(RESConsoleHeader);
 
3327                 // Store the menu items in a global variable for easy access by the menu selector function.
 
3328                 RESConsole.RESMenuItems = RESMenu.querySelectorAll('li');
 
3329                 // Create a container for each management panel
 
3330                 this.RESConsoleContent = createElementWithID('div', 'RESConsoleContent');
 
3331                 this.RESConsoleContainer.appendChild(this.RESConsoleContent);
 
3332                 // Okay, the console is done. Add it to the document body.
 
3333                 document.body.appendChild(this.RESConsoleContainer);
 
3335                 window.addEventListener("keydown", function(e) {
 
3336                         if ((RESConsole.captureKey) && (e.keyCode !== 16) && (e.keyCode !== 17) && (e.keyCode !== 18)) {
 
3337                                 // capture the key, display something nice for it, and then close the popup...
 
3339                                 var keyArray = [e.keyCode, e.altKey, e.ctrlKey, e.shiftKey, e.metaKey];
 
3340                                 document.getElementById(RESConsole.captureKeyID).value = keyArray.join(",");
 
3341                                 document.getElementById(RESConsole.captureKeyID+'-display').value = RESUtils.niceKeyCode(keyArray);
 
3342                                 RESConsole.keyCodeModal.style.display = 'none';
 
3343                                 RESConsole.captureKey = false;
 
3347                 $("#RESConsoleContent").delegate(".keycode + input[type=text][displayonly]", {
 
3348                         focus: function(e) {
 
3349                                 var thisXY=RESUtils.getXYpos(this, true);
 
3350                                 // show dialog box to grab keycode, but display something nice...
 
3351                                 $(RESConsole.keyCodeModal).css({
 
3353                                         top: RESUtils.mouseY + "px",
 
3354                                         left: RESUtils.mouseX + "px;"
 
3356                                 // RESConsole.keyCodeModal.style.display = 'block';
 
3357                                 RESConsole.captureKey = true;
 
3358                                 RESConsole.captureKeyID = this.getAttribute('capturefor');
 
3361                                 $(RESConsole.keyCodeModal).css("display", "none");
 
3365                 this.keyCodeModal = createElementWithID('div', 'keyCodeModal');
 
3366                 $(this.keyCodeModal).text('Press a key (or combination with shift, alt and/or ctrl) to assign this action.');
 
3367                 document.body.appendChild(this.keyCodeModal);
 
3369         drawConfigPanel: function(category) {
 
3370                 if (!category) return;
 
3372                 this.drawConfigPanelCategory(category);
 
3374         getModuleIDsByCategory: function(category) {
 
3375                 var moduleList = [];
 
3376                 for (var i in modules) {
 
3377                         if (modules[i].category == category && !modules[i].hidden) moduleList.push(i);
 
3379                 moduleList.sort(function(a,b) {
 
3380                         if (modules[a].moduleName.toLowerCase() > modules[b].moduleName.toLowerCase()) return 1;
 
3386         drawConfigPanelCategory: function(category, moduleList) {
 
3387                 $(this.RESConsoleConfigPanel).empty();
 
3390                 var moduleTest = RESStorage.getItem('moduleTest');
 
3392                         console.log(moduleTest);
 
3393                         // TEST loading stored modules...
 
3394                         var evalTest = eval(moduleTest);
 
3397                 moduleList = moduleList || this.getModuleIDsByCategory(category);
 
3399                 this.RESConfigPanelModulesPane = createElementWithID('div', 'RESConfigPanelModulesPane');
 
3400                 for (var i=0, len=moduleList.length; i<len; i++) {
 
3401                         var thisModuleButton = createElementWithID('div', 'module-'+moduleList[i]);
 
3402                         thisModuleButton.classList.add('moduleButton');
 
3403                         var thisModule = moduleList[i];
 
3404                         $(thisModuleButton).text(modules[thisModule].moduleName);
 
3405                         if (modules[thisModule].isEnabled()) {
 
3406                                 thisModuleButton.classList.add('enabled');
 
3408                         thisModuleButton.setAttribute('moduleID', modules[thisModule].moduleID);
 
3409                         thisModuleButton.addEventListener('click', function(e) {
 
3410                                 RESConsole.showConfigOptions(this.getAttribute('moduleID'));
 
3412                         this.RESConfigPanelModulesPane.appendChild(thisModuleButton);
 
3414                 this.RESConsoleConfigPanel.appendChild(this.RESConfigPanelModulesPane);
 
3416                 this.RESConfigPanelOptions = createElementWithID('div', 'RESConfigPanelOptions');
 
3417                 $(this.RESConfigPanelOptions).html('<h1>RES Module Configuration</h1> Select a module from the column at the left to enable or disable it, and configure its various options.');
 
3418                 this.RESConsoleConfigPanel.appendChild(this.RESConfigPanelOptions);
 
3419                 this.RESConsoleContent.appendChild(this.RESConsoleConfigPanel);
 
3421         updateSelectedModule: function (moduleID) {
 
3422                 var moduleButtons = $(RESConsole.RESConsoleConfigPanel).find('.moduleButton');
 
3423                 moduleButtons.removeClass('active');
 
3424                 moduleButtons.filter(function() { return this.getAttribute('moduleID') === moduleID; })
 
3425                         .addClass('active');
 
3427         drawOptionInput: function(moduleID, optionName, optionObject, isTable) {
 
3428                 var thisOptionFormEle;
 
3429                 switch(optionObject.type) {
 
3432                                 thisOptionFormEle = createElementWithID('textarea', optionName);
 
3433                                 thisOptionFormEle.setAttribute('type','textarea');
 
3434                                 thisOptionFormEle.setAttribute('moduleID',moduleID);
 
3435                                 $(thisOptionFormEle).html(escapeHTML(optionObject.value));
 
3439                                 thisOptionFormEle = createElementWithID('input', optionName);
 
3440                                 thisOptionFormEle.setAttribute('type','text');
 
3441                                 thisOptionFormEle.setAttribute('moduleID',moduleID);
 
3442                                 thisOptionFormEle.setAttribute('placeHolder',optionObject.placeHolder || '');
 
3443                                 thisOptionFormEle.setAttribute('value',optionObject.value);
 
3447                                 thisOptionFormEle = createElementWithID('button', optionName);
 
3448                                 thisOptionFormEle.classList.add('RESConsoleButton');
 
3449                                 thisOptionFormEle.setAttribute('moduleID',moduleID);
 
3450                                 thisOptionFormEle.innerText = optionObject.text;
 
3451                                 thisOptionFormEle.addEventListener('click', optionObject.callback, false);
 
3455                                 thisOptionFormEle = createElementWithID('input', optionName);
 
3456                                 thisOptionFormEle.setAttribute('class','RESInputList');
 
3457                                 thisOptionFormEle.setAttribute('type','text');
 
3458                                 thisOptionFormEle.setAttribute('moduleID',moduleID);
 
3459                                 // thisOptionFormEle.setAttribute('value',optionObject.value);
 
3460                                 existingOptions = optionObject.value;
 
3461                                 if (typeof existingOptions === 'undefined') existingOptions = '';
 
3463                                 var optionArray = existingOptions.split(',');
 
3464                                 for (var i=0, len=optionArray.length; i<len; i++) {
 
3465                                         if (optionArray[i] !== '') prepop.push({id: optionArray[i], name: optionArray[i]});
 
3467                                 setTimeout(function() {
 
3468                                         $(thisOptionFormEle).tokenInput(optionObject.source, {
 
3470                                                 queryParam: "query",
 
3472                                                 allowFreeTagging: true,
 
3474                                                 onResult: (typeof optionObject.onResult === 'function') ? optionObject.onResult : null,
 
3475                                                 onCachedResult: (typeof optionObject.onCachedResult === 'function') ? optionObject.onCachedResult : null,
 
3476                                                 prePopulate: prepop,
 
3477                                                 hintText: (typeof optionObject.hintText === 'string') ? optionObject.hintText : null
 
3483                                 thisOptionFormEle = createElementWithID('input', optionName);
 
3484                                 thisOptionFormEle.setAttribute('type','password');
 
3485                                 thisOptionFormEle.setAttribute('moduleID',moduleID);
 
3486                                 thisOptionFormEle.setAttribute('value',optionObject.value);
 
3491                                 var thisOptionFormEle = createElementWithID('input', optionName);
 
3492                                 thisOptionFormEle.setAttribute('type','checkbox');
 
3493                                 thisOptionFormEle.setAttribute('moduleID',moduleID);
 
3494                                 thisOptionFormEle.setAttribute('value',optionObject.value);
 
3495                                 if (optionObject.value) {
 
3496                                         thisOptionFormEle.setAttribute('checked',true);
 
3499                                 thisOptionFormEle = RESUtils.toggleButton(optionName, optionObject.value, null, null, isTable);
 
3503                                 if (typeof optionObject.values === 'undefined') {
 
3504                                         alert('misconfigured enum option in module: ' + moduleID);
 
3506                                         thisOptionFormEle = createElementWithID('div', optionName);
 
3507                                         thisOptionFormEle.setAttribute('class', 'enum');
 
3508                                         for (var j = 0; j < optionObject.values.length; j++) {
 
3509                                                 var thisDisplay = optionObject.values[j].display;
 
3510                                                 var thisValue = optionObject.values[j].value;
 
3511                                                 var thisOptionFormSubEle = createElementWithID('input', optionName+'-'+j);
 
3512                                                 if (isTable) thisOptionFormSubEle.setAttribute('tableOption', 'true');
 
3513                                                 thisOptionFormSubEle.setAttribute('type', 'radio');
 
3514                                                 thisOptionFormSubEle.setAttribute('name', optionName);
 
3515                                                 thisOptionFormSubEle.setAttribute('moduleID', moduleID);
 
3516                                                 thisOptionFormSubEle.setAttribute('value', optionObject.values[j].value);
 
3517                                                 var nullEqualsEmpty = ((optionObject.value == null) && (optionObject.values[j].value === ''));
 
3518                                                 // we also need to check for null == '' - which are technically equal.
 
3519                                                 if ((optionObject.value == optionObject.values[j].value) || nullEqualsEmpty)  {
 
3520                                                         thisOptionFormSubEle.setAttribute('checked', 'checked');
 
3522                                                 var thisOptionFormSubEleText = document.createTextNode(' ' + optionObject.values[j].name + ' ');
 
3523                                                 thisOptionFormEle.appendChild(thisOptionFormSubEle);
 
3524                                                 thisOptionFormEle.appendChild(thisOptionFormSubEleText);
 
3525                                                 var thisBR = document.createElement('br');
 
3526                                                 thisOptionFormEle.appendChild(thisBR);
 
3531                                 // keycode - shows a key value, but stores a keycode and possibly shift/alt/ctrl combo.
 
3532                                 var realOptionFormEle = $("<input>").attr({
 
3538                                         border: "1px solid red",
 
3540                                 }).val(optionObject.value);
 
3541                                 if (isTable) realOptionFormEle.attr('tableOption','true');
 
3543                                 var thisKeyCodeDisplay = $("<input>").attr({
 
3544                                         id: optionName+"-display",
 
3546                                         capturefor: optionName,
 
3548                                 }).val(RESUtils.niceKeyCode(optionObject.value));
 
3549                                 thisOptionFormEle = $("<div>").append(realOptionFormEle).append(thisKeyCodeDisplay)[0];
 
3552                                 console.log('misconfigured option in module: ' + moduleID);
 
3556                         thisOptionFormEle.setAttribute('tableOption','true');
 
3558                 return thisOptionFormEle;
 
3560         enableModule: function(moduleID, onOrOff) {
 
3561                 var prefs = this.getAllModulePrefs(true);
 
3562                 prefs[moduleID] = !!onOrOff;
 
3563                 this.setModulePrefs(prefs);
 
3565         showConfigOptions: function(moduleID) {
 
3566                 if (!modules[moduleID]) return;
 
3567                 RESConsole.drawConfigOptions(moduleID);
 
3568                 RESConsole.updateSelectedModule(moduleID);
 
3569                 RESConsole.currentModule = moduleID;
 
3571                 RESConsole.RESConsoleContent.scrollTop = 0;
 
3573                 modules['settingsNavigation'].setUrlHash(moduleID);
 
3575         drawConfigOptions: function(moduleID) {
 
3576                 if (modules[moduleID] && modules[moduleID].hidden) return;
 
3577                 var thisOptions = RESUtils.getOptions(moduleID);
 
3580                 this.RESConfigPanelOptions.setAttribute('style','display: block;');
 
3581                 $(this.RESConfigPanelOptions).html('');
 
3582                 // put in the description, and a button to enable/disable the module, first..
 
3583                 var thisHeader = document.createElement('div');
 
3584                 thisHeader.classList.add('moduleHeader');
 
3585                 $(thisHeader).html('<span class="moduleName">' + modules[moduleID].moduleName + '</span>');
 
3586                 var thisToggle = document.createElement('div');
 
3587                 thisToggle.classList.add('moduleToggle');
 
3588                 if (moduleID === 'dashboard') thisToggle.style.display = 'none';
 
3589                 $(thisToggle).html('<span class="toggleOn">on</span><span class="toggleOff">off</span>');
 
3590                 if (modules[moduleID].isEnabled()) thisToggle.classList.add('enabled');
 
3591                 thisToggle.setAttribute('moduleID',moduleID);
 
3592                 thisToggle.addEventListener('click', function(e) {
 
3593                         var activePane = RESConsole.RESConfigPanelModulesPane.querySelector('.active');
 
3594                         var enabled = this.classList.contains('enabled');
 
3596                                 activePane.classList.remove('enabled');
 
3597                                 this.classList.remove('enabled');
 
3598                                 RESConsole.moduleOptionsScrim.classList.add('visible');
 
3599                                 $('#moduleOptionsSave').hide();
 
3601                                 activePane.classList.add('enabled');
 
3602                                 this.classList.add('enabled');
 
3603                                 RESConsole.moduleOptionsScrim.classList.remove('visible');
 
3604                                 $('#moduleOptionsSave').fadeIn();
 
3606                         RESConsole.enableModule(this.getAttribute('moduleID'), !enabled);
 
3608                 thisHeader.appendChild(thisToggle);
 
3609                 // not really looping here, just only executing if there's 1 or more options...
 
3610                 for (var i in thisOptions) {
 
3611                         var thisSaveButton = createElementWithID('input','moduleOptionsSave');
 
3612                         thisSaveButton.setAttribute('type','button');
 
3613                         thisSaveButton.setAttribute('value','save options');
 
3614                         thisSaveButton.addEventListener('click', function(e) {
 
3615                                 RESConsole.saveCurrentModuleOptions(e);
 
3617                         this.RESConsoleConfigPanel.appendChild(thisSaveButton);
 
3618                         var thisSaveStatus = createElementWithID('div','moduleOptionsSaveStatus','saveStatus');
 
3619                         thisHeader.appendChild(thisSaveStatus);
 
3622                 var thisDescription = document.createElement('div');
 
3623                 thisDescription.classList.add('moduleDescription');
 
3624                 $(thisDescription).html(modules[moduleID].description);
 
3625                 thisHeader.appendChild(thisDescription);
 
3626                 this.RESConfigPanelOptions.appendChild(thisHeader);
 
3627                 var allOptionsContainer = createElementWithID('div', 'allOptionsContainer');
 
3628                 this.RESConfigPanelOptions.appendChild(allOptionsContainer);
 
3629                 // now draw all the options...
 
3630                 for (var i in thisOptions) {
 
3631                         if (!(thisOptions[i].noconfig)) {
 
3633                                 var thisOptionContainer = createElementWithID('div', null, 'optionContainer');
 
3634                                 var thisLabel = document.createElement('label');
 
3635                                 thisLabel.setAttribute('for',i);
 
3636                                 $(thisLabel).text(i);
 
3637                                 var thisOptionDescription = createElementWithID('div', null, 'optionDescription');
 
3638                                 $(thisOptionDescription).html(thisOptions[i].description);
 
3639                                 thisOptionContainer.appendChild(thisLabel);
 
3640                                 if (thisOptions[i].type === 'table') {
 
3641                                         thisOptionDescription.classList.add('table');
 
3642                                         // table - has a list of fields (headers of table), users can add/remove rows...
 
3643                                         if (typeof thisOptions[i].fields === 'undefined') {
 
3644                                                 alert('misconfigured table option in module: ' + moduleID + ' - options of type "table" must have fields defined');
 
3646                                                 // get field names...
 
3647                                                 var fieldNames = [];
 
3648                                                 // now that we know the field names, get table rows...
 
3649                                                 var thisTable = document.createElement('table');
 
3650                                                 thisTable.setAttribute('moduleID',moduleID);
 
3651                                                 thisTable.setAttribute('optionName',i);
 
3652                                                 thisTable.setAttribute('class','optionsTable');
 
3653                                                 var thisThead = document.createElement('thead');
 
3654                                                 var thisTableHeader = document.createElement('tr'), thisTH;
 
3655                                                 thisTable.appendChild(thisThead);
 
3656                                                 for (var j=0;j<thisOptions[i].fields.length;j++) {
 
3657                                                         fieldNames[j] = thisOptions[i].fields[j].name;
 
3658                                                         thisTH = document.createElement('th');
 
3659                                                         $(thisTH).text(thisOptions[i].fields[j].name);
 
3660                                                         thisTableHeader.appendChild(thisTH);
 
3662                                                 // add delete column
 
3663                                                 thisTH = document.createElement('th');
 
3664                                                 $(thisTH).text('delete');
 
3665                                                 thisTableHeader.appendChild(thisTH);
 
3667                                                 thisTH = document.createElement('th');
 
3668                                                 $(thisTH).text('move')
 
3669                                                         .attr('title', 'click, drag, and drop')
 
3670                                                         .css('cursor', 'help');
 
3671                                                 thisTableHeader.appendChild(thisTH);
 
3672                                                 thisThead.appendChild(thisTableHeader);
 
3673                                                 thisTable.appendChild(thisThead);
 
3674                                                 var thisTbody = document.createElement('tbody');
 
3675                                                 thisTbody.setAttribute('id','tbody_'+i);
 
3676                                                 if (thisOptions[i].value) {
 
3677                                                         for (var j=0;j<thisOptions[i].value.length;j++) {
 
3678                                                                 var thisTR = document.createElement('tr'), thisTD;
 
3679                                                                 $(thisTR).data('itemidx-orig', j);
 
3680                                                                 for (var k=0;k<thisOptions[i].fields.length;k++) {
 
3681                                                                         thisTD = document.createElement('td');
 
3682                                                                         thisTD.className = 'hasTableOption';
 
3683                                                                         var thisOpt = thisOptions[i].fields[k];
 
3684                                                                         var thisFullOpt = i + '_' + thisOptions[i].fields[k].name;
 
3685                                                                         thisOpt.value = thisOptions[i].value[j][k];
 
3686                                                                         // var thisOptInputName = thisOpt.name + '_' + j;
 
3687                                                                         var thisOptInputName = thisFullOpt + '_' + j;
 
3688                                                                         var thisTableEle = this.drawOptionInput(moduleID, thisOptInputName, thisOpt, true);
 
3689                                                                         thisTD.appendChild(thisTableEle);
 
3690                                                                         thisTR.appendChild(thisTD);
 
3692                                                                 // add delete button
 
3693                                                                 thisTD = document.createElement('td');
 
3694                                                                 var thisDeleteButton = document.createElement('div');
 
3695                                                                 thisDeleteButton.className = 'deleteButton';
 
3696                                                                 thisDeleteButton.addEventListener('click', RESConsole.deleteOptionRow);
 
3697                                                                 thisTD.appendChild(thisDeleteButton);
 
3698                                                                 thisTR.appendChild(thisTD);
 
3700                                                                 thisTD = document.createElement('td');
 
3701                                                                 var thisHandle = document.createElement('div');
 
3703                                                                         .html("⋮⋮")
 
3706                                                                 thisTR.appendChild(thisTD);
 
3707                                                                 thisTbody.appendChild(thisTR);
 
3710                                                 thisTable.appendChild(thisTbody);
 
3711                                                 var thisOptionFormEle = thisTable;
 
3713                                         thisOptionContainer.appendChild(thisOptionDescription);
 
3714                                         thisOptionContainer.appendChild(thisOptionFormEle);
 
3715                                         // Create an "add row" button...
 
3716                                         var addRowText = thisOptions[i].addRowText || 'Add Row';
 
3717                                         var addRowButton = document.createElement('input');
 
3718                                         addRowButton.classList.add('addRowButton');
 
3719                                         addRowButton.setAttribute('type','button');
 
3720                                         addRowButton.setAttribute('value',addRowText);
 
3721                                         addRowButton.setAttribute('optionName',i);
 
3722                                         addRowButton.setAttribute('moduleID',moduleID);
 
3723                                         addRowButton.addEventListener('click', function() {
 
3724                                                 var optionName = this.getAttribute('optionName');
 
3725                                                 var thisTbodyName = 'tbody_' + optionName;
 
3726                                                 var thisTbody = document.getElementById(thisTbodyName);
 
3727                                                 var newRow = document.createElement('tr');
 
3728                                                 var rowCount = (thisTbody.querySelectorAll('tr')) ? thisTbody.querySelectorAll('tr').length + 1 : 1;
 
3729                                                 for (var i=0, len=modules[moduleID].options[optionName].fields.length;i<len;i++) {
 
3730                                                         var newCell = document.createElement('td');
 
3731                                                         newCell.className = 'hasTableOption';
 
3732                                                         var thisOpt = modules[moduleID].options[optionName].fields[i];
 
3733                                                         if (thisOpt.type !== 'enum') thisOpt.value = '';
 
3734                                                         var optionNameWithRow = optionName+'_'+thisOpt.name+'_'+rowCount;
 
3735                                                         var thisInput = RESConsole.drawOptionInput(moduleID, optionNameWithRow, thisOpt, true);
 
3736                                                         newCell.appendChild(thisInput);
 
3737                                                         newRow.appendChild(newCell);
 
3738                                                         $(newRow).data('option-index', rowCount - 1);
 
3739                                                         var firstText = newRow.querySelector('input[type=text]');
 
3740                                                         if (!firstText) firstText = newRow.querySelector('textarea');
 
3742                                                                 setTimeout(function() {
 
3747                                                 // add delete button
 
3748                                                 thisTD = document.createElement('td');
 
3749                                                 var thisDeleteButton = document.createElement('div');
 
3750                                                 thisDeleteButton.className = 'deleteButton';
 
3751                                                 thisDeleteButton.addEventListener('click', RESConsole.deleteOptionRow);
 
3752                                                 thisTD.appendChild(thisDeleteButton);
 
3753                                                 newRow.appendChild(thisTD);
 
3755                                                 thisTD = document.createElement('td');
 
3756                                                 var thisHandle = document.createElement('div');
 
3758                                                         .html("⋮⋮")
 
3762                                                 var thisLen = (modules[moduleID].options[optionName].value) ? modules[moduleID].options[optionName].value.length : 0;
 
3763                                                 $(thisTR).data('itemidx-orig', thisLen);
 
3765                                                 thisTbody.appendChild(newRow);
 
3767                                         thisOptionContainer.appendChild(addRowButton);
 
3769                                         (function(moduleID, optionKey) {
 
3770                                                 $(thisTbody).dragsort({
 
3772                                                         dragSelector: ".handle",
 
3773                                                         dragEnd: function() {
 
3774                                                                 var $this = $(this);
 
3775                                                                 var oldIndex = $this.data('itemidx-orig');
 
3776                                                                 var newIndex = $this.data('itemidx');
 
3777                                                                 var rows = modules[moduleID].options[optionKey].value;
 
3778                                                                 var row = rows.splice(oldIndex, 1)[0];
 
3779                                                                 rows.splice(newIndex, 0, row);
 
3782                                                         scrollContainer: this.RESConfigPanelOptions,
 
3783                                                         placeHolderTemplate: "<tr><td>---</td></tr>",
 
3784                                                         placeholderTemplate: "<tr><td>---</td></tr>",
 
3788                                         if ((thisOptions[i].type === 'text') || (thisOptions[i].type === 'password') || (thisOptions[i].type === 'keycode')) thisOptionDescription.classList.add('textInput');
 
3789                                         var thisOptionFormEle = this.drawOptionInput(moduleID, i, thisOptions[i]);
 
3790                                         thisOptionContainer.appendChild(thisOptionFormEle);
 
3791                                         thisOptionContainer.appendChild(thisOptionDescription);
 
3793                                 var thisClear = document.createElement('div');
 
3794                                 thisClear.setAttribute('class','clear');
 
3795                                 thisOptionContainer.appendChild(thisClear);
 
3796                                 allOptionsContainer.appendChild(thisOptionContainer);
 
3800                 if (optCount === 0) {
 
3801                         var noOptions = createElementWithID('div','noOptions');
 
3802                         noOptions.classList.add('optionContainer');
 
3803                         $(noOptions).text('There are no configurable options for this module');
 
3804                         this.RESConfigPanelOptions.appendChild(noOptions);
 
3806                         // var thisSaveStatusBottom = createElementWithID('div','moduleOptionsSaveStatusBottom','saveStatus');
 
3807                         // this.RESConfigPanelOptions.appendChild(thisBottomSaveButton);
 
3808                         // this.RESConfigPanelOptions.appendChild(thisSaveStatusBottom);
 
3809                         this.moduleOptionsScrim = createElementWithID('div','moduleOptionsScrim');
 
3810                         if (modules[moduleID].isEnabled()) {
 
3811                                 RESConsole.moduleOptionsScrim.classList.remove('visible');
 
3812                                 $('#moduleOptionsSave').fadeIn();
 
3814                                 RESConsole.moduleOptionsScrim.classList.add('visible');
 
3815                                 $('#moduleOptionsSave').fadeOut();
 
3817                         allOptionsContainer.appendChild(this.moduleOptionsScrim);
 
3818                         // console.log($(thisSaveButton).position());
 
3821         deleteOptionRow: function(e) {
 
3822                 var thisRow = e.target.parentNode.parentNode;
 
3823                 $(thisRow).remove();
 
3825         saveCurrentModuleOptions: function(e) {
 
3827                 var panelOptionsDiv = this.RESConfigPanelOptions;
 
3828                 // first, go through inputs that aren't a part of a "table of options"...
 
3829                 var inputs = panelOptionsDiv.querySelectorAll('input, textarea');
 
3830                 for (var i=0, len=inputs.length;i<len;i++) {
 
3831                         // save values of any inputs onscreen, but skip ones with 'capturefor' - those are display only.
 
3832                         var notTokenPrefix = (inputs[i].getAttribute('id') !== null) && (inputs[i].getAttribute('id').indexOf('token-input-') === -1);
 
3833                         if ((notTokenPrefix) && (inputs[i].getAttribute('type') !== 'button') && (inputs[i].getAttribute('displayonly') !== 'true') && (inputs[i].getAttribute('tableOption') !== 'true')) {
 
3834                                 // get the option name out of the input field id - unless it's a radio button...
 
3836                                 if (inputs[i].getAttribute('type') === 'radio') {
 
3837                                         optionName = inputs[i].getAttribute('name');
 
3839                                         optionName = inputs[i].getAttribute('id');
 
3841                                 // get the module name out of the input's moduleid attribute
 
3842                                 var optionValue, moduleID = RESConsole.currentModule;
 
3843                                 if (inputs[i].getAttribute('type') === 'checkbox') {
 
3844                                         optionValue = !!inputs[i].checked;
 
3845                                 } else if (inputs[i].getAttribute('type') === 'radio') {
 
3846                                         if (inputs[i].checked) {
 
3847                                                 optionValue = inputs[i].value;
 
3850                                         // check if it's a keycode, in which case we need to parse it into an array...
 
3851                                         if ((inputs[i].getAttribute('class')) && (inputs[i].getAttribute('class').indexOf('keycode') !== -1)) {
 
3852                                                 var tempArray = inputs[i].value.split(',');
 
3853                                                 // convert the internal values of this array into their respective types (int, bool, bool, bool)
 
3854                                                 optionValue = [parseInt(tempArray[0], 10), (tempArray[1] === 'true'), (tempArray[2] === 'true'), (tempArray[3] === 'true'), (tempArray[4] === 'true')];
 
3856                                                 optionValue = inputs[i].value;
 
3859                                 if (typeof optionValue !== 'undefined') {
 
3860                                         RESUtils.setOption(moduleID, optionName, optionValue);
 
3864                 // Check if there are any tables of options on this panel...
 
3865                 var optionsTables = panelOptionsDiv.querySelectorAll('.optionsTable');
 
3866                 if (typeof optionsTables !== 'undefined') {
 
3867                         // For each table, we need to go through each row in the tbody, and then go through each option and make a multidimensional array.
 
3868                         // For example, something like: [['foo','bar','baz'],['pants','warez','cats']]
 
3869                         for (var i = 0, len = optionsTables.length; i < len; i++) {
 
3870                                 var moduleID = optionsTables[i].getAttribute('moduleID');
 
3871                                 var optionName = optionsTables[i].getAttribute('optionName');
 
3872                                 var thisTBODY = optionsTables[i].querySelector('tbody');
 
3873                                 var thisRows = thisTBODY.querySelectorAll('tr');
 
3874                                 // check if there are any rows...
 
3875                                 if (typeof thisRows !== 'undefined') {
 
3876                                         // go through each row, and get all of the inputs...
 
3877                                         var optionMulti = [];
 
3878                                         var optionRowCount = 0;
 
3879                                         for (var j = 0; j < thisRows.length; j++) {
 
3881                                                 var cells = thisRows[j].querySelectorAll('td.hasTableOption');
 
3882                                                 var notAllBlank = false;
 
3883                                                 for (var k = 0; k < cells.length; k++) {
 
3884                                                         var inputs = cells[k].querySelectorAll('input[tableOption=true], textarea[tableOption=true]');
 
3885                                                         var optionValue = null;
 
3886                                                         for (var l = 0; l < inputs.length; l++) {
 
3887                                                                 // get the module name out of the input's moduleid attribute
 
3888                                                                 // var moduleID = inputs[l].getAttribute('moduleID');
 
3889                                                                 if (inputs[l].getAttribute('type') === 'checkbox') {
 
3890                                                                         optionValue = inputs[l].checked;
 
3891                                                                 } else if (inputs[l].getAttribute('type') === 'radio') {
 
3892                                                                         if (inputs[l].checked) {
 
3893                                                                                 optionValue = inputs[l].value;
 
3896                                                                         // check if it's a keycode, in which case we need to parse it into an array...
 
3897                                                                         if ((inputs[l].getAttribute('class')) && (inputs[l].getAttribute('class').indexOf('keycode') !== -1)) {
 
3898                                                                                 var tempArray = inputs[l].value.split(',');
 
3899                                                                                 // convert the internal values of this array into their respective types (int, bool, bool, bool)
 
3900                                                                                 optionValue = [parseInt(tempArray[0], 10), (tempArray[1] === 'true'), (tempArray[2] === 'true'), (tempArray[3] === 'true')];
 
3902                                                                                 optionValue = inputs[l].value;
 
3905                                                                 if ((optionValue !== '') && (inputs[l].getAttribute('type') !== 'radio')
 
3906                                                                         //If no keyCode is set, then discard the value
 
3907                                                                         && !(Array.isArray(optionValue) && isNaN(optionValue[0]))) {
 
3910                                                                 // optionRow[k] = optionValue;
 
3912                                                         optionRow.push(optionValue);
 
3914                                                 // just to be safe, added a check for optionRow !== null...
 
3915                                                 if ((notAllBlank) && (optionRow !== null)) {
 
3916                                                         optionMulti[optionRowCount] = optionRow;
 
3920                                         if (optionMulti == null) {
 
3923                                         // ok, we've got all the rows... set the option.
 
3924                                         if (typeof optionValue !== 'undefined') {
 
3925                                                 RESUtils.setOption(moduleID, optionName, optionMulti);
 
3931                 var statusEle = document.getElementById('moduleOptionsSaveStatus');
 
3933                         $(statusEle).text('Options have been saved...');
 
3934                         statusEle.setAttribute('style','display: block; opacity: 1');
 
3936                 RESUtils.fadeElementOut(statusEle, 0.1);
 
3937                 if (moduleID === 'RESPro') RESStorage.removeItem('RESmodules.RESPro.lastAuthFailed');
 
3939         drawAboutPanel: function() {
 
3940                 var RESConsoleAboutPanel = this.RESConsoleAboutPanel;
 
3941                 var AboutPanelHTML = ' \
 
3942 <div id="RESAboutPane"> \
 
3943         <div id="Button-DonateRES" class="moduleButton active">Donate</div> \
 
3944         <div id="Button-AboutRES" class="moduleButton">About RES</div> \
 
3945         <div id="Button-RESTeam" class="moduleButton">About the RES Team</div> \
 
3946         <div id="Button-SearchRES" class="moduleButton">Search RES Settings</div> \
 
3948 <div id="RESAboutDetails"> \
 
3949         <div id="DonateRES" class="aboutPanel"> \
 
3950                 <h3>Contribute to support RES</h3> \
 
3951                 <p>RES is entirely free - as in beer, as in open source, as in everything.  If you like our work, a contribution would be greatly appreciated.</p> \
 
3952                 <p>When you contribute, you make it possible for the team to cover hosting costs and other expenses so that we can focus on doing what we do best: making your Reddit experience even better.</p> \
 
3954                 <strong style="font-weight: bold;">Dwolla and bitcoin are the preferred methods of contribution</strong>, because they charge much smaller fees than PayPal and Google: <br><br>\
 
3955                 <a target="_blank" href="https://www.dwolla.com/u/812-686-0217"><img src="https://www.dwolla.com/content/images/btn-donate-with-dwolla.png"></a>\
 
3958                 We also do have PayPal: <br> \
 
3959                 <form action="https://www.paypal.com/cgi-bin/webscr" method="post"><input type="hidden" name="cmd" value="_s-xclick"><input type="hidden" name="hosted_button_id" value="S7TAR7QU39H22"><input type="image" src="https://www.paypalobjects.com/en_US/i/logo/PayPal_mark_60x38.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!"><img alt="" border="0" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1"></form> \
 
3964                 Or Google Checkout: \
 
3965                 <form action="https://checkout.google.com/api/checkout/v2/checkoutForm/Merchant/474530516020369" id="BB_BuyButtonForm" method="post" name="BB_BuyButtonForm" target="_top"> \
 
3966                         <input name="item_name_1" type="hidden" value="Purchase - Reddit Enhancement Suite"/> \
 
3967                         <input name="item_description_1" type="hidden" value="purchase"/> \
 
3968                         <input name="item_quantity_1" type="hidden" value="1"/> \
 
3969                         $<input name="item_price_1" type="text" value="" size="2" /> \
 
3970                         <input name="item_currency_1" type="hidden" value="USD"/> \
 
3971                         <input name="_charset_" type="hidden" value="utf-8"/> \
 
3972                         <input alt="" src="https://checkout.google.com/buttons/buy.gif?merchant_id=474530516020369&w=117&h=48&style=white&variant=text&loc=en_US" type="image"/> \
 
3977                 <form action="https://bitpay.com/checkout" method="post" > \
 
3978                         <input type="hidden" name="action" value="checkout" /> \
 
3979                         <input type="hidden" name="posData" value="" /> \
 
3980                         <input type="hidden" name="data" value="ddFrz5v8dCQ/2oQV9a+OLm3nVrlinxOo1WYFsRjZR5IoouplTgMj7zg8OB3i5xSYiPTbyUmBiNjoY9z/iuEwqPvQClXqQdGINb+IVIzjuZobCUDsLUztc2qdQHvU/sLQzf3a339vzs9JAwSj6W/IpNt90WN1Bdab491xtPpeCIwcS84WY0T+QDjN0c5+1k8/rNqr6A6hFX4TWwZrxQiv35Bl/xyo+YLrE9OUM0K+2cu1hsc7/sOMNsqIB1v4W5CMkQFP40sq9CWf14nyvYbZtg==" /> \
 
3981                         <input type="text" name="price" size="2" value="" /> bitcoins \
 
3982                         <input type="image" src="https://bitpay.com/img/button8.png" border="0" name="submit" alt="BitPay, the easy way to pay with bitcoins." > \
 
3986         <div id="AboutRES" class="aboutPanel"> \
 
3987                 <h3>About RES</h3> \
 
3988                 <p>Author: <a target="_blank" href="http://www.honestbleeps.com/">honestbleeps</a><br></p> \
 
3989                 <p>Description: Reddit Enhancement Suite is a collection of modules that makes browsing reddit a whole lot easier.</p> \
 
3990                 <p>It\'s built with <a target="_blank" href="http://redditenhancementsuite.com/api">an API</a> that allows you to contribute and include your own modules!</p> \
 
3991                 <p>If you\'ve got bug reports or issues with RES, please see the <a target="_blank" href="http://www.reddit.com/r/RESIssues/">RESIssues</a> subreddit. If you\'d like to follow progress on RES, or you\'d like to converse with other users, please see the <a target="_blank" href="http://www.reddit.com/r/Enhancement/">Enhancement subreddit</a>. You can also check the <a href="http://www.reddit.com/r/Enhancement/wiki/index" target="_blank">wiki</a> for the FAQ, and more detailed info on each module.</p> \
 
3992                 <p>If you want to contribute to the RES code base, submit bug reports, or make suggestions, you can also visit RES at <a href="https://github.com/honestbleeps/Reddit-Enhancement-Suite" target="_blank">Github</a>.</p> \
 
3993                 <p>If you want to contact me directly with suggestions, bug reports or just want to say you appreciate the work, an <a href="mailto:steve@honestbleeps.com">email</a> would be great.</p> \
 
3994                 <p>License: Reddit Enhancement Suite is released under the <a target="_blank" href="http://www.gnu.org/licenses/gpl-3.0.html">GPL v3.0</a>.</p> \
 
3995                 <p><strong>Note:</strong> Reddit Enhancement Suite will check, at most once a day, to see if a new version is available.  No data about you is sent to me nor is it stored.</p> \
 
3997         <div id="RESTeam" class="aboutPanel"> \
 
3998                 <h3>About the RES Team</h3> \
 
3999                 <p>Steve Sobel (<a target="_blank" href="http://www.reddit.com/user/honestbleeps/">honestbleeps</a>) is the primary developer of RES.  Beyond that, there are a number of people who have contributed code, design and/or great ideas to RES.  To read more about the RES team, visit <a target="_blank" href="http://redditenhancementsuite.com/about.html">the RES website.</a></p> \
 
4001         <div id="SearchRES" class="aboutPanel"></div> \
 
4004                 $(RESConsoleAboutPanel).html(AboutPanelHTML);
 
4005                 var searchPanel = modules['settingsNavigation'].renderSearchPanel();
 
4006                 $('#SearchRES', RESConsoleAboutPanel).append(searchPanel);
 
4007                 $(RESConsoleAboutPanel).find('.moduleButton').click(function(e) {
 
4008                         $('.moduleButton').removeClass('active');
 
4009                         $(this).addClass('active');
 
4010                         var thisID = $(this).attr('id');
 
4011                         var thisPanel = thisID.replace('Button-','');
 
4012                         var visiblePanel = $(this).parent().parent().find('.aboutPanel:visible');
 
4014                         var duration = (e.data && e.data.duration) || $(this).hasClass('active') ? 0 : 400;
 
4015                         $(visiblePanel).fadeOut(duration, function () {
 
4016                                 $('#'+thisPanel).fadeIn();
 
4019                 this.RESConsoleContent.appendChild(RESConsoleAboutPanel);
 
4021         drawProPanel: function() {
 
4022                 RESConsoleProPanel = this.RESConsoleProPanel;
 
4023                 var proPanelHeader = document.createElement('div');
 
4024                 $(proPanelHeader).html('RES Pro allows you to save your preferences to the RES Pro server.<br><br><strong>Please note:</strong> this is beta functionality right now. Please don\'t consider this to be a "backup" solution just yet. To start, you will need to <a target="_blank" href="http://redditenhancementsuite.com/register.php">register for a PRO account</a> first, then email <a href="mailto:steve@honestbleeps.com">steve@honestbleeps.com</a> with your RES Pro username to get access.');
 
4025                 RESConsoleProPanel.appendChild(proPanelHeader);
 
4026                 this.proSetupButton = createElementWithID('div','RESProSetup');
 
4027                 this.proSetupButton.setAttribute('class','RESButton');
 
4028                 $(this.proSetupButton).text('Configure RES Pro');
 
4029                 this.proSetupButton.addEventListener('click', function(e) {
 
4031                         modules['RESPro'].configure();
 
4033                 RESConsoleProPanel.appendChild(this.proSetupButton);
 
4035                 this.proAuthButton = createElementWithID('div','RESProAuth');
 
4036                 this.proAuthButton.setAttribute('class','RESButton');
 
4037                 $(this.proAuthButton).html('Authenticate');
 
4038                 this.proAuthButton.addEventListener('click', function(e) {
 
4040                         modules['RESPro'].authenticate();
 
4042                 RESConsoleProPanel.appendChild(this.proAuthButton);
 
4044                 this.proSaveButton = createElementWithID('div','RESProSave');
 
4045                 this.proSaveButton.setAttribute('class','RESButton');
 
4046                 $(this.proSaveButton).text('Save Module Options');
 
4047                 this.proSaveButton.addEventListener('click', function(e) {
 
4049                         // modules['RESPro'].savePrefs();
 
4050                         modules['RESPro'].authenticate(modules['RESPro'].savePrefs());
 
4052                 RESConsoleProPanel.appendChild(this.proSaveButton);
 
4055                 this.proUserTaggerSaveButton = createElementWithID('div','RESProSave');
 
4056                 this.proUserTaggerSaveButton.setAttribute('class','RESButton');
 
4057                 $(this.proUserTaggerSaveButton).html('Save user tags to Server');
 
4058                 this.proUserTaggerSaveButton.addEventListener('click', function(e) {
 
4060                         modules['RESPro'].saveModuleData('userTagger');
 
4062                 RESConsoleProPanel.appendChild(this.proUserTaggerSaveButton);
 
4065                 this.proSaveCommentsSaveButton = createElementWithID('div','RESProSaveCommentsSave');
 
4066                 this.proSaveCommentsSaveButton.setAttribute('class','RESButton');
 
4067                 $(this.proSaveCommentsSaveButton).text('Save saved comments to Server');
 
4068                 this.proSaveCommentsSaveButton.addEventListener('click', function(e) {
 
4070                         // modules['RESPro'].saveModuleData('saveComments');
 
4071                         modules['RESPro'].authenticate(modules['RESPro'].saveModuleData('saveComments'));
 
4073                 RESConsoleProPanel.appendChild(this.proSaveCommentsSaveButton);
 
4075                 this.proSubredditManagerSaveButton = createElementWithID('div','RESProSubredditManagerSave');
 
4076                 this.proSubredditManagerSaveButton.setAttribute('class','RESButton');
 
4077                 $(this.proSubredditManagerSaveButton).text('Save subreddits to server');
 
4078                 this.proSubredditManagerSaveButton.addEventListener('click', function(e) {
 
4080                         // modules['RESPro'].saveModuleData('SubredditManager');
 
4081                         modules['RESPro'].authenticate(modules['RESPro'].saveModuleData('subredditManager'));
 
4083                 RESConsoleProPanel.appendChild(this.proSubredditManagerSaveButton);
 
4085                 this.proSaveCommentsGetButton = createElementWithID('div','RESProGetSavedComments');
 
4086                 this.proSaveCommentsGetButton.setAttribute('class','RESButton');
 
4087                 $(this.proSaveCommentsGetButton).text('Get saved comments from Server');
 
4088                 this.proSaveCommentsGetButton.addEventListener('click', function(e) {
 
4090                         // modules['RESPro'].getModuleData('saveComments');
 
4091                         modules['RESPro'].authenticate(modules['RESPro'].getModuleData('saveComments'));
 
4093                 RESConsoleProPanel.appendChild(this.proSaveCommentsGetButton);
 
4095                 this.proSubredditManagerGetButton = createElementWithID('div','RESProGetSubredditManager');
 
4096                 this.proSubredditManagerGetButton.setAttribute('class','RESButton');
 
4097                 $(this.proSubredditManagerGetButton).text('Get subreddits from Server');
 
4098                 this.proSubredditManagerGetButton.addEventListener('click', function(e) {
 
4100                         // modules['RESPro'].getModuleData('SubredditManager');
 
4101                         modules['RESPro'].authenticate(modules['RESPro'].getModuleData('subredditManager'));
 
4103                 RESConsoleProPanel.appendChild(this.proSubredditManagerGetButton);
 
4105                 this.proGetButton = createElementWithID('div','RESProGet');
 
4106                 this.proGetButton.setAttribute('class','RESButton');
 
4107                 $(this.proGetButton).text('Get options from Server');
 
4108                 this.proGetButton.addEventListener('click', function(e) {
 
4110                         // modules['RESPro'].getPrefs();
 
4111                         modules['RESPro'].authenticate(modules['RESPro'].getPrefs());
 
4113                 RESConsoleProPanel.appendChild(this.proGetButton);
 
4114                 this.RESConsoleContent.appendChild(RESConsoleProPanel);
 
4116         open: function(moduleIdOrCategory) {
 
4117                 var category, moduleID;
 
4118                 if (moduleIdOrCategory === 'search') {
 
4119                         moduleID = moduleIdOrCategory;
 
4120                         category = 'About RES';
 
4122                         var module = modules[moduleIdOrCategory];
 
4123                         moduleID = module && module.moduleID;
 
4124                         category = module && module.category;
 
4126                 category = category || moduleIdOrCategory || this.categories[0];
 
4127                 moduleID = moduleID || this.getModuleIDsByCategory(category)[0];
 
4130                 // Draw the config panel
 
4131                 this.drawConfigPanel();
 
4132                 // Draw the about panel
 
4133                 this.drawAboutPanel();
 
4134                 // Draw the RES Pro panel
 
4135                 // this.drawProPanel();
 
4136                 this.openCategoryPanel(category);
 
4137                 this.showConfigOptions(moduleID);
 
4140                 // hide the ad-frame div in case it's flash, because then it covers up the settings console and makes it impossible to see the save button!
 
4141                 var adFrame = document.getElementById('ad-frame');
 
4142                 if ((typeof adFrame !== 'undefined') && (adFrame !== null)) {
 
4143                         adFrame.style.display = 'none';
 
4145                 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'RESConsole');
 
4146                 // var leftCentered = Math.floor((window.innerWidth - 720) / 2);
 
4147                 // modalOverlay.setAttribute('style','display: block; height: ' + document.documentElement.scrollHeight + 'px');
 
4148                 this.modalOverlay.classList.remove('fadeOut');
 
4149                 this.modalOverlay.classList.add('fadeIn');
 
4151                 // this.RESConsoleContainer.setAttribute('style','display: block; left: ' + leftCentered + 'px');
 
4152                 // this.RESConsoleContainer.setAttribute('style','display: block; left: 1.5%;');
 
4153                 this.RESConsoleContainer.classList.remove('slideOut');
 
4154                 this.RESConsoleContainer.classList.add('slideIn');
 
4156                 RESStorage.setItem('RESConsole.hasOpenedConsole', true);
 
4158                 document.body.addEventListener('keyup', RESConsole.handleEscapeKey, false);
 
4160         handleEscapeKey: function(e) {
 
4161                 // don't close if the user is in a token input field (e.g. adding subreddits to a list)
 
4162                 // because they probably just want to cancel the dropdown list
 
4163                 if (e.keyCode === 27 && (document.activeElement.id.indexOf('token-input') === -1)) {
 
4165                         document.body.removeEventListener('keyup', RESConsole.handleEscapeKey, false);
 
4169                 $('#moduleOptionsSave').fadeOut();
 
4170                 this.isOpen = false;
 
4171                 // Let's be nice to reddit and put their ad frame back now...
 
4172                 var adFrame = document.getElementById('ad-frame');
 
4173                 if ((typeof adFrame !== 'undefined') && (adFrame !== null)) {
 
4174                         adFrame.style.display = 'block';
 
4177                 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'RESConsole');
 
4179                 // this.RESConsoleContainer.setAttribute('style','display: none;');
 
4180                 this.modalOverlay.classList.remove('fadeIn');
 
4181                 this.modalOverlay.classList.add('fadeOut');
 
4182                 this.RESConsoleContainer.classList.remove('slideIn');
 
4183                 this.RESConsoleContainer.classList.add('slideOut');
 
4184                 // just in case the user was in the middle of setting a key and decided to close the dialog, clean that up.
 
4185                 if (typeof RESConsole.keyCodeModal !== 'undefined') {
 
4186                         RESConsole.keyCodeModal.style.display = 'none';
 
4187                         RESConsole.captureKey = false;
 
4190                 modules['settingsNavigation'].resetUrlHash();
 
4192         menuClick: function(obj) {
 
4195                 var objID = obj.getAttribute('id');
 
4196                 var category = objID.split('-'); category = category[category.length - 1];
 
4197                 var moduleID = this.getModuleIDsByCategory(category)[0];
 
4198                 this.openCategoryPanel(category);
 
4199                 this.showConfigOptions(moduleID);
 
4201         openCategoryPanel: function(category) {
 
4202                 // make all menu items look unselected
 
4203                 $(RESConsole.RESMenuItems).removeClass('active');
 
4205                 // make selected menu item look selected
 
4206                 $(RESConsole.RESMenuItems).filter(function() {
 
4207                         var thisCategory = (this.getAttribute('id') || '').split('-');
 
4208                         thisCategory = thisCategory[thisCategory.length - 1];
 
4210                         if (thisCategory == category) return true;
 
4211                 }).addClass('active');
 
4213                 // hide all console panels
 
4214                 $(RESConsole.RESConsoleContent).find('.RESPanel').hide();
 
4217                         case 'Menu-About RES': // cruft
 
4219                                 // show the about panel
 
4220                                 $(this.RESConsoleAboutPanel).show();
 
4222                         case 'Menu-RES Pro': // cruft
 
4224                                 // show the pro panel
 
4225                                 $(this.RESConsoleProPanel).show();
 
4228                                 // show the config panel for the given category
 
4229                                 $(this.RESConsoleConfigPanel).show();
 
4230                                 this.drawConfigPanelCategory(category);
 
4237 /************************************************************************************************************
 
4239 Creating your own module:
 
4241 Modules must have the following format, with required functions:
 
4242 - moduleID - the name of the module, i.e. myModule
 
4243 - moduleName - a "nice name" for your module...
 
4244 - description - for the config panel, explains what the module is
 
4245 - isEnabled - should always return RESConsole.getModulePrefs('moduleID') - where moduleID is your module name.
 
4246 - isMatchURL - should always return RESUtils.isMatchURL('moduleID') - checks your include and exclude URL matches.
 
4247 - include - an array of regexes to match against location.href (basically like include in GM)
 
4248 - exclude (optional) - an array of regexes to exclude against location.href
 
4249 - go - always checks both if isEnabled() and if RESUtils.isMatchURL(), and if so, runs your main code.
 
4251 modules['myModule'] = {
 
4252         moduleID: 'myModule',
 
4253         moduleName: 'my module',
 
4254         category: 'CategoryName',
 
4256                 // any configurable options you have go here...
 
4257                 // options must have a type and a value.. 
 
4258                 // valid types are: text, boolean (if boolean, value must be true or false)
 
4262                         value: 'this is default text',
 
4263                         description: 'explanation of what this option is for'
 
4268                         description: 'explanation of what this option is for'
 
4271         description: 'This is my module!',
 
4272         isEnabled: function() {
 
4273                 return RESConsole.getModulePrefs(this.moduleID);
 
4276                 /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.]+/i,
 
4277                 /^https?:\/\/([a-z]+)\.reddit\.com\/message\/comments\/[-\w\.]+/i
 
4279         isMatchURL: function() {
 
4280                 return RESUtils.isMatchURL(this.moduleID);
 
4283                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
4285                         // this is where your code goes...
 
4288 }; // note: you NEED this semicolon at the end!
 
4290 ************************************************************************************************************/
 
4293 modules['subRedditTagger'] = {
 
4294         moduleID: 'subRedditTagger',
 
4295         moduleName: 'Subreddit Tagger',
 
4296         category: 'Filters',
 
4300                         addRowText: '+add tag',
 
4302                                 { name: 'subreddit', type: 'text' },
 
4303                                 { name: 'doesntContain', type: 'text' },
 
4304                                 { name: 'tag', type: 'text' }
 
4308                                 ['somebodymakethis','SMT','[SMT]'],
 
4309                                 ['pics','pic','[pic]']
 
4312                         description: 'Set your subreddits below. For that subreddit, if the title of the post doesn\'t contain what you place in the "doesn\'t contain" field, the subreddit will be tagged with whatever you specify.'
 
4315         description: 'Adds tags to posts on subreddits (i.e. [SMT] on SomebodyMakeThis when the user leaves it out)',
 
4316         isEnabled: function() {
 
4317                 return RESConsole.getModulePrefs(this.moduleID);
 
4320                 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
 
4322         isMatchURL: function() {
 
4323                 return RESUtils.isMatchURL(this.moduleID);
 
4326                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
4327                         this.checkForOldSettings();
 
4328                         this.SRTDoesntContain = [];
 
4329                         this.SRTTagWith = [];
 
4330                         this.loadSRTRules();
 
4332                         RESUtils.watchForElement('siteTable', modules['subRedditTagger'].scanTitles);
 
4337         loadSRTRules: function () {
 
4338                 var subReddits = this.options.subReddits.value;
 
4339                 for (var i=0, len=subReddits.length; i<len; i++) {
 
4340                         var thisGetArray = subReddits[i];
 
4342                                 modules['subRedditTagger'].SRTDoesntContain[thisGetArray[0].toLowerCase()] = thisGetArray[1];
 
4343                                 modules['subRedditTagger'].SRTTagWith[thisGetArray[0].toLowerCase()] = thisGetArray[2];
 
4347         scanTitles: function(obj) {
 
4348                 var qs = '#siteTable > .thing > DIV.entry';
 
4350                         qs = '.thing > DIV.entry';
 
4354                 var entries = obj.querySelectorAll(qs);
 
4355                 for (var i=0, len=entries.length; i<len;i++) {
 
4356                         var thisSubRedditEle = entries[i].querySelector('A.subreddit');
 
4357                         if ((typeof thisSubRedditEle !== 'undefined') && (thisSubRedditEle !== null)) {
 
4358                                 var thisSubReddit = thisSubRedditEle.innerHTML.toLowerCase();
 
4359                                 if (typeof modules['subRedditTagger'].SRTTagWith[thisSubReddit] !== 'undefined') {
 
4360                                         if (thisSubReddit && !modules['subRedditTagger'].SRTDoesntContain[thisSubReddit]) {
 
4361                                                 modules['subRedditTagger'].SRTDoesntContain[thisSubReddit] = '['+thisSubReddit+']';
 
4363                                         var thisTitle = entries[i].querySelector('a.title');
 
4364                                         if (!thisTitle.classList.contains('srTagged')) {
 
4365                                                 thisTitle.classList.add('srTagged');
 
4366                                                 var thisString = modules['subRedditTagger'].SRTDoesntContain[thisSubReddit];
 
4367                                                 var thisTagWith = modules['subRedditTagger'].SRTTagWith[thisSubReddit];
 
4368                                                 if (thisTitle.text.indexOf(thisString) === -1) {
 
4369                                                         $(thisTitle).text(escapeHTML(thisTagWith) + ' ' + $(thisTitle).text());
 
4376         checkForOldSettings: function() {
 
4377                 var settingsCopy = [];
 
4378                 var subRedditCount = 0;
 
4379                 while (RESStorage.getItem('subreddit_' + subRedditCount)) {
 
4380                         var thisGet = RESStorage.getItem('subreddit_' + subRedditCount).replace(/\"/g,"");
 
4381                         var thisGetArray = thisGet.split("|");
 
4382                         settingsCopy[subRedditCount] = thisGetArray;
 
4383                         RESStorage.removeItem('subreddit_' + subRedditCount);
 
4386                 if (subRedditCount > 0) {
 
4387                         RESUtils.setOption('subRedditTagger', 'subReddits', settingsCopy);
 
4393 modules['uppersAndDowners'] = {
 
4394         moduleID: 'uppersAndDowners',
 
4395         moduleName: 'Uppers and Downers Enhanced',
 
4401                         description: 'Show +/- signs next to upvote/downvote tallies.'
 
4406                         description: 'Uppers and Downers on links.'
 
4410                         value: 'color:rgb(255, 139, 36); font-weight:normal;',
 
4411                         description: 'CSS style for post upvotes'
 
4413                 postDownvoteStyle: {
 
4415                         value: 'color:rgb(148, 148, 255); font-weight:normal;',
 
4416                         description: 'CSS style for post upvotes'
 
4418                 commentUpvoteStyle: {
 
4420                         value: 'color:rgb(255, 139, 36); font-weight:bold;',
 
4421                         description: 'CSS style for comment upvotes'
 
4423                 commentDownvoteStyle: {
 
4425                         value: 'color:rgb(148, 148, 255); font-weight:bold;',
 
4426                         description: 'CSS style for comment upvotes'
 
4431                         description: 'Force upvote/downvote counts to be visible (when subreddit CSS tries to hide them)'
 
4434         description: 'Displays up/down vote counts on comments.',
 
4435         isEnabled: function() {
 
4436                 return RESConsole.getModulePrefs(this.moduleID);
 
4439                 /^https?:\/\/([a-z]+)\.reddit\.com\/?(?:\??[\w]+=[\w]+&?)*/i,
 
4440                 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\w]+\/?(?:\??[\w]+=[\w]+&?)*$/i,
 
4441                 /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.]+/i,
 
4442                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/?[-\w\.]*/i,
 
4443                 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
 
4445         isMatchURL: function() {
 
4446                 return RESUtils.isMatchURL(this.moduleID);
 
4448         beforeLoad: function() {
 
4449                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
4450                         // added code to force inline-block and opacity: 1 to prevent CSS from hiding .res_* classes...
 
4451                         var forceVisible = (this.options.forceVisible.value) ? '; visibility: visible !important; opacity: 1 !important; display: inline-block !important;' : '';
 
4452                         var css = '.res_comment_ups { '+this.options.commentUpvoteStyle.value+forceVisible+' } .res_comment_downs { '+this.options.commentDownvoteStyle.value+forceVisible+' }';
 
4453                         css += '.res_post_ups { '+this.options.postUpvoteStyle.value+forceVisible+' } .res_post_downs { '+this.options.postDownvoteStyle.value+forceVisible+' }';
 
4454                         RESUtils.addCSS(css);
 
4458                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
4459                         // get rid of the showTimeStamp options since Reddit now has this feature natively.
 
4460                         if (typeof this.options.showTimestamp !== 'undefined') {
 
4461                                 delete this.options.showTimestamp;
 
4462                                 RESStorage.setItem('RESoptions.uppersAndDowners', JSON.stringify(modules['uppersAndDowners'].options));
 
4464                         if (RESUtils.pageType() === 'comments') {
 
4465                                 this.commentsWithMoos = [];
 
4466                                 this.moreCommentsIDs = [];
 
4467                                 this.applyUppersAndDownersToComments();
 
4468                                 RESUtils.watchForElement('newComments', modules['uppersAndDowners'].applyUppersAndDownersToComments);
 
4469                         } else if (RESUtils.pageType() === 'profile') {
 
4470                                 this.commentsWithMoos = [];
 
4471                                 this.moreCommentsIDs = [];
 
4472                                 this.applyUppersAndDownersToMixed();
 
4473                                 RESUtils.watchForElement('siteTable', modules['uppersAndDowners'].applyUppersAndDownersToMixed);
 
4475                         } else if ((RESUtils.pageType() === 'linklist') && (this.options.applyToLinks.value)) {
 
4476                                 this.linksWithMoos = [];
 
4477                                 this.applyUppersAndDownersToLinks();
 
4478                                 RESUtils.watchForElement('siteTable', modules['uppersAndDowners'].applyUppersAndDownersToLinks);
 
4482         applyUppersAndDownersToComments: function(ele) {
 
4484                         ele = document.body;
 
4486                 if (ele.classList.contains('comment')) {
 
4487                         modules['uppersAndDowners'].showUppersAndDownersOnComment(ele);
 
4488                 } else if (ele.classList.contains('entry')) {
 
4489                         modules['uppersAndDowners'].showUppersAndDownersOnComment(ele.parentNode);
 
4491                         var allComments = ele.querySelectorAll('div.comment');
 
4492                         RESUtils.forEachChunked(allComments, 15, 1000, function(comment, i, array) {
 
4493                                 modules['uppersAndDowners'].showUppersAndDownersOnComment(comment);
 
4497         applyUppersAndDownersToMixed: function(ele) {
 
4498                 ele = ele || document.body;
 
4499                 var linkList = ele.querySelectorAll('div.thing.link, div.thing.comment'),
 
4500                     displayType = 'regular',
 
4501                     thisPlus, thisMinus;
 
4502                 if (modules['uppersAndDowners'].options.showSigns.value) {
 
4509                 for (var i=0, len=linkList.length; i<len; i++) {
 
4510                         if (linkList[i].classList.contains('link')) {
 
4511                                 var thisups = linkList[i].getAttribute('data-ups');
 
4512                                 var thisdowns = linkList[i].getAttribute('data-downs');
 
4514                                 var thisTagline = linkList[i].querySelector('p.tagline');
 
4515                                 // Check if compressed link display or regular...
 
4516                                 if ((typeof thisTagline !== 'undefined') && (thisTagline !== null)) {
 
4517                                         var upsAndDownsEle = $("<span> (<span class='res_post_ups'>"+thisPlus+thisups+"</span>|<span class='res_post_downs'>"+thisMinus+thisdowns+"</span>) </span>");
 
4518                                         if (displayType === 'regular') {
 
4519                                                 // thisTagline.insertBefore(upsAndDownsEle, thisTagline.firstChild);
 
4520                                                 $(thisTagline).prepend(upsAndDownsEle);
 
4522                                                 $(thisTagline).after(upsAndDownsEle);
 
4526                                 modules['uppersAndDowners'].showUppersAndDownersOnComment(linkList[i]);
 
4531         showUppersAndDownersOnComment: function(commentEle) {
 
4532                 // if this is not a valid comment (e.g. a load more comments div, which has the same classes for some reason)
 
4533                 if ((commentEle.getAttribute('data-votesvisible') === 'true')
 
4534                         || (commentEle.classList.contains('morechildren'))
 
4535                         || (commentEle.classList.contains('morerecursion'))
 
4536                         || (commentEle.classList.contains('score-hidden'))) {
 
4539                 commentEle.setAttribute('data-votesvisible', 'true');
 
4540                 var tagline = commentEle.querySelector('p.tagline');
 
4541                 var ups = commentEle.getAttribute('data-ups');
 
4542                 var downs = commentEle.getAttribute('data-downs');
 
4543                 var openparen, closeparen, mooups, moodowns, voteUps, voteDowns, pipe;
 
4544                 var frag = document.createDocumentFragment(); //using a fragment speeds this up by a factor of about 2
 
4547                 if (modules['uppersAndDowners'].options.showSigns.value) {
 
4552                 openparen = document.createTextNode(" (");
 
4553                 frag.appendChild(openparen);
 
4555                 mooups = document.createElement("span");
 
4556                 mooups.className = "res_comment_ups";
 
4557                 voteUps = document.createTextNode(ups);
 
4559                 mooups.appendChild(voteUps);
 
4560                 frag.appendChild(mooups);
 
4562                 pipe = document.createTextNode("|");
 
4563                 tagline.appendChild(pipe);
 
4565                 moodowns = document.createElement("span");
 
4566                 moodowns.className = "res_comment_downs";
 
4568                 voteDowns = document.createTextNode(downs);
 
4569                 moodowns.appendChild(voteDowns);
 
4571                 frag.appendChild(moodowns);
 
4573                 closeparen = document.createTextNode(")");
 
4574                 frag.appendChild(closeparen);
 
4576                 frag.appendChild(openparen);
 
4577                 frag.appendChild(mooups);
 
4578                 frag.appendChild(pipe);
 
4579                 frag.appendChild(moodowns);
 
4580                 frag.appendChild(closeparen);
 
4582                 tagline.appendChild(frag);
 
4584         applyUppersAndDownersToLinks: function(ele) {
 
4585                 // Since we're dealing with max 100 links at a time, we don't need a chunker here...
 
4586                 ele = ele || document.body;
 
4587                 var linkList = ele.querySelectorAll('div.thing.link'),
 
4588                     displayType = 'regular',
 
4589                     thisPlus, thisMinus;
 
4590                 if (modules['uppersAndDowners'].options.showSigns.value) {
 
4597                 for (var i=0, len=linkList.length; i<len; i++) {
 
4598                         var thisups = linkList[i].getAttribute('data-ups');
 
4599                         var thisdowns = linkList[i].getAttribute('data-downs');
 
4601                         var thisTagline = linkList[i].querySelector('p.tagline');
 
4602                         // Check if compressed link display or regular...
 
4603                         if ((typeof thisTagline !== 'undefined') && (thisTagline !== null)) {
 
4604                                 var upsAndDownsEle = $("<span> (<span class='res_post_ups'>"+thisPlus+thisups+"</span>|<span class='res_post_downs'>"+thisMinus+thisdowns+"</span>) </span>");
 
4605                                 if (displayType === 'regular') {
 
4606                                         // thisTagline.insertBefore(upsAndDownsEle, thisTagline.firstChild);
 
4607                                         $(thisTagline).prepend(upsAndDownsEle);
 
4609                                         $(thisTagline).after(upsAndDownsEle);
 
4616 modules['keyboardNav'] = {
 
4617         moduleID: 'keyboardNav',
 
4618         moduleName: 'Keyboard Navigation',
 
4621                 // any configurable options you have go here...
 
4622                 // options must have a type and a value.. 
 
4623                 // valid types are: text, boolean (if boolean, value must be true or false)
 
4628                         description: 'Background color of focused element'
 
4633                         description: 'border style (e.g. 1px dashed gray) for focused element'
 
4635                 focusBGColorNight: {
 
4638                         description: 'Background color of focused element in Night Mode'
 
4640                 focusFGColorNight: {
 
4643                         description: 'Foreground color of focused element in Night Mode'
 
4648                         description: 'border style (e.g. 1px dashed gray) for focused element'
 
4650                 autoSelectOnScroll: {
 
4653                         description: 'Automatically select the topmost element for keyboard navigation on window scroll'
 
4658                         description: 'Scroll window to top of link when expando key is used (to keep pics etc in view)'
 
4663                                 { name: 'directional', value: 'directional' },
 
4664                                 { name: 'page up/down', value: 'page' },
 
4665                                 { name: 'lock to top', value: 'top' }
 
4667                         value: 'directional',
 
4668                         description: 'When moving up/down with keynav, when and how should RES scroll the window?'
 
4670                 commentsLinkNumbers: {
 
4673                         description: 'Assign number keys (e.g. [1]) to links within selected comment'
 
4675                 commentsLinkNumberPosition: {
 
4678                                 { name: 'Place on right', value: 'right' },
 
4679                                 { name: 'Place on left', value: 'left' }
 
4682                         description: 'Which side commentsLinkNumbers are displayed'
 
4684                 commentsLinkNewTab: {
 
4687                         description: 'Open number key links in a new tab'
 
4692                         description: 'Move keyboard focus to a link or comment when clicked with the mouse'
 
4697                         description: 'After hiding a link, automatically select the next link'
 
4702                         description: 'After voting on a link, automatically select the next link'
 
4706                         value: [191, false, false, true], // ? (note the true in the shift slot)
 
4707                         description: 'Show help for keyboard shortcuts'
 
4711                         value: [190, false, false, false], // .
 
4712                         description: 'Show/hide commandline box'
 
4716                         value: [72, false, false, false], // h
 
4717                         description: 'Hide link'
 
4721                         value: [75, false, false, false], // k
 
4722                         description: 'Move up (previous link or comment)'
 
4726                         value: [74, false, false, false], // j
 
4727                         description: 'Move down (next link or comment)'
 
4731                         value: [75, false, false, true], // shift-k
 
4732                         description: 'Move to top of list (on link pages)'
 
4736                         value: [74, false, false, true], // shift-j
 
4737                         description: 'Move to bottom of list (on link pages)'
 
4741                         value: [75, false, false, true], // shift-k
 
4742                         description: 'Move to previous sibling (in comments) - skips to previous sibling at the same depth.'
 
4746                         value: [74, false, false, true], // shift-j
 
4747                         description: 'Move to next sibling (in comments) - skips to next sibling at the same depth.'
 
4751                         value: [75, true, false, true], // shift-alt-k
 
4752                         description: 'Move to the topmost comment of the previous thread (in comments).'
 
4756                         value: [74, true, false, true], // shift-alt-j
 
4757                         description: 'Move to the topmost comment of the next thread (in comments).'
 
4761                         value: [84, false, false, false], // t
 
4762                         description: 'Move to the topmost comment of the current thread (in comments).'
 
4766                         value: [80, false, false, false], // p
 
4767                         description: 'Move to parent (in comments).'
 
4771                         value: [80, false, false, true], // p
 
4772                         description: 'Display parent comments.'
 
4776                         value: [13, false, false, false], // enter
 
4777                         description: 'Follow link (hold shift to open it in a new tab) (link pages only)'
 
4781                         value: [13, false, false, true], // shift-enter
 
4782                         description: 'Follow link in new tab (link pages only)'
 
4784                 followLinkNewTabFocus: {
 
4787                         description: 'When following a link in new tab - focus the tab?'
 
4791                         value: [88, false, false, false], // x
 
4792                         description: 'Toggle expando (image/text/video) (link pages only)'
 
4796                         value: [187, false, false, false],
 
4797                         description: 'Increase the size of image(s) in the highlighted post area'
 
4801                         value: [189, false, false, false],
 
4802                         description: 'Increase the size of image(s) in the highlighted post area'
 
4806                         value: [187, false, false, true],
 
4807                         description: 'Increase the size of image(s) in the highlighted post area (finer control)'
 
4809                 imageSizeDownFine: {
 
4811                         value: [189, false, false, true],
 
4812                         description: 'Increase the size of image(s) in the highlighted post area (finer control)'
 
4814                 previousGalleryImage: {
 
4816                         value: [219, false, false, false], //[
 
4817                         description: 'View the previous image of an inline gallery.'
 
4821                         value: [221, false, false, false], //]
 
4822                         description: 'View the next image of an inline gallery.'
 
4826                         value: [88, false, false, true], // shift-x
 
4827                         description: 'Toggle "view images" button'
 
4831                         value: [13, false, false, false], // enter
 
4832                         description: 'Expand/collapse comments (comments pages only)'
 
4836                         value: [67, false, false, false], // c
 
4837                         description: 'View comments for link (shift opens them in a new tab)'
 
4839                 followCommentsNewTab: {
 
4841                         value: [67, false, false, true], // shift-c
 
4842                         description: 'View comments for link in a new tab'
 
4844                 followLinkAndCommentsNewTab: {
 
4846                         value: [76, false, false, false], // l
 
4847                         description: 'View link and comments in new tabs'
 
4849                 followLinkAndCommentsNewTabBG: {
 
4851                         value: [76, false, false, true], // shift-l
 
4852                         description: 'View link and comments in new background tabs'
 
4856                         value: [65, false, false, false], // a
 
4857                         description: 'Upvote selected link or comment'
 
4861                         value: [90, false, false, false], // z
 
4862                         description: 'Downvote selected link or comment'
 
4866                         value: [83, false, false, false], // s
 
4867                         description: 'Save the current link'
 
4871                         value: [82, false, false, false], // r
 
4872                         description: 'Reply to current comment (comment pages only)'
 
4876                         value: [69, false, true, false], // control-e
 
4877                         description: 'Open the current markdown field in the big editor. (Only when a markdown form is focused)'
 
4881                         value: [82, false, false, false], // r
 
4882                         description: 'Go to subreddit of selected link (link pages only)'
 
4884                 followSubredditNewTab: {
 
4886                         value: [82, false, false, true], // shift-r
 
4887                         description: 'Go to subreddit of selected link in a new tab (link pages only)'
 
4891                         value: [73, false, false, false], // i
 
4892                         description: 'Go to inbox'
 
4896                         value: [73, false, false, true], // shift+i
 
4897                         description: 'Go to inbox in a new tab'
 
4901                         value: [85, false, false, false], // u
 
4902                         description: 'Go to profile'
 
4906                         value: [85, false, false, true], // shift+u
 
4907                         description: 'Go to profile in a new tab'
 
4911                         value: [70, false, false, false], // f
 
4912                         description: 'Go to front page'
 
4914                 subredditFrontPage: {
 
4916                         value: [70, false, false, true], // shift-f
 
4917                         description: 'Go to subreddit front page'
 
4921                         value: [78, false, false, false], // n
 
4922                         description: 'Go to next page (link list pages only)'
 
4926                         value: [80, false, false, false], // p
 
4927                         description: 'Go to prev page (link list pages only)'
 
4931                         value: [49, false, false, false], // 1
 
4932                         description: 'Open first link within comment.',
 
4937                         value: [50, false, false, false], // 2
 
4938                         description: 'Open link #2 within comment.',
 
4943                         value: [51, false, false, false], // 3
 
4944                         description: 'Open link #3 within comment.',
 
4949                         value: [52, false, false, false], // 4
 
4950                         description: 'Open link #4 within comment.',
 
4955                         value: [53, false, false, false], // 5
 
4956                         description: 'Open link #5 within comment.',
 
4961                         value: [54, false, false, false], // 6
 
4962                         description: 'Open link #6 within comment.',
 
4967                         value: [55, false, false, false], // 7
 
4968                         description: 'Open link #7 within comment.',
 
4973                         value: [56, false, false, false], // 8
 
4974                         description: 'Open link #8 within comment.',
 
4979                         value: [57, false, false, false], // 9
 
4980                         description: 'Open link #9 within comment.',
 
4985                         value: [48, false, false, false], // 0
 
4986                         description: 'Open link #10 within comment.',
 
4990         description: 'Keyboard navigation for reddit!',
 
4991         isEnabled: function() {
 
4992                 return RESConsole.getModulePrefs(this.moduleID);
 
4995                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
 
4997         isMatchURL: function() {
 
4998                 return RESUtils.isMatchURL(this.moduleID);
 
5000         beforeLoad: function() {
 
5001                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
5002                         var focusFGColorNight, focusBGColor, focusBGColorNight;
 
5003                         if (typeof this.options.focusBGColor === 'undefined') {
 
5004                                 focusBGColor = '#F0F3FC';
 
5006                                 focusBGColor = this.options.focusBGColor.value;
 
5008                         var borderType = 'outline';
 
5009                         if (BrowserDetect.isOpera()) borderType = 'border';
 
5010                         if (typeof this.options.focusBorder === 'undefined') {
 
5013                                 focusBorder = borderType+': ' + this.options.focusBorder.value + ';';
 
5015                         if (!(this.options.focusBGColorNight.value)) {
 
5016                                 focusBGColorNight = '#666';
 
5018                                 focusBGColorNight = this.options.focusBGColorNight.value;
 
5020                         if (!(this.options.focusFGColorNight.value)) {
 
5021                                 focusFGColorNight = '#DDD';
 
5023                                 focusFGColorNight = this.options.focusFGColorNight.value;
 
5025                         if (typeof this.options.focusBorderNight === 'undefined') {
 
5026                                 focusBorderNight = '';
 
5028                                 focusBorderNight = borderType+': ' + this.options.focusBorderNight.value + ';';
 
5030                         // old style: .RES-keyNav-activeElement { '+borderType+': '+focusBorder+'; background-color: '+focusBGColor+'; } \
 
5031                         // this new pure CSS arrow will not work because to position it we must have .RES-keyNav-activeElement position relative, but that screws up image viewer's absolute positioning to
 
5032                         // overlay over the sidebar... yikes.
 
5033                         // .RES-keyNav-activeElement:after { content: ""; float: right; margin-right: -5px; border-color: transparent '+focusBorderColor+' transparent transparent; border-style: solid; border-width: 3px 4px 3px 0; } \
 
5035                         // why !important on .RES-keyNav-activeElement?  Because some subreddits are unfortunately using !important for no good reason on .entry divs... 
 
5037                                 .entry { padding-right: 5px; } \
 
5038                                 .RES-keyNav-activeElement, .commentarea .RES-keyNav-activeElement .md, .commentarea .RES-keyNav-activeElement.entry .noncollapsed { background-color: '+focusBGColor+' !important; } \
 
5039                                 .RES-keyNav-activeElement { '+focusBorder+' } \
 
5040                                 .res-nightmode .RES-keyNav-activeElement { '+focusBorderNight+' } \
 
5041                                 .res-nightmode .RES-keyNav-activeElement, .res-nightmode .RES-keyNav-activeElement .usertext-body, .res-nightmode .RES-keyNav-activeElement .usertext-body .md, .res-nightmode .RES-keyNav-activeElement .usertext-body .md p, .res-nightmode .commentarea .RES-keyNav-activeElement .noncollapsed, .res-nightmode .RES-keyNav-activeElement .noncollapsed .md, .res-nightmode .RES-keyNav-activeElement .noncollapsed .md p { background-color: '+focusBGColorNight+' !important; color: '+focusFGColorNight+' !important;} \
 
5042                                 .res-nightmode .RES-keyNav-activeElement a.title:first-of-type {color: ' + focusFGColorNight + ' !important; } \
 
5043                                 #keyHelp { display: none; position: fixed; height: 90%; overflow-y: auto; right: 20px; top: 20px; z-index: 1000; border: 2px solid #aaa; border-radius: 5px; width: 300px; padding: 5px; background-color: #fff; } \
 
5044                                 #keyHelp th { font-weight: bold; padding: 2px; border-bottom: 1px dashed #ddd; } \
 
5045                                 #keyHelp td { padding: 2px; border-bottom: 1px dashed #ddd; } \
 
5046                                 #keyHelp td:first-child { width: 70px; } \
 
5047                                 #keyCommandLineWidget { font-size: 14px; display: none; position: fixed; top: 200px; left: 50%; margin-left: -275px; z-index: 100000110; width: 550px; border: 3px solid #555; border-radius: 10px; padding: 10px; background-color: #333; color: #CCC; opacity: 0.95; } \
 
5048                                 #keyCommandInput { width: 240px; background-color: #999; margin-right: 10px; } \
 
5049                                 #keyCommandInputTip { margin-top: 5px; color: #9F9; } \
 
5050                                 #keyCommandInputTip ul { font-size: 11px; list-style-type: disc; }  \
 
5051                                 #keyCommandInputTip li { margin-left: 15px; }  \
 
5052                                 #keyCommandInputError { margin-top: 5px; color: red; font-weight: bold; } \
 
5053                                 .keyNavAnnotation { font-size: 9px; position: relative; top: -6px; } \
 
5058                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
5059                         // get rid of antequated option we've removed
 
5060                         this.keyboardNavLastIndexCache = safeJSON.parse(RESStorage.getItem('RESmodules.keyboardNavLastIndex'), false, true);
 
5061                         var idx, now = new Date().getTime();
 
5062                         if (! this.keyboardNavLastIndexCache) {
 
5063                                 // this is a one time function to delete old keyboardNavLastIndex junk.
 
5064                                 this.keyboardNavLastIndexCache = {};
 
5065                                 for (idx in RESStorage) {
 
5066                                         if (idx.match(/keyboardNavLastIndex/)) {
 
5067                                                 var url = idx.replace('RESmodules.keyboardNavLastIndex.','');
 
5068                                                 this.keyboardNavLastIndexCache[url] = {
 
5069                                                         index: RESStorage[idx],
 
5072                                                 RESStorage.removeItem(idx);
 
5075                                 this.keyboardNavLastIndexCache.lastScan = now;
 
5076                                 RESStorage.setItem('RESmodules.keyboardNavLastIndex', JSON.stringify(this.keyboardNavLastIndexCache));
 
5078                                 // clean cache every 6 hours - delete any urls that haven't been visited in an hour.
 
5079                                 if ((typeof this.keyboardNavLastIndexCache.lastScan === 'undefined') || (now - this.keyboardNavLastIndexCache.lastScan > 21600000)) {
 
5080                                         for (idx in this.keyboardNavLastIndexCache) {
 
5081                                                 if ((typeof this.keyboardNavLastIndexCache[idx] === 'object') && (now - this.keyboardNavLastIndexCache[idx].updated > 3600000)) {
 
5082                                                         delete this.keyboardNavLastIndexCache[idx];
 
5085                                         this.keyboardNavLastIndexCache.lastScan = now;
 
5086                                         RESStorage.setItem('RESmodules.keyboardNavLastIndex', JSON.stringify(this.keyboardNavLastIndexCache));
 
5090                         if (this.options.autoSelectOnScroll.value) {
 
5091                                 window.addEventListener('scroll', modules['keyboardNav'].handleScroll, false);
 
5093                         if (typeof this.options.scrollTop !== 'undefined') {
 
5094                                 if (this.options.scrollTop.value) this.options.scrollStyle.value = 'top';
 
5095                                 delete this.options.scrollTop;
 
5096                                 RESStorage.setItem('RESoptions.keyboardNav', JSON.stringify(modules['keyboardNav'].options));
 
5099                         this.attachCommandLineWidget();
 
5100                         window.addEventListener('keydown', function(e) {
 
5101                                 // console.log(e.keyCode);
 
5102                                 modules['keyboardNav'].handleKeyPress(e);
 
5104                         this.scanPageForKeyboardLinks();
 
5105                         // listen for new DOM nodes so that modules like autopager, never ending reddit, "load more comments" etc still get keyboard nav.
 
5106                         if (RESUtils.pageType() === 'comments') {
 
5107                                 RESUtils.watchForElement('newComments', modules['keyboardNav'].scanPageForNewKeyboardLinks);
 
5109                                 RESUtils.watchForElement('siteTable', modules['keyboardNav'].scanPageForNewKeyboardLinks);
 
5113         scanPageForNewKeyboardLinks: function() {
 
5114                 modules['keyboardNav'].scanPageForKeyboardLinks(true);
 
5116         setKeyIndex: function() {
 
5117                 var trimLoc = location.href;
 
5118                 // remove any trailing slash from the URL
 
5119                 if (trimLoc.substr(-1) === '/') trimLoc = trimLoc.substr(0,trimLoc.length-1);
 
5120                 if (typeof this.keyboardNavLastIndexCache[trimLoc] === 'undefined') {
 
5121                         this.keyboardNavLastIndexCache[trimLoc] = {};
 
5123                 var now = new Date().getTime();
 
5124                 this.keyboardNavLastIndexCache[trimLoc] = {
 
5125                         index: this.activeIndex,
 
5128                 RESStorage.setItem('RESmodules.keyboardNavLastIndex', JSON.stringify(this.keyboardNavLastIndexCache));
 
5130         handleScroll: function(e) {
 
5131                 if (modules['keyboardNav'].scrollTimer) clearTimeout(modules['keyboardNav'].scrollTimer);
 
5132                 modules['keyboardNav'].scrollTimer = setTimeout(modules['keyboardNav'].handleScrollAfterTimer, 300);
 
5134         handleScrollAfterTimer: function() {
 
5135                 if ((! modules['keyboardNav'].recentKeyPress) && (! RESUtils.elementInViewport(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]))) {
 
5136                         for (var i=0, len=modules['keyboardNav'].keyboardLinks.length; i<len; i++) {
 
5137                                 if (RESUtils.elementInViewport(modules['keyboardNav'].keyboardLinks[i])) {
 
5138                                         modules['keyboardNav'].keyUnfocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
 
5139                                         modules['keyboardNav'].activeIndex = i;
 
5140                                         modules['keyboardNav'].keyFocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
 
5146         attachCommandLineWidget: function() {
 
5147                 this.commandLineWidget = createElementWithID('div','keyCommandLineWidget');
 
5148                 this.commandLineInput = createElementWithID('input','keyCommandInput');
 
5149                 this.commandLineInput.setAttribute('type','text');
 
5150                 this.commandLineInput.addEventListener('blur', function(e) {
 
5151                         modules['keyboardNav'].toggleCmdLine(false);
 
5153                 this.commandLineInput.addEventListener('keyup', function(e) {
 
5154                         if (e.keyCode === 27) {
 
5156                                 modules['keyboardNav'].toggleCmdLine(false);
 
5159                                 modules['keyboardNav'].cmdLineHelper(e.target.value);
 
5162                 this.commandLineInputTip = createElementWithID('div','keyCommandInputTip');
 
5163                 this.commandLineInputError = createElementWithID('div','keyCommandInputError');
 
5166                 this.commandLineSubmit = createElementWithID('input','keyCommandInput');
 
5167                 this.commandLineSubmit.setAttribute('type','submit');
 
5168                 this.commandLineSubmit.setAttribute('value','go');
 
5170                 this.commandLineForm = createElementWithID('form','keyCommandForm');
 
5171                 this.commandLineForm.appendChild(this.commandLineInput);
 
5172                 // this.commandLineForm.appendChild(this.commandLineSubmit);
 
5173                 var txt = document.createTextNode('type a command, ? for help, esc to close');
 
5174                 this.commandLineForm.appendChild(txt);
 
5175                 this.commandLineForm.appendChild(this.commandLineInputTip);
 
5176                 this.commandLineForm.appendChild(this.commandLineInputError);
 
5177                 this.commandLineForm.addEventListener('submit', modules['keyboardNav'].cmdLineSubmit, false);
 
5178                 this.commandLineWidget.appendChild(this.commandLineForm);
 
5179                 document.body.appendChild(this.commandLineWidget);
 
5182         cmdLineHelper: function (val) {
 
5183                 var splitWords = val.split(' '),
 
5184                     command = splitWords[0],
 
5186                 splitWords.splice(0,1);
 
5187                 val = splitWords.join(' ');
 
5188                 if (command.slice(0,2) === 'r/') {
 
5189                         // get the subreddit name they've typed so far (anything after r/)...
 
5190                         srString = command.slice(2);
 
5191                         this.cmdLineShowTip('navigate to subreddit: ' + srString);
 
5192                 } else if (command.match('/?u/\\w+/m/')) {
 
5193                         str = 'navigate to multi-reddit: ';
 
5194                         str += (command.indexOf('/') > 0 ? '/' : '') + command;
 
5195                         this.cmdLineShowTip(str);
 
5196                 } else if (command.slice(0,2) === 'm/') {
 
5197                         str = 'navigate to multi-reddit: /me/' + command;
 
5198                         this.cmdLineShowTip(str);
 
5199                 } else if (command.slice(0,2) === 'u/') {
 
5200                         // get the user name they've typed so far (anything after u/)...
 
5201                         var userString = command.slice(2);
 
5202                         this.cmdLineShowTip('navigate to user profile: ' + userString);
 
5203                 } else if (command.slice(0,1) === '/') {
 
5204                         srString = command.slice(1);
 
5205                         this.cmdLineShowTip('sort by ([n]ew, [t]op, [h]ot, [c]ontroversial): ' + srString);
 
5206                 } else if (command === 'tag') {
 
5207                         if ((typeof this.cmdLineTagUsername === 'undefined') || (this.cmdLineTagUsername === '')) {
 
5208                                 var searchArea = modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex];
 
5209                                 var authorLink = searchArea.querySelector('a.author');
 
5210                                 this.cmdLineTagUsername = authorLink.innerHTML;
 
5212                         str = 'tag user ' + this.cmdLineTagUsername;
 
5214                                 str += ' as: ' + val;
 
5216                         this.cmdLineShowTip(str);
 
5217                 } else if (command === 'user') {
 
5218                         str = 'go to profile';
 
5220                                 str += ' for: ' + val;
 
5222                         this.cmdLineShowTip(str);
 
5223                 } else if (command === 'sw') {
 
5224                         this.cmdLineShowTip('Switch users to: ' + val);
 
5225                 } else if (command === 'm') {
 
5226                         this.cmdLineShowTip('View messages.');
 
5227                 } else if (command === 'mm') {
 
5228                         this.cmdLineShowTip('View moderator mail.');
 
5229                 } else if (command === 'ls') {
 
5230                         this.cmdLineShowTip('Toggle lightSwitch.');
 
5231                 } else if (command === 'nsfw') {
 
5232                         this.cmdLineShowTip('Toggle nsfw filter on or off');
 
5233                 } else if (command === 'srstyle') {
 
5234                         str = 'toggle subreddit style';
 
5236                                 str += ' for: ' + val;
 
5238                                 if (RESUtils.currentSubreddit()) {
 
5239                                         str += ' for: ' + RESUtils.currentSubreddit();
 
5242                         this.cmdLineShowTip(str);
 
5243                 } else if (command === 'search') {
 
5244                         this.cmdLineShowTip('Search RES settings for: ' + val);
 
5245                 } else if (command === 'XHRCache') {
 
5246                         this.cmdLineShowTip('clear - clear the cache (use if inline images aren\'t loading properly)');
 
5247                 } else if (command.slice(0,1) === '?') {
 
5248                         str = 'Currently supported commands:';
 
5250                         str += '<li>r/[subreddit] - navigates to subreddit</li>';
 
5251                         str += '<li>/n, /t, /h or /c - goes to new, top, hot or controversial sort of current subreddit</li>';
 
5252                         str += '<li>[number] - navigates to the link with that number (comments pages) or rank (link pages)</li>';
 
5253                         str += '<li>tag [text] - tags author of currently selected link/comment as text</li>';
 
5254                         str += '<li>sw [username] - switch users to [username]</li>';
 
5255                         str += '<li>user [username] or u/[username] - view profile for [username]</li>';
 
5256                         str += '<li>u/[username]/m/[multi] - view the multireddit [multi] curated by [username]</li>';
 
5257                         str += '<li>m/[multi] - view your multireddit [multi]';
 
5258                         str += '<li>m - go to inbox</li>';
 
5259                         str += '<li>mm - go to moderator mail</li>';
 
5260                         str += '<li>ls - toggle lightSwitch</li>';
 
5261                         str += '<li>nsfw [on|off] - toggle nsfw filter on/off</li>';
 
5262                         str += '<li>srstyle [subreddit] [on|off] - toggle subreddit style on/off (if no subreddit is specified, uses current subreddit)</li>';
 
5263                         str += '<li>search [words to search for]- search RES settings</li>';
 
5264                         str += '<li>RESStorage [get|set|update|remove] [key] [value] - For debug use only, you shouldn\'t mess with this unless you know what you\'re doing.</li>';
 
5265                         str += '<li>XHRCache clear - manipulate the XHR cache </li>';
 
5267                         this.cmdLineShowTip(str);
 
5269                         this.cmdLineShowTip('');
 
5272         cmdLineShowTip: function(str) {
 
5273                 $(this.commandLineInputTip).html(str);
 
5275         cmdLineShowError: function(str) {
 
5276                 $(this.commandLineInputError).html(str);
 
5278         toggleCmdLine: function(force) {
 
5279                 var open = ((force == null || force) && (this.commandLineWidget.style.display !== 'block'));
 
5280                 delete this.cmdLineTagUsername;
 
5282                         this.cmdLineShowError('');
 
5283                         this.commandLineWidget.style.display = 'block';
 
5284                         setTimeout(function() {
 
5285                                 modules['keyboardNav'].commandLineInput.focus();
 
5287                         this.commandLineInput.value = '';
 
5289                         modules['keyboardNav'].commandLineInput.blur();
 
5290                         this.commandLineWidget.style.display = 'none';
 
5292                 modules['styleTweaks'].setSRStyleToggleVisibility(!open, 'cmdline');
 
5294         cmdLineSubmit: function(e) {
 
5296                 $(modules['keyboardNav'].commandLineInputError).html('');
 
5297                 var theInput = modules['keyboardNav'].commandLineInput.value;
 
5298                 // see what kind of input it is:
 
5299                 if (theInput.match('^\/?r/')) {
 
5300                         // subreddit? (r/subreddit or /r/subreddit)
 
5301                         theInput = theInput.replace(/^\/?r\//,'');
 
5302                         location.href = '/r/'+theInput;
 
5303                 } else if (theInput.match('^\/?m/')) {
 
5304                         theInput = theInput.replace(/^\/?m\//,'');
 
5305                         location.href = '/me/m/'+theInput;
 
5306                 } else if (theInput.match('^\/?u/')) {
 
5307                         // subreddit? (r/subreddit or /r/subreddit)
 
5308                         theInput = theInput.replace(/^\/?u\//,'');
 
5309                         location.href = '/u/'+theInput;
 
5310                 } else if (theInput.indexOf('/') === 0) {
 
5312                         theInput = theInput.slice(1);
 
5324                                         theInput = 'controversial';
 
5327                         validSorts = ['new','top','hot','controversial'];
 
5328                         if (validSorts.indexOf(theInput) !== -1) {
 
5329                                 if (RESUtils.currentUserProfile()) {
 
5330                                         location.href = '/user/'+RESUtils.currentUserProfile()+'?sort='+theInput;
 
5331                                 } else if (RESUtils.currentSubreddit()) {
 
5332                                         location.href = '/r/'+RESUtils.currentSubreddit()+'/'+theInput;
 
5334                                         location.href = '/'+theInput;
 
5337                                 modules['keyboardNav'].cmdLineShowError('invalid sort command - must be [n]ew, [t]op, [h]ot or [c]ontroversial');
 
5340                 } else if (!(isNaN(parseInt(theInput, 10)))) {
 
5341                         if (RESUtils.pageType() === 'comments') {
 
5342                                 // comment link number? (integer)
 
5343                                 modules['keyboardNav'].commentLink(parseInt(theInput, 10)-1);
 
5344                         } else if (RESUtils.pageType() === 'linklist') {
 
5345                                 modules['keyboardNav'].keyUnfocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
 
5346                                 modules['keyboardNav'].activeIndex = parseInt(theInput, 10) - 1;
 
5347                                 modules['keyboardNav'].keyFocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
 
5348                                 modules['keyboardNav'].followLink();
 
5351                         var splitWords = theInput.split(' ');
 
5352                         var command = splitWords[0];
 
5353                         splitWords.splice(0,1);
 
5354                         var val = splitWords.join(' ');
 
5357                                         var searchArea = modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex];
 
5358                                         var tagLink = searchArea.querySelector('a.userTagLink');
 
5360                                                 RESUtils.click(tagLink);
 
5361                                                 setTimeout(function() {
 
5363                                                                 document.getElementById('userTaggerTag').value = val;
 
5369                                         // switch accounts (username is required)
 
5370                                         if (val.length <= 1) {
 
5371                                                 modules['keyboardNav'].cmdLineShowError('No username specified.');
 
5374                                                 // first make sure the account exists...
 
5375                                                 var accounts = modules['accountSwitcher'].options.accounts.value;
 
5377                                                 for (var i=0, len=accounts.length; i<len; i++) {
 
5378                                                         var thisPair = accounts[i];
 
5379                                                         if (thisPair[0] == val) {
 
5384                                                         modules['accountSwitcher'].switchTo(val);
 
5386                                                         modules['keyboardNav'].cmdLineShowError('No such username in accountSwitcher.');
 
5392                                         // view profile for username (username is required)
 
5393                                         if (val.length <= 1) {
 
5394                                                 modules['keyboardNav'].cmdLineShowError('No username specified.');
 
5397                                                 location.href = '/user/' + val;
 
5401                                         // view JSON data for username (username is required)
 
5402                                         if (val.length <= 1) {
 
5403                                                 modules['keyboardNav'].cmdLineShowError('No username specified.');
 
5408                                                         url:    location.protocol + "//"+location.hostname+"/user/" + val + "/about.json?app=res",
 
5409                                                         onload: function(response) {
 
5410                                                                 alert(response.responseText);
 
5416                                         // get CSS code for a badge for username (username is required)
 
5417                                         if (val.length <= 1) {
 
5418                                                 modules['keyboardNav'].cmdLineShowError('No username specified.');
 
5423                                                         url:    location.protocol + "//"+location.hostname+"/user/" + val + "/about.json?app=res",
 
5424                                                         onload: function(response) {
 
5425                                                                 var thisResponse = JSON.parse(response.responseText);
 
5426                                                                 var css = ', .id-t2_'+thisResponse.data.id+':before';
 
5434                                         location.href = '/message/inbox/';
 
5438                                         location.href = '/message/moderator/';
 
5441                                         // toggle lightSwitch
 
5442                                         RESUtils.click(modules['styleTweaks'].lightSwitch);
 
5446                                         switch (val && val.toLowerCase()) {
 
5454                                         modules['filteReddit'].toggleNsfwFilter(toggle, true);
 
5457                                         // toggle subreddit style
 
5460                                         splitWords = val.split(' ');
 
5461                                         if (splitWords.length === 2) {
 
5463                                                 toggleText = splitWords[1];
 
5465                                                 sr = RESUtils.currentSubreddit();
 
5466                                                 toggleText = splitWords[0];
 
5469                                                 modules['keyboardNav'].cmdLineShowError('No subreddit specified.');
 
5472                                         if (toggleText === 'on') {
 
5474                                         } else if (toggleText === 'off') {
 
5477                                                 modules['keyboardNav'].cmdLineShowError('You must specify "on" or "off".');
 
5480                                         var action = (toggle) ? 'enabled' : 'disabled';
 
5481                                         modules['styleTweaks'].toggleSubredditStyle(toggle, sr);
 
5482                                         RESUtils.notification({
 
5483                                                 header: 'Subreddit Style',
 
5484                                                 moduleID: 'styleTweaks',
 
5485                                                 message: 'Subreddit style '+action+' for subreddit: '+sr
 
5488                                 case 'notification':
 
5489                                         // test notification
 
5490                                         RESUtils.notification(val, 4000);
 
5493                                         modules['settingsNavigation'].search(val);
 
5496                                         // get or set RESStorage data
 
5497                                         splitWords = val.split(' ');
 
5498                                         if (splitWords.length < 2) {
 
5499                                                 modules['keyboardNav'].cmdLineShowError('You must specify "get [key]", "update [key]" or "set [key] [value]"');
 
5501                                                 var command = splitWords[0];
 
5502                                                 var key = splitWords[1];
 
5503                                                 if (splitWords.length > 2) {
 
5504                                                         splitWords.splice(0,2);
 
5505                                                         var value = splitWords.join(' ');
 
5507                                                 // console.log(command);
 
5508                                                 if (command === 'get') {
 
5509                                                         alert('Value of RESStorage['+key+']: <br><br><textarea rows="5" cols="50">' + RESStorage.getItem(key) + '</textarea>');
 
5510                                                 } else if (command === 'update') {
 
5511                                                         var now = new Date().getTime();
 
5512                                                         alert('Value of RESStorage['+key+']: <br><br><textarea id="RESStorageUpdate'+now+'" rows="5" cols="50">' + RESStorage.getItem(key) + '</textarea>', function() {
 
5513                                                                 var textArea = document.getElementById('RESStorageUpdate'+now);
 
5515                                                                         var value = textArea.value;
 
5516                                                                         RESStorage.setItem(key, value);
 
5519                                                 } else if (command === 'remove') {
 
5520                                                         RESStorage.removeItem(key);
 
5521                                                         alert('RESStorage['+key+'] deleted');
 
5522                                                 } else if (command === 'set') {
 
5523                                                         RESStorage.setItem(key, value);
 
5524                                                         alert('RESStorage['+key+'] set to:<br><br><textarea rows="5" cols="50">' + value + '</textarea>');
 
5526                                                         modules['keyboardNav'].cmdLineShowError('You must specify either "get [key]" or "set [key] [value]"');
 
5531                                         splitWords = val.split(' ');
 
5532                                         if (splitWords.length < 1) {
 
5533                                                 modules['keyboardNav'].cmdLineShowError('Operation required [clear]');
 
5535                                                 switch (splitWords[0]) {
 
5537                                                                 RESUtils.xhrCache('clear');
 
5540                                                                 modules['keyboardNav'].cmdLineShowError('The only accepted operation is <tt>clear</tt>');
 
5546                                         // user is already looking at help... do nothing.
 
5550                                         modules['keyboardNav'].cmdLineShowError('unknown command - type ? for help');
 
5555                 // hide the commandline tool...
 
5556                 modules['keyboardNav'].toggleCmdLine(false);
 
5558         scanPageForKeyboardLinks: function(isNew) {
 
5559                 if (typeof isNew === 'undefined') {
 
5562                 // check if we're on a link listing (regular page, subreddit page, etc) or comments listing...
 
5563                 this.pageType = RESUtils.pageType();
 
5564                 switch(this.pageType) {
 
5567                                 // get all links into an array...
 
5568                                 var siteTable = document.querySelector('#siteTable');
 
5569                                 var stMultiCheck = document.querySelectorAll('#siteTable');
 
5570                                 // stupid sponsored links create a second div with ID of sitetable (bad reddit! you should never have 2 IDs with the same name! naughty, naughty reddit!)
 
5571                                 if (stMultiCheck.length === 2) {
 
5572                                         siteTable = stMultiCheck[1];
 
5575                                         this.keyboardLinks = document.body.querySelectorAll('div.linklisting .entry');
 
5577                                                 if ((this.keyboardNavLastIndexCache[location.href]) && (this.keyboardNavLastIndexCache[location.href].index > 0)) {
 
5578                                                         this.activeIndex = this.keyboardNavLastIndexCache[location.href].index;
 
5580                                                         this.activeIndex = 0;
 
5582                                                 if ((this.keyboardNavLastIndexCache[location.href]) && (this.keyboardNavLastIndexCache[location.href].index >= this.keyboardLinks.length)) {
 
5583                                                         this.activeIndex = 0;
 
5589                                 // get all links into an array...
 
5590                                 this.keyboardLinks = document.body.querySelectorAll('#siteTable .entry, div.content > div.commentarea .entry');
 
5592                                         this.activeIndex = 0;
 
5596                                 var siteTable = document.querySelector('#siteTable');
 
5598                                         this.keyboardLinks = siteTable.querySelectorAll('.entry');
 
5599                                         this.activeIndex = 0;
 
5603                 // wire up keyboard links for mouse clicky selecty goodness...
 
5604                 if ((typeof this.keyboardLinks !== 'undefined') && (this.options.clickFocus.value)) {
 
5605                         for (var i=0, len=this.keyboardLinks.length;i<len;i++) {
 
5606                                 this.keyboardLinks[i].setAttribute('keyIndex', i);
 
5607                                 // changed parentElement to parentNode for FF3.6 compatibility.
 
5608                                 this.keyboardLinks[i].parentNode.addEventListener('click', (function(e) {
 
5609                                         var thisIndex = parseInt(this.getAttribute('keyIndex'), 10);
 
5610                                         if (modules['keyboardNav'].activeIndex != thisIndex) {
 
5611                                                 modules['keyboardNav'].keyUnfocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
 
5612                                                 modules['keyboardNav'].activeIndex = thisIndex;
 
5613                                                 modules['keyboardNav'].keyFocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
 
5615                                 }).bind(this.keyboardLinks[i]), true);
 
5617                         this.keyFocus(this.keyboardLinks[this.activeIndex]);
 
5620         recentKey: function() {
 
5621                 modules['keyboardNav'].recentKeyPress = true;
 
5622                 clearTimeout(modules['keyboardNav'].recentKey);
 
5623                 modules['keyboardNav'].recentKeyTimer = setTimeout(function() {
 
5624                         modules['keyboardNav'].recentKeyPress = false;
 
5627         keyFocus: function(obj) {
 
5628                 if ((typeof obj !== 'undefined') && (obj.classList.contains('RES-keyNav-activeElement'))) {
 
5630                 } else if (typeof obj !== 'undefined') {
 
5631                         obj.classList.add('RES-keyNav-activeElement');
 
5632                         this.activeElement = obj;
 
5633                         if ((this.pageType === 'linklist') || (this.pageType === 'profile')) {
 
5636                         if ((this.pageType === 'comments') && (this.options.commentsLinkNumbers.value)) {
 
5637                                 var links = this.getCommentLinks(obj);
 
5638                                 var annotationCount = 0;
 
5639                                 for (var i=0, len=links.length; i<len; i++) {
 
5640                                         if (!(links[i].classList.contains('madeVisible') ||
 
5641                                               links[i].classList.contains('toggleImage') ||
 
5642                                               links[i].classList.contains('noKeyNav') ||
 
5643                                               RESUtils.isCommentCode(links[i]))) {
 
5644                                                 var annotation = document.createElement('span');
 
5646                                                 $(annotation).text('['+annotationCount+'] ');
 
5647                                                 annotation.title = 'press '+annotationCount+' to open link';
 
5648                                                 annotation.classList.add('keyNavAnnotation');
 
5650                                                 if (!(hasClass(links[i],'hasListener'))) {
 
5651                                                         addClass(links[i],'hasListener');
 
5652                                                         links[i].addEventListener('click', modules['keyboardNav'].handleKeyLink, true);
 
5655                                                 if (modules['keyboardNav'].options.commentsLinkNumberPosition.value === 'right') {
 
5656                                                         insertAfter(links[i], annotation);
 
5658                                                         links[i].parentNode.insertBefore(annotation, links[i]);
 
5665         handleKeyLink: function(link) {
 
5667                 if ((modules['keyboardNav'].options.commentsLinkNewTab.value) || e.ctrlKey) {
 
5670                 if (link.classList.contains('toggleImage')) {
 
5671                         RESUtils.click(link);
 
5674                 var thisURL = link.getAttribute('href'),
 
5675                     isLocalToPage = (thisURL.indexOf('reddit') !== -1) && (thisURL.indexOf('comments') !== -1) && (thisURL.indexOf('#') !== -1);
 
5676                 if ((!isLocalToPage) && (button === 1)) {
 
5678                         if (BrowserDetect.isChrome()) {
 
5680                                         requestType: 'keyboardNav',
 
5684                                 chrome.extension.sendMessage(thisJSON);
 
5685                         } else if (BrowserDetect.isSafari()) {
 
5687                                         requestType: 'keyboardNav',
 
5691                                 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
 
5692                         } else if (BrowserDetect.isOpera()) {
 
5694                                         requestType: 'keyboardNav',
 
5698                                 opera.extension.postMessage(JSON.stringify(thisJSON));
 
5699                         } else if (BrowserDetect.isFirefox()) {
 
5701                                         requestType: 'keyboardNav',
 
5705                                 self.postMessage(thisJSON);
 
5707                                 window.open(this.getAttribute('href'));
 
5710                         location.href = this.getAttribute('href');
 
5713         keyUnfocus: function(obj) {
 
5714                 obj.classList.remove('RES-keyNav-activeElement');
 
5715                 if (this.pageType === 'comments') {
 
5716                         var annotations = obj.querySelectorAll('div.md .keyNavAnnotation');
 
5717                         for (var i=0, len=annotations.length; i<len; i++) {
 
5718                                 annotations[i].parentNode.removeChild(annotations[i]);
 
5721                 RESUtils.hover.close(false);
 
5723         drawHelp: function() {
 
5724                 var thisHelp = createElementWithID('div','keyHelp');
 
5725                 var helpTable = document.createElement('table');
 
5726                 thisHelp.appendChild(helpTable);
 
5727                 var helpTableHeader = document.createElement('thead');
 
5728                 var helpTableHeaderRow = document.createElement('tr');
 
5729                 var helpTableHeaderKey = document.createElement('th');
 
5730                 $(helpTableHeaderKey).text('Key');
 
5731                 helpTableHeaderRow.appendChild(helpTableHeaderKey);
 
5732                 var helpTableHeaderFunction = document.createElement('th');
 
5733                 $(helpTableHeaderFunction).text('Function');
 
5734                 helpTableHeaderRow.appendChild(helpTableHeaderFunction);
 
5735                 helpTableHeader.appendChild(helpTableHeaderRow);
 
5736                 helpTable.appendChild(helpTableHeader);
 
5737                 var helpTableBody = document.createElement('tbody');
 
5738                 var isLink = /^link[\d]+$/i;
 
5739                 for (var i in this.options) {
 
5740                         if ((this.options[i].type === 'keycode') && (!isLink.test(i))) {
 
5741                                 var thisRow = document.createElement('tr');
 
5742                                 var thisRowKey = document.createElement('td');
 
5743                                 var thisKeyCode = this.getNiceKeyCode(i);
 
5744                                 $(thisRowKey).html(thisKeyCode);
 
5745                                 thisRow.appendChild(thisRowKey);
 
5746                                 var thisRowDesc = document.createElement('td');
 
5747                                 $(thisRowDesc).html(this.options[i].description);
 
5748                                 thisRow.appendChild(thisRowDesc);
 
5749                                 helpTableBody.appendChild(thisRow);
 
5752                 helpTable.appendChild(helpTableBody);
 
5753                 document.body.appendChild(thisHelp);
 
5755         getNiceKeyCode: function(optionKey) {
 
5756                 var keyCodeArray = this.options[optionKey].value;
 
5757                 if (!keyCodeArray) return;
 
5759                 if (typeof keyCodeArray === 'string') {
 
5760                         keyCodeArray = parseInt(keyCodeArray);
 
5762                 if (typeof keyCodeArray === 'number') {
 
5763                         keyCodeArray = [keyCodeArray, false, false, false, false];
 
5765                 var niceKeyCode = RESUtils.niceKeyCode(keyCodeArray);
 
5768         handleKeyPress: function(e) {
 
5769                 var konamitest = (typeof konami === 'undefined') || (!konami.almostThere);
 
5770                 if ((document.activeElement.tagName === 'BODY') && (konamitest)) {
 
5771                         // comments page, or link list?
 
5772                         var keyArray = [e.keyCode, e.altKey, e.ctrlKey, e.shiftKey, e.metaKey];
 
5773                         switch(this.pageType) {
 
5777                                                 case checkKeysForEvent(e, this.options.moveUp.value):
 
5780                                                 case checkKeysForEvent(e, this.options.moveDown.value):
 
5783                                                 case checkKeysForEvent(e, this.options.moveTop.value):
 
5786                                                 case checkKeysForEvent(e, this.options.moveBottom.value):
 
5789                                                 case checkKeysForEvent(e, this.options.followLink.value):
 
5792                                                 case checkKeysForEvent(e, this.options.followLinkNewTab.value):
 
5794                                                         this.followLink(true);
 
5796                                                 case checkKeysForEvent(e, this.options.followComments.value):
 
5797                                                         this.followComments();
 
5799                                                 case checkKeysForEvent(e, this.options.followCommentsNewTab.value):
 
5801                                                         this.followComments(true);
 
5803                                                 case checkKeysForEvent(e, this.options.toggleExpando.value):
 
5804                                                         this.toggleExpando();
 
5806                                                 case checkKeysForEvent(e, this.options.imageSizeUp.value):
 
5809                                                 case checkKeysForEvent(e, this.options.imageSizeDown.value):
 
5810                                                         this.imageSizeDown();
 
5812                                                 case checkKeysForEvent(e, this.options.imageSizeUpFine.value):
 
5813                                                         this.imageSizeUp(true);
 
5815                                                 case checkKeysForEvent(e, this.options.imageSizeDownFine.value):
 
5816                                                         this.imageSizeDown(true);
 
5818                                                 case checkKeysForEvent(e, this.options.previousGalleryImage.value):
 
5819                                                         this.previousGalleryImage();
 
5821                                                 case checkKeysForEvent(e, this.options.nextGalleryImage.value):
 
5822                                                         this.nextGalleryImage();
 
5824                                                 case checkKeysForEvent(e, this.options.toggleViewImages.value):
 
5825                                                         this.toggleViewImages();
 
5827                                                 case checkKeysForEvent(e, this.options.followLinkAndCommentsNewTab.value):
 
5829                                                         this.followLinkAndComments();
 
5831                                                 case checkKeysForEvent(e, this.options.followLinkAndCommentsNewTabBG.value):
 
5833                                                         this.followLinkAndComments(true);
 
5835                                                 case checkKeysForEvent(e, this.options.upVote.value):
 
5838                                                 case checkKeysForEvent(e, this.options.downVote.value):
 
5839                                                         this.downVote(true);
 
5841                                                 case checkKeysForEvent(e, this.options.save.value):
 
5844                                                 case checkKeysForEvent(e, this.options.inbox.value):
 
5848                                                 case checkKeysForEvent(e, this.options.inboxNewTab.value):
 
5852                                                 case checkKeysForEvent(e, this.options.profile.value):
 
5856                                                 case checkKeysForEvent(e, this.options.profileNewTab.value):
 
5860                                                 case checkKeysForEvent(e, this.options.frontPage.value):
 
5864                                                 case checkKeysForEvent(e, this.options.nextPage.value):
 
5868                                                 case checkKeysForEvent(e, this.options.prevPage.value):
 
5872                                                 case checkKeysForEvent(e, this.options.toggleHelp.value):
 
5875                                                 case checkKeysForEvent(e, this.options.toggleCmdLine.value):
 
5876                                                         this.toggleCmdLine();
 
5878                                                 case checkKeysForEvent(e, this.options.hide.value):
 
5881                                                 case checkKeysForEvent(e, this.options.followSubreddit.value):
 
5882                                                         this.followSubreddit();
 
5884                                                 case checkKeysForEvent(e, this.options.followSubredditNewTab.value):
 
5885                                                         this.followSubreddit(true);
 
5888                                                         // do nothing. unrecognized key.
 
5894                                                 case checkKeysForEvent(e, this.options.toggleHelp.value):
 
5897                                                 case checkKeysForEvent(e, this.options.toggleCmdLine.value):
 
5898                                                         this.toggleCmdLine();
 
5900                                                 case checkKeysForEvent(e, this.options.moveUp.value):
 
5903                                                 case checkKeysForEvent(e, this.options.moveDown.value):
 
5906                                                 case checkKeysForEvent(e, this.options.moveUpSibling.value):
 
5907                                                         this.moveUpSibling();
 
5909                                                 case checkKeysForEvent(e, this.options.moveDownSibling.value):
 
5910                                                         this.moveDownSibling();
 
5912                                                 case checkKeysForEvent(e, this.options.moveUpThread.value):
 
5913                                                         this.moveUpThread();
 
5915                                                 case checkKeysForEvent(e, this.options.moveDownThread.value):
 
5916                                                         this.moveDownThread();
 
5918                                                 case checkKeysForEvent(e, this.options.moveToTopComment.value):
 
5919                                                         this.moveToTopComment();
 
5921                                                 case checkKeysForEvent(e, this.options.moveToParent.value):
 
5922                                                         this.moveToParent();
 
5924                                                 case checkKeysForEvent(e, this.options.showParents.value):
 
5927                                                 case checkKeysForEvent(e, this.options.toggleChildren.value):
 
5928                                                         this.toggleChildren();
 
5930                                                 case checkKeysForEvent(e, this.options.followLinkNewTab.value):
 
5931                                                         // only execute if the link is selected on a comments page...
 
5932                                                         if (this.activeIndex === 0) {
 
5934                                                                 this.followLink(true);
 
5937                                                 case checkKeysForEvent(e, this.options.save.value):
 
5938                                                         if (this.activeIndex === 0) {
 
5944                                                 case checkKeysForEvent(e, this.options.toggleExpando.value):
 
5945                                                         this.toggleAllExpandos();
 
5947                                                 case checkKeysForEvent(e, this.options.previousGalleryImage.value):
 
5948                                                         this.previousGalleryImage();
 
5950                                                 case checkKeysForEvent(e, this.options.imageSizeUp.value):
 
5953                                                 case checkKeysForEvent(e, this.options.imageSizeDown.value):
 
5954                                                         this.imageSizeDown();
 
5956                                                 case checkKeysForEvent(e, this.options.imageSizeUpFine.value):
 
5957                                                         this.imageSizeUp(true);
 
5959                                                 case checkKeysForEvent(e, this.options.imageSizeDownFine.value):
 
5960                                                         this.imageSizeDown(true);
 
5962                                                 case checkKeysForEvent(e, this.options.nextGalleryImage.value):
 
5963                                                         this.nextGalleryImage();
 
5965                                                 case checkKeysForEvent(e, this.options.toggleViewImages.value):
 
5966                                                         this.toggleViewImages();
 
5968                                                 case checkKeysForEvent(e, this.options.upVote.value):
 
5971                                                 case checkKeysForEvent(e, this.options.downVote.value):
 
5974                                                 case checkKeysForEvent(e, this.options.reply.value):
 
5978                                                 case checkKeysForEvent(e, this.options.inbox.value):
 
5982                                                 case checkKeysForEvent(e, this.options.inboxNewTab.value):
 
5986                                                 case checkKeysForEvent(e, this.options.profile.value):
 
5990                                                 case checkKeysForEvent(e, this.options.profileNewTab.value):
 
5994                                                 case checkKeysForEvent(e, this.options.frontPage.value):
 
5998                                                 case checkKeysForEvent(e, this.options.subredditFrontPage.value):
 
6000                                                         this.frontPage(true);
 
6002                                                 case checkKeysForEvent(e, this.options.link1.value):
 
6004                                                         this.commentLink(0);
 
6006                                                 case checkKeysForEvent(e, this.options.link2.value):
 
6008                                                         this.commentLink(1);
 
6010                                                 case checkKeysForEvent(e, this.options.link3.value):
 
6012                                                         this.commentLink(2);
 
6014                                                 case checkKeysForEvent(e, this.options.link4.value):
 
6016                                                         this.commentLink(3);
 
6018                                                 case checkKeysForEvent(e, this.options.link5.value):
 
6020                                                         this.commentLink(4);
 
6022                                                 case checkKeysForEvent(e, this.options.link6.value):
 
6024                                                         this.commentLink(5);
 
6026                                                 case checkKeysForEvent(e, this.options.link7.value):
 
6028                                                         this.commentLink(6);
 
6030                                                 case checkKeysForEvent(e, this.options.link8.value):
 
6032                                                         this.commentLink(7);
 
6034                                                 case checkKeysForEvent(e, this.options.link9.value):
 
6036                                                         this.commentLink(8);
 
6038                                                 case checkKeysForEvent(e, this.options.link10.value):
 
6040                                                         this.commentLink(9);
 
6043                                                         // do nothing. unrecognized key.
 
6049                                                 case checkKeysForEvent(e, this.options.toggleHelp.value):
 
6052                                                 case checkKeysForEvent(e, this.options.toggleCmdLine.value):
 
6053                                                         this.toggleCmdLine();
 
6055                                                 case checkKeysForEvent(e, this.options.moveUp.value):
 
6058                                                 case checkKeysForEvent(e, this.options.moveDown.value):
 
6061                                                 case checkKeysForEvent(e, this.options.toggleChildren.value):
 
6062                                                         this.toggleChildren();
 
6064                                                 case checkKeysForEvent(e, this.options.upVote.value):
 
6067                                                 case checkKeysForEvent(e, this.options.downVote.value):
 
6070                                                 case checkKeysForEvent(e, this.options.reply.value):
 
6074                                                 case checkKeysForEvent(e, this.options.frontPage.value):
 
6079                                                         // do nothing. unrecognized key.
 
6085                         // console.log('ignored keypress');
 
6088         toggleHelp: function() {
 
6089                 (document.getElementById('keyHelp').style.display === 'block') ? this.hideHelp() : this.showHelp();
 
6091         showHelp: function() {
 
6093                 RESUtils.fadeElementIn(document.getElementById('keyHelp'), 0.3);
 
6094                 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'keyboardnavhelp');
 
6096         hideHelp: function() {
 
6098                 RESUtils.fadeElementOut(document.getElementById('keyHelp'), 0.3);
 
6099                 modules['styleTweaks'].setSRStyleToggleVisibility(true,  'keyboardnavhelp');
 
6102                 // find the hide link and click it...
 
6103                 var hideLink = this.keyboardLinks[this.activeIndex].querySelector('form.hide-button > span > a');
 
6104                 RESUtils.click(hideLink);
 
6105                 // if ((this.options.onHideMoveDown.value) && (!modules['betteReddit'].options.fixHideLink.value)) {
 
6106                 if (this.options.onHideMoveDown.value) {
 
6110         followSubreddit: function(newWindow) {
 
6111                 // find the subreddit link and click it...
 
6112                 var srLink = this.keyboardLinks[this.activeIndex].querySelector('A.subreddit');
 
6114                         var thisHREF = srLink.getAttribute('href');
 
6116                                 var button = (this.options.followLinkNewTabFocus.value) ? 0 : 1,
 
6118                                 if (BrowserDetect.isChrome()) {
 
6120                                                 requestType: 'keyboardNav',
 
6124                                         chrome.extension.sendMessage(thisJSON);
 
6125                                 } else if (BrowserDetect.isSafari()) {
 
6127                                                 requestType: 'keyboardNav',
 
6131                                         safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
 
6132                                 } else if (BrowserDetect.isOpera()) {
 
6134                                                 requestType: 'keyboardNav',
 
6138                                         opera.extension.postMessage(JSON.stringify(thisJSON));
 
6139                                 } else if (BrowserDetect.isFirefox()) {
 
6141                                                 requestType: 'keyboardNav',
 
6145                                         self.postMessage(thisJSON);
 
6147                                         window.open(thisHREF);
 
6150                                 location.href = thisHREF;
 
6154         moveUp: function() {
 
6155                 if (this.activeIndex > 0) {
 
6156                         this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
 
6158                         var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
 
6159                         // skip over hidden elements...
 
6160                         while ((thisXY.x === 0) && (thisXY.y === 0) && (this.activeIndex > 0)) {
 
6162                                 thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
 
6164                         this.keyFocus(this.keyboardLinks[this.activeIndex]);
 
6165                         if ((!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) || (this.options.scrollStyle.value === 'top')) {
 
6166                                 RESUtils.scrollTo(0,thisXY.y);
 
6169                         modules['keyboardNav'].recentKey();
 
6172         moveDown: function() {
 
6173                 if (this.activeIndex < this.keyboardLinks.length-1) {
 
6174                         this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
 
6176                         var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
 
6177                         // skip over hidden elements...
 
6178                         while ((thisXY.x === 0) && (thisXY.y === 0) && (this.activeIndex < this.keyboardLinks.length-1)) {
 
6180                                 thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
 
6182                         this.keyFocus(this.keyboardLinks[this.activeIndex]);
 
6183                         // console.log('xy: ' + RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]).toSource());
 
6185                         if ((!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) || (this.options.scrollTop.value)) {
 
6186                                 RESUtils.scrollTo(0,thisXY.y);
 
6189                         if (this.options.scrollStyle.value === 'top') {
 
6190                                 RESUtils.scrollTo(0,thisXY.y);
 
6191                         } else if ((!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex])))) {
 
6192                                 var thisHeight = this.keyboardLinks[this.activeIndex].offsetHeight;
 
6193                                 if (this.options.scrollStyle.value === 'page') {
 
6194                                         RESUtils.scrollTo(0,thisXY.y);
 
6196                                         RESUtils.scrollTo(0,thisXY.y - window.innerHeight + thisHeight + 5);
 
6199                         if ((RESUtils.pageType() === 'linklist') && (this.activeIndex == (this.keyboardLinks.length-1) && (modules['neverEndingReddit'].isEnabled() && modules['neverEndingReddit'].options.autoLoad.value))) {
 
6202                         modules['keyboardNav'].recentKey();
 
6205         moveTop: function() {
 
6206                         this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
 
6207                         this.activeIndex = 0;
 
6208                         this.keyFocus(this.keyboardLinks[this.activeIndex]);
 
6209                         var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
 
6210                         if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
 
6211                                 RESUtils.scrollTo(0,thisXY.y);
 
6213                         modules['keyboardNav'].recentKey();
 
6215         moveBottom: function() {
 
6216                         this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
 
6217                         this.activeIndex = this.keyboardLinks.length-1;
 
6218                         this.keyFocus(this.keyboardLinks[this.activeIndex]);
 
6219                         var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
 
6220                         if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
 
6221                                 RESUtils.scrollTo(0,thisXY.y);
 
6223                         modules['keyboardNav'].recentKey();
 
6225         moveDownSibling: function() {
 
6226                 if (this.activeIndex < this.keyboardLinks.length-1) {
 
6227                         this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
 
6228                         var thisParent = this.keyboardLinks[this.activeIndex].parentNode;
 
6229                         var childCount = thisParent.querySelectorAll('.entry').length;
 
6230                         this.activeIndex += childCount;
 
6231                         // skip over hidden elements...
 
6232                         var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
 
6233                         while ((thisXY.x === 0) && (thisXY.y === 0) && (this.activeIndex < this.keyboardLinks.length-1)) {
 
6235                                 thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
 
6237                         if ((this.pageType === 'linklist') || (this.pageType === 'profile')) {
 
6240                         this.keyFocus(this.keyboardLinks[this.activeIndex]);
 
6241                         if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
 
6242                                 RESUtils.scrollTo(0,thisXY.y);
 
6245                 modules['keyboardNav'].recentKey();
 
6247         moveUpSibling: function() {
 
6248                 if (this.activeIndex < this.keyboardLinks.length-1) {
 
6249                         this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
 
6250                         var thisParent = this.keyboardLinks[this.activeIndex].parentNode,
 
6252                         if (thisParent.previousSibling !== null) {
 
6253                                 childCount = thisParent.previousSibling.previousSibling.querySelectorAll('.entry').length;
 
6257                         this.activeIndex -= childCount;
 
6258                         // skip over hidden elements...
 
6259                         var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
 
6260                         while ((thisXY.x === 0) && (thisXY.y === 0) && (this.activeIndex < this.keyboardLinks.length-1)) {
 
6262                                 thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
 
6264                         if ((this.pageType === 'linklist') || (this.pageType === 'profile')) {
 
6267                         this.keyFocus(this.keyboardLinks[this.activeIndex]);
 
6268                         if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
 
6269                                 RESUtils.scrollTo(0,thisXY.y);
 
6272                 modules['keyboardNav'].recentKey();
 
6274         moveUpThread: function() {
 
6275                 if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
 
6276                         this.moveToTopComment();
 
6278                 this.moveUpSibling();
 
6280         moveDownThread: function() {
 
6281                 if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
 
6282                         this.moveToTopComment();
 
6284                 this.moveDownSibling();
 
6286         moveToTopComment: function() {
 
6287                 if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
 
6288                         var firstParent = this.keyboardLinks[this.activeIndex].parentNode;
 
6289                         //goes up to the root of the current thread
 
6290                         while (!firstParent.parentNode.parentNode.parentNode.classList.contains('content') && (firstParent !== null)) {
 
6291                                 this.moveToParent();
 
6292                                 firstParent = this.keyboardLinks[this.activeIndex].parentNode;
 
6296         moveToParent: function() {
 
6297                 if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
 
6298                         var firstParent = this.keyboardLinks[this.activeIndex].parentNode;
 
6299                         // check if we're at the top parent, first... if the great grandparent has a class of content, do nothing.
 
6300                         if (!firstParent.parentNode.parentNode.parentNode.classList.contains('content')) {
 
6301                                 if (firstParent !== null) {
 
6302                                         this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
 
6303                                         var thisParent = firstParent.parentNode.parentNode.previousSibling;
 
6304                                         var newKeyIndex = parseInt(thisParent.getAttribute('keyindex'), 10);
 
6305                                         this.activeIndex = newKeyIndex;
 
6306                                         this.keyFocus(this.keyboardLinks[this.activeIndex]);
 
6307                                         var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
 
6308                                         if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
 
6309                                                 RESUtils.scrollTo(0,thisXY.y);
 
6314                 modules['keyboardNav'].recentKey();
 
6316         showParents: function() {
 
6317                 if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
 
6318                         var firstParent = this.keyboardLinks[this.activeIndex].parentNode;
 
6319                         if (firstParent != null) {
 
6320                                 var button = $(this.keyboardLinks[this.activeIndex]).find('.buttons :not(:first-child) .bylink:first').get(0);
 
6321                                 RESUtils.hover.begin(button, {}, modules['showParent'].showCommentHover, {});
 
6325         toggleChildren: function() {
 
6326                 if (this.activeIndex === 0) {
 
6327                         // Ahh, we're not in a comment, but in the main story... that key should follow the link.
 
6330                         // find out if this is a collapsed or uncollapsed view...
 
6331                         var thisCollapsed = this.keyboardLinks[this.activeIndex].querySelector('div.collapsed');
 
6332                         var thisNonCollapsed = this.keyboardLinks[this.activeIndex].querySelector('div.noncollapsed');
 
6333                         if (thisCollapsed.style.display !== 'none') {
 
6334                                 thisToggle = thisCollapsed.querySelector('a.expand');
 
6336                                 // check if this is a "show more comments" box, or just contracted content...
 
6337                                 moreComments = thisNonCollapsed.querySelector('span.morecomments > a');
 
6339                                         thisToggle = moreComments;
 
6341                                         thisToggle = thisNonCollapsed.querySelector('a.expand');
 
6343                                 // 'continue this thread' links
 
6344                                 contThread = thisNonCollapsed.querySelector('span.deepthread > a');
 
6346                                         thisToggle = contThread;
 
6349                         RESUtils.click(thisToggle);
 
6352         toggleExpando: function() {
 
6353                 var thisExpando = this.keyboardLinks[this.activeIndex].querySelector('.expando-button');
 
6355                         RESUtils.click(thisExpando);
 
6356                         if (this.options.scrollOnExpando.value) {
 
6357                                 var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
 
6358                                 RESUtils.scrollTo(0,thisXY.y);
 
6362         imageResize: function(factor) {
 
6363                 var images = $(this.activeElement).find('.RESImage.loaded'),
 
6366                 for (var i=0, len=images.length; i<len; i++) {
 
6367                         thisWidth = images[i].width;
 
6368                         modules['showImages'].resizeImage(images[i], thisWidth + factor);
 
6371         imageSizeUp: function(fineControl) {
 
6372                 var factor = (fineControl) ? 50 : 150;
 
6373                 this.imageResize(factor);
 
6375         imageSizeDown: function(fineControl) {
 
6376                 var factor = (fineControl) ? -50 : -150;
 
6377                 this.imageResize(factor);
 
6379         previousGalleryImage: function() {
 
6380                 var previousButton = this.keyboardLinks[this.activeIndex].querySelector('.RESGalleryControls .previous');
 
6381                 if (previousButton) {
 
6382                         RESUtils.click(previousButton);
 
6385         nextGalleryImage: function() {
 
6386                 var nextButton = this.keyboardLinks[this.activeIndex].querySelector('.RESGalleryControls .next');
 
6388                         RESUtils.click(nextButton);
 
6391         toggleViewImages: function() {
 
6392                 var thisViewImages = document.body.querySelector('#viewImagesButton');
 
6393                 if (thisViewImages) {
 
6394                         RESUtils.click(thisViewImages);
 
6397         toggleAllExpandos: function() {
 
6398                 var thisExpandos = this.keyboardLinks[this.activeIndex].querySelectorAll('.expando-button');
 
6400                         for (var i=0,len=thisExpandos.length; i<len; i++) {
 
6401                                 RESUtils.click(thisExpandos[i]);
 
6405         followLink: function(newWindow) {
 
6406                 var thisA = this.keyboardLinks[this.activeIndex].querySelector('a.title');
 
6407                 var thisHREF = thisA.getAttribute('href');
 
6408                 // console.log(thisA);
 
6410                         var button = (this.options.followLinkNewTabFocus.value) ? 0 : 1,
 
6412                         if (BrowserDetect.isChrome()) {
 
6414                                         requestType: 'keyboardNav',
 
6418                                 chrome.extension.sendMessage(thisJSON);
 
6419                         } else if (BrowserDetect.isSafari()) {
 
6421                                         requestType: 'keyboardNav',
 
6425                                 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
 
6426                         } else if (BrowserDetect.isOpera()) {
 
6428                                         requestType: 'keyboardNav',
 
6432                                 opera.extension.postMessage(JSON.stringify(thisJSON));
 
6433                         } else if (BrowserDetect.isFirefox()) {
 
6435                                         requestType: 'keyboardNav',
 
6439                                 self.postMessage(thisJSON);
 
6441                                 window.open(thisHREF);
 
6444                         location.href = thisHREF;
 
6447         followComments: function(newWindow) {
 
6448                 var thisA = this.keyboardLinks[this.activeIndex].querySelector('a.comments'),
 
6449                     thisHREF = thisA.getAttribute('href');
 
6452                         if (BrowserDetect.isChrome()) {
 
6454                                         requestType: 'keyboardNav',
 
6457                                 chrome.extension.sendMessage(thisJSON);
 
6458                         } else if (BrowserDetect.isSafari()) {
 
6460                                         requestType: 'keyboardNav',
 
6463                                 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
 
6464                         } else if (BrowserDetect.isOpera()) {
 
6466                                         requestType: 'keyboardNav',
 
6469                                 opera.extension.postMessage(JSON.stringify(thisJSON));
 
6470                         } else if (BrowserDetect.isFirefox()) {
 
6472                                         requestType: 'keyboardNav',
 
6475                                 self.postMessage(thisJSON);
 
6477                                 window.open(thisHREF);
 
6480                         location.href = thisHREF;
 
6483         followLinkAndComments: function(background) {
 
6484                 // find the [l+c] link and click it...
 
6485                 var lcLink = this.keyboardLinks[this.activeIndex].querySelector('.redditSingleClick');
 
6486                 RESUtils.mousedown(lcLink, background);
 
6488         upVote: function(link) {
 
6489                 if (typeof this.keyboardLinks[this.activeIndex] === 'undefined') return false;
 
6492                 if (this.keyboardLinks[this.activeIndex].previousSibling.tagName === 'A') {
 
6493                         upVoteButton = this.keyboardLinks[this.activeIndex].previousSibling.previousSibling.querySelector('div.up') || this.keyboardLinks[this.activeIndex].previousSibling.previousSibling.querySelector('div.upmod');
 
6495                         upVoteButton = this.keyboardLinks[this.activeIndex].previousSibling.querySelector('div.up') || this.keyboardLinks[this.activeIndex].previousSibling.querySelector('div.upmod');
 
6498                 RESUtils.click(upVoteButton);
 
6500                 if (link && this.options.onVoteMoveDown.value) {
 
6504         downVote: function(link) {
 
6505                 if (typeof this.keyboardLinks[this.activeIndex] === 'undefined') return false;
 
6508                 if (this.keyboardLinks[this.activeIndex].previousSibling.tagName === 'A') {
 
6509                         downVoteButton = this.keyboardLinks[this.activeIndex].previousSibling.previousSibling.querySelector('div.down') || this.keyboardLinks[this.activeIndex].previousSibling.previousSibling.querySelector('div.downmod');
 
6511                         downVoteButton = this.keyboardLinks[this.activeIndex].previousSibling.querySelector('div.down') || this.keyboardLinks[this.activeIndex].previousSibling.querySelector('div.downmod');
 
6514                 RESUtils.click(downVoteButton);
 
6516                 if (link && this.options.onVoteMoveDown.value) {
 
6520         saveLink: function() {
 
6521                 var saveLink = this.keyboardLinks[this.activeIndex].querySelector('form.save-button > span > a');
 
6522                 if (saveLink) RESUtils.click(saveLink);
 
6524         saveComment: function() {
 
6525                 var saveComment = this.keyboardLinks[this.activeIndex].querySelector('.saveComments');
 
6526                 if (saveComment) RESUtils.click(saveComment);
 
6529                 // activeIndex = 0 means we're at the original post, not a comment
 
6530                 if ((this.activeIndex > 0) || (RESUtils.pageType() !== 'comments')) {
 
6531                         if ((RESUtils.pageType() === 'comments') && (this.activeIndex === 0) && (! location.href.match('/message/'))) {
 
6532                                 $('.usertext-edit textarea:first').focus();
 
6534                                 var commentButtons = this.keyboardLinks[this.activeIndex].querySelectorAll('ul.buttons > li > a');
 
6535                                 for (var i=0, len=commentButtons.length;i<len;i++) {
 
6536                                         if (commentButtons[i].innerHTML === 'reply') {
 
6537                                                 RESUtils.click(commentButtons[i]);
 
6542                         infoBar = document.body.querySelector('.infobar');
 
6543                         // We're on the original post, so shift keyboard focus to the comment reply box.
 
6545                                 // uh oh, we must be in a subpage, there is no first comment box. The user probably wants to reply to the OP. Let's take them to the comments page.
 
6546                                 var commentButton = this.keyboardLinks[this.activeIndex].querySelector('ul.buttons > li > a.comments');
 
6547                                 location.href = commentButton.getAttribute('href');
 
6549                                 var firstCommentBox = document.querySelector('.commentarea textarea[name=text]');
 
6550                                 firstCommentBox.focus();
 
6554         navigateTo: function(newWindow,thisHREF) {
 
6557                         if (BrowserDetect.isChrome()) {
 
6559                                         requestType: 'keyboardNav',
 
6562                                 chrome.extension.sendMessage(thisJSON);
 
6563                         } else if (BrowserDetect.isSafari()) {
 
6565                                         requestType: 'keyboardNav',
 
6568                                 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
 
6569                         } else if (BrowserDetect.isOpera()) {
 
6571                                         requestType: 'keyboardNav',
 
6574                                 opera.extension.postMessage(JSON.stringify(thisJSON));
 
6576                                 window.open(thisHREF);
 
6579                         location.href = thisHREF;
 
6582         inbox: function(newWindow) {
 
6583                 var thisHREF = location.protocol + '//'+location.hostname+'/message/inbox/';
 
6584                 modules['keyboardNav'].navigateTo(newWindow,thisHREF);
 
6586         profile: function(newWindow) {
 
6587                 var thisHREF = location.protocol + '//'+location.hostname+'/user/'+RESUtils.loggedInUser();
 
6588                 modules['keyboardNav'].navigateTo(newWindow,thisHREF);
 
6590         frontPage: function(subreddit) {
 
6591                 var newhref = location.protocol + '//'+location.hostname+'/';
 
6593                         newhref += 'r/' + RESUtils.currentSubreddit();
 
6595                 location.href = newhref;
 
6597         nextPage: function() {
 
6598                 // if Never Ending Reddit is enabled, just scroll to the bottom.  Otherwise, click the 'next' link.
 
6599                 if ((modules['neverEndingReddit'].isEnabled()) && (modules['neverEndingReddit'].progressIndicator)) {
 
6600                         RESUtils.click(modules['neverEndingReddit'].progressIndicator);
 
6603                         // get the first link to the next page of reddit...
 
6604                         var nextPrevLinks = document.body.querySelectorAll('.content .nextprev a');
 
6605                         if (nextPrevLinks.length > 0) {
 
6606                                 var nextLink = nextPrevLinks[nextPrevLinks.length-1];
 
6607                                 // RESUtils.click(nextLink);
 
6608                                 location.href = nextLink.getAttribute('href');
 
6612         prevPage: function() {
 
6613                 // if Never Ending Reddit is enabled, do nothing.  Otherwise, click the 'prev' link.
 
6614                 if (modules['neverEndingReddit'].isEnabled()) {
 
6617                         // get the first link to the next page of reddit...
 
6618                         var nextPrevLinks = document.body.querySelectorAll('.content .nextprev a');
 
6619                         if (nextPrevLinks.length > 0) {
 
6620                                 var prevLink = nextPrevLinks[0];
 
6621                                 // RESUtils.click(prevLink);
 
6622                                 location.href = prevLink.getAttribute('href');
 
6626         getCommentLinks: function(obj) {
 
6627                 if (!obj) obj = this.keyboardLinks[this.activeIndex];
 
6628                 return obj.querySelectorAll('div.md a:not(.expando-button):not(.madeVisible):not([href^="javascript:"])');
 
6630         commentLink: function(num) {
 
6631                 if (this.options.commentsLinkNumbers.value) {
 
6632                         var links = this.getCommentLinks();
 
6633                         if (typeof links[num] !== 'undefined') {
 
6634                                 var thisLink = links[num];
 
6635                                 if ((thisLink.nextSibling) && (typeof thisLink.nextSibling.tagName !== 'undefined') && (thisLink.nextSibling.classList.contains('expando-button'))) {
 
6636                                         thisLink = thisLink.nextSibling;
 
6638                                 // RESUtils.click(thisLink);
 
6639                                 this.handleKeyLink(thisLink);
 
6645 // user tagger functions
 
6646 modules['userTagger'] = {
 
6647         moduleID: 'userTagger',
 
6648         moduleName: 'User Tagger',
 
6655                         description: 'clickable mark for users with no tag'
 
6661                         description: 'If "hard ignore" is off, only post titles and comment text is hidden. If it is on, the entire block is hidden (or in comments, collapsed).'
 
6666                         description: 'Color users based on cumulative upvotes / downvotes'
 
6671                         description: 'By default, store a link to the link/comment you tagged a user on'
 
6676                         description: 'Show information on user (karma, how long they\'ve been a redditor) on hover.'
 
6681                         description: 'Delay, in milliseconds, before hover tooltip loads. Default is 800.'
 
6686                         description: 'Delay, in milliseconds, before hover tooltip fades away. Default is 200.'
 
6691                         description: 'Fade animation\'s speed. Default is 0.3, the range is 0-1. Setting the speed to 1 will disable the animation.'
 
6696                         description: 'When clicking the "give gold" button on the user hover info on a comment, give gold to the comment.' 
 
6701                         description: 'Show "highlight" button in user hover info, for distinguishing posts/comments from particular users.'
 
6706                         description: 'Color used to highlight a selected user, when "highlighted" from hover info.'
 
6708                 highlightColorHover: {
 
6711                         description: 'Color used to highlight a selected user on hover.'
 
6716                         description: 'Show date (redditor since...) in US format (i.e. 08-31-2010)'
 
6721                         description: 'Show the number (i.e. [+6]) rather than [vw]'
 
6726                         description: 'Show the vote weight tooltip on hover (i.e. "your votes for...")'
 
6729         description: 'Adds a great deal of customization around users - tagging them, ignoring them, and more.',
 
6730         isEnabled: function() {
 
6731                 return RESConsole.getModulePrefs(this.moduleID);
 
6733         isMatchURL: function() {
 
6734                 return RESUtils.isMatchURL(this.moduleID);
 
6737                 /^https?:\/\/([-\w\.]+\.)?reddit\.com\/[-\w\.]*/i
 
6739         beforeLoad: function() {
 
6740                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
6741                         var css = '.comment .tagline { display: inline; }';
 
6742                         css += '#userTaggerToolTip { display: none; position: absolute; width: 334px; height: 248px; }';
 
6743                         css += '#userTaggerToolTip label { margin-top: 5px; clear: both; float: left; width: 110px; }';
 
6744                         css += '#userTaggerToolTip input[type=text], #userTaggerToolTip select { margin-top: 5px; float: left; width: 195px; border: 1px solid #c7c7c7; border-radius: 3px; margin-bottom: 6px; }';
 
6745                         css += '#userTaggerToolTip input[type=checkbox] { margin-top: 5px; float: left; }';
 
6746                         css += '#userTaggerToolTip input[type=submit] { cursor: pointer; position: absolute; right: 16px; bottom: 16px; padding: 3px 5px; font-size: 12px; color: #fff; border: 1px solid #636363; border-radius: 3px; background-color: #5cc410; } ';
 
6747                         css += '#userTaggerToolTip .toggleButton { margin-top: 5px; margin-bottom: 5px; }';
 
6748                         css += '#userTaggerClose { position: absolute; right: 7px; top: 7px; z-index: 11; }';
 
6750                         css += '.ignoredUserComment { color: #CACACA; padding: 3px; font-size: 10px; }';
 
6751                         css += '.ignoredUserPost { color: #CACACA; padding: 3px; font-size: 10px; }';
 
6752                         css += 'a.voteWeight { text-decoration: none; color: #369; }';
 
6753                         css += 'a.voteWeight:hover { text-decoration: none; }';
 
6754                         css += '#authorInfoToolTip { display: none; position: absolute; min-width: 450px; z-index: 10001; }';
 
6755                         css += '#authorInfoToolTip:before { content: ""; position: absolute; top: 10px; left: -26px; border-style: solid; border-width: 10px 29px 10px 0; border-color: transparent #c7c7c7; display: block; width: 0; z-index: 1; }'
 
6756                         css += '#authorInfoToolTip:after { content: ""; position: absolute; top: 10px; left: -24px; border-style: solid; border-width: 10px 29px 10px 0; border-color: transparent #f0f3fc; display: block; width: 0; z-index: 1; }'
 
6757                         css += '#authorInfoToolTip.right:before { content: ""; position: absolute; top: 10px; right: -26px; left: auto; border-style: solid; border-width: 10px 0 10px 29px; border-color: transparent #c7c7c7; display: block; width: 0; z-index: 1; }'
 
6758                         css += '#authorInfoToolTip.right:after { content: ""; position: absolute; top: 10px; right: -24px; left: auto; border-style: solid; border-width: 10px 0 10px 29px; border-color: transparent #f0f3fc; display: block; width: 0; z-index: 1; }'
 
6759                         css += '#authorInfoToolTip .authorFieldPair { clear: both; overflow: auto; margin-bottom: 12px; }';
 
6760                         css += '#authorInfoToolTip .authorLabel { float: left; width: 140px; }';
 
6761                         css += '#authorInfoToolTip .authorDetail { float: left; min-width: 240px; }';
 
6762                         css += '#authorInfoToolTip .blueButton { float: right; margin-left: 8px; margin-top: 12px; }';
 
6763                         css += '#authorInfoToolTip .redButton { float: right; margin-left: 8px; }';
 
6765                         css += '#benefits { width: 200px; margin-left: 0; }';
 
6766                         css += '#userTaggerToolTip #userTaggerVoteWeight { width: 30px; }';
 
6767                         css += '.RESUserTagImage { display: inline-block; width: 16px; height: 8px; background-image: url(\'http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png\'); background-repeat: no-repeat; background-position: -16px -137px; }';
 
6768                         css += '.userTagLink { display: inline-block; }';
 
6769                         css += '.hoverHelp { margin-left: 3px; cursor: pointer; color: #369; text-decoration: underline; }';
 
6770                         css += '.userTagLink.hasTag, #userTaggerPreview { display: inline-block; padding: 0 4px; border: 1px solid #c7c7c7; border-radius: 3px; }';
 
6771                         css += '#userTaggerPreview { float: left; height: 16px; margin-bottom: 10px; }';
 
6772                         css += '#userTaggerToolTip .toggleButton .toggleOn { background-color: #107ac4; color: #fff;  }';
 
6773                         css += '#userTaggerToolTip .toggleButton.enabled .toggleOn { background-color: #ddd ; color: #636363; }';
 
6774                         css += '#userTaggerToolTip .toggleButton.enabled .toggleOff { background-color: #d02020; color: #fff; }'; 
 
6775                         css += '#userTaggerToolTip .toggleButton .toggleOff { background-color: #ddd; color: #636363; } ';
 
6776                         css += '#userTaggerTable th { -moz-user-select: none; -webkit-user-select: none; -o-user-select: none; user-select: none; }'
 
6777                         css += '#userTaggerTable tbody .deleteButton { cursor: pointer; width: 16px; height: 16px; background-image: url(data:image/gif;base64,R0lGODlhEAAQAOZOAP///3F6hcopAJMAAP/M//Hz9OTr8ZqksMTL1P8pAP9MDP9sFP+DIP8zAO7x8/D1/LnEz+vx+Flha+Ln7OLm61hhayk0QCo1QMfR2eDo8b/K1M/U2pqiqcfP15WcpcLK05ymsig0P2lyftnf5naBi8XJzZ6lrJGdqmBqdKissYyZpf/+/puotNzk66ayvtbc4rC7x9Xd5n+KlbG7xpiirnJ+ivDz9KKrtrvH1Ojv9ePq8HF8h2x2gvj9/yYyPmRueFxlb4eRm+71+kFLVdrb3c/X4KOnrYGMl3uGke/0+5Sgq1ZfaY6Xn/X4+f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAE4ALAAAAAAQABAAAAexgE6CggGFAYOIiAEPEREPh4lOhpOUgwEAmJmaABuQAUktMUUYGhAwLiwnKp41REYmHB5MQUcyN0iQTjsAHU05ICM4SjMQJIg8AAgFBgcvE5gUJYgiycsHDisCApjagj/VzAACBATa5AJOKOAHAAMMDOTvA05A6w7tC/kL804V9uIKAipA52QJgA82dNAQRyBBgwYJyjmRgKmHkAztHA4YAJHfEB8hLFxI0W4AACcbnQQCADs=)}';
 
6779                         RESUtils.addCSS(css);
 
6783                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
6785                         this.usernameRE = /(?:u|user)\/([\w\-]+)/;
 
6786                         // Get user tag data...
 
6787                         var tags = RESStorage.getItem('RESmodules.userTagger.tags');
 
6789                         if (typeof tags !== 'undefined') this.tags = safeJSON.parse(tags, 'RESmodules.userTagger.tags', true);
 
6790                         // check if we're using the old method of storing user tags... yuck!
 
6791                         if (this.tags === null) {
 
6792                                 this.updateTagStorage();
 
6794                         // If we're on the dashboard, add a tab to it...
 
6795                         if (RESUtils.currentSubreddit('dashboard')) {
 
6796                                 // add tab to dashboard
 
6797                                 modules['dashboard'].addTab('userTaggerContents','My User Tags');
 
6798                                 // populate the contents of the tab
 
6799                                 var showDiv = $('<div class="show">Show:</div>')
 
6800                                 var tagFilter = $('<select id="tagFilter"><option>tagged users</option><option>all users</option></select>')
 
6801                                 $(showDiv).append(tagFilter);
 
6802                                 $('#userTaggerContents').append(showDiv);
 
6803                                 $('#tagFilter').change(function(){ 
 
6804                                         modules['userTagger'].drawUserTagTable();
 
6807                                 var tagsPerPage = parseInt(modules['dashboard'].options['tagsPerPage'].value, 10);
 
6809                                         var controlWrapper = document.createElement('div');
 
6810                                         controlWrapper.id = 'tagPageControls';
 
6811                                         controlWrapper.className  = 'RESGalleryControls';
 
6812                                         controlWrapper.page = 1;
 
6813                                         controlWrapper.pageCount = 1;
 
6815                                         var leftButton = document.createElement("a");
 
6816                                         leftButton.className = 'previous noKeyNav';
 
6817                                         leftButton.addEventListener('click', function(e){
 
6818                                                 if (controlWrapper.page === 1) {
 
6819                                                         controlWrapper.page = controlWrapper.pageCount;
 
6821                                                         controlWrapper.page -= 1;
 
6823                                                 modules['userTagger'].drawUserTagTable();
 
6825                                         controlWrapper.appendChild(leftButton);
 
6827                                         var posLabel = document.createElement('span');
 
6828                                         posLabel.className = 'RESGalleryLabel';
 
6829                                         posLabel.textContent = "1 of 2";
 
6830                                         controlWrapper.appendChild(posLabel);
 
6832                                         var rightButton = document.createElement("a");
 
6833                                         rightButton.className = 'next noKeyNav';
 
6834                                         rightButton.addEventListener('click', function(e){
 
6835                                                 if (controlWrapper.page === controlWrapper.pageCount) {
 
6836                                                         controlWrapper.page = 1;
 
6838                                                         controlWrapper.page += 1;
 
6840                                                 modules['userTagger'].drawUserTagTable();
 
6842                                         controlWrapper.appendChild(rightButton);
 
6844                                         $('#userTaggerContents').append(controlWrapper);
 
6847                                 var thisTable = $('<table id="userTaggerTable" />');
 
6848                                 $(thisTable).append('<thead><tr><th sort="" class="active">Username <span class="sortAsc"></span></th><th sort="tag">Tag</th><th sort="ignore">Ignored</th><th sort="color">Color</th><th sort="votes">Vote Weight</th></tr></thead><tbody></tbody>');
 
6849                                 $('#userTaggerContents').append(thisTable);
 
6850                                 $('#userTaggerTable thead th').click(function(e) {
 
6852                                         if ($(this).hasClass('delete')) {
 
6855                                         if ($(this).hasClass('active')) {
 
6856                                                 $(this).toggleClass('descending');
 
6858                                         $(this).addClass('active');
 
6859                                         $(this).siblings().removeClass('active').find('SPAN').remove();
 
6860                                         $(this).find('.sortAsc, .sortDesc').remove();
 
6861                                         ($(e.target).hasClass('descending')) ? $(this).append('<span class="sortDesc" />') : $(this).append('<span class="sortAsc" />');
 
6862                                         modules['userTagger'].drawUserTagTable($(e.target).attr('sort'), $(e.target).hasClass('descending'));
 
6864                                 this.drawUserTagTable();
 
6869                         // set up an array to cache user data
 
6870                         this.authorInfoCache = [];
 
6871                         if (this.options.colorUser.value) {
 
6872                                 this.attachVoteHandlers(document.body);
 
6874                         // add tooltip to document body...
 
6875                         this.userTaggerToolTip = createElementWithID('div','userTaggerToolTip', 'RESDialogSmall');
 
6876                         var thisHTML = '<h3>Tag User</h3><div id="userTaggerToolTipContents" class="RESDialogContents clear">';
 
6877                         thisHTML += '<form name="userTaggerForm" action=""><input type="hidden" id="userTaggerName" value="">';
 
6878                         thisHTML += '<label for="userTaggerTag">Tag</label> <input type="text" id="userTaggerTag" value="">';
 
6879                         thisHTML += '<div id="userTaggerClose" class="RESCloseButton">×</div>';
 
6880                         thisHTML += '<label for="userTaggerColor">Color</label> <select id="userTaggerColor">';
 
6881                         for (var color in this.bgToTextColorMap) {
 
6882                                 var bgColor = (color === 'none') ? 'transparent' : color;
 
6883                                 thisHTML += '<option style="background-color: '+bgColor+'; color: '+this.bgToTextColorMap[color]+' !important;" value="'+color+'">'+color+'</option>';
 
6885                         thisHTML += '</select>';
 
6886                         thisHTML += '<label for="userTaggerPreview">Preview</label> <span id="userTaggerPreview"></span>';
 
6887                         thisHTML += '<label for="userTaggerIgnore">Ignore</label>';// <input type="checkbox" id="userTaggerIgnore" value="true">';
 
6888                         thisHTML += '<label for="userTaggerLink">Link<span class="hoverHelp" title="add a link for this user (shows up in hover pane)">?</span></label> <input type="text" id="userTaggerLink" value="">';
 
6889                         thisHTML += '<label for="userTaggerVoteWeight">Vote Weight<span class="hoverHelp" title="manually edit vote weight for this user">?</span></label> <input type="text" size="2" id="userTaggerVoteWeight" value="">';
 
6890                         thisHTML += '<div class="clear"></div><input type="submit" id="userTaggerSave" value="Save"></form></div>';
 
6891                         $(this.userTaggerToolTip).html(thisHTML);
 
6892                         var ignoreLabel = this.userTaggerToolTip.querySelector('label[for=userTaggerIgnore]');
 
6893                         insertAfter(ignoreLabel, RESUtils.toggleButton('userTaggerIgnore', false, 'no', 'yes'));
 
6894                         this.userTaggerTag = this.userTaggerToolTip.querySelector('#userTaggerTag');
 
6895                         this.userTaggerTag.addEventListener('keyup', modules['userTagger'].updateTagPreview, false);
 
6896                         this.userTaggerColor = this.userTaggerToolTip.querySelector('#userTaggerColor');
 
6897                         this.userTaggerColor.addEventListener('change', modules['userTagger'].updateTagPreview, false);
 
6898                         this.userTaggerPreview = this.userTaggerToolTip.querySelector('#userTaggerPreview');
 
6899                         var userTaggerSave = this.userTaggerToolTip.querySelector('#userTaggerSave');
 
6900                         userTaggerSave.setAttribute('type','submit');
 
6901                         userTaggerSave.setAttribute('value','✓ save tag');
 
6902                         userTaggerSave.addEventListener('click', function(e) {
 
6904                                 modules['userTagger'].saveTagForm();
 
6906                         var userTaggerClose = this.userTaggerToolTip.querySelector('#userTaggerClose');
 
6907                         userTaggerClose.addEventListener('click', function(e) {
 
6908                                 modules['userTagger'].closeUserTagPrompt();
 
6910                         //this.userTaggerToolTip.appendChild(userTaggerSave);
 
6911                         this.userTaggerForm = this.userTaggerToolTip.querySelector('FORM');
 
6912                         this.userTaggerForm.addEventListener('submit', function(e) {
 
6914                                 modules['userTagger'].saveTagForm();
 
6916                         document.body.appendChild(this.userTaggerToolTip);
 
6917                         if (this.options.hoverInfo.value) {
 
6918                                 this.authorInfoToolTip = createElementWithID('div', 'authorInfoToolTip', 'RESDialogSmall');
 
6919                                 this.authorInfoToolTipHeader = document.createElement('h3');
 
6920                                 this.authorInfoToolTip.appendChild(this.authorInfoToolTipHeader);
 
6921                                 this.authorInfoToolTipCloseButton = createElementWithID('div', 'authorInfoToolTipClose', 'RESCloseButton');
 
6922                                 $(this.authorInfoToolTipCloseButton).text('×');
 
6923                                 this.authorInfoToolTip.appendChild(this.authorInfoToolTipCloseButton);
 
6924                                 this.authorInfoToolTipCloseButton.addEventListener('click', function(e) {
 
6925                                         if (typeof modules['userTagger'].hideTimer !== 'undefined') {
 
6926                                                 clearTimeout(modules['userTagger'].hideTimer);
 
6928                                         modules['userTagger'].hideAuthorInfo();
 
6930                                 this.authorInfoToolTipContents = createElementWithID('div','authorInfoToolTipContents', 'RESDialogContents');
 
6931                                 this.authorInfoToolTip.appendChild(this.authorInfoToolTipContents);
 
6932                                 this.authorInfoToolTip.addEventListener('mouseover', function(e) {
 
6933                                         if (typeof modules['userTagger'].hideTimer !== 'undefined') {
 
6934                                                 clearTimeout(modules['userTagger'].hideTimer);
 
6937                                 this.authorInfoToolTip.addEventListener('mouseout', function(e) {
 
6938                                         if (e.target.getAttribute('class') !== 'hoverAuthor') {
 
6939                                                 modules['userTagger'].hideTimer = setTimeout(function() {
 
6940                                                         modules['userTagger'].hideAuthorInfo();
 
6941                                                 }, modules['userTagger'].options.fadeDelay.value);
 
6944                                 document.body.appendChild(this.authorInfoToolTip);
 
6946                         document.getElementById('userTaggerTag').addEventListener('keydown', function(e) {
 
6947                                 if (e.keyCode === 27) {
 
6949                                         modules['userTagger'].closeUserTagPrompt();
 
6952                         //console.log('before applytags: ' + Date());
 
6954                         //console.log('after applytags: ' + Date());
 
6955                         if (RESUtils.pageType() === 'comments') {
 
6956                                 RESUtils.watchForElement('newComments', modules['userTagger'].attachVoteHandlers);
 
6957                                 RESUtils.watchForElement('newComments', modules['userTagger'].applyTags);
 
6959                                 RESUtils.watchForElement('siteTable', modules['userTagger'].attachVoteHandlers);
 
6960                                 RESUtils.watchForElement('siteTable', modules['userTagger'].applyTags);
 
6963                         var userpagere = /^https?:\/\/([a-z]+).reddit.com\/user\/[-\w\.]+\/?/i;
 
6964                         if (userpagere.test(location.href)) {
 
6965                                 var friendButton = document.querySelector('.titlebox .fancy-toggle-button');
 
6966                                 if ((typeof friendButton !== 'undefined') && (friendButton !== null)) {
 
6967                                         var firstAuthor = document.querySelector('a.author');
 
6968                                         if ((typeof firstAuthor !== 'undefined') && (firstAuthor !== null)) {
 
6969                                                 var thisFriendComment = firstAuthor.getAttribute('title');
 
6970                                                 thisFriendComment = (thisFriendComment !== null) ? thisFriendComment.substring(8,thisFriendComment.length-1) : '';
 
6972                                                 var thisFriendComment = '';
 
6974                                         // this stopped working. commenting it out for now.  if i add this back I need to check if you're reddit gold anyway.
 
6976                                         var benefitsForm = document.createElement('div');
 
6977                                         var thisUser = document.querySelector('.titlebox > h1').innerHTML;
 
6978                                         $(benefitsForm).html('<form action="/post/friendnote" id="friendnote-r9_2vt1" method="post" class="pretty-form medium-text friend-note" onsubmit="return post_form(this, \'friendnote\');"><input type="hidden" name="name" value="'+thisUser+'"><input type="text" maxlength="300" name="note" id="benefits" class="tiny" onfocus="$(this).parent().addClass(\'edited\')" value="'+thisFriendComment+'"><button onclick="$(this).parent().removeClass(\'edited\')" type="submit">submit</button><span class="status"></span></form>');
 
6979                                         insertAfter( friendButton, benefitsForm );
 
6985         attachVoteHandlers: function(obj) {
 
6986                 var voteButtons = obj.querySelectorAll('.arrow');
 
6987                 this.voteStates = [];
 
6988                 for (var i=0, len=voteButtons.length;i<len;i++) {
 
6989                         // get current vote states so that when we listen, we check the delta...
 
6990                         // pairNum is just the index of the "pair" of vote arrows... it's i/2 with no remainder...
 
6991                         var pairNum = Math.floor(i/2);
 
6992                         if (typeof this.voteStates[pairNum] === 'undefined') {
 
6993                                 this.voteStates[pairNum] = 0;
 
6995                         if (voteButtons[i].classList.contains('upmod')) {
 
6996                                 this.voteStates[pairNum] = 1;
 
6997                         } else if (voteButtons[i].classList.contains('downmod')) {
 
6998                                 this.voteStates[pairNum] = -1;
 
7000                         // add an event listener to vote buttons to track votes, but only if we're logged in....
 
7001                         voteButtons[i].setAttribute('pairNum',pairNum);
 
7002                         if (RESUtils.loggedInUser()) {
 
7003                                 voteButtons[i].addEventListener('click', modules['userTagger'].handleVoteClick, true);
 
7007         handleVoteClick: function(e) {
 
7008                 var tags = RESStorage.getItem('RESmodules.userTagger.tags');
 
7009                 if (typeof tags !== 'undefined') modules['userTagger'].tags = safeJSON.parse(tags, 'RESmodules.userTagger.tags', true);
 
7010                 if (e.target.getAttribute('onclick').indexOf('unvotable') === -1) {
 
7011                         var pairNum = e.target.getAttribute('pairNum');
 
7012                         if (pairNum) pairNum = parseInt(pairNum, 10);
 
7013                         var thisAuthorA = this.parentNode.nextSibling.querySelector('p.tagline a.author');
 
7014                         // if this is a post with a thumbnail, we need to adjust the query a bit...
 
7015                         if (thisAuthorA === null && this.parentNode.nextSibling.classList.contains('thumbnail')) {
 
7016                                 thisAuthorA = this.parentNode.nextSibling.nextSibling.querySelector('p.tagline a.author');
 
7019                                 var thisVWobj = this.parentNode.nextSibling.querySelector('.voteWeight');
 
7020                                 if (!thisVWobj) thisVWobj = this.parentNode.parentNode.querySelector('.voteWeight');
 
7021                                 // but what if no obj exists
 
7022                                 var thisAuthor = thisAuthorA.textContent;
 
7024                                 if (typeof modules['userTagger'].tags[thisAuthor] !== 'undefined') {
 
7025                                         if (typeof modules['userTagger'].tags[thisAuthor].votes !== 'undefined') {
 
7026                                                 votes = parseInt(modules['userTagger'].tags[thisAuthor].votes, 10);
 
7029                                         modules['userTagger'].tags[thisAuthor] = {};
 
7031                                 // there are 6 possibilities here:
 
7032                                 // 1) no vote yet, click upmod
 
7033                                 // 2) no vote yet, click downmod
 
7034                                 // 3) already upmodded, undoing
 
7035                                 // 4) already downmodded, undoing
 
7036                                 // 5) upmodded before, switching to downmod
 
7037                                 // 6) downmodded before, switching to upmod
 
7038                                 var upOrDown = ((this.classList.contains('up')) || (this.classList.contains('upmod'))) ? 'up' : 'down';
 
7039                                 // did they click the up arrow, or down arrow?
 
7042                                                 // the class changes BEFORE the click event is triggered, so we have to look at them backwards.
 
7043                                                 // if the arrow now has class "up" instead of "upmod", then it was "upmod" before, which means
 
7044                                                 // we are undoing an upvote...
 
7045                                                 if (this.classList.contains('up')) {
 
7046                                                         // this is an undo of an upvote. subtract one from votes. We end on no vote.
 
7048                                                         modules['userTagger'].voteStates[pairNum] = 0;
 
7050                                                         // They've upvoted... the question is, is it an upvote alone, or an an undo of a downvote?
 
7051                                                         // add one vote either way...
 
7053                                                         // if it was previously downvoted, add another!
 
7054                                                         if (modules['userTagger'].voteStates[pairNum] === -1) {
 
7057                                                         modules['userTagger'].voteStates[pairNum] = 1;
 
7061                                                 // the class changes BEFORE the click event is triggered, so we have to look at them backwards.
 
7062                                                 // if the arrow now has class "up" instead of "upmod", then it was "upmod" before, which means
 
7063                                                 // we are undoing an downvote...
 
7064                                                 if (this.classList.contains('down')) {
 
7065                                                         // this is an undo of an downvote. subtract one from votes. We end on no vote.
 
7067                                                         modules['userTagger'].voteStates[pairNum] = 0;
 
7069                                                         // They've downvoted... the question is, is it an downvote alone, or an an undo of an upvote?
 
7070                                                         // subtract one vote either way...
 
7072                                                         // if it was previously upvoted, subtract another!
 
7073                                                         if (modules['userTagger'].voteStates[pairNum] === 1) {
 
7076                                                         modules['userTagger'].voteStates[pairNum] = -1;
 
7081                                 if ((hasClass(this, 'upmod')) || (hasClass(this, 'down'))) {
 
7082                                         // upmod = upvote.  down = undo of downvote.
 
7084                                 } else if ((hasClass(this, 'downmod')) || (hasClass(this, 'up'))) {
 
7085                                         // downmod = downvote.  up = undo of downvote.
 
7089                                 modules['userTagger'].tags[thisAuthor].votes = votes;
 
7090                                 RESStorage.setItem('RESmodules.userTagger.tags', JSON.stringify(modules['userTagger'].tags));
 
7091                                 modules['userTagger'].colorUser(thisVWobj, thisAuthor, votes);
 
7095         drawUserTagTable: function(sortMethod, descending) {
 
7096                 this.currentSortMethod = sortMethod || this.currentSortMethod;
 
7097                 this.descending = (descending == null) ? this.descending : descending == true;
 
7098                 var taggedUsers = [];
 
7099                 var filterType = $('#tagFilter').val();
 
7100                 for (var i in this.tags) {
 
7101                         if (filterType === 'tagged users') {
 
7102                                 if (typeof this.tags[i].tag !== 'undefined') taggedUsers.push(i);
 
7104                                 taggedUsers.push(i);
 
7107                 switch (this.currentSortMethod) {
 
7109                                 taggedUsers.sort(function(a,b) { 
 
7110                                         var tagA = (typeof modules['userTagger'].tags[a].tag === 'undefined') ? 'zzzzz' : modules['userTagger'].tags[a].tag.toLowerCase();
 
7111                                         var tagB = (typeof modules['userTagger'].tags[b].tag === 'undefined') ? 'zzzzz' : modules['userTagger'].tags[b].tag.toLowerCase();
 
7112                                         return (tagA > tagB) ? 1 : (tagB > tagA) ? -1 : 0;
 
7114                                 if (this.descending) taggedUsers.reverse();
 
7117                                 taggedUsers.sort(function(a,b) { 
 
7118                                         var tagA = (typeof modules['userTagger'].tags[a].ignore === 'undefined') ? 'z' : 'a';
 
7119                                         var tagB = (typeof modules['userTagger'].tags[b].ignore === 'undefined') ? 'z' : 'a';
 
7120                                         return (tagA > tagB) ? 1 : (tagB > tagA) ? -1 : 0;
 
7122                                 if (this.descending) taggedUsers.reverse();
 
7125                                 taggedUsers.sort(function(a,b) { 
 
7126                                         var colorA = (typeof modules['userTagger'].tags[a].color === 'undefined') ? 'zzzzz' : modules['userTagger'].tags[a].color.toLowerCase();
 
7127                                         var colorB = (typeof modules['userTagger'].tags[b].color === 'undefined') ? 'zzzzz' : modules['userTagger'].tags[b].color.toLowerCase();
 
7128                                         return (colorA > colorB) ? 1 : (colorB > colorA) ? -1 : 0;
 
7130                                 if (this.descending) taggedUsers.reverse();
 
7133                                 taggedUsers.sort(function(a,b) { 
 
7134                                         var tagA = (typeof modules['userTagger'].tags[a].votes === 'undefined') ? 0 : modules['userTagger'].tags[a].votes;
 
7135                                         var tagB = (typeof modules['userTagger'].tags[b].votes === 'undefined') ? 0 : modules['userTagger'].tags[b].votes;
 
7136                                         return (tagA > tagB) ? 1 : (tagB > tagA) ? -1 : (a.toLowerCase() > b.toLowerCase());
 
7138                                 if (this.descending) taggedUsers.reverse();
 
7141                                 // sort users, ignoring case
 
7142                                 taggedUsers.sort(function(a,b) { 
 
7143                                         return (a.toLowerCase() > b.toLowerCase()) ? 1 : (b.toLowerCase() > a.toLowerCase()) ? -1 : 0;
 
7145                                 if (this.descending) taggedUsers.reverse();
 
7148                 $('#userTaggerTable tbody').html('');
 
7149                 var tagsPerPage = parseInt(modules['dashboard'].options['tagsPerPage'].value, 10);
 
7150                 var count = taggedUsers.length;
 
7155                         var tagControls = $('#tagPageControls');
 
7156                         var page = tagControls.prop('page');
 
7157                         var pages = Math.ceil(count / tagsPerPage);
 
7158                         page = Math.min(page, pages);
 
7159                         page = Math.max(page, 1);
 
7160                         tagControls.prop('page', page).prop('pageCount', pages);
 
7161                         tagControls.find('.RESGalleryLabel').text(page + ' of ' + pages);
 
7162                         start = tagsPerPage*(page-1);
 
7163                         end = Math.min(count, tagsPerPage*page);
 
7166                 for (var i = start; i < end; i++) {
 
7167                         var thisUser = taggedUsers[i];
 
7168                         var thisTag = (typeof this.tags[thisUser].tag === 'undefined') ? '' : this.tags[thisUser].tag;
 
7169                         var thisVotes = (typeof this.tags[thisUser].votes === 'undefined') ? 0 : this.tags[thisUser].votes;
 
7170                         var thisColor = (typeof this.tags[thisUser].color === 'undefined') ? '' : this.tags[thisUser].color;
 
7171                         var thisIgnore = (typeof this.tags[thisUser].ignore === 'undefined') ? 'no' : 'yes';
 
7173                         var userTagLink = document.createElement('a');
 
7174                         if (thisTag === '') {
 
7175                                 // thisTag = '<div class="RESUserTagImage"></div>';
 
7176                                 userTagLink.setAttribute('class','userTagLink RESUserTagImage');
 
7178                                 userTagLink.setAttribute('class','userTagLink hasTag');
 
7180                         $(userTagLink).html(escapeHTML(thisTag));
 
7182                                 var bgColor = (thisColor === 'none') ? 'transparent' : thisColor;
 
7183                                 userTagLink.setAttribute('style','background-color: '+bgColor+'; color: '+this.bgToTextColorMap[thisColor]+' !important;');
 
7185                         userTagLink.setAttribute('username',thisUser);
 
7186                         userTagLink.setAttribute('title','set a tag');
 
7187                         userTagLink.setAttribute('href','javascript:void(0)');
 
7188                         userTagLink.addEventListener('click', function(e) {
 
7189                                 modules['userTagger'].openUserTagPrompt(e.target, this.getAttribute('username'));
 
7192                         $('#userTaggerTable tbody').append('<tr><td><a class="author" href="/user/'+thisUser+'">'+thisUser+'</a> <span class="deleteButton" user="'+thisUser+'"></span></td><td id="tag_'+i+'"></td><td id="ignore_'+i+'">'+thisIgnore+'</td><td><span style="color: '+thisColor+'">'+thisColor+'</span></td><td>'+thisVotes+'</td></tr>');
 
7193                         $('#tag_'+i).append(userTagLink);
 
7195                 $('#userTaggerTable tbody .deleteButton').click(function(e) {
 
7196                         var thisUser = $(this).attr('user');
 
7197                         var answer = confirm("Are you sure you want to delete the tag for user: "+thisUser+"?");
 
7199                                 delete modules['userTagger'].tags[thisUser];
 
7200                                 RESStorage.setItem('RESmodules.userTagger.tags', JSON.stringify(modules['userTagger'].tags));
 
7201                                 $(this).closest('tr').remove();
 
7205         saveTagForm: function() {
 
7206                 var thisName = document.getElementById('userTaggerName').value;
 
7207                 var thisTag = document.getElementById('userTaggerTag').value;
 
7208                 var thisColor = document.getElementById('userTaggerColor').value;
 
7209                 var thisIgnore = document.getElementById('userTaggerIgnore').checked;
 
7210                 var thisLink = document.getElementById('userTaggerLink').value;
 
7211                 var thisVotes = parseInt(document.getElementById('userTaggerVoteWeight').value, 10);
 
7212                 if (isNaN(thisVotes)) thisVotes = 0;
 
7213                 modules['userTagger'].setUserTag(thisName, thisTag, thisColor, thisIgnore, thisLink, thisVotes);
 
7236         openUserTagPrompt: function(obj, username) {
 
7237                 var thisXY=RESUtils.getXYpos(obj);
 
7238                 this.clickedTag = obj;
 
7239                 var thisH3 = document.querySelector('#userTaggerToolTip h3');
 
7240                 thisH3.textContent = 'Tag '+username;
 
7241                 document.getElementById('userTaggerName').value = username;
 
7243                 var thisIgnore = null;
 
7244                 if (typeof this.tags[username] !== 'undefined') {
 
7245                         if (typeof this.tags[username].link !== 'undefined') {
 
7246                                 document.getElementById('userTaggerLink').value = this.tags[username].link;
 
7248                                 document.getElementById('userTaggerLink').value = '';
 
7250                         if (typeof this.tags[username].tag !== 'undefined') {
 
7251                                 document.getElementById('userTaggerTag').value = this.tags[username].tag;
 
7253                                 document.getElementById('userTaggerTag').value = '';
 
7254                                 if (typeof this.tags[username].link === 'undefined') {
 
7255                                         // since we haven't yet set a tag or a link for this user, auto populate a link for the
 
7256                                         // user based on where we are tagging from.
 
7257                                         this.setLinkBasedOnTagLocation(obj);
 
7260                         if (typeof this.tags[username].ignore !== 'undefined') {
 
7261                                 document.getElementById('userTaggerIgnore').checked = this.tags[username].ignore;
 
7262                                 var thisToggle = document.getElementById('userTaggerIgnoreContainer');
 
7263                                 if (this.tags[username].ignore) thisToggle.classList.add('enabled');
 
7265                                 document.getElementById('userTaggerIgnore').checked = false;
 
7267                         if (typeof this.tags[username].votes !== 'undefined') {
 
7268                                 document.getElementById('userTaggerVoteWeight').value = this.tags[username].votes;
 
7270                                 document.getElementById('userTaggerVoteWeight').value = '';
 
7272                         if (typeof this.tags[username].color !== 'undefined') {
 
7273                                 RESUtils.setSelectValue(document.getElementById('userTaggerColor'), this.tags[username].color);
 
7275                                 document.getElementById('userTaggerColor').selectedIndex = 0;
 
7278                         document.getElementById('userTaggerTag').value = '';
 
7279                         document.getElementById('userTaggerIgnore').checked = false;
 
7280                         document.getElementById('userTaggerVoteWeight').value = '';
 
7281                         document.getElementById('userTaggerLink').value = '';
 
7282                         if (this.options.storeSourceLink.value) {
 
7283                                 this.setLinkBasedOnTagLocation(obj);
 
7285                         document.getElementById('userTaggerColor').selectedIndex = 0;
 
7287                 this.userTaggerToolTip.setAttribute('style', 'display: block; top: ' + thisXY.y + 'px; left: ' + thisXY.x + 'px;');
 
7288                 document.getElementById('userTaggerTag').focus();
 
7289                 modules['userTagger'].updateTagPreview();
 
7292         setLinkBasedOnTagLocation: function(obj) {
 
7293                 var closestEntry = $(obj).closest('.entry');
 
7294                 var linkTitle = $(closestEntry).find('a.title');
 
7295                 // if we didn't find anything, try a new search (works on inbox)
 
7296                 if (!linkTitle.length) {
 
7297                         linkTitle = $(closestEntry).find('a.bylink');
 
7299                 if (linkTitle.length) {
 
7300                         document.getElementById('userTaggerLink').value = $(linkTitle).attr('href');
 
7302                         var permaLink = $(closestEntry).find('.flat-list.buttons li.first a');
 
7303                         if (permaLink.length) {
 
7304                                 document.getElementById('userTaggerLink').value = $(permaLink).attr('href');
 
7308         updateTagPreview: function() {
 
7309                 $(modules['userTagger'].userTaggerPreview).text(modules['userTagger'].userTaggerTag.value);
 
7310                 var bgcolor = modules['userTagger'].userTaggerColor[modules['userTagger'].userTaggerColor.selectedIndex].value;
 
7311                 modules['userTagger'].userTaggerPreview.style.backgroundColor = bgcolor;
 
7312                 modules['userTagger'].userTaggerPreview.style.color = modules['userTagger'].bgToTextColorMap[bgcolor];
 
7314         closeUserTagPrompt: function() {
 
7315                 this.userTaggerToolTip.setAttribute('style','display: none');
 
7316                 if (modules['keyboardNav'].isEnabled()) {
 
7317                         var inputs = this.userTaggerToolTip.querySelectorAll('INPUT, BUTTON');
 
7318                         // remove focus from any input fields from the prompt so that keyboard navigation works again...
 
7319                         for (var i=0,len=inputs.length; i<len; i++) {
 
7324         setUserTag: function(username, tag, color, ignore, link, votes, noclick) {
 
7325                 if (((tag !== null) && (tag !== '')) || (ignore)) {
 
7326                         if (tag === '') tag = 'ignored';
 
7327                         if (typeof this.tags[username] === 'undefined') this.tags[username] = {};
 
7328                         this.tags[username].tag = tag;
 
7329                         this.tags[username].link = link;
 
7330                         this.tags[username].color = color;
 
7331                         var bgColor = (color === "none") ? "transparent" : color;
 
7333                                 this.tags[username].ignore = true;
 
7335                                 delete this.tags[username].ignore;
 
7338                                 this.clickedTag.setAttribute('class', 'userTagLink hasTag');
 
7339                                 this.clickedTag.setAttribute('style', 'background-color: '+bgColor+'; color: ' + this.bgToTextColorMap[color]+' !important;');
 
7340                                 $(this.clickedTag).html(escapeHTML(tag));
 
7343                         if (typeof this.tags[username] !== 'undefined') {
 
7344                                 delete this.tags[username].tag;
 
7345                                 delete this.tags[username].color;
 
7346                                 delete this.tags[username].link;
 
7347                                 if (this.tags[username].tag === 'ignored') delete this.tags[username].tag;
 
7348                                 delete this.tags[username].ignore;
 
7351                                 this.clickedTag.setAttribute('style', 'background-color: transparent');
 
7352                                 this.clickedTag.setAttribute('class', 'userTagLink RESUserTagImage');
 
7353                                 $(this.clickedTag).html('');
 
7357                 if (typeof this.tags[username] !== 'undefined') {
 
7358                         this.tags[username].votes = (isNaN(votes)) ? 0 : votes;
 
7361                         var thisVW = this.clickedTag.parentNode.parentNode.querySelector('a.voteWeight');
 
7363                                 this.colorUser(thisVW, username, votes);
 
7366                 if (RESUtils.isEmpty(this.tags[username])) delete this.tags[username];
 
7367                 RESStorage.setItem('RESmodules.userTagger.tags', JSON.stringify(this.tags));
 
7368                 this.closeUserTagPrompt();
 
7370         applyTags: function(ele) {
 
7371                 if (ele == null) ele = document;
 
7372                 var authors = ele.querySelectorAll('.noncollapsed a.author, p.tagline a.author, #friend-table span.user a, .sidecontentbox .author, div.md a[href^="/u/"]:not([href*="/m/"]), div.md a[href*="reddit.com/u/"]:not([href*="/m/"]), .usertable a.author');
 
7373                 RESUtils.forEachChunked(authors, 15, 1000, function(arrayElement, index, array) {
 
7374                         modules['userTagger'].applyTagToAuthor(arrayElement);
 
7377         applyTagToAuthor: function(thisAuthorObj) {
 
7378                 var userObject = [];
 
7379                 // var thisAuthorObj = this.authors[authorNum];
 
7380                 if ((thisAuthorObj) && (!(thisAuthorObj.classList.contains('userTagged'))) && (typeof thisAuthorObj !== 'undefined') && (thisAuthorObj !== null)) {
 
7381                         if (this.options.hoverInfo.value) {
 
7382                                 // add event listener to hover, so we can grab user data on hover...
 
7383                                 thisAuthorObj.addEventListener('mouseover', function(e) {
 
7384                                         modules['userTagger'].showTimer = setTimeout(function() {
 
7385                                                 modules['userTagger'].showAuthorInfo(thisAuthorObj);
 
7386                                         }, modules['userTagger'].options.hoverDelay.value);
 
7388                                 thisAuthorObj.addEventListener('mouseout', function(e) {
 
7389                                         clearTimeout(modules['userTagger'].showTimer);
 
7391                                 thisAuthorObj.addEventListener('click', function(e) {
 
7392                                         clearTimeout(modules['userTagger'].showTimer);
 
7395                         var test = thisAuthorObj.href.match(this.usernameRE);
 
7396                         if (test) var thisAuthor = test[1];
 
7397                         // var thisAuthor = thisAuthorObj.text;
 
7399                         if ((thisAuthor) && (thisAuthor.substr(0,3) === '/u/')) {
 
7401                                 thisAuthor = thisAuthor.substr(3);
 
7404                                 thisAuthorObj.classList.add('userTagged');
 
7405                                 if (typeof userObject[thisAuthor] === 'undefined') {
 
7408                                         var thisColor = null;
 
7409                                         var thisIgnore = null;
 
7410                                         if ((this.tags !== null) && (typeof this.tags[thisAuthor] !== 'undefined')) {
 
7411                                                 if (typeof this.tags[thisAuthor].votes !== 'undefined') {
 
7412                                                         thisVotes = parseInt(this.tags[thisAuthor].votes, 10);
 
7414                                                 if (typeof this.tags[thisAuthor].tag !== 'undefined') {
 
7415                                                         thisTag = this.tags[thisAuthor].tag;
 
7417                                                 if (typeof this.tags[thisAuthor].color !== 'undefined') {
 
7418                                                         thisColor = this.tags[thisAuthor].color;
 
7420                                                 if (typeof this.tags[thisAuthor].ignore !== 'undefined') {
 
7421                                                         thisIgnore = this.tags[thisAuthor].ignore;
 
7424                                         userObject[thisAuthor] = {
 
7432                                 var userTagFrag = document.createDocumentFragment();
 
7434                                 var userTagLink = document.createElement('a');
 
7436                                         // thisTag = '<div class="RESUserTagImage"></div>';
 
7437                                         userTagLink.setAttribute('class','userTagLink RESUserTagImage');
 
7439                                         userTagLink.setAttribute('class','userTagLink hasTag');
 
7441                                 $(userTagLink).html(escapeHTML(thisTag));
 
7443                                         var bgColor = (thisColor === 'none') ? 'transparent' : thisColor;
 
7444                                         userTagLink.setAttribute('style','background-color: '+bgColor+'; color: '+this.bgToTextColorMap[thisColor]+' !important;');
 
7446                                 userTagLink.setAttribute('username',thisAuthor);
 
7447                                 userTagLink.setAttribute('title','set a tag');
 
7448                                 userTagLink.setAttribute('href','javascript:void(0)');
 
7449                                 userTagLink.addEventListener('click', function(e) {
 
7450                                         modules['userTagger'].openUserTagPrompt(e.target, this.getAttribute('username'));
 
7452                                 var userTag = document.createElement('span');
 
7453                                 userTag.classList.add('RESUserTag');
 
7454                                 // var lp = document.createTextNode(' (');
 
7455                                 // var rp = document.createTextNode(')');
 
7456                                 userTag.appendChild(userTagLink);
 
7457                                 // userTagFrag.appendChild(lp);
 
7458                                 userTagFrag.appendChild(userTag);
 
7459                                 // userTagFrag.appendChild(rp);
 
7460                                 if (this.options.colorUser.value) {
 
7461                                         var userVoteFrag = document.createDocumentFragment();
 
7462                                         var spacer = document.createTextNode(' ');
 
7463                                         userVoteFrag.appendChild(spacer);
 
7464                                         var userVoteWeight = document.createElement('a');
 
7465                                         userVoteWeight.setAttribute('href','javascript:void(0)');
 
7466                                         userVoteWeight.setAttribute('class','voteWeight');
 
7467                                         $(userVoteWeight).text('[vw]');
 
7468                                         userVoteWeight.addEventListener('click', function(e) {
 
7469                                                 var theTag = this.parentNode.querySelector('.userTagLink');
 
7470                                                 modules['userTagger'].openUserTagPrompt(theTag, theTag.getAttribute('username'));
 
7472                                         this.colorUser(userVoteWeight, thisAuthor, userObject[thisAuthor].votes);
 
7473                                         userVoteFrag.appendChild(userVoteWeight);
 
7474                                         userTagFrag.appendChild(userVoteFrag);
 
7476                                 insertAfter( thisAuthorObj, userTagFrag );
 
7477                                 thisIgnore = userObject[thisAuthor].ignore;
 
7478                                 if (thisIgnore && (RESUtils.pageType() !== 'profile')) {
 
7479                                         if (this.options.hardIgnore.value) {
 
7480                                                 if (RESUtils.pageType() === 'comments') {
 
7481                                                         var thisComment = thisAuthorObj.parentNode.parentNode.querySelector('.usertext');
 
7483                                                                 $(thisComment).textContent = thisAuthor + ' is an ignored user';
 
7484                                                                 thisComment.classList.add('ignoredUserComment');
 
7486                                                                 var toggle = thisComment.parentNode.querySelector('a.expand');
 
7487                                                                 RESUtils.click(toggle);
 
7489                                                         // firefox fails when we use this jquery call, so we're ditching it
 
7490                                                         // in favor of the above lines (grabbing toggle, using RESUtils.click...)
 
7491                                                         // $(thisComment).parent().find('a.expand').click();
 
7493                                                         var thisPost = thisAuthorObj.parentNode.parentNode.parentNode;
 
7494                                                         // hide post block first...
 
7495                                                         thisPost.style.display = 'none';
 
7496                                                         // hide associated voting block...
 
7497                                                         if (thisPost.previousSibling) {
 
7498                                                                 thisPost.previousSibling.style.display = 'none';
 
7502                                                 if (RESUtils.pageType() === 'comments') {
 
7503                                                         var thisComment = thisAuthorObj.parentNode.parentNode.querySelector('.usertext');
 
7505                                                                 thisComment.textContent = thisAuthor + ' is an ignored user';
 
7506                                                                 thisComment.classList.add('ignoredUserComment');
 
7509                                                         var thisPost = thisAuthorObj.parentNode.parentNode.parentNode.querySelector('p.title');
 
7511                                                                 // need this setTimeout, potentially because destroying the innerHTML causes conflict with other modules?
 
7512                                                                 setTimeout(function() {
 
7513                                                                         thisPost.textContent = thisAuthor + ' is an ignored user';
 
7515                                                                 thisPost.setAttribute('class','ignoredUserPost');
 
7523         colorUser: function(obj, author, votes) {
 
7524                 if (this.options.colorUser.value) {
 
7525                         votes = parseInt(votes, 10);
 
7529                         var voteString = '+';
 
7531                                 red = Math.max(0, (255-(8*votes)));
 
7533                                 blue = Math.max(0, (255-(8*votes)));
 
7534                         } else if (votes < 0) {
 
7536                                 green = Math.max(0, (255-Math.abs(8*votes)));
 
7537                                 blue = Math.max(0, (255-Math.abs(8*votes)));
 
7540                         voteString = voteString + votes;
 
7541                         var rgb='rgb('+red+','+green+','+blue+')';
 
7544                                         obj.style.display = 'none';
 
7546                                         obj.style.display = 'inline';
 
7547                                         (modules['styleTweaks'].options.lightOrDark.value === 'dark') ? obj.style.color = rgb : obj.style.backgroundColor = rgb;
 
7548                                         if (this.options.vwNumber.value) obj.textContent = '[' + voteString + ']';
 
7549                                         if (this.options.vwTooltip.value) obj.setAttribute('title','your votes for '+escapeHTML(author)+': '+escapeHTML(voteString));
 
7554         showAuthorInfo: function(obj) {
 
7555                 var isFriend = obj.classList.contains('friend');
 
7556                 var thisXY=RESUtils.getXYpos(obj);
 
7557                 var thisWidth = $(obj).width();
 
7558                 // var thisUserName = obj.textContent;
 
7559                 var test = obj.href.match(this.usernameRE);
 
7560                 if (test) var thisUserName = test[1];
 
7561                 // if (thisUserName.substr(0,3) === '/u/') thisUserName = thisUserName.substr(3);
 
7562                 $(this.authorInfoToolTipHeader).html('<a href="/user/'+escapeHTML(thisUserName)+'">' + escapeHTML(thisUserName) + '</a> (<a href="/user/'+escapeHTML(thisUserName)+'/submitted/">Links</a>) (<a href="/user/'+escapeHTML(thisUserName)+'/comments/">Comments</a>)');
 
7563                 RESUtils.getUserInfo(function(userInfo) {
 
7564                         var myID = 't2_'+userInfo.data.id;
 
7566                                 var friendButton = '<span class="fancy-toggle-button toggle" style="display: inline-block; margin-left: 12px;"><a class="option active remove" href="#" tabindex="100" onclick="return toggle(this, unfriend(\''+obj.textContent+'\', \''+myID+'\', \'friend\'), friend(\''+obj.textContent+'\', \''+myID+'\', \'friend\'))">- friends</a><a class="option add" href="#">+ friends</a></span>';
 
7568                                 var friendButton = '<span class="fancy-toggle-button toggle" style="display: inline-block; margin-left: 12px;"><a class="option active add" href="#" tabindex="100" onclick="return toggle(this, friend(\''+obj.textContent+'\', \''+myID+'\', \'friend\'), unfriend(\''+obj.textContent+'\', \''+myID+'\', \'friend\'))">+ friends</a><a class="option remove" href="#">- friends</a></span>';
 
7570                         var friendButtonEle = $(friendButton);
 
7571                         $(modules['userTagger'].authorInfoToolTipHeader).append(friendButtonEle);
 
7573                 $(this.authorInfoToolTipContents).html('<a class="hoverAuthor" href="/user/'+escapeHTML(thisUserName)+'">'+escapeHTML(thisUserName)+'</a>:<br><img src="'+RESConsole.loader+'"> loading...');
 
7574                 if((window.innerWidth-thisXY.x-thisWidth)<=450){
 
7575                         // tooltip would go off right edge - reverse it.
 
7576                         this.authorInfoToolTip.classList.add('right');
 
7577                         var tooltipWidth = $(this.authorInfoToolTip).width();
 
7578                         this.authorInfoToolTip.setAttribute('style', 'top: ' + (thisXY.y - 14) + 'px; left: ' + (thisXY.x - tooltipWidth - 30) + 'px;');
 
7580                         this.authorInfoToolTip.classList.remove('right');
 
7581                         this.authorInfoToolTip.setAttribute('style', 'top: ' + (thisXY.y - 14) + 'px; left: ' + (thisXY.x + thisWidth + 25) + 'px;');
 
7583                 if(this.options.fadeSpeed.value < 0 || this.options.fadeSpeed.value > 1 || isNaN(this.options.fadeSpeed.value)) {
 
7584                         this.options.fadeSpeed.value = 0.3;
 
7586                 RESUtils.fadeElementIn(this.authorInfoToolTip, this.options.fadeSpeed.value);
 
7587                 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'authorInfo');
 
7588                 setTimeout(function() {
 
7589                         if (!RESUtils.elementUnderMouse(modules['userTagger'].authorInfoToolTip) && (!RESUtils.elementUnderMouse(obj))) {
 
7590                                 modules['userTagger'].hideAuthorInfo();
 
7593                 obj.addEventListener('mouseout', modules['userTagger'].delayedHideAuthorInfo);
 
7594                 if (typeof this.authorInfoCache[thisUserName] !== 'undefined') {
 
7595                         this.writeAuthorInfo(this.authorInfoCache[thisUserName], obj);
 
7599                                 url:    location.protocol + "//"+location.hostname+"/user/" + thisUserName + "/about.json?app=res",
 
7600                                 onload: function(response) {
 
7601                                         var thisResponse = JSON.parse(response.responseText);
 
7602                                         modules['userTagger'].authorInfoCache[thisUserName] = thisResponse;
 
7603                                         modules['userTagger'].writeAuthorInfo(thisResponse, obj);
 
7608         delayedHideAuthorInfo: function(e) {
 
7609                 modules['userTagger'].hideTimer = setTimeout(function() {
 
7610                         e.target.removeEventListener('mouseout', modules['userTagger'].delayedHideAuthorInfo);
 
7611                         modules['userTagger'].hideAuthorInfo();
 
7612                 }, modules['userTagger'].options.fadeDelay.value);
 
7614         writeAuthorInfo: function(jsonData, authorLink) {
 
7615                 if (!jsonData.data) {
 
7616                         $(this.authorInfoToolTipContents).text("User not found");
 
7619                 var utctime = jsonData.data.created_utc;
 
7620                 var d = new Date(utctime * 1000);
 
7621                 // var userHTML = '<a class="hoverAuthor" href="/user/'+jsonData.data.name+'">'+jsonData.data.name+'</a>:';
 
7622                 var userHTML = '<div class="authorFieldPair"><div class="authorLabel">Redditor since:</div> <div class="authorDetail">' + RESUtils.niceDate(d, this.options.USDateFormat.value) + ' (' + RESUtils.niceDateDiff(d) + ')</div></div>';
 
7623                 userHTML += '<div class="authorFieldPair"><div class="authorLabel">Link Karma:</div> <div class="authorDetail">' + escapeHTML(jsonData.data.link_karma) + '</div></div>';
 
7624                 userHTML += '<div class="authorFieldPair"><div class="authorLabel">Comment Karma:</div> <div class="authorDetail">' + escapeHTML(jsonData.data.comment_karma) + '</div></div>';
 
7625                 if ((typeof modules['userTagger'].tags[jsonData.data.name] !== 'undefined') && (modules['userTagger'].tags[jsonData.data.name].link)) {
 
7626                         userHTML += '<div class="authorFieldPair"><div class="authorLabel">Link:</div> <div class="authorDetail"><a target="_blank" href="'+escapeHTML(modules['userTagger'].tags[jsonData.data.name].link)+'">website link</a></div></div>';
 
7628                 userHTML += '<div class="clear"></div><div class="bottomButtons">';
 
7629                 userHTML += '<a target="_blank" class="blueButton" href="http://www.reddit.com/message/compose/?to='+escapeHTML(jsonData.data.name)+'"><img src="http://www.redditstatic.com/mailgray.png"> send message</a>';
 
7630                 if (jsonData.data.is_gold) {
 
7631                         userHTML += '<a target="_blank" class="blueButton" href="http://www.reddit.com/gold/about">User has Reddit Gold</a>';
 
7633                         userHTML += '<a target="_blank" id="gildUser" class="blueButton" href="http://www.reddit.com/gold?goldtype=gift&recipient='+escapeHTML(jsonData.data.name)+'">Gift Reddit Gold</a>';
 
7636                 if (this.options.highlightButton.value) {
 
7637                         if (!this.highlightedUsers || !this.highlightedUsers[jsonData.data.name]) {
 
7638                                 userHTML += '<div class="blueButton" id="highlightUser" user="'+escapeHTML(jsonData.data.name)+'">Highlight</div>';
 
7640                                 userHTML += '<div class="redButton" id="highlightUser" user="'+escapeHTML(jsonData.data.name)+'">Unhighlight</div>';
 
7644                 if ((modules['userTagger'].tags[jsonData.data.name]) && (modules['userTagger'].tags[jsonData.data.name].ignore)) {
 
7645                         userHTML += '<div class="redButton" id="ignoreUser" user="'+escapeHTML(jsonData.data.name)+'">∅ Unignore</div>';
 
7647                         userHTML += '<div class="blueButton" id="ignoreUser" user="'+escapeHTML(jsonData.data.name)+'">∅ Ignore</div>';
 
7649                 userHTML += '<div class="clear"></div></div>'; // closes bottomButtons div
 
7650                 $(this.authorInfoToolTipContents).html(userHTML);
 
7651                 this.authorInfoToolTipIgnore = this.authorInfoToolTipContents.querySelector('#ignoreUser');
 
7652                 this.authorInfoToolTipIgnore.addEventListener('click', modules['userTagger'].ignoreUser, false);
 
7653                 if (modules['userTagger'].options.highlightButton.value) {
 
7654                         this.authorInfoToolTipHighlight = this.authorInfoToolTipContents.querySelector('#highlightUser');
 
7655                         if (this.authorInfoToolTipHighlight) {
 
7656                                 this.authorInfoToolTipHighlight.addEventListener('click', function(e) {
 
7657                                         var username = e.target.getAttribute('user');
 
7658                                         modules['userTagger'].toggleUserHighlight(username);
 
7662                 if (modules['userTagger'].options.gildComments.value && RESUtils.pageType() === 'comments') {
 
7663                         var giveGold = this.authorInfoToolTipContents.querySelector('#gildUser');
 
7664                         giveGold && giveGold.addEventListener('click', function(e) {
 
7665                                 if (e.ctrlKey || e.cmdKey || e.shiftKey) return;
 
7667                                 var comment = $(authorLink).closest('.comment');
 
7668                                 if (!comment) return;
 
7670                                 modules['userTagger'].hideAuthorInfo();
 
7671                                 var giveGold = comment.find('.give-gold')[0];
 
7672                                 RESUtils.click(giveGold);
 
7677         toggleUserHighlight: function(username) {
 
7678                 if (!this.highlightedUsers) this.highlightedUsers = {};
 
7680                 if (this.highlightedUsers[username]) {
 
7681                         this.highlightedUsers[username].remove();
 
7682                         delete this.highlightedUsers[username];
 
7683                         this.toggleUserHighlightButton(true);
 
7686                         this.highlightedUsers[username] = 
 
7687                                 modules['userHighlight'].highlightUser(username);
 
7688                         this.toggleUserHighlightButton(false);
 
7691         toggleUserHighlightButton: function(canHighlight) {     
 
7692                 $(this.authorInfoToolTipHighlight)
 
7693                         .toggleClass('blueButton', canHighlight)
 
7694                         .toggleClass('redButton', !canHighlight)
 
7695                         .text(canHighlight ? 'Highlight' : 'Unhighlight');
 
7697         ignoreUser: function(e) {
 
7698                 if (e.target.classList.contains('blueButton')) {
 
7699                         e.target.classList.remove('blueButton');
 
7700                         e.target.classList.add('redButton');
 
7701                         $(e.target).html('∅ Unignore');
 
7702                         var thisIgnore = true;
 
7704                         e.target.classList.remove('redButton');
 
7705                         e.target.classList.add('blueButton');
 
7706                         $(e.target).html('∅ Ignore');
 
7707                         var thisIgnore = false;
 
7709                 var thisName = e.target.getAttribute('user');
 
7710                 var thisColor, thisLink, thisVotes, thisTag;
 
7711                 if (modules['userTagger'].tags[thisName]) {
 
7712                         thisColor = modules['userTagger'].tags[thisName].color || '';
 
7713                         thisLink = modules['userTagger'].tags[thisName].link || '';
 
7714                         thisVotes = modules['userTagger'].tags[thisName].votes || 0;
 
7715                         thisTag = modules['userTagger'].tags[thisName].tag || '';
 
7717                 if ((thisIgnore) && (thisTag === '')) {
 
7718                         thisTag = 'ignored';
 
7719                 } else if ((!thisIgnore) && (thisTag === 'ignored')) {
 
7722                 modules['userTagger'].setUserTag(thisName, thisTag, thisColor, thisIgnore, thisLink, thisVotes, true); // last true is for noclick param
 
7724         hideAuthorInfo: function(obj) {
 
7725                 // this.authorInfoToolTip.setAttribute('style', 'display: none');
 
7726                 if(this.options.fadeSpeed.value < 0 || this.options.fadeSpeed.value > 1 || isNaN(this.options.fadeSpeed.value)) {
 
7727                         this.options.fadeSpeed.value = 0.3;
 
7729                 RESUtils.fadeElementOut(this.authorInfoToolTip, this.options.fadeSpeed.value);
 
7730                 modules['styleTweaks'].setSRStyleToggleVisibility(true,  'authorInfo');
 
7732         updateTagStorage: function() {
 
7733                 // update tag storage format from the old individual bits to a big JSON blob
 
7734                 // It's OK that we're directly accessing localStorage here because if they have old school tag storage, it IS in localStorage.
 
7735                 ls = (typeof unsafeWindow !== 'undefined') ? unsafeWindow.localStorage : localStorage;
 
7738                 for (var i = 0, len=ls.length; i < len; i++){
 
7739                         var keySplit = null;
 
7740                         if (ls.key(i)) keySplit = ls.key(i).split('.');
 
7742                                 var keyRoot = keySplit[0];
 
7745                                                 var thisNode = keySplit[1];
 
7746                                                 if (typeof tags[keySplit[2]] === 'undefined') {
 
7747                                                         tags[keySplit[2]] = {};
 
7749                                                 if (thisNode === 'votes') {
 
7750                                                         tags[keySplit[2]].votes = ls.getItem(ls.key(i));
 
7751                                                 } else if (thisNode === 'tag') {
 
7752                                                         tags[keySplit[2]].tag = ls.getItem(ls.key(i));
 
7753                                                 } else if (thisNode === 'color') {
 
7754                                                         tags[keySplit[2]].color = ls.getItem(ls.key(i));
 
7755                                                 } else if (thisNode === 'ignore') {
 
7756                                                         tags[keySplit[2]].ignore = ls.getItem(ls.key(i));
 
7758                                                 // now delete the old stored garbage...
 
7759                                                 var keyString = 'reddituser.'+thisNode+'.'+keySplit[2];
 
7760                                                 toRemove.push(keyString);
 
7763                                                 // console.log('Not currently handling keys with root: ' + keyRoot);
 
7769                 RESStorage.setItem('RESmodules.userTagger.tags', JSON.stringify(this.tags));
 
7770                 // now remove the old garbage...
 
7771                 for (var i=0, len=toRemove.length; i<len; i++) {
 
7772                         ls.removeItem(toRemove[i]);
 
7778 modules['betteReddit'] = {
 
7779         moduleID: 'betteReddit',
 
7780         moduleName: 'betteReddit',
 
7786                         description: 'add "full comments" link to comment replies, etc'
 
7790                         value: 'full comments',
 
7791                         description: 'text of full comments link'
 
7793                 commentsLinksNewTabs: {
 
7796                         description: 'Open links found in comments in a new tab'
 
7801                         description: 'Make "save" links change to "unsave" links when clicked'
 
7806                         description: 'Make "hide" links change to "unhide" links when clicked, and provide a 5 second delay prior to hiding the link'
 
7808                 searchSubredditByDefault: {
 
7811                         description: 'Search the current subreddit by default when using the search box, instead of all of reddit.'
 
7816                         description: 'Show unread message count next to orangered?'
 
7818                 showUnreadCountInTitle: {
 
7821                         description: 'Show unread message count in page/tab title?'
 
7823                 showUnreadCountInFavicon: {
 
7826                         description: 'Show unread message count in favicon?'
 
7828                 unreadLinksToInbox: {
 
7831                         description: 'Always go to the inbox, not unread messages, when clicking on orangered'
 
7836                         description: 'Show lengths of videos when possible'
 
7841                         description: 'Show upload date of videos when possible'
 
7846                         description: 'Don\'t use Reddit Toolbar when linking to sites that may not function (twitter, youtube and others)'
 
7851                            { name: 'None', value: 'none' },
 
7852                            { name: 'Subreddit Bar only', value: 'sub' },
 
7853                            { name: 'User Bar', value: 'userbar' },
 
7854                            { name: 'Subreddit Bar and User bar', value: 'subanduser' },
 
7855                            { name: 'Full Header', value: 'header' }
 
7858                    description: 'Pin the subreddit bar or header to the top, even when you scroll.'
 
7863                         description: 'Preload selftext data to make selftext expandos faster (preloads after first expando)'
 
7865                 showLastEditedTimestamp: {
 
7868                         description: 'Show the time that a text post/comment was edited, without having to hover the timestamp.'
 
7873                         description: 'The saved tab on pages with the multireddit side bar is now located in that collapsible bar. This will restore it to the header. If you have the \'Save Comments\' module enabled then you\'ll see both the links <em>and</em> comments tabs.'
 
7876         description: 'Adds a number of interface enhancements to Reddit, such as "full comments" links, the ability to unhide accidentally hidden posts, and more',
 
7877         isEnabled: function() {
 
7878                 return RESConsole.getModulePrefs(this.moduleID);
 
7880         isMatchURL: function() {
 
7881                 return RESUtils.isMatchURL(this.moduleID);
 
7884                 /^https?:\/\/([a-z]+)\.reddit\.com\/.*/i
 
7887                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
7889                         if ((this.options.toolbarFix.value) && ((RESUtils.pageType() === 'linklist') || RESUtils.pageType() === 'comments')) { 
 
7892                         if ((RESUtils.pageType() === 'comments') && (this.options.commentsLinksNewTabs.value)) {
 
7893                                 this.commentsLinksNewTabs();
 
7895                         // if (((RESUtils.pageType() === 'inbox') || (RESUtils.pageType() === 'profile') || ((RESUtils.pageType() === 'comments') && (RESUtils.currentSubreddit('friends')))) && (this.options.fullCommentsLink.value)) {
 
7896                         // removed profile pages since Reddit does this natively now for those...
 
7897                         if (((RESUtils.pageType() === 'inbox') || ((RESUtils.pageType() === 'comments') && (RESUtils.currentSubreddit('friends') === false))) && (this.options.fullCommentsLink.value)) {
 
7898                                 // RESUtils.addCSS('a.redditFullCommentsSub { font-size: 9px !important; color: #BBB !important; }');
 
7899                                 this.fullComments();
 
7901                         if ((RESUtils.pageType() === 'profile') && (location.href.split('/').indexOf(RESUtils.loggedInUser()) !== -1)) {
 
7902                                 this.editMyComments();
 
7904                         if (((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments')) && (this.options.fixSaveLinks.value)) {
 
7905                                 this.fixSaveLinks();
 
7907                         if (((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments')) && (this.options.fixHideLinks.value)) {
 
7908                                 this.fixHideLinks();
 
7910                         if ((this.options.turboSelfText.value) && (RESUtils.pageType() === 'linklist')) {
 
7911                                 this.setUpTurboSelfText();
 
7913                         if (this.options.showUnreadCountInFavicon.value) {
 
7914                                 var faviconDataurl = 'data:image/x-icon;base64,AAABAAIAEBAAAAAAAABoBQAAJgAAACAgAAAAAAAAqAgAAI4FAAAoAAAAEAAAACAAAAABAAgAAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wBBRP4Aq5qHAFNOSgCkpv4A4Mu0AA8Q/gCAc2cAzM7+ADYvKQCys7QA/urQAGVo/gCGh4kA1NbYAMWvmgBjZWYA5efoAJucngD//+MAwcPFACIk/gCbinkAycGtADo8PgDz2sIAal5RADAy/gBvcHEA9PbzALijkACMfm8AqqmoALCy/gCAgHsAc2piAMnMzwDBw/4AXVxcAJGUlwDOuKMAR0dHANfDrABLTv4Aubu+ADIzNQBbVlEA++HJAP/23ACho6UAa2RdAJOEdQD3+P4A2tzeAO7UvACEeW4AjY2KALCfjgBsa2oArq6uAM/R0wB2cGwA//DWAKOYiACCg4MAemxfAPDw8ABOSkYA4+PjAMm7pgBeWlYAgHt3AGZhWwBtamQA+t7EAGVbUgB7b2QAoaGgAP7kzADdyLEA9Pf6AP774ACwnIoAZ15VAJiHdwBbXWAAy8vLAIh8cAC0oY0AZ2VkALy+vwD+7dMA9t3FAHJwbgB/f38Aqq2vAP//5wB4bWMA7ta/AHVqXwCCfXsAmYp8AIJ1aQDS0tIApKSkAP7jyQDOuaYA/vjeAH1xZQCBgYEArp2MAJ2enwD5+v4A//LYAH5zaQD13MMAd2xhAJWFdgD+69IA/ujPAGxsbABycXAAtqOPAN/IsQDLu6YA/v7+AP/84QD/+t8A/vfbAP7v1QD64MgA1dfZAH5+fgD//uIA/v7jAP/43QD+990AZVxTAP/x1wD+8tcA/u7UAOTk4wCYiHgA///kAP/+4wD++t8A/vneAP743QD/79UA/u3SAP7s0gD+69EA/unQAPXcxACrra8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AD8/Pz8/Pz8/Pz8/Pz8/Pz8/XV1dKY8IYgiPKV1dXV0/P11QVDubPYQ9mztUUF1dP4krRC1+V26Fbld+LUQrXT+UiiV+fiFoQ2ghfn4lil0/P15+fn5+fn5+fn5+fl5dPx8qfn4iHAl+CRwifn4qHz9IHTx+BQcmfiYHBX48HUg/PjYZDh5+fn5+fh4OGTY+P0YzOCBKOTILcCNJIDgzRj8/XV1dfFkXClUQGl1qXV0/P11dXTB4XWdrXV1YRzpdPz9dXV1dXV1vZBhCVlEEXT8/XV1dXV2DY0xtQCRaZl0/P11dXV1dXV1dXV1dXV1dPz8/Pz8/Pz8/Pz8/Pz8/Pz8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAACAAAABAAAAAAQAIAAAAAACABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8AJSr/AJmJegCIjf4AREZIANzFsADGy/4AUFb/ACgjHwBpamsAAAD/AP/mzQCmp6cA2dnaAL29vgDl6P4AYllPAMWzoAAQEhQAMjQ2AHp7fQCxtv4AlZaWAP784AASFv4A7NfAALGgjwCCdGYAy8vLAPPz9ACJiosARz83AFZNRAB1aV0A1Nj+AFJTVQCzs7MAjX9yAFtdXwDi4uIAqJaGALyplgDRvagAMiwnABkbHQB9g/4ACgoKAOLPuQD+8dcA6+vrAGphVwCdnp4A9t3GAD43LwB0dHQAgYOEADw8PAAdIf8A0tLSAPf5/wC5vv4AxcXGAJ+QgAAbFxMA3eH/AGJjZACtra0A9+rSAE9HQADs7/4AIyQmAHl1aQAsLzEATE5QAEpP/wAhHRoAXFJIAI+PjwAeICIAAwUHAD9BQgBWWFoAwa6bAMm8qAATEA0A///nAOnSuwAVFxgA3d3dAHltYACJe20AlIV2APj4+AA4My4A/vfcAP7s0gDUwa0AuaSSAPviyQA4ODgAt7e3AKGiogAODg8AKyssAMvP/gCBh/4ALiklAIR3awDOzs8A38u2AH9wYwAIBgQAb2NYADYwKgD7+/sA8PDwAPPbxABJS00AwcLCABke/gDY2/8ADgwJAAUDAgAjIB0A+eXMAF9gYgCbm5sAkpOUABQSEQBEPDUAUVFRAIWFhQAZGRkAIyMjAPP1/gDp7P4AtaORAAYHBwAfGxgA39/gAC8xMwDv2cIASUE4AK+vsADmz7kAAQMFABAQEAD+6dAAMSokAEE4MQBVS0EAyLWhAGVlZQCai30AjIyMAP7+4wAUFBQA/vneAP7z2gD+7tUALy8vADs1LgComYgA/f39APb29gAbHR8A/ePLAPngyADb29sAQkRGAEZISgDWw68A07+qAFpQRgBkW1EAvquYAHZ2dgCWh3gADw0MACAiJAD8584ANTc5ADc5OwDZxrEAV1dXAAMCAgDiy7UAjI2PAAwLCwDb3/4Aw7CcAJ+foACDdmgAoZGCABIUFgAjHxoAJSUlAPbcxADIzf4A79fAAOzZwgC3vP4A59G7AOLNtwBdXV0Av62aAAQEBAAGBQQACQgHABYWFgD09PUAFxocADAzNQDDw8MAWE5FAMOynwBaXF0AbmJWAGBiYwC3opAAAgEBABISEgD+6M8A/OXNADAqJgAyLSkANi8oADQ1NgDTvakAtba3AMGwnQBkWk8Aq6ytAG1jWQBjZWYAnY+AAP//5QD+9dsA+N7GAODNtwDRv6oAw66bAAcICQD+/v4A+vr6ABQRDgD5+fkAFhgZAP/43QAaGhoA8fHxAP/w1gD/7dMA6urqAP///wAAVvz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8VgD8NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTX8AKA1NTU1NTU1NagM/fz8oKCg/KDfqDU1NTU1NTU1NaAAoDU1NTU1NQz8p63CWq4gIJciA5g1oJQ1NTU1NTU1oACgNTU1Naf8uCKLz8NJq9knStX4z/ZNEmDfNTU1NTWgAPw1NTX9Gq/dRzhtXQEBAQEBAaQOF7cAgmH8NTU1NfwAoDU1oNhVtJD09PT09PSlHqT09PT09B1RAOwx7zU1oACgNaDOAEL19PT09CWDhtKd+mSbXfT09PROACb8NTWgAKDgy3sK9PT09PTNALl/D9ZDsdLF+/T09PRmAM5gNaAA/PyuRwEBAQEBAU4dAQEBAQEBdIR0AQEBAQFSxLWo/ADuV93p9PT09PT09PT09PT09PT09PT09PT09IwA8pSgABgqijL09PT09PT09PT09PT09PT09PT09PT09EccoKAAnLBn0/T09PT09COH9PT09PT09GlG9PT09PT0jRxfoADtcQBZAQEBATwICwJBAQEBATxLCzp5AQEBAQFY48n5AIlQkzf09PT0BwsLC2r09PT0xwsLCy709PT0Zc9HLJ8AlhVlADv09PQQGQsLyvT09PSIeAsLPfT09PfSmR5Q3ACVvPSDZ6n09PQjLhb09PT09PS+BD309PTTOYX79NXBAG/UdAG53g0BAQEBAQEBAQEBAQEBAQEBPmihdAH+kmIARGsT23YAALaQXfT09PT09PT09PT0PiTdAKrmNC1y7gD5dVuC186sIdDDJIAPWf4yjB3Afk/doufxIHpV6I5fAKBjoKCglN+gzLKPQNGK82cv0FU2wa1glN+UyHWgY6AA/DU1NTU1NTWnoKBjkSspAD8wdf2gDDU1DJQMqDU1/ACgNTU1NTU1NTU1NainlPx8bPmoNTU1Y5Tlv3XfxjWgAKA1NTU1NTU1NTU1NTU1MeprlDU1NajgEbqKTGHfNaAAoDU1NTU1NTU1NTU1NTWUsLq7pzVjnlySdygFCX2ooAD8NTU1NTU1NTU1NTU1NWN1vcLt/Y5UxOsBAQ4ABgz8AKA1NTU1NTU1NTU1NTU1NfxF4kheky+z5HP0H3DIY6AAoDU1NTU1NTU1NTU1NTU1YBuB4TOj8G6LphQAmmA1oACgNTU1NTU1NTU1NTU1NTU1qMiU/GCnlMvB2lNgNTWgAPw1NTU1NTU1NTU1NTU1NTXvY+81NTU1p6D8lDU1NfwAVvz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8VgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAP////8K';
 
7915                                 // remove current favicons and replace accordingly, or tinycon has a cross domain issue since the real favicon is on redditstatic.com.
 
7916                                 $('head link[rel="shortcut icon"], head link[rel="icon"]').attr('href',faviconDataurl);
 
7918                         if ((modules['betteReddit'].options.showLastEditedTimestamp.value) && ((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments'))) {
 
7919                                 RESUtils.addCSS('.edited-timestamp[title]:after{content:" (" attr(title) ")";font-size: 90%;}');
 
7921                         if ((modules['betteReddit'].options.restoreSavedTab.value) && (RESUtils.loggedInUser() !== null) && document.querySelector('.with-listing-chooser:not(.profile-page)')) {
 
7922                                 this.restoreSavedTab();
 
7924                         if ((modules['betteReddit'].options.toolbarFix.value) && (RESUtils.pageType() === 'linklist')) {
 
7925                                 RESUtils.watchForElement('siteTable', modules['betteReddit'].toolbarFix);
 
7927                         if ((RESUtils.pageType() === 'inbox') && (modules['betteReddit'].options.fullCommentsLink.value)) {
 
7928                                 RESUtils.watchForElement('siteTable', modules['betteReddit'].fullComments);
 
7930                         if (((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments')) && (modules['betteReddit'].options.fixSaveLinks.value)) {
 
7931                                 RESUtils.watchForElement('siteTable', modules['betteReddit'].fixSaveLinks);
 
7933                         if (((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments')) && (modules['betteReddit'].options.fixHideLinks.value)) {
 
7934                                 RESUtils.watchForElement('siteTable', modules['betteReddit'].fixHideLinks);
 
7936                         if ((RESUtils.pageType() === 'comments') && (modules['betteReddit'].options.commentsLinksNewTabs.value)) {
 
7937                                 RESUtils.watchForElement('newComments', modules['betteReddit'].commentsLinksNewTabs);
 
7940                         if (this.options.searchSubredditByDefault.value) {
 
7941                                 // make sure we're not on a search results page...
 
7942                                 if (!location.href.match('/[r|m]/[\\w+\\-]+/search')) {
 
7943                                         this.searchSubredditByDefault();
 
7946                         if ((this.options.videoTimes.value) && ((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments'))) {
 
7947                                 this.getVideoTimes();
 
7948                                 // listen for new DOM nodes so that modules like autopager, river of reddit, etc still get l+c links...
 
7950                                 RESUtils.watchForElement('siteTable', modules['betteReddit'].getVideoTimes);
 
7952                         if ((RESUtils.loggedInUser() !== null) && ((this.options.showUnreadCount.value) || (this.options.showUnreadCountInTitle.value) || (this.options.showUnreadCountInFavicon.value))) {
 
7953                                 // Reddit CSS change broke this when they went to sprite sheets.. new CSS will fix the issue.
 
7954                                 // RESUtils.addCSS('#mail { min-width: 16px !important; width: auto !important; text-indent: 18px !important; background-repeat: no-repeat !important; line-height: 8px !important; }');
 
7955                                 // removing text indent - on 11/14/11 reddit changed the mail sprites, so I have to change how this is handled..
 
7956                                 RESUtils.addCSS('#mail { top: 2px; min-width: 16px !important; width: auto !important; background-repeat: no-repeat !important; line-height: 8px !important; }');
 
7957                                 // RESUtils.addCSS('#mail.havemail { top: 2px !important; margin-right: 1px; }');
 
7958                                 RESUtils.addCSS('#mail.havemail { top: 2px !important; }');
 
7959                                 if ((BrowserDetect.isChrome()) || (BrowserDetect.isSafari())) {
 
7960                                         // I hate that I have this conditional CSS in here but I can't figure out why it's needed for webkit and screws up firefox.
 
7961                                         RESUtils.addCSS('#mail.havemail { top: 0; }');
 
7963                                 this.showUnreadCount();
 
7965                         switch(this.options.pinHeader.value) {
 
7968                                         $('body').addClass('pinHeader-header');
 
7971                                         this.pinSubredditBar();
 
7972                                         $('body').addClass('pinHeader-sub');
 
7975                                         this.pinSubredditBar();
 
7977                                         $('body').addClass('pinHeader-subanduser');
 
7981                                         $('body').addClass('pinHeader-userbar');
 
7988         commentsLinksNewTabs: function(ele) {
 
7989                 ele = ele || document.body;
 
7990                 var links = ele.querySelectorAll('div.md a');
 
7991                 for (var i=0, len=links.length; i<len; i++) {
 
7992                         links[i].target = '_blank';
 
7995         setUpTurboSelfText: function() {
 
7996                 // TODO: Turbo selftext seems a little wonky on NER pages
 
7997                 modules['betteReddit'].selfTextHash = {};
 
7998                 $('body').on('click', '.expando-button.selftext:not(".twitter"):not(.toggleImage)', modules['betteReddit'].showSelfText);
 
7999                 $('#siteTable').data('jsonURL', location.href+'.json');
 
8001                 RESUtils.watchForElement('siteTable', modules['betteReddit'].setNextSelftextURL);
 
8003         setNextSelftextURL: function(ele) {
 
8004                 if (modules['neverEndingReddit'].nextPageURL) {
 
8005                         var jsonURL = modules['neverEndingReddit'].nextPageURL.replace('/?','/.json?');
 
8006                         $(ele).data('jsonURL',jsonURL);
 
8009         showSelfText: function(event) {
 
8010                 var thisID = $(event.target).parent().parent().attr('data-fullname');
 
8011                 if (typeof modules['betteReddit'].selfTextHash[thisID] === 'undefined') {
 
8012                         // we haven't gotten JSON data for this set of links yet... get it, then replace the click listeners with our own...
 
8013                         var jsonURL = $(event.target).closest('.sitetable.linklisting').data('jsonURL');
 
8014                         modules['betteReddit'].getSelfTextData(jsonURL);
 
8016                         if ($(event.target).hasClass('collapsed') || $(event.target).hasClass('collapsedExpando')) {
 
8017                                 // the duplicate classes here unfortunately have to exist due to Reddit clobbering things with .collapsed
 
8018                                 // and no real elegant way that I've thought of to fix the fact that selfText expandos still have that class.
 
8019                                 $(event.target).removeClass('collapsed collapsedExpando');
 
8020                                 $(event.target).addClass('expanded');
 
8021                                 $(event.target).parent().find('.expando').html(
 
8022                                         '<form class="usertext"><div class="usertext-body">' + 
 
8023                                         $('<div/>').html(modules['betteReddit'].selfTextHash[thisID]).text() + 
 
8027                                 $(event.target).removeClass('expanded');
 
8028                                 $(event.target).addClass('collapsedExpando');
 
8029                                 $(event.target).addClass('collapsed');
 
8030                                 $(event.target).parent().find('.expando').hide();
 
8035         getSelfTextData: function(href) {
 
8036                 if (!modules['betteReddit'].gettingSelfTextData) {
 
8037                         modules['betteReddit'].gettingSelfTextData = true;
 
8038                         $.getJSON(href, modules['betteReddit'].applyTurboSelfText);
 
8041         applyTurboSelfText: function(data) {
 
8042                 var linkList = data.data.children;
 
8044                 delete modules['betteReddit'].gettingSelfTextData;
 
8045                 for (var i=0, len=linkList.length; i<len; i++) {
 
8046                         var thisID = linkList[i].data.name;
 
8048                                 var thisSiteTable = $('.id-'+thisID).closest('.sitetable.linklisting');
 
8049                                 $(thisSiteTable).find('.expando-button.selftext').removeAttr('onclick');
 
8051                         modules['betteReddit'].selfTextHash[thisID] = linkList[i].data.selftext_html;
 
8054         getInboxLink: function (havemail) {
 
8055                 if (havemail && !modules['betteReddit'].options.unreadLinksToInbox.value) { 
 
8056                         return '/message/unread/';
 
8059                 return '/message/inbox/';
 
8061         showUnreadCount: function() {
 
8062                 if (typeof this.mail === 'undefined') {
 
8063                         this.mail = document.querySelector('#mail');
 
8065                                 this.mailCount = createElementWithID('a','mailCount');
 
8066                                 this.mailCount.display = 'none';
 
8067                                 this.mailCount.setAttribute('href', this.getInboxLink(true));
 
8068                                 insertAfter(this.mail, this.mailCount);
 
8072                         $(modules['betteReddit'].mail).html('');
 
8073                         if (this.mail.classList.contains('havemail')) {
 
8074                                 this.mail.setAttribute('href', this.getInboxLink(true));
 
8075                                 var lastCheck = parseInt(RESStorage.getItem('RESmodules.betteReddit.msgCount.lastCheck.'+RESUtils.loggedInUser()), 10) || 0;
 
8076                                 var now = new Date();
 
8077                                 // 300000 = 5 minutes... we don't want to annoy Reddit's servers too much with this query...
 
8078                                 if ((now.getTime() - lastCheck) > 300000) {
 
8081                                                 url:    location.protocol + '//' + location.hostname + "/message/unread/.json?mark=false&app=res",
 
8082                                                 onload: function(response) {
 
8083                                                         // save that we've checked in the last 5 minutes
 
8084                                                         var now = new Date();
 
8085                                                         RESStorage.setItem('RESmodules.betteReddit.msgCount.lastCheck.'+RESUtils.loggedInUser(), now.getTime());
 
8086                                                         var data = JSON.parse(response.responseText);
 
8087                                                         var count = data.data.children.length;
 
8088                                                         RESStorage.setItem('RESmodules.betteReddit.msgCount.'+RESUtils.loggedInUser(), count);
 
8089                                                         modules['betteReddit'].setUnreadCount(count);
 
8093                                         var count = RESStorage.getItem('RESmodules.betteReddit.msgCount.'+RESUtils.loggedInUser());
 
8094                                         modules['betteReddit'].setUnreadCount(count);
 
8097                                 // console.log('no need to get count - no new mail. resetting lastCheck');
 
8098                                 modules['betteReddit'].setUnreadCount(0);
 
8099                                 RESStorage.setItem('RESmodules.betteReddit.msgCount.lastCheck.'+RESUtils.loggedInUser(), 0);
 
8103         setUnreadCount: function(count) {
 
8104                 if (this.options.showUnreadCountInFavicon.value) {
 
8105                         //window.Tinycon.setOptions({ fallback: false });
 
8108                         if (this.options.showUnreadCountInTitle.value) {
 
8109                                 var newTitle = '[' + count + '] ' + document.title.replace(/^\[[\d]+\]\s/,'');
 
8110                                 document.title = newTitle;
 
8112                         if (this.options.showUnreadCountInFavicon.value) {
 
8113                                 //window.Tinycon.setBubble(count);
 
8115                         if (this.options.showUnreadCount.value) {
 
8116                                 modules['betteReddit'].mailCount.display = 'inline-block'
 
8117                                 modules['betteReddit'].mailCount.textContent = '['+count+']';
 
8118                                 if (modules['neverEndingReddit'].NREMailCount) {
 
8119                                         modules['neverEndingReddit'].NREMailCount.display = 'inline-block'
 
8120                                         modules['neverEndingReddit'].NREMailCount.textContent = '['+count+']';
 
8124                         var newTitle = document.title.replace(/^\[[\d]+\]\s/,'');
 
8125                         document.title = newTitle;
 
8126                         if (modules['betteReddit'].mailCount) {
 
8127                                 modules['betteReddit'].mailCount.display = 'none';
 
8128                                 $(modules['betteReddit'].mailCount).html('');
 
8129                                 if (modules['neverEndingReddit'].NREMailCount) {
 
8130                                         modules['neverEndingReddit'].NREMailCount.display = 'none'
 
8131                                         $(modules['neverEndingReddit'].NREMailCount).html('');
 
8134                         if (this.options.showUnreadCountInFavicon.value) {
 
8135                                 //window.Tinycon.setBubble(0);
 
8152         checkToolbarLink: function(url) {
 
8153                 for (var i=0, len=this.toolbarFixLinks.length; i<len; i++) {
 
8154                         if (url.indexOf(this.toolbarFixLinks[i]) !== -1) return true;
 
8158         toolbarFix: function(ele) {
 
8159                 var root = ele || document;
 
8160                 var links = root.querySelectorAll('div.entry a.title');
 
8161                 for (var i=0, len=links.length; i<len; i++) {
 
8162                         if (modules['betteReddit'].checkToolbarLink(links[i].getAttribute('href'))) {
 
8163                                 links[i].removeAttribute('onmousedown');
 
8165                         // patch below for comments pages thanks to redditor and resident helperninja gavin19
 
8166                         if (links[i].getAttribute('srcurl')) {
 
8167                                 if (modules['betteReddit'].checkToolbarLink(links[i].getAttribute('srcurl'))) {
 
8168                                         links[i].removeAttribute('onmousedown');
 
8173         fullComments: function(ele) {
 
8174                 var root = ele || document;
 
8175                 var entries = root.querySelectorAll('#siteTable .entry');
 
8177                 for (var i=0, len=entries.length; i<len;i++) {
 
8178                         var linkEle = entries[i].querySelector('A.bylink');
 
8179                         var thisCommentsLink = '';
 
8180                         if ((typeof linkEle !== 'undefined') && (linkEle !== null)) {
 
8181                                 thisCommentsLink = linkEle.getAttribute('href');
 
8183                         if (thisCommentsLink !== '') {
 
8184                                 thisCommentsSplit = thisCommentsLink.split("/");
 
8185                                 thisCommentsSplit.pop();
 
8186                                 thisCommentsLink = thisCommentsSplit.join("/");
 
8187                                 linkList = entries[i].querySelector('.flat-list');
 
8188                                 var fullCommentsLink = document.createElement('li');
 
8189                                 $(fullCommentsLink).html('<a class="redditFullComments" href="' + escapeHTML(thisCommentsLink) + '">'+ escapeHTML(this.options.fullCommentsText.value) +'</a>');
 
8190                                 linkList.appendChild(fullCommentsLink);
 
8194         editMyComments: function(ele) {
 
8195                 var root = ele || document;
 
8196                 var entries = root.querySelectorAll('#siteTable .entry');
 
8197                 for (var i=0, len=entries.length; i<len;i++) {
 
8198                         var linkEle = entries[i].querySelector('A.bylink');
 
8199                         var thisCommentsLink = '';
 
8200                         if ((typeof linkEle !== 'undefined') && (linkEle !== null)) {
 
8201                                 thisCommentsLink = linkEle.getAttribute('href');
 
8203                         if (thisCommentsLink !== '') {
 
8204                                 permalink = entries[i].querySelector('.flat-list li.first');
 
8205                                 var editLink = document.createElement('li');
 
8206                                 $(editLink).html('<a onclick="return edit_usertext(this)" href="javascript:void(0);">edit</a>');
 
8207                                 insertAfter(permalink, editLink);
 
8211         fixSaveLinks: function(ele) {
 
8212                 var root = ele || document;
 
8213                 var saveLinks = root.querySelectorAll('li:not(.comment-save-button) > FORM.save-button > SPAN > A');
 
8214                 for (var i=0, len=saveLinks.length; i<len; i++) {
 
8215                         saveLinks[i].removeAttribute('onclick');
 
8216                         saveLinks[i].setAttribute('action','save');
 
8217                         saveLinks[i].addEventListener('click', modules['betteReddit'].saveLink, false);
 
8219                 var unsaveLinks = document.querySelectorAll('FORM.unsave-button > SPAN > A');
 
8220                 for (var i=0, len=saveLinks.length; i<len; i++) {
 
8221                         if (typeof unsaveLinks[i] !== 'undefined') {
 
8222                                 unsaveLinks[i].removeAttribute('onclick');
 
8223                                 unsaveLinks[i].setAttribute('action','unsave');
 
8224                                 unsaveLinks[i].addEventListener('click', modules['betteReddit'].saveLink, false);
 
8228         fixHideLinks: function(ele) {
 
8229                 var root = ele || document;
 
8230                 var hideLinks = root.querySelectorAll('FORM.hide-button > SPAN > A');
 
8231                 for (var i=0, len=hideLinks.length; i<len; i++) {
 
8232                         hideLinks[i].removeAttribute('onclick');
 
8233                         hideLinks[i].setAttribute('action','hide');
 
8234                         hideLinks[i].addEventListener('click', modules['betteReddit'].hideLinkEventHandler, false);
 
8236                 var unhideLinks = document.querySelectorAll('FORM.unhide-button > SPAN > A');
 
8237                 for (var i=0, len=hideLinks.length; i<len; i++) {
 
8238                         if (typeof unhideLinks[i] !== 'undefined') {
 
8239                                 unhideLinks[i].removeAttribute('onclick');
 
8240                                 unhideLinks[i].setAttribute('action','unhide');
 
8241                                 unhideLinks[i].addEventListener('click', modules['betteReddit'].hideLinkEventHandler, false);
 
8245         saveLink: function(e) {
 
8246                 if (e) modules['betteReddit'].saveLinkClicked = e.target;
 
8247                 if (modules['betteReddit'].saveLinkClicked.getAttribute('action') === 'unsave') {
 
8248                         $(modules['betteReddit'].saveLinkClicked).text('unsaving...');
 
8250                         $(modules['betteReddit'].saveLinkClicked).text('saving...');
 
8252                 if (modules['betteReddit'].modhash) {
 
8253                         var action = modules['betteReddit'].saveLinkClicked.getAttribute('action');
 
8254                         var parentThing = modules['betteReddit'].saveLinkClicked.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
 
8255                         var idRe = /id-([\w]+)/i;
 
8256                         var getLinkid = idRe.exec(parentThing.getAttribute('class'));
 
8257                         var linkid = getLinkid[1];
 
8258                         if (action === 'unsave') {
 
8259                                 var executed = 'unsaved';
 
8260                                 var apiURL = location.protocol + '//'+location.hostname+'/api/unsave';
 
8262                                 var executed = 'saved';
 
8263                                 var apiURL = location.protocol + '//'+location.hostname+'/api/save';
 
8265                         var params = 'id='+linkid+'&executed='+executed+'&uh='+modules['betteReddit'].modhash+'&renderstyle=html';
 
8271                                         "Content-Type": "application/x-www-form-urlencoded"
 
8273                                 onload: function(response) {
 
8274                                         if (response.status === 200) {
 
8275                                                 if (modules['betteReddit'].saveLinkClicked.getAttribute('action') === 'unsave') {
 
8276                                                         $(modules['betteReddit'].saveLinkClicked).text('save');
 
8277                                                         modules['betteReddit'].saveLinkClicked.setAttribute('action','save');
 
8279                                                         $(modules['betteReddit'].saveLinkClicked).text('unsave');
 
8280                                                         modules['betteReddit'].saveLinkClicked.setAttribute('action','unsave');
 
8283                                                 delete modules['betteReddit'].modhash;
 
8284                                                 alert('Sorry, there was an error trying to '+modules['betteReddit'].saveLinkClicked.getAttribute('action')+' your submission. Try clicking again.');
 
8291                                 url:    location.protocol + '//'+location.hostname+'/api/me.json?app=res',
 
8292                                 onload: function(response) {
 
8293                                         var data = safeJSON.parse(response.responseText);
 
8294                                         if (typeof data.data === 'undefined') {
 
8295                                                 alert('Sorry, there was an error trying to '+modules['betteReddit'].saveLinkClicked.getAttribute('action')+' your submission. You may have third party cookies disabled. You will need to either enable third party cookies, or add an exception for *.reddit.com');
 
8296                                         } else if ((typeof data.data.modhash !== 'undefined') && (data.data.modhash)) {
 
8297                                                 modules['betteReddit'].modhash = data.data.modhash;
 
8298                                                 modules['betteReddit'].saveLink();
 
8304         hideLinkEventHandler: function(e) {
 
8305                 modules['betteReddit'].hideLink(e.target);
 
8307         hideLink: function(clickedLink) {
 
8308                 if (clickedLink.getAttribute('action') === 'unhide') {
 
8309                         $(clickedLink).text('unhiding...');
 
8311                         $(clickedLink).text('hiding...');
 
8313                 if (modules['betteReddit'].modhash) {
 
8314                         var action = clickedLink.getAttribute('action');
 
8315                         var parentThing = clickedLink.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
 
8316                         var idRe = /id-([\w]+)/i;
 
8317                         var getLinkid = idRe.exec(parentThing.getAttribute('class'));
 
8318                         var linkid = getLinkid[1];
 
8319                         if (action === 'unhide') {
 
8320                                 var executed = 'unhidden';
 
8321                                 var apiURL = 'http://'+location.hostname+'/api/unhide';
 
8323                                 var executed = 'hidden';
 
8324                                 var apiURL = 'http://'+location.hostname+'/api/hide';
 
8326                         var params = 'id='+linkid+'&executed='+executed+'&uh='+modules['betteReddit'].modhash+'&renderstyle=html';
 
8333                                         "Content-Type": "application/x-www-form-urlencoded"
 
8335                                 onload: function(response) {
 
8336                                         if (response.status === 200) {
 
8337                                                 if (clickedLink.getAttribute('action') === 'unhide') {
 
8338                                                         $(clickedLink).text('hide');
 
8339                                                         clickedLink.setAttribute('action','hide');
 
8340                                                         if (typeof modules['betteReddit'].hideTimer !== 'undefined') clearTimeout(modules['betteReddit'].hideTimer);
 
8342                                                         $(clickedLink).text('unhide');
 
8343                                                         clickedLink.setAttribute('action','unhide');
 
8344                                                         modules['betteReddit'].hideTimer = setTimeout(function() {
 
8345                                                                 modules['betteReddit'].hideFader(clickedLink);
 
8349                                                 delete modules['betteReddit'].modhash;
 
8350                                                 alert('Sorry, there was an error trying to '+clickedLink.getAttribute('action')+' your submission. Try clicking again.');
 
8357                                 url:    location.protocol + '//'+location.hostname+'/api/me.json?app=res',
 
8358                                 onload: function(response) {
 
8359                                         var data = safeJSON.parse(response.responseText);
 
8360                                         if (typeof data.data === 'undefined') {
 
8361                                                 alert('Sorry, there was an error trying to '+clickedLink.getAttribute('action')+' your submission. You may have third party cookies disabled. You will need to either enable third party cookies, or add an exception for *.reddit.com');
 
8362                                         } else if ((typeof data.data.modhash !== 'undefined') && (data.data.modhash)) {
 
8363                                                 modules['betteReddit'].modhash = data.data.modhash;
 
8364                                                 modules['betteReddit'].hideLink(clickedLink);
 
8370         hideFader: function(ele) {
 
8371                 var parentThing = ele.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
 
8372                 RESUtils.fadeElementOut(parentThing, 0.3);
 
8374         searchSubredditByDefault: function() {
 
8375                 // Reddit now has this feature... but for some reason the box isn't checked by default, so we'll still do that...
 
8376                 var restrictSearch = document.body.querySelector('INPUT[name=restrict_sr]');
 
8377                 if (restrictSearch) {
 
8378                         restrictSearch.checked = true;
 
8381         getVideoTimes: function(obj) {
 
8382                 obj = obj || document;
 
8383                 var youtubeLinks = obj.querySelectorAll('a.title[href*="youtube.com"], a.title[href*="youtu.be"]');
 
8384                 var shortenedYoutubeLinks = obj.querySelectorAll('a.title[href*="youtube.com"]');
 
8385                 var titleHasTimeRegex = /[\[|\(][0-9]*:[0-9]*[\]|\)]/;
 
8388                         for (var i=0, len=youtubeLinks.length; i<len; i+=1) {
 
8389                                 if(!youtubeLinks[i].innerHTML.match(titleHasTimeRegex)) {
 
8390                                         ytLinks.push(youtubeLinks[i]);
 
8393                         youtubeLinks = ytLinks;
 
8394                         var getYoutubeIDRegex = /\/?[\&|\?]?v\/?=?([\w\-]{11})&?/i;
 
8395                         var getShortenedYoutubeIDRegex = /([\w\-]{11})&?/i;
 
8396                         var getYoutubeStartTimeRegex = /\[[\d]+:[\d]+\]/i;
 
8398                         modules['betteReddit'].youtubeLinkIDs = {};
 
8399                         modules['betteReddit'].youtubeLinkRefs = [];
 
8400                         for (var i = 0, len = youtubeLinks.length; i < len; i++) {
 
8401                                 var match = getYoutubeIDRegex.exec(youtubeLinks[i].getAttribute('href'));
 
8402                                 var shortened = /youtu\.be/i;
 
8403                                 var isShortened = shortened.exec(youtubeLinks[i].getAttribute('href'));
 
8405                                         var smatch = getShortenedYoutubeIDRegex.exec(youtubeLinks[i].getAttribute('href'));
 
8407                                                 var thisYTID = '"' + smatch[1] + '"';
 
8408                                                 modules['betteReddit'].youtubeLinkIDs[thisYTID] = youtubeLinks[i];
 
8409                                                 modules['betteReddit'].youtubeLinkRefs.push([thisYTID, youtubeLinks[i]]);
 
8412                                         // add quotes so URL creation is doable with just a join...
 
8413                                         var thisYTID = '"' + match[1] + '"';
 
8414                                         modules['betteReddit'].youtubeLinkIDs[thisYTID] = youtubeLinks[i];
 
8415                                         modules['betteReddit'].youtubeLinkRefs.push([thisYTID, youtubeLinks[i]]);
 
8417                                 var timeMatch = getYoutubeStartTimeRegex.exec(youtubeLinks[i].getAttribute('href'));
 
8418                                 var titleMatch = youtubeLinks[i].innerHTML.match(titleHasTimeRegex);
 
8419                                 if (timeMatch && !titleMatch) {
 
8420                                         youtubeLinks[i].textContent += ' (@' + timeMatch[1] + ')';
 
8423                         for (var id in modules['betteReddit'].youtubeLinkIDs){
 
8426                         modules['betteReddit'].youtubeLinkIDs = tempIDs;
 
8427                         modules['betteReddit'].getVideoJSON();
 
8430         getVideoJSON: function() {
 
8431                 var thisBatch = modules['betteReddit'].youtubeLinkIDs.splice(0,8);
 
8432                 if (thisBatch.length) {
 
8433                         var thisIDString = thisBatch.join('%7C');
 
8434                         // var jsonURL = 'http://gdata.youtube.com/feeds/api/videos?q='+thisIDString+'&fields=entry(id,media:group(yt:duration))&alt=json';
 
8435                         var jsonURL = 'http://gdata.youtube.com/feeds/api/videos?q='+thisIDString+'&v=2&fields=entry(id,title,media:group(yt:duration,yt:videoid,yt:uploaded))&alt=json';
 
8439                                 onload: function(response) {
 
8440                                         var data = safeJSON.parse(response.responseText, null, true);
 
8441                                         if ((typeof data.feed !== 'undefined') && (typeof data.feed.entry !== 'undefined')) {
 
8442                                                 for (var i=0, len=data.feed.entry.length; i<len; i++) {
 
8443                                                         var thisYTID = '"'+data.feed.entry[i]['media$group']['yt$videoid']['$t']+'"';
 
8444                                                         var thisTotalSecs = data.feed.entry[i]['media$group']['yt$duration']['seconds'];
 
8445                                                         var thisTitle = data.feed.entry[i]['title']['$t'];
 
8446                                                         var thisMins = Math.floor(thisTotalSecs/60);
 
8447                                                         var thisSecs = (thisTotalSecs%60);
 
8448                                                         if (thisSecs < 10) thisSecs = '0'+thisSecs;
 
8449                                                         var thisTime = ' - [' + thisMins + ':' + thisSecs + ']';
 
8450                                                         if(modules['betteReddit'].options.videoUploaded.value){
 
8451                                                                 var thisUploaded = data.feed.entry[i]['media$group']['yt$uploaded']['$t'];
 
8452                                                                 thisUploaded = thisUploaded.match(/[^T]*/);
 
8453                                                                 thisTime += '['+ thisUploaded +']';
 
8455                                                         for (var j = 0, lenj = modules['betteReddit'].youtubeLinkRefs.length; j < lenj; j += 1){
 
8456                                                                 if (modules['betteReddit'].youtubeLinkRefs[j][0] === thisYTID) {
 
8457                                                                         modules['betteReddit'].youtubeLinkRefs[j][1].textContent += ' ' + thisTime;
 
8458                                                                         modules['betteReddit'].youtubeLinkRefs[j][1].setAttribute('title','YouTube title: '+thisTitle);
 
8462                                                 // wait a bit, make another request...
 
8463                                                 setTimeout(modules['betteReddit'].getVideoJSON, 500);
 
8469         pinSubredditBar: function() {
 
8470                 // Make the subreddit bar at the top of the page a fixed element
 
8471                 // The subreddit manager code changes the document's structure
 
8472                 var sm = modules['subredditManager'].isEnabled();
 
8474                 var sb = document.getElementById('sr-header-area');
 
8475                 if (sb == null) return; // reddit is under heavy load
 
8476                 var header = document.getElementById('header');
 
8478                 // add a dummy <div> inside the header to replace the subreddit bar (for spacing)
 
8479                 var spacer = document.createElement('div');
 
8480                 // null parameter is necessary for FF3.6 compatibility.
 
8481                 spacer.style.paddingTop = window.getComputedStyle(sb, null).paddingTop;
 
8482                 spacer.style.paddingBottom = window.getComputedStyle(sb, null).paddingBottom;
 
8484                 // HACK: for some reason, if the SM is enabled, the SB gets squeezed horizontally,
 
8485                 //       and takes up three rows of vertical space (even at low horizontal resolution).
 
8486                 if (sm) spacer.style.height = (parseInt(window.getComputedStyle(sb, null).height, 10) / 3 - 3)+'px';
 
8487                 else    spacer.style.height = window.getComputedStyle(sb, null).height;
 
8489                 //window.setTimeout(function(){
 
8490                 // add the spacer; take the subreddit bar out of the header and put it above
 
8491                 header.insertBefore(spacer, sb);
 
8492                 document.body.insertBefore(sb,header);
 
8495                 // RESUtils.addCSS('div#sr-header-area {position: fixed; z-index: 10000 !important; left: 0; right: 0; box-shadow: 0 2px 2px #AAA;}');
 
8496                 // something changed on Reddit on 1/31/2012 that made this header-bottom-left margin break subreddit stylesheets... commenting out seems to fix it?
 
8497                 // and now later on 1/31 they've changed it back and I need to add this line back in...
 
8498                 RESUtils.addCSS('#header-bottom-left { margin-top: 19px; }');
 
8499                 RESUtils.addCSS('div#sr-header-area {position: fixed; z-index: 10000 !important; left: 0; right: 0; }');
 
8500                 this.pinCommonElements(sm);
 
8502         pinUserBar: function() {
 
8503                 // Make the user bar at the top of the page a fixed element
 
8504                 this.userBarElement = document.getElementById('header-bottom-right');
 
8505                 var thisHeight = $('#header-bottom-right').height();
 
8506                 RESUtils.addCSS('#header-bottom-right:hover { opacity: 1 !important;  }');
 
8507                 RESUtils.addCSS('#header-bottom-right { height: '+parseInt(thisHeight+1, 10)+'px; }');
 
8508                 // make the account switcher menu fixed
 
8509                 window.addEventListener('scroll', modules['betteReddit'].handleScroll, false);
 
8510                 this.pinCommonElements();
 
8512         handleScroll: function(e) {
 
8513                 if (modules['betteReddit'].scrollTimer) clearTimeout(modules['betteReddit'].scrollTimer);
 
8514                 modules['betteReddit'].scrollTimer = setTimeout(modules['betteReddit'].handleScrollAfterTimer, 300);
 
8516         handleScrollAfterTimer: function(e) {
 
8517                 if (RESUtils.elementInViewport(modules['betteReddit'].userBarElement)) {
 
8518                         modules['betteReddit'].userBarElement.setAttribute('style','');
 
8519                         if (typeof modules['accountSwitcher'].accountMenu !== 'undefined') {
 
8520                                 $(modules['accountSwitcher'].accountMenu).attr('style','position: absolute;');
 
8522                 } else if (modules['betteReddit'].options.pinHeader.value === 'subanduser') {
 
8523                         if (typeof modules['accountSwitcher'].accountMenu !== 'undefined') {
 
8524                                 $(modules['accountSwitcher'].accountMenu).attr('style','position: fixed;');
 
8526                         modules['betteReddit'].userBarElement.setAttribute('style','position: fixed; z-index: 10000 !important; top: 19px; right: 0; opacity: 0.6; -webkit-transition:opacity 0.3s ease-in; -moz-transition:opacity 0.3s ease-in; -o-transition:opacity 0.3s ease-in; -ms-transition:opacity 0.3s ease-in; -transition:opacity 0.3s ease-in;');
 
8528                         if (typeof modules['accountSwitcher'].accountMenu !== 'undefined') {
 
8529                                 $(modules['accountSwitcher'].accountMenu).attr('style','position: fixed;');
 
8531                         modules['betteReddit'].userBarElement.setAttribute('style','position: fixed; z-index: 10000 !important; top: 0; right: 0; opacity: 0.6; -webkit-transition:opacity 0.3s ease-in; -moz-transition:opacity 0.3s ease-in; -o-transition:opacity 0.3s ease-in; -ms-transition:opacity 0.3s ease-in; -transition:opacity 0.3s ease-in;');
 
8534         pinHeader: function() {
 
8535                 // Makes the Full header a fixed element
 
8537                 // the subreddit manager code changes the document's structure
 
8538                 var sm = modules['subredditManager'].isEnabled();
 
8540                 var header = document.getElementById('header');
 
8541                 if (header == null) return; // reddit is under heavy load
 
8543                 // add a dummy <div> to the document for spacing
 
8544                 var spacer = document.createElement('div');
 
8545                 spacer.id = 'RESPinnedHeaderSpacer';
 
8547                 // without the next line, the subreddit manager would make the subreddit bar three lines tall and very narrow
 
8548                 RESUtils.addCSS('#sr-header-area {left: 0; right: 0;}');
 
8549                 spacer.style.height = $('#header').outerHeight() + "px";
 
8551                 // insert the spacer
 
8552                 document.body.insertBefore(spacer, header.nextSibling);
 
8554                 // make the header fixed
 
8555                 RESUtils.addCSS('#header, #RESAccountSwitcherDropdown {position:fixed;}');
 
8556                 // RESUtils.addCSS('#header {left: 0; right: 0; box-shadow: 0 2px 2px #AAA;}');
 
8557                 RESUtils.addCSS('#header {left: 0; right: 0; }');
 
8558                 var headerHeight = $('#header').height() + 15;
 
8559                 RESUtils.addCSS('#RESNotifications { top: '+headerHeight+'px } ');
 
8560                 this.pinCommonElements(sm);
 
8562                 // TODO Needs testing
 
8563                 // Sometimes this gets executed before the subreddit logo has finished loading. When that
 
8564                 // happens, the spacer gets created too short, so when the SR logo finally loads, the header
 
8565                 // grows and overlaps the top of the page, potentially obscuring the first link. This checks
 
8566                 // to see if the image is finished loading. If it is, then the spacer's height is set. Otherwise,
 
8567                 // it pauses, then loops.
 
8568                 // added a check that this element exists, so it doesn't error out RES.
 
8569                 if (document.getElementById('header-img') && (!document.getElementById('header-img').complete)) setTimeout(function(){
 
8570                                            if (document.getElementById('header-img').complete)
 
8571                                                            // null parameter is necessary for FF3.6 compatibility.
 
8572                                                            document.getElementById('RESPinnedHeaderSpacer').style.height = window.getComputedStyle(document.getElementById('header'), null).height;
 
8573                                            else setTimeout(arguments.callee, 10);
 
8576         pinCommonElements: function(sm) {
 
8577                 // pin the elements common to both pinHeader() and pinSubredditBar()
 
8579                            // RES's subreddit menu
 
8580                            RESUtils.addCSS('#RESSubredditGroupDropdown, #srList, #RESShortcutsAddFormContainer, #editShortcutDialog {position: fixed !important;}');
 
8582                            RESUtils.addCSS('#sr-more-link: {position: fixed;}');
 
8585         restoreSavedTab: function() {
 
8586                 var tabmenu = document.querySelector('#header .tabmenu'),
 
8587                     li = document.createElement('li'),
 
8588                     a = document.createElement('a'),
 
8589                     user = RESUtils.loggedInUser();
 
8590                 a.textContent = 'saved';
 
8591                 a.href = '/user/' + user + '/saved/';
 
8593                 tabmenu.appendChild(li);
 
8597 modules['singleClick'] = {
 
8598         moduleID: 'singleClick',
 
8599         moduleName: 'Single Click Opener',
 
8605                                 { name: 'open comments then link', value: 'commentsfirst' },
 
8606                                 { name: 'open link then comments', value: 'linkfirst' }
 
8608                         value: 'commentsfirst',
 
8609                         description: 'What order to open the link/comments in.'
 
8614                         description: 'Hide the [l=c] when the link is the same as the comments page'
 
8619                         description: 'Open the [l+c] link in background tabs'
 
8622         description: 'Adds an [l+c] link that opens a link and the comments page in new tabs for you in one click.',
 
8623         isEnabled: function() {
 
8624                 return RESConsole.getModulePrefs(this.moduleID);
 
8626         isMatchURL: function() {
 
8627                 return RESUtils.isMatchURL(this.moduleID);
 
8630                 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i,
 
8631                 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\._]*\//i
 
8634                 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\._\/\?]*\/comments[-\w\._\/\?=]*/i
 
8636         beforeLoad: function() {
 
8637                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
8638                         RESUtils.addCSS('.redditSingleClick { color: #888; font-weight: bold; cursor: pointer; padding: 0 1px; }');
 
8642                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
8645                         // listen for new DOM nodes so that modules like autopager, river of reddit, etc still get l+c links...
 
8646                         RESUtils.watchForElement('siteTable', modules['singleClick'].applyLinks);
 
8649         applyLinks: function(ele) {
 
8650                 var ele = ele || document;
 
8651                 var entries = ele.querySelectorAll('#siteTable .entry, #siteTable_organic .entry');
 
8652                 for (var i=0, len=entries.length; i<len;i++) {
 
8653                         if ((typeof entries[i] !== 'undefined') && (!entries[i].classList.contains('lcTagged'))) {
 
8654                                 entries[i].classList.add('lcTagged')
 
8655                                 var thisLA = entries[i].querySelector('A.title');
 
8656                                 if (thisLA !== null) {
 
8657                                         var thisLink = thisLA.getAttribute('href');
 
8658                                         var thisComments = entries[i].querySelector('.comments');
 
8659                                         if (!(thisLink.match(/^https?/i))) {
 
8660                                                 thisLink = location.protocol + '//' + document.domain + thisLink;
 
8662                                         var thisUL = entries[i].querySelector('ul.flat-list');
 
8663                                         var singleClickLI = document.createElement('li');
 
8664                                         // changed from a link to a span because you can't cancel a new window on middle click of a link during the mousedown event, and a click event isn't triggered.
 
8665                                         var singleClickLink = document.createElement('span');
 
8666                                         // singleClickLink.setAttribute('href','javascript:void(0);');
 
8667                                         singleClickLink.setAttribute('class','redditSingleClick');
 
8668                                         singleClickLink.setAttribute('thisLink',thisLink);
 
8669                                         singleClickLink.setAttribute('thisComments',thisComments);
 
8670                                         if (thisLink != thisComments) {
 
8671                                                 singleClickLink.textContent = '[l+c]';
 
8672                                         } else if (!(modules['singleClick'].options.hideLEC.value)) {
 
8673                                                 singleClickLink.textContent = '[l=c]';
 
8675                                         singleClickLI.appendChild(singleClickLink);
 
8676                                         thisUL.appendChild(singleClickLI);
 
8677                                         // we have to switch to mousedown because Webkit is being a douche and not triggering click events on middle click.  
 
8678                                         // ?? We should still preventDefault on a click though, maybe?
 
8679                                         singleClickLink.addEventListener('mousedown', function(e) {
 
8681                                                 var lcMouseBtn = (modules['singleClick'].options.openBackground.value) ? 1 : 0;
 
8682                                                 if (e.button !== 2) {
 
8683                                                         // check if it's a relative link (no http://domain) because chrome barfs on these when creating a new tab...
 
8684                                                         var thisLink = $(this).parent().parent().parent().find('a.title').attr('href');
 
8685                                                         if (!(thisLink.match(/^http/i))) {
 
8686                                                                 thisLink = 'http://' + document.domain + thisLink;
 
8688                                                         if (BrowserDetect.isChrome()) {
 
8690                                                                         requestType: 'singleClick',
 
8692                                                                         openOrder: modules['singleClick'].options.openOrder.value,
 
8693                                                                         commentsURL: this.getAttribute('thisComments'),
 
8697                                                                 chrome.extension.sendMessage(thisJSON);
 
8698                                                         } else if (BrowserDetect.isSafari()) {
 
8700                                                                         requestType: 'singleClick',
 
8702                                                                         openOrder: modules['singleClick'].options.openOrder.value,
 
8703                                                                         commentsURL: this.getAttribute('thisComments'),
 
8707                                                                 safari.self.tab.dispatchMessage("singleClick", thisJSON);
 
8708                                                         } else if (BrowserDetect.isOpera()) {
 
8710                                                                         requestType: 'singleClick',
 
8712                                                                         openOrder: modules['singleClick'].options.openOrder.value,
 
8713                                                                         commentsURL: this.getAttribute('thisComments'),
 
8717                                                                 opera.extension.postMessage(JSON.stringify(thisJSON));
 
8718                                                         } else if (BrowserDetect.isFirefox()) {
 
8720                                                                         requestType: 'singleClick',
 
8722                                                                         openOrder: modules['singleClick'].options.openOrder.value,
 
8723                                                                         commentsURL: this.getAttribute('thisComments'),
 
8727                                                                 self.postMessage(thisJSON);
 
8729                                                                 var thisLink = $(this).parent().parent().parent().find('a.title').attr('href');
 
8730                                                                 if (!(thisLink.match(/^http/i))) {
 
8731                                                                         thisLink = 'http://' + document.domain + thisLink;
 
8733                                                                 if (modules['singleClick'].options.openOrder.value === 'commentsfirst') {
 
8734                                                                         if (thisLink !== this.getAttribute('thisComments')) {
 
8735                                                                                 // console.log('open comments');
 
8736                                                                                 window.open(this.getAttribute('thisComments'));
 
8738                                                                         window.open(thisLink);
 
8740                                                                         window.open(thisLink);
 
8741                                                                         if (thisLink !== this.getAttribute('thisComments')) {
 
8742                                                                                 // console.log('open comments');
 
8743                                                                                 window.open(this.getAttribute('thisComments'));
 
8756 modules['commentPreview'] = {
 
8757         moduleID: 'commentPreview',
 
8758         moduleName: 'Live Comment Preview',
 
8759         category: 'Comments',
 
8764                         description: 'Enable the 2 column editor.'
 
8767         description: 'Provides a live preview of comments, as well as a two column editor for writing walls of text.',
 
8768         isEnabled: function() {
 
8769                 return RESConsole.getModulePrefs(this.moduleID);
 
8772                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/?[-\w\.]*/i,
 
8773                 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i,
 
8774                 /^https?:\/\/([a-z]+)\.reddit\.com\/message\/[-\w\.]*\/?[-\w\.]*/i,
 
8775                 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\.]*\/submit\/?/i,
 
8776                 /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.\/]*\/?/i,
 
8777                 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\-\w\.]+\/about\/edit/i,
 
8778                 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\-\w\.]+\/wiki\/create(\/\w+)?/i,
 
8779                 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\-\w\.]+\/wiki\/edit(\/\w+)?/i,
 
8780                 /^https?:\/\/([a-z]+)\.reddit\.com\/submit\/?/i
 
8782         isMatchURL: function() {
 
8783                 return RESUtils.isMatchURL(this.moduleID);
 
8785         beforeLoad: function() {
 
8786                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
8787                         RESUtils.addCSS('.RESDialogSmall.livePreview { position: relative; width: auto; margin-bottom: 15px; }');
 
8788                         RESUtils.addCSS('.RESDialogSmall.livePreview .RESDialogContents h3 { font-weight: bold; }');
 
8792                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
8793                         this.isWiki = $(document.body).is(".wiki-page");
 
8795                                 this.converter = window.SnuOwnd.getParser();
 
8797                                 //We need to configure non-default renderers here
 
8798                                 var SnuOwnd = window.SnuOwnd;
 
8799                                 var redditCallbacks = SnuOwnd.getRedditCallbacks();
 
8800                                 var rendererConfig = SnuOwnd.defaultRenderState();
 
8801                                 rendererConfig.flags = SnuOwnd.DEFAULT_WIKI_FLAGS;
 
8802                                 rendererConfig.html_element_whitelist = SnuOwnd.DEFAULT_HTML_ELEMENT_WHITELIST;
 
8803                                 rendererConfig.html_attr_whitelist = SnuOwnd.DEFAULT_HTML_ATTR_WHITELIST;
 
8804                                 this.converter = SnuOwnd.getParser({
 
8805                                         callbacks: redditCallbacks,
 
8806                                         context: rendererConfig
 
8809                                 this.tocConverter = SnuOwnd.getParser(SnuOwnd.getTocRenderer());
 
8812                         this.bigTextTarget = null;
 
8813                         if (this.options.enableBigEditor.value) {
 
8814                                 // Install the 2 column editor
 
8815                                 modules['commentPreview'].addBigEditor();
 
8817                                 if (modules['keyboardNav'].isEnabled()) {
 
8818                                         $("body").delegate(".usertext-edit textarea, #wiki_page_content", "keydown", function(e) {
 
8819                                                 if (checkKeysForEvent(e, modules["keyboardNav"].options.openBigEditor.value)) {
 
8820                                                         modules["commentPreview"].showBigEditor.call(this, e);
 
8826                         //Close the preview on submit
 
8827                         $("body").delegate("form", {
 
8828                                 submit: function(e) {
 
8829                                         $(this).find(".livePreview").hide();
 
8834                                 //Perform initial setup of tools over the whole page}
 
8835                                 this.attachPreview();
 
8836                                 // Wireup reply editors
 
8837                                 RESUtils.watchForElement("newCommentsForms", modules["commentPreview"].attachPreview);
 
8838                                 // Wireup edit editors (usertext-edit already exists in the page)
 
8839                                 RESUtils.watchForElement("newComments", modules["commentPreview"].attachPreview);
 
8841                                 this.attachWikiPreview()
 
8845         markdownToHTML: function(md) {
 
8846                 var body = this.converter.render(md);
 
8849                         <s>Snudown, and therefore SnuOwnd, is a bit funny about how it generates its table of contents entries.
 
8850                         To when it encounters a header it tries to perform some of the usual inline formatting such as emphasis, strikethoughs, or superscript in headers. The text containing generated HTML then gets passed into cb_toc_header which escapes all of the passed HTML. When reddit gets it escaped tags are stripped.
 
8852                         It would be nicer if they just used different functions for rendering the emphasis when making headers.</s>
 
8854                         It seems that my understanding was wrong, for some reason reddit doesn't even use snudown's TOC renderer.
 
8856                         var doc = $('<body>').html(body);
 
8857                         var header_ids = {};
 
8858                         var headers = doc.find('h1, h2, h3, h4, h5, h6');
 
8859                         var tocDiv = $('<div>').addClass('toc');
 
8860                         var parent = $('<ul>');
 
8861                         parent.data('level', 0);
 
8862                         tocDiv.append(parent);
 
8863                         var level = 0, previous = 0;
 
8865                         headers.each(function(i, e) {
 
8866                                 var contents = $(this).text();
 
8867                                 var aid = $('<div>').html(contents).text();
 
8868                                 aid = prefix + '_' + aid.replace(/ /g, '_').toLowerCase();
 
8869                                 aid = aid.replace(/[^\w.-]/g, function(s) {
 
8870                                         return '.' + s.charCodeAt(0).toString(16).toUpperCase();
 
8872                                 if (!(aid in header_ids)) header_ids[aid] = 0;
 
8873                                 var id_num = header_ids[aid] + 1;
 
8874                                 header_ids[aid] += 1;
 
8876                                 if (id_num > 1) aid = aid + id_num;
 
8878                                 $(this).attr('id', aid);
 
8880                                 var li = $('<li>').addClass(aid);
 
8881                                 var a = $('<a>').attr('href', '#'+aid).text(contents);
 
8884                                 var thisLevel = +this.tagName.slice(-1);
 
8885                                 if (previous && thisLevel > previous) {
 
8886                                         var newUL = $('<ul>');
 
8887                                         newUL.data('level', thisLevel);
 
8888                                         parent.append(newUL);
 
8891                                 } else if (level && thisLevel < previous) {
 
8892                                         while (level && parent.data('level') > thisLevel) {
 
8893                                                 parent = parent.closest('ul');
 
8897                                 previous = thisLevel;
 
8900                         doc.prepend(tocDiv);
 
8906         makeBigEditorButton: function() {
 
8907                 return $('<button class="RESBigEditorPop" tabIndex="3">big editor</button>');
 
8909         attachPreview: function(usertext) {
 
8910                 if (usertext == null) usertext = document.body;
 
8911                 if (modules['commentPreview'].options.enableBigEditor.value) {
 
8912                         modules['commentPreview'].makeBigEditorButton().prependTo($('.bottom-area:not(:has(.RESBigEditorPop))', usertext));
 
8914                 $(usertext).find(".usertext-edit").each(function() {
 
8915                         var preview = $(this).find(".livePreview");
 
8916                         if (preview.length === 0) {
 
8917                                 preview = modules["commentPreview"].makePreviewBox();
 
8918                                 $(this).append(preview);
 
8920                         var contents = preview.find(".RESDialogContents");
 
8921                         $(this).find("textarea[name=text], textarea[name=description], textarea[name=public_description]").bind("input", function() {
 
8922                                 var textarea = $(this);
 
8923                                 RESUtils.debounce('refreshPreview', 250, function() {
 
8924                                         var markdownText = textarea.val();
 
8925                                         if (markdownText.length > 0) {
 
8926                                                 var html = modules["commentPreview"].markdownToHTML(markdownText);
 
8928                                                 contents.html(html);
 
8937         attachWikiPreview: function() {
 
8938                 if (modules['commentPreview'].options.enableBigEditor.value) {
 
8939                         modules['commentPreview'].makeBigEditorButton().insertAfter($('.pageactions'));
 
8941                 var preview = modules["commentPreview"].makePreviewBox();
 
8942                 preview.find(".md").addClass("wiki");
 
8943                 preview.insertAfter($("#editform > br").first());
 
8945                 var contents = preview.find(".RESDialogContents");
 
8946                 $("#wiki_page_content").bind("input", function() {
 
8948                         var textarea = $(this);
 
8949                         RESUtils.debounce('refreshPreview', 250, function() {
 
8950                                 var markdownText = textarea.val();
 
8951                                 if (markdownText.length > 0) {
 
8952                                         var html = modules["commentPreview"].markdownToHTML(markdownText)
 
8954                                         contents.html(html);
 
8962         makePreviewBox: function() {
 
8963                 return $("<div style=\"display: none\" class=\"RESDialogSmall livePreview\"><h3>Live Preview</h3><div class=\"md RESDialogContents\"></div></div>");
 
8965         addBigEditor: function() {
 
8966                 var editor = $('<div id="BigEditor">').hide();
 
8967                 var left = $('<div class="BELeft RESDialogSmall"><h3>Editor</h3></div>');
 
8968                 var contents = $('<div class="RESDialogContents"><textarea id="BigText" class=""></textarea></div>');
 
8969                 var foot = $('<div class="BEFoot">');
 
8970                 foot.append($('<button style="float:left;">save</button>').bind('click', function() {
 
8971                         var len = $('#BigText').val().length;
 
8972                         var max = $('#BigText').attr("data-max-length");
 
8974                                 $('#BigEditor .errorList .error').hide().filter('.TOO_LONG').text('this is too long (max: '+max+')').show();
 
8975                         } else if (len === 0) {
 
8976                                 $('#BigEditor .errorList .error').hide().filter('.NO_TEXT').show();
 
8977                         } else if (modules['commentPreview'].bigTextTarget) {
 
8978                                 modules['commentPreview'].bigTextTarget.submit();
 
8979                                 modules['commentPreview'].bigTextTarget.parents('.usertext-edit:first').find('.livePreview .md').html('');
 
8980                                 modules.commentPreview.hideBigEditor(false, true);
 
8982                                 $('#BigEditor .errorList .error').hide().filter('.NO_TARGET').show();
 
8986                 foot.append($('<button style="float:left;">close</button>').bind('click', modules.commentPreview.hideBigEditor));
 
8988                 foot.append($('<span class="errorList">\
 
8989                         <span style="display: none;" class="error NO_TEXT">we need something here</span>\
 
8990                         <span style="display: none;" class="error TOO_LONG">this is too long (max: 10000)</span>\
 
8991                         <span style="display: none;" class="error NO_TARGET">there is no associated textarea</span>\
 
8994                 contents.append(foot);
 
8995                 left.append(contents);
 
8997                 var right = $('<div class="BERight RESDialogSmall"><h3>Preview</h3><div class="RESCloseButton RESFadeButton"></div><div class="RESCloseButton close">X</div>\
 
8998                         <div class="RESDialogContents"><div id="BigPreview" class=" md"></div></div></div>');
 
8999                 editor.append(left).append(right);
 
9001                 $(document.body).append(editor);
 
9003                 $('.BERight .RESCloseButton.close').bind("click", modules.commentPreview.hideBigEditor);
 
9004                 $('.BERight .RESFadeButton').bind({
 
9005                         click: function(e) {
 
9006                                 $("#BigEditor").fadeTo(300, 0.3);
 
9007                                 $(document.body).removeClass("RESScrollLock");
 
9008                                 this.isFaded = true;
 
9010                         mouseout: function(e) {
 
9011                                 if (this.isFaded) $("#BigEditor").fadeTo(300, 1.0);
 
9012                                 $(document.body).addClass("RESScrollLock");
 
9013                                 this.isFaded = false;
 
9016                 $('body').delegate('.RESBigEditorPop', 'click', modules.commentPreview.showBigEditor);
 
9018                 $('#BigText').bind('input', function() {
 
9019                         RESUtils.debounce('refreshBigPreview', 250, function() {
 
9020                                 var text = $('#BigText').val();
 
9021                                 var html = modules['commentPreview'].markdownToHTML(text);
 
9022                                 $('#BigPreview').html(html);
 
9023                                 if (modules['commentPreview'].bigTextTarget) {
 
9024                                         modules['commentPreview'].bigTextTarget.val(text);
 
9027                 }).bind("keydown", function(e) {
 
9028                         //Close big editor on escape
 
9029                         if (e.keyCode === modules["commentTools"].KEYS.ESCAPE) {
 
9030                                 modules["commentPreview"].hideBigEditor();
 
9035                 if (modules['commentTools'].isEnabled()) {
 
9036                         contents.prepend(modules['commentTools'].makeEditBar());
 
9039         showBigEditor: function(e) {
 
9041                 // modules.commentPreview.bigTextTarget = null;
 
9042                 modules.commentPreview.hideBigEditor(true);
 
9043                 $('.side').addClass('BESideHide');
 
9044                 $('body').addClass('RESScrollLock');
 
9045                 RESUtils.fadeElementIn(document.getElementById('BigEditor'), 0.3);
 
9047                 if (!modules['commentPreview'].isWiki) {
 
9048                         baseText = $(this).parents('.usertext-edit:first').find('textarea');
 
9049                         $("#BigPreview").removeClass("wiki");
 
9050                         $(".BERight .RESDialogContents").removeClass("wiki-page-content");
 
9052                         baseText = $('#wiki_page_content');
 
9053                         $("#BigPreview").addClass("wiki");
 
9054                         $(".BERight .RESDialogContents").addClass("wiki-page-content");
 
9057                 var markdown = baseText.val();
 
9058                 var maxLength = baseText.attr("data-max-length");
 
9059                 $("#BigText").attr("data-max-length", maxLength).val(markdown).focus();
 
9060                 modules['commentTools'].updateCounter($("#BigText")[0]);
 
9061                 $('#BigPreview').html(modules['commentPreview'].markdownToHTML(markdown));
 
9062                 modules.commentPreview.bigTextTarget = baseText;
 
9064         hideBigEditor: function(quick, submitted) {
 
9065                 if (quick === true) {
 
9066                         $('#BigEditor').hide();
 
9068                         RESUtils.fadeElementOut(document.getElementById('BigEditor'), 0.3);
 
9070                 $('.side').removeClass('BESideHide');
 
9071                 $('body').removeClass('RESScrollLock');
 
9072                 var target = modules['commentPreview'].bigTextTarget;
 
9074                 if (target != null) {
 
9075                         target.val($('#BigText').val())
 
9077                         if (submitted !== true) {
 
9078                                 var inputEvent = document.createEvent("HTMLEvents");
 
9079                                 inputEvent.initEvent("input", true, true);
 
9080                                 target[0].dispatchEvent(inputEvent);
 
9082                         modules['commentPreview'].bigTextTarget = null;
 
9089 modules['commentTools'] = {
 
9090         moduleID: 'commentTools',
 
9091         moduleName: 'Comment Tools',
 
9092         category: 'Comments',
 
9097                         description: 'Shows your currently logged in username to avoid posting from the wrong account.'
 
9102                         description: 'Show user autocomplete tool when typing in posts, comments and replies'
 
9104                 subredditAutocomplete: {
 
9107                         description: 'Show subreddit autocomplete tool when typing in posts, comments and replies'
 
9112                         description: 'When submitting, display the number of characters entered in the title and text fields and indicate when you go over the 300 character limit for titles.'
 
9114                 keyboardShortcuts: {
 
9117                         description: 'Use keyboard shortcuts to apply styles to selected text'
 
9121                         addRowText: '+add shortcut',
 
9123                                 { name: 'label', type: 'text' },
 
9124                                 { name: 'text', type: 'textarea' },
 
9125                                 { name: 'category', type: 'text' },
 
9126                                 { name: 'key', type: 'keycode' }
 
9130                         description: "Add buttons to insert frequently used snippets of text."
 
9132                 keepMacroListOpen: {
 
9135                         description: 'After selecting a macro from the dropdown list, do not hide the list.'
 
9138         description: 'Provides shortcuts for easier markdown.',
 
9139         isEnabled: function() {
 
9140                 return RESConsole.getModulePrefs(this.moduleID);
 
9143                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/?[-\w\.]*/i,
 
9144                 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i,
 
9145                 /^https?:\/\/([a-z]+)\.reddit\.com\/message\/[-\w\.]*\/?[-\w\.]*/i,
 
9146                 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\.]*\/submit\/?/i,
 
9147                 /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.\/]*\/?/i,
 
9148                 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\-\w\.]+\/about\/edit/i,
 
9149                 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\-\w\.]+\/wiki\/create(\/\w+)?/i,
 
9150                 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\-\w\.]+\/wiki\/edit(\/\w+)?/i,
 
9151                 /^https?:\/\/([a-z]+)\.reddit\.com\/submit\/?/i
 
9153         isMatchURL: function() {
 
9154                 return RESUtils.isMatchURL(this.moduleID);
 
9156         beforeLoad: function() {
 
9157                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
9158                         RESUtils.addCSS('.markdownEditor { white-space: nowrap;  }');
 
9159                         RESUtils.addCSS('.RESMacroWrappingSpan { white-space: normal;  }');
 
9160                         RESUtils.addCSS('.markdownEditor a { margin-right: 8px; text-decoration: none; font-size: 11px; }');
 
9161                         RESUtils.addCSS('.markdownEditor .RESMacroDropdown {font-size: 10px; }');
 
9162                         RESUtils.addCSS('.selectedItem { color: #fff; background-color: #5f99cf; }');
 
9163                         // RESUtils.addCSS('.RESDialogSmall.livePreview { position: relative; width: auto; margin-bottom: 15px; }');
 
9164                         // RESUtils.addCSS('.RESDialogSmall.livePreview .RESDialogContents h3 { font-weight: bold; }');
 
9165                         RESUtils.addCSS('.RESMacroDropdownTitle, .RESMacroDropdownTitleOverlay { cursor: pointer; display: inline-block; font-size: 11px; text-decoration: underline; color: gray; padding-left: 2px; padding-right: 21px; background-image: url(http://www.redditstatic.com/droparrowgray.gif); background-position: 100% 50%; background-repeat: no-repeat; }');
 
9166                         RESUtils.addCSS('.RESMacroDropdownTitleOverlay { cursor: pointer; }');
 
9167                         RESUtils.addCSS('.RESMacroDropdownContainer { display: none; position: absolute; }');
 
9168                         RESUtils.addCSS('.RESMacroDropdown { display: none; position: absolute; z-index: 2001; }');
 
9169                         RESUtils.addCSS('.RESMacroDropdownList { margin-top: 0; width: auto; max-width: 300px; }');
 
9170                         RESUtils.addCSS('.RESMacroDropdownList a, .RESMacroDropdownList li { font-size: 10px; }');
 
9171                         RESUtils.addCSS('.RESMacroDropdown li { padding-right: 10px; height: 25px; line-height: 24px; }');
 
9175                 STYLESHEET: 128*1024,
 
9182         //Moved this out of go() because the large commentPreview may need it.
 
9183         macroCallbackTable: [],
 
9186                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
9187                         this.isWiki = $(document.body).is(".wiki-page");
 
9189                         $("body").delegate("li.viewSource a", {
 
9190                                 click: function(e) {
 
9192                                         modules["commentTools"].viewSource(this);
 
9194                         }).delegate(".usertext-edit.viewSource .cancel", {
 
9196                                         $(this).parents(".usertext-edit.viewSource").hide();
 
9199                         }).delegate("div.markdownEditor a", {
 
9200                                 click: function(e) {
 
9203                                         var index = $(this).attr("data-macro-index");
 
9204                                         var box = modules["commentTools"].findTextareaForElement(this)[0];
 
9205                                         // var box = $(this).closest(".usertext-edit, .RESDialogContents, .wiki-page-content").find("textarea[name=text], textarea[name=description], textarea[name=public_description]")[0];
 
9207                                                 console.error("Failed to locate textarea.");
 
9210                                         var handler = modules["commentTools"].macroCallbackTable[index];
 
9212                                                 console.error("Failed to locate find callback.");
 
9215                                         handler.call(modules["commentTools"], this, box);
 
9218                                         //Fire an input event to refresh the preview
 
9219                                         var inputEvent = document.createEvent("HTMLEvents");
 
9220                                         inputEvent.initEvent("input", true, true);
 
9221                                         box.dispatchEvent(inputEvent);
 
9223                         }).delegate(".RESMacroDropdownTitle", {
 
9224                                 click: function(e) {
 
9225                                         var pos = $(this).position();
 
9226                                         $(this).next().css({
 
9227                                                 top: (pos).top+"px",
 
9228                                                 left: (pos).left+"px"
 
9231                         }).delegate(".RESMacroDropdown", {
 
9232                                 mouseleave: function(e) {
 
9237                         if (this.options.showInputLength.value) {
 
9239                                 $("body").delegate(".usertext-edit textarea, #title-field textarea, #BigEditor textarea, #wiki_page_content", {
 
9241                                                 modules['commentTools'].updateCounter(this);
 
9246                         if (this.options.keyboardShortcuts.value) {
 
9247                                 $("body").delegate(".usertext-edit textarea, #BigEditor textarea, #wiki_page_content", {
 
9248                                         keydown: function(e) {
 
9249                                                 if (e.keyCode === modules["commentTools"].KEYS.ESCAPE) {
 
9250                                                         if (!modules["commentTools"].autoCompletePop.is(':visible')) {
 
9251                                                                 // Blur from the editor on escape, so we can return to using the keyboard nav.
 
9252                                                                 // NOTE: The big editor closes on ESC so this won't be reached in that case.
 
9261                                                 for (var i = 0; i < modules["commentTools"].macroKeyTable.length; i++) {
 
9262                                                         var row = modules["commentTools"].macroKeyTable[i];
 
9263                                                         var testedKeyArray = row[0], macroIndex = row[1];
 
9264                                                         if (checkKeysForEvent(e, testedKeyArray)) {
 
9265                                                                 var handler = modules["commentTools"].macroCallbackTable[macroIndex];
 
9266                                                                 handler.call(modules["commentTools"], null, this);
 
9267                                                                 //Fire an input event to refresh the preview
 
9268                                                                 var inputEvent = document.createEvent("HTMLEvents");
 
9269                                                                 inputEvent.initEvent("input", true, true);
 
9270                                                                 this.dispatchEvent(inputEvent);
 
9278                         if (this.options.subredditAutocomplete.value || this.options.userAutocomplete.value) {
 
9279                                 this.addAutoCompletePop();
 
9282                         //Perform initial setup of tools over the whole page
 
9283                         this.attachCommentTools();
 
9284                         this.attatchViewSourceButtons()
 
9286                         //These are no longer necessary but I am saving them in case Reddit changes how they make their reply forms.
 
9287                         // Wireup reply editors
 
9288                         RESUtils.watchForElement("newCommentsForms", modules["commentTools"].attachCommentTools);
 
9289                         // Wireup edit editors (usertext-edit already exists in the page)
 
9290                         RESUtils.watchForElement("newComments", modules["commentTools"].attachCommentTools);
 
9292                         RESUtils.watchForElement("newComments", modules["commentTools"].attatchViewSourceButtons);
 
9295         migrateData: function() {
 
9296                 var LATEST_MACRO_DATA_VERSION = "2";
 
9297                 var macroVersion = RESStorage.getItem("RESmodules.commentTools.macroDataVersion");
 
9298                 if (macroVersion == null || macroVersion === "0") {
 
9299                         //In this case it is unmigrated or uncreated
 
9300                         var previewOptionString = RESStorage.getItem("RESoptions.commentPreview");
 
9301                         var previewOptions = safeJSON.parse(previewOptionString, "commentPreview");
 
9302                         if (previewOptions != null) {
 
9303                                 if (typeof previewOptions.commentingAs !== "undefined") {
 
9304                                         this.options.commentingAs.value = previewOptions.commentingAs.value
 
9305                                         delete previewOptions.commentingAs;
 
9307                                 if (typeof previewOptions.keyboardShortcuts !== "undefined") {
 
9308                                         this.options.keyboardShortcuts.value = previewOptions.keyboardShortcuts.value
 
9309                                         delete previewOptions.keyboardShortcuts;
 
9311                                 if (typeof previewOptions.subredditAutocomplete !== "undefined") {
 
9312                                         this.options.subredditAutocomplete.value = previewOptions.subredditAutocomplete.value
 
9313                                         delete previewOptions.subredditAutocomplete;
 
9315                                 if (typeof previewOptions.macros !== "undefined") {
 
9317                                         macros = this.options.macros.value = previewOptions.macros.value
 
9318                                         for (var i = 0; i < macros.length; i++) {
 
9319                                                 while (macros[i].length < 4) macros[i].push("");
 
9321                                         delete previewOptions.macros;
 
9323                                 RESStorage.setItem("RESoptions.commentTools", JSON.stringify(this.options));
 
9324                                 RESStorage.setItem("RESoptions.commentPreview", JSON.stringify(previewOptions));
 
9325                                 RESStorage.setItem("RESmodules.commentTools.macroDataVersion", LATEST_MACRO_DATA_VERSION);
 
9327                                 //No migration will be performed
 
9328                                 RESStorage.setItem("RESmodules.commentTools.macroDataVersion", LATEST_MACRO_DATA_VERSION);
 
9330                 } if (macroVersion === "1") {
 
9331                         var macros = this.options.macros.value;
 
9332                         for (var i = 0; i < macros.length; i++) {
 
9333                                 while (macros[i].length < 4) macros[i].push("");
 
9335                         RESStorage.setItem("RESmodules.commentTools.macroDataVersion", LATEST_MACRO_DATA_VERSION);
 
9338         attatchViewSourceButtons: function(entry) {
 
9339                 var entries = entry == null ? $(".entry", document.body) : $(entry);
 
9340                 if (RESUtils.pageType() === "comments" || RESUtils.pageType() === "inbox")  {
 
9341                         //Disabled syncronous version
 
9342                         // $(".flat-list.buttons", entries).find("li:nth-child(2), li:only-child").after('<li class="viewSource"><a href="javascript:void(0)">source</a></li>');
 
9343                         var menus = $(".flat-list.buttons li:first-child", entries);
 
9344                         RESUtils.forEachChunked(menus, 30, 500, function(item, i, array) {
 
9345                                 $(item).after('<li class="viewSource"><a href="javascript:void(0)">source</a></li>');
 
9349         viewSource: function(button) {
 
9350                 var buttonList = $(button).parent().parent();
 
9351                 var sourceDiv = $(button).closest('.thing').find(".usertext-edit.viewSource:first");
 
9352                 if (sourceDiv.length !== 0) {
 
9355                         var permaLink = buttonList.find(".first a");
 
9356                         var jsonURL = permaLink.attr("href");
 
9357                         var urlSplit = jsonURL.split('/');
 
9358                         var postID = urlSplit[urlSplit.length - 1];
 
9360                         var isSelfText = permaLink.is(".comments");
 
9361                         if (jsonURL.indexOf('?context') !== -1) {
 
9362                                 jsonURL = jsonURL.replace('?context=3','.json?');
 
9364                                 jsonURL += '/.json';
 
9366                         this.gettingSource = this.gettingSource || {};
 
9367                         if (this.gettingSource[postID]) return;
 
9368                         this.gettingSource[postID] = true;
 
9373                                 onload: function(response) {
 
9374                                         var thisResponse = JSON.parse(response.responseText);
 
9375                                         var userTextForm = $('<div class="usertext-edit viewSource"><div><textarea rows="1" cols="1" name="text"></textarea></div><div class="bottom-area"><div class="usertext-buttons"><button type="button" class="cancel">hide</button></div></div></div>');
 
9377                                                 var sourceText = null;
 
9378                                                 if (typeof thisResponse[1] !== 'undefined') {
 
9379                                                         sourceText = thisResponse[1].data.children[0].data.body;
 
9381                                                         var thisData = thisResponse.data.children[0].data;
 
9382                                                         if (thisData.id == postID) {
 
9383                                                                 sourceText = thisData.body;
 
9385                                                                 // The message we want is a reply to a PM/modmail, but reddit returns the whole thread.
 
9386                                                                 // So, we have to dig into the replies to find the message we want.
 
9387                                                                 for (var i=0, len=thisData.replies.data.children.length; i<len; i++) {
 
9388                                                                         var replyData = thisData.replies.data.children[i].data;
 
9389                                                                         if (replyData.id == postID) {
 
9390                                                                                 sourceText = replyData.body;
 
9396                                                 // sourceText in this case is reddit markdown. escaping it would screw it up.
 
9397                                                 userTextForm.find("textarea[name=text]").html(sourceText);
 
9399                                                 var sourceText = thisResponse[0].data.children[0].data.selftext;
 
9400 //                                              console.log(sourceText);
 
9401                                                 // sourceText in this case is reddit markdown. escaping it would screw it up.
 
9402                                                 userTextForm.find("textarea[name=text]").html(sourceText);
 
9404                                         buttonList.before(userTextForm);
 
9409         attachCommentTools: function (elem) {
 
9410                 if (elem == null) elem = document.body;
 
9411                 $(elem).find("textarea[name]").each(modules["commentTools"].attachEditorToUsertext);
 
9413         attachEditorToUsertext: function() {
 
9414                 if (this.hasAttribute("data-max-length")) return;
 
9416                 switch (this.name) {
 
9417                         case "title": limit = modules['commentTools'].SUBMIT_LIMITS.POST_TITLE; break;
 
9418                         case "text": limit = modules['commentTools'].SUBMIT_LIMITS.POST; break;
 
9419                         case "description": limit = modules['commentTools'].SUBMIT_LIMITS.SIDEBAR; break;
 
9420                         case "public_description": limit = modules['commentTools'].SUBMIT_LIMITS.DESCRIPTION; break;
 
9421                         case "content": limit = modules['commentTools'].SUBMIT_LIMITS.WIKI; break;
 
9422                         case "description_conflict_old": return;
 
9423                         case "public_description_conflict_old": return;
 
9425                                 // console.warn("unhandled form", this);
 
9428                 $(this).attr("data-max-length", limit);
 
9431                 if (this.name === "title") return;
 
9433                 var bar = modules['commentTools'].makeEditBar();
 
9434                 if (this.id === "wiki_page_content") {
 
9435                         $(this).parent().prepend(bar);
 
9437                         $(this).parent().before(bar);
 
9439                 modules['commentTools'].updateCounter(this);
 
9441         updateCounter: function(textarea) {
 
9442                 var length = $(textarea).val().length;
 
9443                 var limit = +$(textarea).attr("data-max-length");
 
9444                 var counter = $(textarea).parent().parent().find(".markdownEditor .RESCharCounter");
 
9445                 counter.attr('title', 'character limit: '+length+'/'+limit);
 
9446                 counter.text(length+'/'+limit);
 
9447                 if (length > limit) {
 
9448                         counter.addClass('tooLong');
 
9450                         counter.removeClass('tooLong');
 
9453         makeEditBar: function() {
 
9454                 if (this.cachedEditBar != null) {
 
9455                         return $(this.cachedEditBar).clone();
 
9458                 var editBar = $('<div class="markdownEditor">');
 
9460                 editBar.append(this.makeEditButton("<b>Bold</b>", "ctrl-b", [66, false, true, false, false], function(button, box) {
 
9461                         this.wrapSelection(box, "**", "**");
 
9463                 editBar.append(this.makeEditButton("<i>Italic</i>", "ctrl-i", [73, false, true, false, false], function(button, box) {
 
9464                         this.wrapSelection(box, "*", "*");
 
9466                 editBar.append(this.makeEditButton("<del>strike</del>", "ctrl-s", 83, function(button, box) {
 
9467                         this.wrapSelection(box, "~~", "~~");
 
9469                 editBar.append(this.makeEditButton("<sup>sup</sup>", "", null, function(button, box) {
 
9470                         this.wrapSelectedWords(box, "^");
 
9472                 editBar.append(this.makeEditButton("Link", "", null, function(button, box) {
 
9473                         this.linkSelection(box);
 
9475                 editBar.append(this.makeEditButton(">Quote", "", null, function(button, box) {
 
9476                         this.wrapSelectedLines(box, "> ", "");
 
9478                 editBar.append(this.makeEditButton("<span style=\"font-family: Courier New\">Code</span>", "", null, function(button, box) {
 
9479                         this.wrapSelectedLines(box, "    ", "");
 
9481                 editBar.append(this.makeEditButton("•Bullets", "", null, function(button, box) {
 
9482                         this.wrapSelectedLines(box, "* ", "");
 
9484                 editBar.append(this.makeEditButton("1.Numbers", "", null, function(button, box) {
 
9485                         this.wrapSelectedLines(box, "1. ", "");
 
9488                 if (modules["commentTools"].options.showInputLength.value) {
 
9489                         var counter = $('<span class="RESCharCounter" title="character limit: 0/?????">0/?????</span>');
 
9490                         editBar.append(counter);
 
9493                 this.addButtonToMacroGroup("", this.makeEditButton("reddiquette", "", null, function(button, box){
 
9494                          var clickCount = $(button).data("clickCount") || 0;
 
9496                          $(button).data("clickCount", clickCount);
 
9497                          if (clickCount > 2) {
 
9500                          this.macroSelection(box, "[reddiquette](http://www.reddit.com/help/reddiquette) ", "");
 
9503                 this.addButtonToMacroGroup("", this.makeEditButton("[Promote]", "", null, function(button, box){
 
9504                         var clickCount = $(button).data("clickCount") || 0;
 
9506                         $(button).data("clickCount", clickCount);
 
9507                         if (clickCount > 2) {
 
9509                                 modules["commentTools"].lod();
 
9511                         this.macroSelection(box, "[Reddit Enhancement Suite](http://redditenhancementsuite.com) ");
 
9514                 this.addButtonToMacroGroup("", this.makeEditButton("ಠ\_ಠ", "Look of disaproval", null, function(button, box) {
 
9515                         this.macroSelection(box, "ಠ\_ಠ");
 
9517                 this.buildMacroDropdowns(editBar);
 
9518                 var addMacroButton = modules['commentTools'].makeEditButton(modules['commentTools'].options.macros.addRowText, null, null, function() {
 
9519                         modules['settingsNavigation'].loadSettingsPage(this.moduleID, 'macros');
 
9520                         $('.RESMacroDropdown').fadeOut(100);
 
9522                 modules['commentTools'].addButtonToMacroGroup('', addMacroButton);
 
9527                 //Wrap the edit bar in a <div> of its own 
 
9528                 var wrappedEditBar = $("<div>").append(editBar);
 
9529                 if (this.options.commentingAs.value && (!modules['usernameHider'].isEnabled())) {
 
9530                         // show who we're commenting as...
 
9531                         var commentingAs = $('<div class="commentingAs">').text('Commenting as: ' + RESUtils.loggedInUser());
 
9532                         wrappedEditBar.append(commentingAs);
 
9535                 this.cachedEditBar = wrappedEditBar;
 
9536                 return this.cachedEditBar;
 
9538         macroDropDownTable: {},
 
9539         getMacroGroup: function(groupName) {
 
9540                 //Normalize and supply a default group name{}
 
9541                 groupName = (groupName||"").toString().trim() || "macros";
 
9543                 if (groupName in this.macroDropDownTable) {
 
9544                         macroGroup = this.macroDropDownTable[groupName];
 
9546                         macroGroup = this.macroDropDownTable[groupName] = {};
 
9547                         macroGroup.titleButton = $('<span class="RESMacroDropdownTitle">'+groupName+'</span>');
 
9548                         macroGroup.container = $('<span class="RESMacroDropdown"><span class="RESMacroDropdownTitleOverlay">'+groupName+'</span></span>').hide();
 
9549                         macroGroup.dropdown = $('<ul class="RESMacroDropdownList RESDropdownList"></ul>');
 
9550                         macroGroup.container.append(macroGroup.dropdown);
 
9554         addButtonToMacroGroup: function(groupName, button) {
 
9555                 var group = this.getMacroGroup(groupName);
 
9556                 group.dropdown.append($("<li>").append(button));
 
9558         buildMacroDropdowns: function(editBar) {
 
9559                 var macros = this.options.macros.value;
 
9561                 for (var i = 0; i < macros.length; i++) {
 
9562                         var macro = macros[i];
 
9564                         //Confound these scoping rules
 
9565                         (function(title, text, category, key) {
 
9566                                 var button = this.makeEditButton(title, null, key, function(button, box) {
 
9567                                         this.macroSelection(box, text, "");
 
9569                                 this.addButtonToMacroGroup(category, button);
 
9570                         }).apply(this, macro);
 
9574                 var macroWrapper = $('<span class="RESMacroWrappingSpan">');
 
9575                 if ("macros" in this.macroDropDownTable) {
 
9576                         macroWrapper.append(this.macroDropDownTable["macros"].titleButton);
 
9577                         macroWrapper.append(this.macroDropDownTable["macros"].container);
 
9579                 for (var category in this.macroDropDownTable) {
 
9580                         if (category === "macros") continue;
 
9581                         macroWrapper.append(this.macroDropDownTable[category].titleButton);
 
9582                         macroWrapper.append(this.macroDropDownTable[category].container);
 
9584                 editBar.append(macroWrapper);
 
9586         makeEditButton: function(label, title, key, handler) {
 
9587                 if (label == null) label = "unlabeled";
 
9588                 if (title == null) title = "";
 
9589                 var macroButtonIndex = this.macroCallbackTable.length;
 
9590                 var button = $("<a>").html(label).attr({
 
9592                         href: "javascript:void(0)",
 
9594                         "data-macro-index": macroButtonIndex
 
9597                 if (key != null && key[0] != null) {
 
9598                         this.macroKeyTable.push([key, macroButtonIndex]);
 
9600                 this.macroCallbackTable[macroButtonIndex] = handler;
 
9603         linkSelection: function(box) {
 
9604                 var url = prompt("Enter the URL:", "");
 
9606                         //escape parens in url
 
9607                         url = url.replace(/\(/, "\\(");
 
9608                         url = url.replace( /\)/, "\\)");
 
9609                         this.wrapSelection(box, "[", "](" + url + ")", function(text) {
 
9610                                 //escape brackets and parens in text
 
9611                                 text = text.replace(/\[/, "\\[");
 
9612                                 text = text.replace(/\]/, "\\]");
 
9613                                 text = text.replace(/\(/, "\\(");
 
9614                                 text = text.replace(/\)/, "\\)");
 
9619         macroSelection: function(box, macroText) {
 
9620                 if (!this.options.keepMacroListOpen.value) $('.RESMacroDropdown').fadeOut(100);
 
9621                 this.wrapSelection(box, macroText, "");
 
9623         wrapSelection: function(box, prefix, suffix, escapeFunction) {
 
9624                 if (box == null) return;
 
9625                 //record scroll top to restore it later.
 
9626                 var scrollTop = box.scrollTop;
 
9628                 //We will restore the selection later, so record the current selection.
 
9629                 var selectionStart = box.selectionStart;
 
9630                 var selectionEnd = box.selectionEnd;
 
9632                 var text = box.value;
 
9633                 var beforeSelection = text.substring(0, selectionStart);
 
9634                 var selectedText = text.substring(selectionStart, selectionEnd);
 
9635                 var afterSelection = text.substring(selectionEnd);
 
9637                 //Markdown doesn't like it when you tag a word like **this **. The space messes it up. So we'll account for that because Firefox selects the word, and the followign space when you double click a word.
 
9638                 var trailingSpace = "";
 
9639                 var cursor = selectedText.length - 1;
 
9640                 while (cursor > 0 && selectedText[cursor] === " ") {
 
9641                         trailingSpace += " ";
 
9644                 selectedText = selectedText.substring(0, cursor+1);
 
9646                 if (escapeFunction != null) {
 
9647                         selectedText = escapeFunction(selectedText);
 
9650                 box.value = beforeSelection + prefix + selectedText + suffix + trailingSpace + afterSelection;
 
9652                 box.selectionStart = selectionStart + prefix.length;
 
9653                 box.selectionEnd = selectionEnd + prefix.length;
 
9655                 box.scrollTop = scrollTop;
 
9657         wrapSelectedLines: function(box, prefix, suffix) {
 
9658                 var scrollTop = box.scrollTop;
 
9659                 var selectionStart = box.selectionStart;
 
9660                 var selectionEnd = box.selectionEnd;
 
9662                 var text = box.value;
 
9663                 var startPosition = 0;
 
9664                 var lines = text.split("\n");
 
9665                 for (var i = 0; i < lines.length; i++) {
 
9666                         var lineStart = startPosition;
 
9667                         var lineEnd = lineStart + lines[i].length;
 
9668                         //Check if either end of the line is within the selection
 
9669                         if (selectionStart <= lineStart && lineStart <= selectionEnd
 
9670                          || selectionStart <= lineEnd && lineEnd <= selectionEnd
 
9671                         //Check if either end of the selection is within the line
 
9672                          || lineStart <= selectionStart && selectionStart <= lineEnd
 
9673                          || lineStart <= selectionEnd && selectionEnd <= lineEnd) {
 
9674                                 lines[i] = prefix + lines[i] + suffix;
 
9675                                 //Move the offsets separately so we don't throw off detection for the other end
 
9676                                 var startMovement = 0, endMovement = 0;
 
9677                                 if (lineStart < selectionStart) startMovement += prefix.length;
 
9678                                 if (lineEnd < selectionStart) startMovement += suffix.length;
 
9679                                 if (lineStart < selectionEnd) endMovement += prefix.length;
 
9680                                 if (lineEnd < selectionEnd) endMovement += suffix.length;
 
9682                                 selectionStart += startMovement;
 
9683                                 selectionEnd += endMovement;
 
9684                                 lineStart += prefix.length;
 
9685                                 lineEnd += prefix.length + suffix.length;
 
9687                         //Remember the newline
 
9688                         startPosition = lineEnd + 1;
 
9691                 box.value = lines.join("\n");
 
9692                 box.selectionStart = selectionStart;
 
9693                 box.selectionEnd = selectionEnd;
 
9694                 box.scrollTop = scrollTop;
 
9696         wrapSelectedWords: function(box, prefix) {
 
9697                 var scrollTop = box.scrollTop;
 
9698                 var selectionStart = box.selectionStart;
 
9699                 var selectionEnd = box.selectionEnd;
 
9701                 var text = box.value;
 
9702                 var beforeSelection = text.substring(0, selectionStart);
 
9703                 var selectedWords = text.substring(selectionStart, selectionEnd).split(" ");
 
9704                 var afterSelection = text.substring(selectionEnd);
 
9706                 var selectionModify = 0;
 
9708                 for (i = 0; i < selectedWords.length; i++) {
 
9709                         if (selectedWords[i] !== "") {
 
9710                                 if (selectedWords[i].indexOf("\n") !== -1)
 
9712                                         newLinePosition = selectedWords[i].lastIndexOf("\n") + 1;
 
9713                                         selectedWords[i] = selectedWords[i].substring(0, newLinePosition) + prefix + selectedWords[i].substring(newLinePosition);
 
9714                                         selectionModify += prefix.length;
 
9716                                 if (selectedWords[i].charAt(0) !== "\n") {
 
9717                                         selectedWords[i] = prefix + selectedWords[i];
 
9719                                 selectionModify += prefix.length;
 
9721                         // If nothing is selected, stick the prefix in there and move the cursor to the right side.
 
9722                         else if (selectedWords[i] === "" && selectedWords.length === 1) {
 
9723                                 selectedWords[i] = prefix + selectedWords[i];
 
9724                                 selectionModify += prefix.length;
 
9725                                 selectionStart += prefix.length;
 
9729                 box.value = beforeSelection + selectedWords.join(" ") + afterSelection;
 
9730                 box.selectionStart = selectionStart;
 
9731                 box.selectionEnd = selectionEnd + selectionModify;
 
9732                 box.scrollTop = scrollTop;
 
9735                 if (typeof this.firstlod === 'undefined') {
 
9736                         this.firstlod = true;
 
9737                         $('body').append('<div id="RESlod" style="display: none; position: fixed; left: 0; top: 0; right: 0; bottom: 0; background-color: #ddd; opacity: 0.9; z-index: 99999;"><div style="position: relative; text-align: center; width: 400px; height: 300px; margin: auto;"><div style="font-size: 100px; margin-bottom: 10px;">ಠ\_ಠ</div> when you do this, people direct their frustrations at <b>me</b>... could we please maybe give this a rest?</div></div>');
 
9739                 $('#RESlod').fadeIn('slow', function() {
 
9740                         setTimeout(function() {
 
9741                                 $('#RESlod').fadeOut('slow');
 
9746                 BACKSPACE: 8, TAB: 9, ENTER: 13,
 
9747                 ESCAPE: 27, SPACE: 32,
 
9748                 PAGE_UP: 33, PAGE_DOWN: 34,
 
9750                 LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40,
 
9751                 NUMPAD_ENTER: 108, COMMA: 188
 
9753         addAutoCompletePop: function() {
 
9755                 this.autoCompleteCache = {};
 
9756                 this.autoCompletePop = $('<div id="autocomplete_dropdown" \
 
9757                         class="drop-choices srdrop inuse" \
 
9758                         style="display:none;">');
 
9759                 this.autoCompletePop.delegate(".choice", "click mousedown", function(e) {
 
9761                         modules["commentTools"].autoCompleteHideDropdown();
 
9762                         modules["commentTools"].autoCompleteInsert(this.innerHTML);
 
9764                 $("body").append(this.autoCompletePop);
 
9766                 $("body").delegate(".usertext .usertext-edit textarea, #BigText, #wiki_page_content", {
 
9767                         keyup: this.autoCompleteTrigger,
 
9768                         keydown: this.autoCompleteNavigate,
 
9769                         blur: this.autoCompleteHideDropdown
 
9772         autoCompleteLastTarget: null,
 
9773         autoCompleteTrigger: function(e) {
 
9774                 var mod = modules["commentTools"];
 
9775                 var KEYS = mod.KEYS;
 
9776                 //\0x08 is backspace
 
9777                 if (/[^A-Za-z0-9 \x08]/.test(String.fromCharCode(e.keyCode))) return true;
 
9778                 mod.autoCompleteLastTarget = this;
 
9779                 var matchRE =           /\W\/?([ru])\/([\w\.]*)$/;
 
9780                 var matchSkipRE =       /\W\/?([ru])\/([\w\.]*)\ $/;
 
9781                 var fullText = $(this).val();
 
9782                 var prefixText = fullText.slice(0, this.selectionStart);
 
9783                 var match = matchRE.exec(" " + prefixText);
 
9784                 if (match != null) {
 
9785                         if (match[1] === "r" && mod.options.subredditAutocomplete.value == false) return;
 
9786                         if (match[1] === "u" && mod.options.userAutocomplete.value == false) return;
 
9789                 if(match==null || match[2] === "" || match[2].length > 10) {
 
9790                         if (e.keyCode === KEYS.SPACE || e.keyCode === KEYS.ENTER) {
 
9791                                 var match = matchSkipRE.exec(" " + prefixText);
 
9793                                         mod.autoCompleteInsert(match[2]);
 
9796                         return mod.autoCompleteHideDropdown();
 
9799                 var type = match[1];
 
9800                 var query = match[2].toLowerCase();
 
9801                 var queryId = type+"/"+query;
 
9802                 var cache  = mod.autoCompleteCache;
 
9803                 if (queryId in cache) {
 
9804                         return mod.autoCompleteUpdateDropdown(cache[queryId]);
 
9807                 RESUtils.debounce("autoComplete", 300, function() {
 
9809                                 mod.getSubredditCompletions(query);
 
9810                         } else if (type === "u") {
 
9811                                 mod.getUserCompletions(query);
 
9815         getSubredditCompletions: function(query) {
 
9816                 var mod = modules['commentTools'];
 
9817                 if (this.options.subredditAutocomplete.value) {
 
9820                                 url: "/api/search_reddit_names.json",
 
9821                                 data: {query: query, app: "res"},
 
9823                                 success: function(data) {
 
9824                                         mod.autoCompleteCache['r/'+query] = data.names;
 
9825                                         mod.autoCompleteUpdateDropdown(data.names);
 
9826                                         mod.autoCompleteSetNavIndex(0);
 
9831         getUserCompletions: function(query) {
 
9832                 if (this.options.userAutocomplete.value) {
 
9833                         var tags = JSON.parse(RESStorage.getItem("RESmodules.userTagger.tags"));
 
9834                         var tagNames = Object.keys(tags);
 
9835                         var pageNames = [].map.call($(".author"), function(e) {
 
9838                         var names = tagNames.concat(pageNames);
 
9839                         names = names.filter(function(e, i, a) {
 
9840                                 return e.toLowerCase().indexOf(query) === 0;
 
9841                         }).sort().reduce(function(prev, current, i, a) {
 
9842                                 //Removing duplicates
 
9843                                 if (prev[prev.length - 1] != current) {
 
9849                         this.autoCompleteCache['u/'+query] = names;
 
9850                         this.autoCompleteUpdateDropdown(names);
 
9851                         this.autoCompleteSetNavIndex(0);
 
9854         autoCompleteNavigate: function(e) {
 
9855                 //Don't mess with shortcuts for fancier cursor movement
 
9856                 if (e.metaKey || e.shiftKey || e.ctrlKey || e.altKey) return;
 
9857                 var mod = modules["commentTools"];
 
9858                 var KEYS = mod.KEYS;
 
9859                 var entries = mod.autoCompletePop.find("a.choice");
 
9860                 var index = +mod.autoCompletePop.find(".selectedItem").attr("data-index");
 
9861                 if (mod.autoCompletePop.is(':visible')) {
 
9862                         switch (e.keyCode) {
 
9866                                         if (index < entries.length-1) index++;
 
9867                                         mod.autoCompleteSetNavIndex(index);
 
9872                                         if (index > 0) index--;
 
9873                                         mod.autoCompleteSetNavIndex(index);
 
9878                                         $(entries[index]).click();
 
9882                                         mod.autoCompleteHideDropdown();
 
9888         autoCompleteSetNavIndex: function(index) {
 
9889                 var entries = modules["commentTools"].autoCompletePop.find("a.choice");
 
9890                 entries.removeClass("selectedItem");
 
9891                 entries.eq(index).addClass("selectedItem");
 
9893         autoCompleteHideDropdown: function() {
 
9894                 modules["commentTools"].autoCompletePop.hide();
 
9896         autoCompleteUpdateDropdown: function(names) {
 
9897                 var mod = modules["commentTools"];
 
9899                 if(!names.length) return mod.autoCompleteHideDropdown();
 
9900                 mod.autoCompletePop.empty();
 
9901                 $.each(names.slice(0, 20), function(i, e) {
 
9902                         mod.autoCompletePop.append('<a class="choice" data-index="'+i+'">'+e+'</a>');
 
9905                 var textareaOffset = $(mod.autoCompleteLastTarget).offset();
 
9906                 textareaOffset.left += $(mod.autoCompleteLastTarget).width();
 
9907                 mod.autoCompletePop.css(textareaOffset).show();
 
9909                 mod.autoCompleteSetNavIndex(0);
 
9912         autoCompleteInsert: function(inputValue) {
 
9913                 var textarea = modules["commentTools"].autoCompleteLastTarget,
 
9914                     caretPos = textarea.selectionStart,
 
9915                     left = textarea.value.substr(0, caretPos),
 
9916                     right = textarea.value.substr(caretPos);
 
9917                 left = left.replace(/\/?([ru])\/(\w*)\ ?$/, '/$1/'+inputValue+' ');
 
9918                 textarea.value = left + right;
 
9919                 textarea.selectionStart = textarea.selectionEnd = left.length;
 
9922         findTextareaForElement: function(elem) {
 
9924                         .closest(".usertext-edit, .RESDialogContents, .wiki-page-content")
 
9926                         .filter("#BigText, [name=text], [name=description], [name=public_description], #wiki_page_content")
 
9932 modules['usernameHider'] = {
 
9933         moduleID: 'usernameHider',
 
9934         moduleName: 'Username Hider',
 
9935         category: 'Accounts',
 
9939                         value: '~anonymous~',
 
9940                         description: 'What to replace your username with, default is ~anonymous~'
 
9943         description: 'This module hides your real username when you\'re logged in to reddit.',
 
9944         isEnabled: function() {
 
9945                 return RESConsole.getModulePrefs(this.moduleID);
 
9948                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i,
 
9949                 /^https?:\/\/reddit\.com\/[-\w\.\/]*/i
 
9951         isMatchURL: function() {
 
9952                 return RESUtils.isMatchURL(this.moduleID);
 
9954         beforeLoad: function() {
 
9955                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
9956                         if (!RESUtils.loggedInUser(true)) {
 
9957                                 this.tryAgain = true;
 
9960                         this.hideUsername();
 
9964                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
9965                         if (this.tryAgain && RESUtils.loggedInUser()) {
 
9966                                 this.hideUsername();
 
9967                                 GM_addStyle(RESUtils.css);
 
9971         hideUsername: function() {
 
9972                 var user = RESUtils.loggedInUser(),
 
9973                     curatedBy = document.querySelector('.multi-details > h2 a');
 
9974                 RESUtils.addCSS('p.tagline > a[href*=\'/'+user+'\'], #header .user > a, .titlebox .tagline a.author, .commentingAs, .bottom a[href*=\'/'+user+'\'] {line-height:0;font-size:0;content:none;}');
 
9975                 RESUtils.addCSS('p.tagline > a[href*=\'/'+user+'\']:after, #header .user > a:after, .titlebox .tagline a.author:after, .bottom a[href*=\'/'+user+'\']:after {content: "'+this.options.displayText.value+'";letter-spacing:normal;font-size:10px;}');
 
9976                 RESUtils.addCSS('.commentingAs:after {content: "Commenting as: '+this.options.displayText.value+'";letter-spacing:normal;font-size:12px;}');
 
9977                 if ( modules['userHighlight'].isEnabled() ){
 
9978                         RESUtils.addCSS('p.tagline > .submitter[href*=\'/'+user+'\']:after, p.tagline > .moderator[href*=\'/'+user+'\']:after{background-color:inherit;padding:0 2px;font-weight:bold;border-radius:3px;color:#fff;}');
 
9979                         RESUtils.addCSS('p.tagline > .submitter[href*=\'/'+user+'\']:after{ background-color:'+modules['userHighlight'].options.OPColor.value+';}');
 
9980                         RESUtils.addCSS('p.tagline > .moderator[href*=\'/'+user+'\']:after{ background-color:'+modules['userHighlight'].options.modColor.value+';}');
 
9981                         RESUtils.addCSS('p.tagline > .submitter[href*=\'/'+user+'\']:hover:after{ background-color:'+modules['userHighlight'].options.OPColorHover.value+';}');
 
9982                         RESUtils.addCSS('p.tagline > .moderator[href*=\'/'+user+'\']:hover:after{ background-color:'+modules['userHighlight'].options.modColorHover.value+';}');
 
9984                 if ( curatedBy && curatedBy.href.slice(-(user.length+1)) === '/' + user ){
 
9985                         curatedBy.textContent = 'curated by /u/' + this.options.displayText.value;
 
9991 /* siteModule format:
 
9993 //Initialization method for things that cannot be performed inline. The method 
 
9994 //is required to be present, but it can be empty
 
9997 //Returns true/false to indicate whether the siteModule will attempt to handle the link
 
9998 //the only parameter is the anchor element
 
9999 //returns true or false
 
10000         detect: function(element) {return true/false;},
 
10002 //This is where links are parsed, cache checks are made, and XHR is performed.
 
10003 //the only parameter is the anchor element
 
10004 //The method is in a jQuery Deferred chain and will be followed by handleInfo. 
 
10005 //A new $.Deferred object should be created and resolved/rejected as necessary and then reterned.
 
10006 //If resolving, the element should be passed along with whatever data is required.
 
10007         handleLink: function(element) {},
 
10009 //This is were the embedding information is added to the link
 
10010 //handleInfo sits in the Deferred chain after handLink
 
10011 //and should recieve both the element and a data object from handleLink
 
10012 //the first parameter should the same anchor element passed to handleLink
 
10013 //the second parameter should module specific data
 
10014 //A new $.Deferred object should be created and resolved/rejected as necessary and then reterned
 
10015 //If resolving, the element should be passed
 
10016         handleInfo: function(elem, info) {}
 
10019 Embedding infomation:
 
10020 all embedding information (except 'site') is to be attatched the 
 
10021 html anchor in the handleInfo function
 
10024         'IMAGE' for single images | 'GALLERY' for image galleries | 'TEXT' html/text to be displayed
 
10026         if type is TEXT then src is HTML (be carefull what is accepted here)
 
10027         if type is IMAGE then src is an image URL string
 
10028         if type is GALLERY then src is an array of objects with the following properties:
 
10029                 required src: URL of the image
 
10030                 optional href: URL of the page containing the image (per image)
 
10031                 optional title: string to displayed directly above the image (per image)
 
10032                 optional caption: string to be displayed directly below the image (per image)
 
10033 optional imageTitle:
 
10034         string to be displayed above the image (gallery level).
 
10036         string to be displayed below the image
 
10038         string to be displayed below caption
 
10039 optional galleryStart:
 
10040         zero-indexed page number to open the gallery to
 
10042 modules['showImages'] = {
 
10043         moduleID: 'showImages',
 
10044         moduleName: 'Inline Image Viewer',
 
10050                         description: 'Max width of image displayed onscreen'
 
10055                         description: 'Max height of image displayed onscreen'
 
10060                         description: 'Open images in a new tab/window when clicked?'
 
10065                         description: 'If checked, do not show images marked NSFW.'
 
10067                 autoExpandSelfText: {
 
10070                         description: 'When loading selftext from an Aa+ expando, auto reveal images.'
 
10075                         description: 'Allow dragging to resize/zoom images.'
 
10080                         description: 'Mark links visited when you view images (does eat some resources).'
 
10086                                 {name: 'Add links to history', value: 'add'},
 
10087                                 {name: 'Color links, but do not add to history', value: 'color'},
 
10088                                 {name: 'Do not add or color links.', value: 'none'}
 
10090                         description: 'Keeps NSFW links from being added to your browser history <span style="font-style: italic">by the markVisited feature</span>.<br/>\
 
10091                                 <span style="font-style: italic">If you chose the second option, then links will be blue again on refresh.</span><br/>\
 
10092                                 <span style="color: red">This does not change your basic browser behavior.\
 
10093                                 If you click on a link then it will still be added to your history normally.\
 
10094                                 This is not a substitute for using your browser\'s privacy mode.</span>'
 
10096                 ignoreDuplicates: {
 
10099                         description: 'Do not create expandos for images that appear multiple times in a page.'
 
10101                 displayImageCaptions: {
 
10104                         description: 'Retrieve image captions/attribution information.'
 
10109                         description: 'Display all images at once in a \'filmstrip\' layout, rather than the default navigable \'slideshow\' style.'
 
10112         description: 'Opens images inline in your browser with the click of a button. Also has configuration options, check it out!',
 
10113         isEnabled: function() {
 
10114                 return RESConsole.getModulePrefs(this.moduleID);
 
10117                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\_\?=]*/i
 
10120                 /^https?:\/\/([a-z]+)\.reddit\.com\/ads\/[-\w\.\_\?=]*/i,
 
10121                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*\/submit\/?$/i
 
10123         isMatchURL: function() {
 
10124                 return RESUtils.isMatchURL(this.moduleID);
 
10126         beforeLoad: function() {
 
10127                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
10128                         if (!this.options.displayImageCaptions.value) {
 
10129                                 RESUtils.addCSS('.imgTitle, .imgCaptions { display: none; }');
 
10134                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
10136                         this.imageList = [];
 
10137                         this.imagesRevealed = {};
 
10138                         this.dupeAnchors = 0;
 
10140                         true: show all images
 
10141                         false: hide all images
 
10142                         'any string': display images match the tab
 
10144                         this.currentImageTab = false;
 
10145                         this.customImageTabs = {};
 
10147                         if (this.options.markVisited.value) {
 
10148                                 // we only need this iFrame hack if we're unable to add to history directly, which Firefox addons and Chrome can do.
 
10149                                 if (!BrowserDetect.isChrome() && !BrowserDetect.isFirefox()) {
 
10150                                         this.imageTrackFrame = document.createElement('iframe');
 
10151                                         this.imageTrackFrame.addEventListener('load', function() {
 
10152                                                 setTimeout(modules['showImages'].imageTrackShift, 300);
 
10154                                         this.imageTrackFrame.style.display = 'none';
 
10155                                         this.imageTrackFrame.style.width = '0px';
 
10156                                         this.imageTrackFrame.style.height = '0px';
 
10157                                         document.body.appendChild(this.imageTrackFrame);
 
10159                                 this.imageTrackStack = [];
 
10162                         //set up all site modules
 
10163                         for (var key in this.siteModules) {
 
10164                                 this.siteModules[key].go();
 
10166                         this.scanningForImages = false;
 
10168                         RESUtils.watchForElement('siteTable', modules['showImages'].findAllImages);
 
10169                         RESUtils.watchForElement('selfText', modules['showImages'].findAllImagesInSelfText);
 
10170                         RESUtils.watchForElement('newComments', modules['showImages'].findAllImagesInSelfText);
 
10172                         this.createImageButtons();
 
10173                         this.findAllImages();
 
10174                         document.addEventListener('dragstart', function(){return false;}, false);
 
10177         findAllImagesInSelfText: function(ele) {
 
10178                 modules['showImages'].findAllImages(ele, true);
 
10180         createImageButtons: function() {
 
10181                 if ((location.href.match(/search\?\/?q\=/)) || (location.href.match(/about\/reports/)) || (location.href.match(/about\/spam/)) || (location.href.match(/about\/unmoderated/)) || (location.href.match(/modqueue/)) || (location.href.toLowerCase().match('dashboard'))) {
 
10182                         var hbl = document.body.querySelector('#header-bottom-left');
 
10184                                 var mainMenuUL = document.createElement('ul');
 
10185                                 mainMenuUL.setAttribute('class','tabmenu viewimages');
 
10186                                 mainMenuUL.setAttribute('style','display: inline-block');
 
10187                                 hbl.appendChild(mainMenuUL);
 
10190                         var mainMenuUL = document.body.querySelector('#header-bottom-left ul.tabmenu');
 
10193                         var li = document.createElement('li');
 
10194                         var a = document.createElement('a');
 
10195                         var text = document.createTextNode('scanning for images...');
 
10196                         this.scanningForImages = true;
 
10198                         a.href = 'javascript:void(0);';
 
10199                         a.id = 'viewImagesButton';
 
10200                         a.addEventListener('click', function(e) {
 
10201                                 e.preventDefault();
 
10202                                 if (!modules['showImages'].scanningForImages) {
 
10203                                         modules['showImages'].setShowImages(null, 'image');
 
10206                         a.appendChild(text);
 
10208                         mainMenuUL.appendChild(li);
 
10209                         this.viewImageButton = a;
 
10211                                 To enable custom image tabs for a subreddit start by adding `[](#/RES_SR_Config/ImageTabs?)` to the markdown code of the sidebar.
 
10212                                 This should not have any visible effect on the HTML.
 
10213                                 Right now no options have been configured, so there won't be any new tabs.
 
10214                                 You can add up to 8 tabs in the following manner:
 
10215                                 A tab is defined by a label and a tag list separated by an equals sign like this: `LABEL=TAGLIST` 
 
10216                                 The label can be up to 32 characters long and may contain english letters, numbers, hyphens, spaces, and underscores. The labels must be URI encoded.
 
10217                                 The tag list can contain up to tag values separated by commas. Individual tags have the same content restrictions a labels. (do not URI encode the commmas)
 
10219                                 The the tab definitions are joined by ampersands (`&`).
 
10220                                 Labels appear to the right of the "view images" button and are surrounded by `[]` brackets.
 
10221                                 Post titles are searched for any place that an entry in the tag list appears surrounded by any kind of bracket <>, [], (), {}.
 
10222                                 Tags are not case sensitive and whitespace is permitted between the brackets and the tag.
 
10224                                 To allow the tabs to be styled, the tabs will have a class that is the tab label with the spaces and hyphens replaced by underscores and then prefixed with `'RESTab-'` so the label 'Feature Request' becomes `'RESTab-feature_request'`.
 
10226                                 We realize that the format is highly restrictive, but you must understand that that is for everyone's protection. If there is demand, the filter can be expanded.
 
10229                                 A hypothetical setup for /r/minecraft that creates tabs for builds, mods, and texture packs:
 
10231                                     [](#/RES_SR_Config/ImageTabs?build=build,project&mod=mod&texture%20pack=texture,textures,pack,texture%20pack)
 
10233                                 To duplicate the behavior originally used for /r/gonewild you would use:
 
10235                                     [](#/RES_SR_Config/ImageTabs?m=m,man,male&f=f,fem,female)
 
10238                         var tabConfig = document.querySelector('.side .md a[href^="#/RES_SR_Config/ImageTabs"]');
 
10239                         //This is hardcoded until the mods of /r/gonewild add the tag 
 
10240                         if (!tabConfig && RESUtils.currentSubreddit('gonewild')) {
 
10241                                 tabConfig = $('<a href="#/RES_SR_Config/ImageTabs?m=m,man,male&f=f,fem,female">')[0];
 
10247                                 var switchCount = 0;
 
10249                                 var whitelist = /^[A-Za-z0-9_ \-]{1,32}$/;
 
10250                                 var configString = tabConfig.hash.match(/\?(.*)/);
 
10251                                 if (configString != null) {
 
10252                                         var pairs = configString[1].split('&');
 
10253                                         for (var i = 0; i < pairs.length && switchCount < 8; i++) {
 
10254                                                 var pair = pairs[i].split('=');
 
10255                                                 if (pair.length !== 2) continue;
 
10256                                                 var label = decodeURIComponent(pair[0]);
 
10257                                                 if (!whitelist.test(label)) continue;
 
10258                                                 var parts = pair[1].split(',');
 
10259                                                 var acceptedParts = [];
 
10260                                                 for (var j = 0; j < parts.length && acceptedParts.length < 8; j++) {
 
10261                                                         var part = decodeURIComponent(parts[j]);
 
10262                                                         if (!whitelist.test(part)) continue;
 
10263                                                         else acceptedParts.push(part);
 
10265                                                 if (acceptedParts.length > 0) {
 
10266                                                         if (!(label in switches)) switchCount++;
 
10267                                                         switches[label] = acceptedParts;
 
10271                                 if (switchCount > 0) {
 
10272                                         for (var key in switches) {
 
10273                                                 this.customImageTabs[key] = new RegExp('[\\[\\{\\<\\(]\s*('+switches[key].join('|')+')\s*[\\]\\}\\>\\)]','i');
 
10278                         if (!/comments\/[-\w\.\/]/i.test(location.href)) {
 
10279                                 for (var mode in this.customImageTabs) {
 
10280                                         var li = document.createElement('li');
 
10281                                         var a = document.createElement('a');
 
10282                                         var text = document.createTextNode('['+mode+']');
 
10283                                         a.href = 'javascript:void(0);';
 
10284                                         a.className = 'RESTab-'+mode.toLowerCase().replace(/- /g, '_');
 
10285                                         a.addEventListener('click', (function(mode) {
 
10286                                                 return function(e) {
 
10287                                                         e.preventDefault();
 
10288                                                         modules['showImages'].setShowImages(mode);
 
10292                                         a.appendChild(text);
 
10294                                         mainMenuUL.appendChild(li);
 
10299         setShowImages: function(newImageTab, type) {
 
10300                 type = type || 'image';
 
10301                 if (newImageTab == null) {
 
10302                         //This is for the all images button
 
10303                         //If we stored `true` then toggle to false, in all other cases turn it to true
 
10304                         if (this.currentImageTab == true) {
 
10305                                 this.currentImageTab = false;
 
10307                                 this.currentImageTab = true;
 
10309                 } else if (this.currentImageTab == newImageTab) {
 
10310                         //If they are the same, turn it off
 
10311                         this.currentImageTab = false;
 
10312                 } else if (newImageTab in this.customImageTabs) {
 
10313                         //If the tab is defined, switch to it
 
10314                         this.currentImageTab = newImageTab;
 
10316                         //Otherwise ignore it
 
10319                 this.updateImageButtons();
 
10320                 this.updateRevealedImages(type);
 
10322         updateImageButtons: function() {
 
10323                 var imgCount = this.imageList.length;
 
10324                 var showHideText = 'view';
 
10325                 if (this.currentImageTab == true) {
 
10326                         showHideText = 'hide';
 
10328                 if (typeof this.viewImageButton !== 'undefined') {
 
10329                         var buttonText = showHideText + ' images ';
 
10330                         if (! RESUtils.currentSubreddit('dashboard')) buttonText += '(' + imgCount + ')';
 
10331                         $(this.viewImageButton).text(buttonText);
 
10334         updateRevealedImages: function(type) {
 
10335                 for (var i = 0, len = this.imageList.length; i < len; i++) {
 
10336                         var image = this.imageList[i];
 
10337                         if ($(image).hasClass(type)) {
 
10338                                 this.revealImage(image, this.findImageFilter(image.imageLink));
 
10342         findImageFilter: function(image) {
 
10343                 var isMatched = false;
 
10344                 if (typeof this.currentImageTab === 'boolean') {
 
10345                         //booleans indicate show all or nothing
 
10346                         isMatched = this.currentImageTab;
 
10347                 } else if (this.currentImageTab in this.customImageTabs) {
 
10348                         var re = this.customImageTabs[this.currentImageTab];
 
10349                         isMatched = re.test(image.text);
 
10351                 //If false then there is no need to go through the NSFW filter
 
10352                 if (!isMatched) return false;
 
10354                 image.NSFW = false;
 
10355                 if (this.options.hideNSFW.value) {
 
10356                         image.NSFW = /nsfw/i.test(image.text);
 
10359                 return !image.NSFW;
 
10361         findAllImages: function(elem, isSelfText) {
 
10362                 modules['showImages'].scanningForImages = true;
 
10363                 if (elem == null) {
 
10364                         elem = document.body;
 
10366                 // get elements common across all pages first...
 
10367                 // if we're on a comments page, get those elements too...
 
10368                 var commentsre = /comments\/[-\w\.\/]/i;
 
10369                 var userre = /user\/[-\w\.\/]/i;
 
10370                 modules['showImages'].scanningSelfText = false;
 
10371                 var allElements = [];
 
10372                 if (commentsre.test(location.href) || userre.test(location.href)) {
 
10373                         allElements = elem.querySelectorAll('#siteTable a.title, .expando .usertext-body > div.md a, .content .usertext-body > div.md a');
 
10374                 } else if (isSelfText) {
 
10375                         // We're scanning newly opened (from an expando) selftext...
 
10376                         allElements = elem.querySelectorAll('.usertext-body > div.md a');
 
10377                         modules['showImages'].scanningSelfText = true;
 
10379                         allElements = elem.querySelectorAll('#siteTable A.title');
 
10382                 if (RESUtils.pageType() === 'comments') {
 
10383                         RESUtils.forEachChunked(allElements, 15, 1000, function(element, i, array) {
 
10384                                 modules['showImages'].checkElementForImage(element);
 
10385                                 if (i >= array.length - 1) {
 
10386                                         modules['showImages'].scanningSelfText = false;
 
10387                                         modules['showImages'].scanningForImages = false;
 
10388                                         modules['showImages'].updateImageButtons(modules['showImages'].imageList.length);
 
10392                         var chunkLength = allElements.length;
 
10393                         for (var i = 0; i < chunkLength; i++) {
 
10394                                 modules['showImages'].checkElementForImage(allElements[i]);
 
10396                         modules['showImages'].scanningSelfText = false;
 
10397                         modules['showImages'].scanningForImages = false;
 
10398                         modules['showImages'].updateImageButtons(modules['showImages'].imageList.length);
 
10401         checkElementForImage: function(elem) {
 
10402                 if (this.options.hideNSFW.value) {
 
10403                         if (elem.classList.contains('title')) {
 
10404                                 elem.NSFW = elem.parentNode.parentNode.parentNode.classList.contains('over18');
 
10409                 var href = elem.href;
 
10410                 if ((!elem.classList.contains('imgScanned') && (typeof this.imagesRevealed[href] === 'undefined' || !this.options.ignoreDuplicates.value || (RESUtils.currentSubreddit('dashboard'))) && href !== null) || this.scanningSelfText) {
 
10411                         elem.classList.add('imgScanned');
 
10412                         this.dupeAnchors++;
 
10413                         var siteFound = false;
 
10414                         if (siteFound = this.siteModules['default'].detect(elem)) {
 
10415                                 elem.site = 'default';
 
10418                                 for (var site in this.siteModules) {
 
10419                                         if (site === 'default') continue;
 
10420                                         if (this.siteModules[site].detect(elem)) {
 
10427                         if (siteFound && !elem.NSFW) {
 
10428                                 this.imagesRevealed[href] = this.dupeAnchors;
 
10429                                 var siteMod = this.siteModules[elem.site];
 
10430                                 $.Deferred().resolve(elem).then(siteMod.handleLink).then(siteMod.handleInfo).
 
10431                                         then(this.createImageExpando, function(){
 
10432                                                 console.error.apply(console, arguments);
 
10435                 } else if (!elem.classList.contains('imgScanned')) {
 
10436                         var textFrag = document.createElement('span');
 
10437                         textFrag.setAttribute('class','RESdupeimg');
 
10438                         $(textFrag).html(' <a class="noKeyNav" href="#img'+escapeHTML(this.imagesRevealed[href])+'" title="click to scroll to original">[RES ignored duplicate image]</a>');
 
10439                         insertAfter(elem, textFrag);
 
10442         createImageExpando: function(elem) {
 
10443                 var mod = modules['showImages'];
 
10444                 if (!elem) return false;
 
10445                 var href = elem.href;
 
10446                 if (!href) return false;
 
10447                 //This should not be reached in the case of duplicates
 
10448                 elem.name = 'img'+mod.imagesRevealed[href];
 
10450                 //expandLink aka the expando button
 
10451                 var expandLink = document.createElement('a');
 
10452                 expandLink.className = 'toggleImage expando-button collapsedExpando';
 
10453                 if (elem.type === 'IMAGE') expandLink.className += ' image';
 
10454                 if (elem.type === 'GALLERY') expandLink.className += ' image gallery';
 
10455                 if (elem.type === 'TEXT') expandLink.className += ' selftext collapsed';
 
10456                 if (elem.type === 'VIDEO') expandLink.className += ' video collapsed';
 
10457                 if (elem.type === 'AUDIO') expandLink.className += ' video collapsed'; // yes, still class "video", that's what reddit uses.
 
10458                 if (elem.type === 'NOEMBED') expandLink.className += ' '+elem.expandoClass;
 
10460                 if (elem.type === 'GALLERY' && elem.src && elem.src.length) expandLink.setAttribute('title', elem.src.length + ' items in gallery');
 
10461                 $(expandLink).html(' ');
 
10462                 expandLink.addEventListener('click', function(e) {
 
10463                         e.preventDefault();
 
10464                         modules['showImages'].revealImage(e.target, (e.target.classList.contains('collapsedExpando')));
 
10466                 var preNode = null;
 
10467                 if (elem.parentNode.classList.contains('title')) {
 
10468                         preNode = elem.parentNode;
 
10469                         expandLink.classList.add('linkImg');
 
10472                         expandLink.classList.add('commentImg');
 
10474                 insertAfter(preNode, expandLink);
 
10476                  * save the link element for later use since some extensions
 
10477                  * like web of trust can place other elements in places that
 
10478                  * confuse the old method
 
10480                 expandLink.imageLink = elem;
 
10481                 mod.imageList.push(expandLink);
 
10483                 if (mod.scanningSelfText && mod.options.autoExpandSelfText.value) {
 
10484                         mod.revealImage(expandLink, true);
 
10486                         // this may have come from an asynchronous call, in which case it'd get missed by findAllImages, so
 
10487                         // if all images are supposed to be visible, expand this link now.
 
10488                         mod.revealImage(expandLink, mod.findImageFilter(expandLink.imageLink));
 
10492                 if (mod.scanningForImages == false) {
 
10493                         // also since this may have come from an asynchronous call, we need to update the view images count.
 
10494                         mod.updateImageButtons(mod.imageList.length);
 
10497         revealImage: function(expandoButton, showHide) {
 
10498                 if ((!expandoButton) || (! $(expandoButton).is(':visible'))) return false;
 
10499                 // showhide = false means hide, true means show!
 
10501                 var imageLink = expandoButton.imageLink;
 
10502                 if (typeof this.siteModules[imageLink.site] === 'undefined') {
 
10503                         console.log('something went wrong scanning image from site: ' + imageLink.site);
 
10506                 if (expandoButton.expandoBox && expandoButton.expandoBox.classList.contains('madeVisible')) {
 
10508                                 $(expandoButton).removeClass('expanded').addClass('collapsed collapsedExpando');
 
10509                                 expandoButton.expandoBox.style.display = 'none';
 
10510                                 if (imageLink.type === 'AUDIO' || imageLink.type === 'VIDEO') {
 
10511                                         var mediaTag = expandoButton.expandoBox.querySelector(imageLink.type);
 
10515                                 $(expandoButton).addClass('expanded').removeClass('collapsed collapsedExpando');
 
10516                                 expandoButton.expandoBox.style.display = 'block';
 
10517                                 var associatedImage = $(expandoButton).data('associatedImage');
 
10518                                 if (associatedImage) {
 
10519                                         modules['showImages'].syncPlaceholder(associatedImage);
 
10522                         this.handleSRStyleToggleVisibility();
 
10523                 } else if (showHide) {
 
10524                         //TODO: flash, custom
 
10525                         switch (imageLink.type) {
 
10528                                         this.generateImageExpando(expandoButton);
 
10531                                         this.generateTextExpando(expandoButton);
 
10534                                         this.generateVideoExpando(expandoButton, imageLink.mediaOptions);
 
10537                                         this.generateAudioExpando(expandoButton);
 
10540                                         this.generateNoEmbedExpando(expandoButton);
 
10545         generateImageExpando: function(expandoButton) {
 
10546                 var imageLink = expandoButton.imageLink;
 
10547                 var which = imageLink.galleryStart || 0;
 
10549                 var imgDiv = document.createElement('div');
 
10550                 imgDiv.classList.add('madeVisible');
 
10551                 imgDiv.currentImage = which;
 
10552                 imgDiv.sources = [];
 
10554                 // Test for a single image or an album/array of image
 
10555                 if (Array.isArray(imageLink.src)) {
 
10556                         imgDiv.sources = imageLink.src;
 
10558                         // Also preload images for an album
 
10559                         this.preloadImages(imageLink.src, 0);
 
10561                         // Only the image is left to display, pack it like a single-image album with no caption or title
 
10562                         singleImage = {src:imageLink.src,href:imageLink.href};
 
10563                         imgDiv.sources[0] = singleImage;
 
10566                 if ('imageTitle' in imageLink) {
 
10567                         var header = document.createElement('h3');
 
10568                         header.classList.add('imgTitle');
 
10569                         $(header).safeHtml(imageLink.imageTitle);
 
10570                         imgDiv.appendChild(header);
 
10573                 if ('imgCaptions' in imageLink) {
 
10574                         var captions = document.createElement('div');
 
10575                         captions.className = 'imgCaptions';
 
10576                         $(captions).safeHtml(imageLink.caption);
 
10577                         imgDiv.appendChild(captions);
 
10580                 if ('credits' in imageLink) {
 
10581                         var credits = document.createElement('div');
 
10582                         credits.className = 'imgCredits';
 
10583                         $(credits).safeHtml(imageLink.credits);
 
10584                         imgDiv.appendChild(credits);
 
10587                 switch(imageLink.type){
 
10589                                 if (this.options.loadAllInAlbum.value) {
 
10590                                         if (imgDiv.sources.length > 1) {
 
10591                                                 var albumLength = " (" + imgDiv.sources.length + " images)";
 
10592                                                 $(header).append(albumLength);
 
10595                                         for (var imgNum = 0; imgNum < imgDiv.sources.length; imgNum++) {
 
10596                                                 addImage(imgDiv, imgNum, this);
 
10600                                         // If we're using the traditional album view, add the controls then fall through to add the IMAGE
 
10601                                         var controlWrapper = document.createElement('div');
 
10602                                         controlWrapper.className  = 'RESGalleryControls';
 
10604                                         var leftButton = document.createElement("a");
 
10605                                         leftButton.className = 'previous noKeyNav';
 
10606                                         leftButton.addEventListener('click', function(e){
 
10607                                                 var topWrapper = e.target.parentElement.parentElement;
 
10608                                                 if (topWrapper.currentImage === 0) {
 
10609                                                         topWrapper.currentImage = topWrapper.sources.length-1;
 
10611                                                         topWrapper.currentImage -= 1;
 
10613                                                 adjustGalleryDisplay(topWrapper);
 
10615                                         controlWrapper.appendChild(leftButton);
 
10617                                         var posLabel = document.createElement('span');
 
10618                                         posLabel.className = 'RESGalleryLabel';
 
10619                                         var niceWhich = ((which+1 < 10)&&(imgDiv.sources.length >= 10)) ? '0'+(which+1) : (which+1);
 
10620                                         if (imgDiv.sources.length) {
 
10621                                                 posLabel.textContent = niceWhich + " of " + imgDiv.sources.length;
 
10623                                                 posLabel.textContent = "Whoops, this gallery seems to be empty.";
 
10625                                         controlWrapper.appendChild(posLabel);
 
10627                                         var rightButton = document.createElement("a");
 
10628                                         rightButton.className = 'next noKeyNav';
 
10629                                         rightButton.addEventListener('click', function(e){
 
10630                                                 var topWrapper = e.target.parentElement.parentElement;
 
10631                                                 if (topWrapper.currentImage == topWrapper.sources.length-1) {
 
10632                                                         topWrapper.currentImage = 0;
 
10634                                                         topWrapper.currentImage += 1;
 
10636                                                 adjustGalleryDisplay(topWrapper);
 
10638                                         controlWrapper.appendChild(rightButton);
 
10640                                         if (!imgDiv.sources.length) {
 
10641                                                 $(leftButton).css('visibility','hidden');
 
10642                                                 $(rightButton).css('visibility','hidden');
 
10645                                         imgDiv.appendChild(controlWrapper);
 
10649                                 addImage(imgDiv, which, this);
 
10652                 function addImage(container, sourceNumber, thisHandle) {
 
10653                         var sourceImage = container.sources[sourceNumber];
 
10655                         var paragraph = document.createElement('p');
 
10657                         if (!sourceImage) {
 
10660                         if ('title' in sourceImage) {
 
10661                                 var imageTitle = document.createElement('h4');
 
10662                                 imageTitle.className = 'imgCaptions';
 
10663                                 $(imageTitle).safeHtml(sourceImage.title);
 
10664                                 paragraph.appendChild(imageTitle);
 
10667                         if ('caption' in sourceImage) {
 
10668                                 var imageCaptions = document.createElement('div');
 
10669                                 imageCaptions.className = 'imgCaptions';
 
10670                                 $(imageCaptions).safeHtml(sourceImage.caption);
 
10671                                 paragraph.appendChild(imageCaptions);
 
10674                         var imageAnchor = document.createElement('a');
 
10675                         imageAnchor.classList.add('madeVisible');
 
10676                         imageAnchor.href = sourceImage.href;
 
10677                         if (thisHandle.options.openInNewWindow.value) {
 
10678                                 imageAnchor.target ='_blank';
 
10681                         var image = document.createElement('img');
 
10682                         $(expandoButton).data('associatedImage', image);
 
10683                         //Unfortunately it is impossible to use a global event handler for these.
 
10684                         image.onerror = function() {
 
10685                                 image.classList.add("RESImageError");
 
10687                         image.onload = function() {
 
10688                                 image.classList.remove("RESImageError");
 
10690                         image.classList.add('RESImage');
 
10691                         image.id = 'RESImage-' + RESUtils.randomHash();
 
10692                         image.src = sourceImage.src;
 
10693                         image.title = 'drag to resize';
 
10694                         image.style.maxWidth = thisHandle.options.maxWidth.value + 'px';
 
10695                         image.style.maxHeight = thisHandle.options.maxHeight.value + 'px';
 
10696                         imageAnchor.appendChild(image);
 
10697                         modules['showImages'].setPlaceholder(image);
 
10698                         thisHandle.makeImageZoomable(image);
 
10699                         thisHandle.trackImageLoad(imageLink, image);
 
10700                         paragraph.appendChild(imageAnchor);
 
10702                         container.appendChild(paragraph);
 
10705                 //Adjusts the images for the gallery navigation buttons as well as the "n of m" display.
 
10706                 function adjustGalleryDisplay(topLevel) {
 
10707                         var source = topLevel.sources[topLevel.currentImage];
 
10708                         var image = topLevel.querySelector('img.RESImage');
 
10709                         var imageAnchor = image.parentElement;
 
10710                         var paragraph = imageAnchor.parentElement;
 
10711                         image.src = source.src;
 
10712                         imageAnchor.href = source.href || imageLink.href;
 
10713                         var paddedImageNumber = ((topLevel.currentImage+1 < 10)&&(imgDiv.sources.length >= 10)) ? '0'+(topLevel.currentImage+1) : topLevel.currentImage+1;
 
10714                         if (imgDiv.sources.length) {
 
10715                                 topLevel.querySelector('.RESGalleryLabel').textContent = (paddedImageNumber+" of "+imgDiv.sources.length);
 
10717                                 topLevel.querySelector('.RESGalleryLabel').textContent = "Whoops, this gallery seems to be empty.";
 
10719                         if (topLevel.currentImage === 0) {
 
10720                                 leftButton.classList.add('end');
 
10721                                 rightButton.classList.remove('end');
 
10722                         } else if (topLevel.currentImage === topLevel.sources.length-1) {
 
10723                                 leftButton.classList.remove('end');
 
10724                                 rightButton.classList.add('end');
 
10726                                 leftButton.classList.remove('end');
 
10727                                 rightButton.classList.remove('end');
 
10730                         $(paragraph).find('.imgCaptions').empty();
 
10731                         var imageTitle = paragraph.querySelector('h4.imgCaptions');
 
10732                         if (imageTitle) $(imageTitle).safeHtml(source.title);
 
10733                         var imageCaptions = paragraph.querySelector('div.imgCaptions');
 
10734                         if (imageCaptions) $(imageCaptions).safeHtml(source.caption);
 
10737                 if (expandoButton.classList.contains('commentImg')) {
 
10738                         insertAfter(expandoButton, imgDiv);
 
10740                         expandoButton.parentNode.appendChild(imgDiv);
 
10742                 expandoButton.expandoBox = imgDiv;
 
10744                 expandoButton.classList.remove('collapsedExpando');
 
10745                 expandoButton.classList.add('expanded');
 
10748          * Recursively loads the images synchronously.
 
10750         preloadImages: function(srcs, i) {
 
10754                 img.onload = img.onerror = function(){
 
10756                         if(typeof srcs[_i] === 'undefined'){
 
10759                         _this.preloadImages(srcs, _i);
 
10761                         delete img; // Delete the image element from the DOM to stop the RAM usage getting to high.
 
10763                 img.src = srcs[i].src;
 
10765         generateTextExpando: function(expandoButton) {
 
10766                 var imageLink = expandoButton.imageLink;
 
10767                 var wrapperDiv = document.createElement('div');
 
10768                 wrapperDiv.className = 'usertext';
 
10770                 var imgDiv = document.createElement('div');
 
10771                 imgDiv.className = 'madeVisible usertext-body';
 
10773                 var header = document.createElement('h3');
 
10774                 header.className = 'imgTitle';
 
10775                 $(header).safeHtml(imageLink.imageTitle);
 
10776                 imgDiv.appendChild(header);
 
10778                 var text = document.createElement('div');
 
10779                 text.className = 'md';
 
10780                 $(text).safeHtml(imageLink.src);
 
10781                 imgDiv.appendChild(text);
 
10783                 var captions = document.createElement('div');
 
10784                 captions.className = 'imgCaptions';
 
10785                 $(captions).safeHtml(imageLink.caption);
 
10786                 imgDiv.appendChild(captions);
 
10788                 if ('credits' in imageLink) {
 
10789                         var credits = document.createElement('div');
 
10790                         credits.className = 'imgCredits';
 
10791                         $(credits).safeHtml(imageLink.credits);
 
10792                         imgDiv.appendChild(credits);
 
10795                 wrapperDiv.appendChild(imgDiv);
 
10796                 if (expandoButton.classList.contains('commentImg')) {
 
10797                         insertAfter(expandoButton, wrapperDiv);
 
10799                         expandoButton.parentNode.appendChild(wrapperDiv);
 
10801                 expandoButton.expandoBox = imgDiv;
 
10803                 expandoButton.classList.remove('collapsedExpando');
 
10804                 expandoButton.classList.remove('collapsed');
 
10805                 expandoButton.classList.add('expanded');
 
10807                 //TODO: Decide how to handle history for this.
 
10808                 //Selfposts already don't mark it, so either don't bother or add marking for selfposts.
 
10810         generateVideoExpando: function(expandoButton, options) {
 
10811                 var imageLink = expandoButton.imageLink;
 
10812                 var wrapperDiv = document.createElement('div');
 
10813                 wrapperDiv.className = 'usertext';
 
10815                 var imgDiv = document.createElement('div');
 
10816                 imgDiv.className = 'madeVisible usertext-body';
 
10818                 var header = document.createElement('h3');
 
10819                 header.className = 'imgTitle';
 
10820                 $(header).safeHtml(imageLink.imageTitle);
 
10821                 imgDiv.appendChild(header);
 
10823                 var video = document.createElement('video');
 
10824                 video.addEventListener('click', modules['showImages'].handleVideoClick);
 
10825                 video.setAttribute('controls','');
 
10826                 video.setAttribute('preload','');
 
10828                         if (options.autoplay) {
 
10829                                 video.setAttribute('autoplay','');
 
10831                         if (options.muted) {
 
10832                                 video.setAttribute('muted','');
 
10834                         if (options.loop) {
 
10835                                 video.setAttribute('loop','');
 
10838                 var sourcesHTML = "",
 
10839                     sources = $(imageLink).data('sources'),
 
10842                 for (var i=0, len=sources.length; i<len; i++) {
 
10843                         source = sources[i];
 
10844                         sourceEle = document.createElement('source');
 
10845                         sourceEle.src = source.file;
 
10846                         sourceEle.type = source.type;
 
10847                         $(video).append(sourceEle);
 
10850                 imgDiv.appendChild(video);
 
10852                 if ('credits' in imageLink) {
 
10853                         var credits = document.createElement('div');
 
10854                         credits.className = 'imgCredits';
 
10855                         $(credits).safeHtml(imageLink.credits);
 
10856                         imgDiv.appendChild(credits);
 
10859                 wrapperDiv.appendChild(imgDiv);
 
10860                 if (expandoButton.classList.contains('commentImg')) {
 
10861                         insertAfter(expandoButton, wrapperDiv);
 
10863                         expandoButton.parentNode.appendChild(wrapperDiv);
 
10865                 expandoButton.expandoBox = imgDiv;
 
10867                 expandoButton.classList.remove('collapsedExpando');
 
10868                 expandoButton.classList.remove('collapsed');
 
10869                 expandoButton.classList.add('expanded');
 
10871                 modules['showImages'].trackImageLoad(imageLink, video);
 
10874         generateAudioExpando: function(expandoButton) {
 
10875                 var imageLink = expandoButton.imageLink;
 
10876                 var wrapperDiv = document.createElement('div');
 
10877                 wrapperDiv.className = 'usertext';
 
10879                 var imgDiv = document.createElement('div');
 
10880                 imgDiv.className = 'madeVisible usertext-body';
 
10882                 var header = document.createElement('h3');
 
10883                 header.className = 'imgTitle';
 
10884                 $(header).safeHtml(imageLink.imageTitle);
 
10885                 imgDiv.appendChild(header);
 
10887                 var audio = document.createElement('audio');
 
10888                 audio.addEventListener('click', modules['showImages'].handleaudioClick);
 
10889                 audio.setAttribute('controls','');
 
10890                 // TODO: add mute/unmute control, play/pause control.
 
10892                 var sourcesHTML = "",
 
10893                     sources = $(imageLink).data('sources'),
 
10896                 for (var i=0, len=sources.length; i<len; i++) {
 
10897                         source = sources[i];
 
10898                         sourceEle = document.createElement('source');
 
10899                         sourceEle.src = source.file;
 
10900                         sourceEle.type = source.type;
 
10901                         $(audio).append(sourceEle);
 
10904                 imgDiv.appendChild(audio);
 
10906                 if ('credits' in imageLink) {
 
10907                         var credits = document.createElement('div');
 
10908                         credits.className = 'imgCredits';
 
10909                         $(credits).safeHtml(imageLink.credits);
 
10910                         imgDiv.appendChild(credits);
 
10913                 wrapperDiv.appendChild(imgDiv);
 
10914                 if (expandoButton.classList.contains('commentImg')) {
 
10915                         insertAfter(expandoButton, wrapperDiv);
 
10917                         expandoButton.parentNode.appendChild(wrapperDiv);
 
10919                 expandoButton.expandoBox = imgDiv;
 
10921                 expandoButton.classList.remove('collapsedExpando');
 
10922                 expandoButton.classList.remove('collapsed');
 
10923                 expandoButton.classList.add('expanded');
 
10925                 modules['showImages'].trackImageLoad(imageLink, audio);
 
10927         handleVideoClick: function(e) {
 
10928                 // for now, this does nothing, because apparently HTML5 video
 
10929                 // doesn't have a way to detect clicks of native controls via
 
10930                 // javascript, which means that even if you're muting/unmuting,
 
10931                 // changing volume etc this event fires and will play/pause the
 
10932                 // video, but e.target and e.currentTarget still point to the video
 
10933                 // and not the controls... yuck.
 
10935                 // if (e.target.paused) {
 
10936                 //     e.target.play();
 
10939                 //     e.target.pause();
 
10942         generateNoEmbedExpando: function(expandoButton) {
 
10943                 var imageLink = expandoButton.imageLink,
 
10944                     siteMod = imageLink.siteMod,
 
10945                     apiURL = 'http://noembed.com/embed?url=' + imageLink.src,
 
10946                     def = $.Deferred();
 
10948                 GM_xmlhttpRequest({
 
10951                         // aggressiveCache: true,
 
10952                         onload: function(response) {
 
10954                                         var json = JSON.parse(response.responseText);
 
10955                                         siteMod.calls[apiURL] = json;
 
10956                                         modules['showImages'].handleNoEmbedQuery(expandoButton, json);
 
10957                                         def.resolve(elem, json);
 
10959                                         siteMod.calls[apiURL] = null;
 
10963                         onerror: function(response) {
 
10968         handleNoEmbedQuery: function(expandoButton, response) {
 
10969                 var imageLink = expandoButton.imageLink;
 
10971                 var wrapperDiv = document.createElement('div');
 
10972                 wrapperDiv.className = 'usertext';
 
10974                 var noEmbedFrame = document.createElement('iframe');
 
10975                 // not all noEmbed responses have a height and width, so if
 
10976                 // this siteMod has a width and/or height set, use them.
 
10977                 if (imageLink.siteMod.width) {
 
10978                         noEmbedFrame.setAttribute('width', imageLink.siteMod.width);
 
10980                 if (imageLink.siteMod.height) {
 
10981                         noEmbedFrame.setAttribute('height', imageLink.siteMod.height);
 
10983                 if (imageLink.siteMod.urlMod) {
 
10984                         noEmbedFrame.setAttribute('src', imageLink.siteMod.urlMod(response.url));
 
10986                 for (key in response) {
 
10989                                         if (!noEmbedFrame.hasAttribute('src')) {
 
10990                                                 noEmbedFrame.setAttribute('src', response[key]);
 
10994                                         noEmbedFrame.setAttribute('width', response[key]);
 
10997                                         noEmbedFrame.setAttribute('height', response[key]);
 
11001                 noEmbedFrame.className = 'madeVisible usertext-body';
 
11003                 wrapperDiv.appendChild(noEmbedFrame);
 
11004                 if (expandoButton.classList.contains('commentImg')) {
 
11005                         insertAfter(expandoButton, wrapperDiv);
 
11007                         expandoButton.parentNode.appendChild(wrapperDiv);
 
11010                 expandoButton.expandoBox = noEmbedFrame;
 
11012                 expandoButton.classList.remove('collapsedExpando');
 
11013                 expandoButton.classList.remove('collapsed');
 
11014                 expandoButton.classList.add('expanded');
 
11016                 modules['showImages'].trackImageLoad(imageLink, video);
 
11018         trackImageLoad: function(link, image) {
 
11019                 if (modules['showImages'].options.markVisited.value) {
 
11020                         var isNSFW = $(link).closest('.thing').is('.over18');
 
11021                         var sfwMode = modules['showImages'].options['sfwHistory'].value;
 
11023                         if ((BrowserDetect.isChrome()) || (BrowserDetect.isFirefox())) {
 
11024                                 var url = link.historyURL || link.href;
 
11025                                 if (!isNSFW || sfwMode !== 'none') link.classList.add('visited');
 
11026                                 if (!isNSFW || sfwMode === 'add') {
 
11027                                         modules['showImages'].imageTrackStack.push(url);
 
11028                                         if (modules['showImages'].imageTrackStack.length === 1) setTimeout(modules['showImages'].imageTrackShift, 300);
 
11031                                 image.addEventListener('load', function(e) {
 
11032                                         var url = link.historyURL || link.href;
 
11033                                         if (!isNSFW || sfwMode !== 'none') link.classList.add('visited');
 
11034                                         if (!isNSFW || sfwMode === 'add') {
 
11035                                                 modules['showImages'].imageTrackStack.push(url);
 
11036                                                 if (modules['showImages'].imageTrackStack.length === 1) setTimeout(modules['showImages'].imageTrackShift, 300);
 
11042                 image.addEventListener('load', function(e) {
 
11043                         modules['showImages'].handleSRStyleToggleVisibility(e.target);
 
11046         imageTrackShift: function() {
 
11047                 var url = modules['showImages'].imageTrackStack.shift();
 
11048                 if (typeof url === 'undefined') {
 
11049                         modules['showImages'].handleSRStyleToggleVisibility();
 
11052                 if (BrowserDetect.isChrome()) {
 
11053                         if (!chrome.extension.inIncognitoContext) {
 
11054                                 chrome.extension.sendMessage({
 
11055                                         requestType: 'addURLToHistory',
 
11059                         modules['showImages'].imageTrackShift();
 
11060                 } else if (BrowserDetect.isFirefox()) {
 
11061                         // update: using XPCOM we may can add URLs to Firefox history without the iframe hack!
 
11063                                 requestType: 'addURLToHistory',
 
11066                         self.postMessage(thisJSON);
 
11067                         modules['showImages'].imageTrackShift();
 
11068                 } else if (BrowserDetect.isOpera()) { 
 
11070                                 requestType: 'addURLToHistory',
 
11073                         opera.extension.postMessage(JSON.stringify(thisJSON));
 
11074                 } else if (BrowserDetect.isSafari()) {
 
11076                                 requestType: 'addURLToHistory',
 
11079                         safari.self.tab.dispatchMessage('addURLToHistory', thisJSON);
 
11080                 } else if (typeof modules['showImages'].imageTrackFrame.contentWindow !== 'undefined') {
 
11081                         modules['showImages'].imageTrackFrame.contentWindow.location.replace(url);
 
11083                         modules['showImages'].imageTrackFrame.location.replace(url);
 
11087                 //numbers just picked as sane initialization values
 
11089                 diagonal: 0, //zero to represent the state where no the mouse button is not down
 
11092         getDragSize: function(e){
 
11093                 var rc = e.target.getBoundingClientRect(),
 
11095                         dragSize = p(p(e.clientX-rc.left, 2)+p(e.clientY-rc.top, 2), .5);
 
11097                 return Math.round(dragSize);
 
11099         handleSRStyleToggleVisibility: function(image) {
 
11100                 RESUtils.debounce('handleSRStyleToggleVisibility', 50, function() {
 
11101                         var toggleEle = modules['styleTweaks'].styleToggleContainer;
 
11102                         if (!toggleEle) return;                 
 
11103                         var imageElems = image ? [ image ] : document.querySelectorAll('.RESImage');
 
11105                         for (var i = 0 ; i < imageElems.length; i++) {
 
11106                                 var imageEle = imageElems[i];
 
11107                                 var imageID = imageEle.getAttribute('id');
 
11109                                 if (RESUtils.doElementsCollide(toggleEle, imageEle, 15)) {
 
11110                                         modules['styleTweaks'].setSRStyleToggleVisibility(false, 'imageZoom-' + imageID);
 
11112                                         modules['styleTweaks'].setSRStyleToggleVisibility(true, 'imageZoom-' + imageID);
 
11117         setPlaceholder: function(imageTag) {
 
11118                 if (!($(imageTag).data('imagePlaceholder'))) {
 
11119                         var thisPH = createElementWithID('div','RESImagePlaceholder');
 
11120                         $(thisPH).addClass('RESImagePlaceholder');
 
11121                         $(imageTag).data('imagePlaceholder', thisPH);
 
11122                         // Add listeners for drag to resize functionality...
 
11123                         $(imageTag).parent().append($(imageTag).data('imagePlaceholder'));
 
11125                 $(imageTag).load(modules['showImages'].syncPlaceholder);
 
11127         syncPlaceholder: function(e) {
 
11128                 var ele = e.target || e;
 
11129                 var thisPH = $(ele).data('imagePlaceholder');
 
11130                 $(thisPH).width($(ele).width() + 'px');
 
11131                 $(thisPH).height($(ele).height() + 'px');
 
11132                 $(ele).addClass('loaded');
 
11134         makeImageZoomable: function(imageTag) {
 
11135                 if (this.options.imageZoom.value) {
 
11136                         imageTag.addEventListener('mousedown', modules['showImages'].mousedownImage, false);
 
11137                         imageTag.addEventListener('mouseup', modules['showImages'].dragImage, false);
 
11138                         imageTag.addEventListener('mousemove', modules['showImages'].dragImage, false);
 
11139                         imageTag.addEventListener('mouseout', modules['showImages'].mouseoutImage, false);
 
11140                         imageTag.addEventListener('click', modules['showImages'].clickImage, false);
 
11143         mousedownImage: function(e) {
 
11144                 if (e.button === 0) {
 
11145                         if (!e.target.minWidth) e.target.minWidth = Math.max(1, Math.min(e.target.width, 100));
 
11146                         modules['showImages'].dragTargetData.imageWidth = e.target.width;
 
11147                         modules['showImages'].dragTargetData.diagonal = modules['showImages'].getDragSize(e);
 
11148                         modules['showImages'].dragTargetData.dragging = false;
 
11149                         modules['showImages'].dragTargetData.hasChangedWidth = false;
 
11150                         e.preventDefault();
 
11153         mouseoutImage: function(e) {
 
11154                 modules['showImages'].dragTargetData.diagonal = 0;
 
11156         dragImage: function(e) {
 
11157                 if (modules['showImages'].dragTargetData.diagonal) {
 
11158                         var newDiagonal = modules['showImages'].getDragSize(e),
 
11159                                 oldDiagonal = modules['showImages'].dragTargetData.diagonal,
 
11160                                 imageWidth = modules['showImages'].dragTargetData.imageWidth,
 
11161                                 maxWidth = Math.max(e.target.minWidth, newDiagonal/oldDiagonal*imageWidth);
 
11163                         modules['showImages'].resizeImage(e.target, maxWidth);
 
11164                         modules['showImages'].dragTargetData.dragging = true;
 
11166                 modules['showImages'].handleSRStyleToggleVisibility(e.target);
 
11167                 if (e.type === 'mouseup') {
 
11168                         modules['showImages'].dragTargetData.diagonal = 0;
 
11171         clickImage: function(e) {
 
11172                 modules['showImages'].dragTargetData.diagonal = 0;
 
11173                 if (modules['showImages'].dragTargetData.hasChangedWidth) {
 
11174                         modules['showImages'].dragTargetData.dragging = false;
 
11175                         e.preventDefault();
 
11178                 modules['showImages'].dragTargetData.hasChangedWidth = false;
 
11180         resizeImage: function(image, newWidth) {
 
11181                 var currWidth = $(image).width();
 
11182                 if (newWidth !== currWidth) {
 
11183                         modules['showImages'].dragTargetData.hasChangedWidth = true;
 
11185                         image.style.width=newWidth + 'px';
 
11186                         image.style.maxWidth=newWidth + 'px';
 
11187                         image.style.maxHeight='';
 
11188                         image.style.height='auto';
 
11190                         var thisPH = $(image).data('imagePlaceholder');
 
11191                         $(thisPH).width($(image).width() + 'px');
 
11192                         $(thisPH).height($(image).height() + 'px');
 
11197                         acceptRegex: /^[^#]+?\.(gif|jpe?g|png)(?:[?&#_].*|$)/i,
 
11198                         rejectRegex: /(wikipedia\.org\/wiki|photobucket\.com|gifsound\.com|\/wiki\/File:.*)/i,
 
11200                         detect: function(elem) {
 
11201                                 var siteMod = modules['showImages'].siteModules['default'];
 
11202                                 var href = elem.href;
 
11203                                 return (siteMod.acceptRegex.test(href) && !siteMod.rejectRegex.test(href));
 
11205                         handleLink: function(elem) {
 
11206                                 var def = $.Deferred();
 
11207                                 var href = elem.href;
 
11209                                 def.resolve(elem, {
 
11213                                 return def.promise()
 
11215                         handleInfo: function(elem, info) {
 
11216                                 var def = $.Deferred();
 
11218                                 elem.type = info.type;
 
11219                                 elem.src = info.src;
 
11220                                 elem.href = info.src;
 
11222                                 if (RESUtils.pageType() === 'linklist') {
 
11223                                         $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
 
11227                                 return def.promise();
 
11231                         APIKey: 'fe266bc9466fe69aa1cf0904e7298eda',
 
11232                         // hashRe: /^https?:\/\/(?:i\.|edge\.|www\.)*imgur\.com\/(?:r\/[\w]+\/)?([\w]{5,}(?:[&,][\w]{5,})?)(\..+)?(?:#(\d*))?$/i,
 
11233                         // the modified regex below fixes detection of "edited" imgur images, but imgur's edited images are broken right now actually, falling into
 
11234                         // a redirect loop.  preserving the old one just in case.  however it also fixes detection of the extension (.jpg, for example) which
 
11235                         // was too greedy a search...
 
11236                         // the hashRe below was provided directly by MrGrim (well, everything after the domain was), using that now.
 
11237                         hashRe: /^https?:\/\/(?:i\.|m\.|edge\.|www\.)*imgur\.com\/(?!gallery)(?!removalrequest)(?!random)(?!memegen)([A-Za-z0-9]{5}|[A-Za-z0-9]{7})[sbtmlh]?(\.(?:jpe?g|gif|png))?(\?.*)?$/i,
 
11238                         albumHashRe: /^https?:\/\/(?:i\.|m\.)?imgur\.com\/(?:a|gallery)\/([\w]+)(\..+)?(?:\/)?(?:#\w*)?$/i,
 
11239                         apiPrefix: 'http://api.imgur.com/2/',
 
11242                         detect: function(elem) {
 
11243                                 return elem.href.toLowerCase().indexOf('imgur.com/') !== -1;
 
11245                         handleLink: function(elem) {
 
11246                                 var siteMod = modules['showImages'].siteModules['imgur'];
 
11247                                 var def = $.Deferred();
 
11248                                 var href = elem.href.split('?')[0];
 
11249                                 var groups = siteMod.hashRe.exec(href);
 
11250                                 if (!groups) var albumGroups = siteMod.albumHashRe.exec(href);
 
11251                                 if (groups && !groups[2]) {
 
11252                                         if (groups[1].search(/[&,]/) > -1) {
 
11253                                                 var hashes = groups[1].split(/[&,]/);
 
11254                                                 def.resolve(elem, {
 
11255                                                         album: {images: hashes.map(function(hash) {
 
11257                                                                         image: {title: '', caption: '', hash: hash},
 
11258                                                                         links: {original: 'http://i.imgur.com/'+hash+'.jpg'}
 
11263                                                 // removed caption API calls as they don't seem to exist/matter for single images, only albums...
 
11264                                                 //If we don't show captions, then we can skip the API call.
 
11265                                                 def.resolve(elem, {image: {
 
11267                                                                 //Imgur doesn't really care about the extension and the browsers don't seem to either.
 
11268                                                                 original: 'http://i.imgur.com/'+groups[1]+'.jpg'
 
11272                                 } else if (albumGroups && !albumGroups[2]) {
 
11273                                         var apiURL = siteMod.apiPrefix + 'album/' + albumGroups[1] + '.json';
 
11274                                         elem.imgHash = albumGroups[1];
 
11275                                         if (apiURL in siteMod.calls) {
 
11276                                                 if (siteMod.calls[apiURL] != null) {
 
11277                                                         def.resolve(elem, siteMod.calls[apiURL]);
 
11282                                                 GM_xmlhttpRequest({
 
11285                                                         // aggressiveCache: true,
 
11286                                                         onload: function(response) {
 
11288                                                                         var json = JSON.parse(response.responseText);
 
11289                                                                         siteMod.calls[apiURL] = json;
 
11290                                                                         def.resolve(elem, json);
 
11292                                                                         siteMod.calls[apiURL] = null;
 
11296                                                         onerror: function(response) {
 
11304                                 return def.promise();
 
11306                         handleInfo: function(elem, info) {
 
11307                                 if ('image' in info) {
 
11308                                         return modules['showImages'].siteModules['imgur'].handleSingleImage(elem, info);
 
11309                                 } else if ('album' in info) {
 
11310                                         return modules['showImages'].siteModules['imgur'].handleGallery(elem, info);
 
11311                                 } else if (info.error && info.error.message === 'Album not found') {
 
11312                                         // This case comes up when there is an imgur.com/gallery/HASH link that
 
11313                                         // links to an image, not an album (not to be confused with the word "gallery", ugh)
 
11317                                                                 original: 'http://i.imgur.com/' + elem.imgHash + '.jpg'
 
11322                                         return modules['showImages'].siteModules['imgur'].handleSingleImage(elem, info);
 
11324                                         return $.Deferred().reject().promise();
 
11325                                         // console.log("ERROR", info);
 
11326                                         // console.log(arguments.callee.caller);
 
11329                         handleSingleImage: function(elem, info) {
 
11330                                 elem.src = info.image.links.original;
 
11331                                 elem.href = info.image.links.original;
 
11332                                 if (RESUtils.pageType() === 'linklist') {
 
11333                                         $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
11335                                 elem.type = 'IMAGE';
 
11336                                 if (info.image.image.caption) elem.caption = info.image.image.caption;
 
11337                                 return $.Deferred().resolve(elem).promise();
 
11339                         handleGallery: function(elem, info) {
 
11340                                 var base = elem.href.split('#')[0];
 
11341                                 elem.src = info.album.images.map(function(e, i, a) {
 
11343                                                 title: e.image.title,
 
11344                                                 src: e.links.original,
 
11345                                                 href: base + '#' + e.image.hash,
 
11346                                                 caption: e.image.caption
 
11350                                         var hash = elem.hash.slice(1);
 
11352                                                 for (var i = 0; i < elem.src.length; i++) {
 
11353                                                         if (hash == info.album.images[i].image.hash) {
 
11354                                                                 elem.galleryStart = i;
 
11359                                                 elem.galleryStart = parseInt(hash, 10);
 
11362                                 elem.imageTitle = info.album.title;
 
11363                                 elem.caption  = info.album.description;
 
11364                                 elem.type = 'GALLERY';
 
11365                                 return $.Deferred().resolve(elem).promise();
 
11372                         detect: function(elem) {
 
11373                                 var href = elem.href.toLowerCase();
 
11374                                 return href.indexOf('gfycat.com') !== -1 && href.substring(-1) !== '+';
 
11376                         handleLink: function(elem) {
 
11377                                 var hashRe = /^http:\/\/[a-zA-Z0-9\-\.]*gfycat\.com\/(\w+)\.?/i;
 
11378                                 var def = $.Deferred();
 
11379                                 var groups = hashRe.exec(elem.href);
 
11381                                 if (!groups) return def.reject();
 
11382                                 var href = elem.href.toLowerCase();
 
11383                                 var hotLink = false;
 
11384                                 if(href.indexOf('giant.gfycat') !== -1 ||
 
11385                                         href.indexOf('fat.gfycat') !== -1 ||
 
11386                                         href.indexOf('zippy.gfycat') !== -1)
 
11389                                 var siteMod = modules['showImages'].siteModules['gfycat'];
 
11390                                 var apiURL = 'http://gfycat.com/cajax/get/' + groups[1];
 
11392                                 if (apiURL in siteMod.calls) {
 
11393                                         if (siteMod.calls[apiURL] != null) {
 
11394                                                 def.resolve(elem, siteMod.calls[apiURL]);
 
11396                                                 siteMod.calls[apiURL] = null;
 
11400                                         GM_xmlhttpRequest({
 
11403                                                 aggressiveCache: true,
 
11404                                                 onload: function(response) {
 
11406                                                                 var json = JSON.parse(response.responseText);
 
11407                                                                 json.gfyItem.src = elem.href;
 
11408                                                                 json.gfyItem.hotLink = hotLink
 
11409                                                                 siteMod.calls[apiURL] = json;
 
11410                                                                 def.resolve(elem, json.gfyItem);
 
11412                                                                 siteMod.calls[apiURL] = null;
 
11416                                                 onerror: function(response) {
 
11421                                 return def.promise();
 
11423                         handleInfo: function(elem, info) {
 
11424                                 function humanSize(bytes) {
 
11425                                         var byteUnits = [' kB', ' MB'];
 
11426                                         for(var i=-1; bytes > 1024; i++) {
 
11427                                                 bytes = bytes / 1024;
 
11429                                         return Math.max(bytes, 0.1).toFixed(1) + byteUnits[i];
 
11433                                         elem.src = info.src;
 
11434                                         elem.imageTitle = humanSize(info.gifSize);
 
11435                                         if(((info.gifSize   > 524288 && (info.gifSize / info.mp4Size) > 5)
 
11436                                                 || (info.gifSize    > 1048576 && (info.gifSize / info.mp4Size) > 2))
 
11437                                                 && document.createElement('video').canPlayType)
 
11438                                                 elem.imageTitle += ' (' + humanSize(info.mp4Size)  + " for <a href='http://gfycat.com/" + info.gfyName + "'>HTML5 version.</a>)";
 
11440                                         if (RESUtils.pageType() === 'linklist') {
 
11441                                                 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
11443                                         return $.Deferred().resolve(elem).promise();
 
11446                                 elem.mediaOptions = {
 
11452                                 sources[0] = {'file': info.mp4Url,'type':'video/mp4'};
 
11453                                 sources[1] = {'file': info.webmUrl,'type':'video/webm'};
 
11454                                 elem.type = 'VIDEO';        
 
11455                                 $(elem).data('sources', sources);
 
11457                                 if (RESUtils.pageType() === 'linklist') {
 
11458                                         $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
11461                                 return $.Deferred().resolve(elem).promise();
 
11466                         detect: function(elem) {
 
11467                                 var href = elem.href.toLowerCase();
 
11468                                 return href.indexOf('eho.st') !== -1 && href.substring(-1) !== '+';
 
11470                         handleLink: function(elem) {
 
11471                                 var hashRe = /^http:\/\/(?:i\.)?(?:\d+\.)?eho\.st\/(\w+)\/?/i;
 
11472                                 var def = $.Deferred();
 
11473                                 var groups = hashRe.exec(elem.href);
 
11475                                         def.resolve(elem, {
 
11476                                                 src: 'http://i.eho.st/'+groups[1]+'.jpg'
 
11481                                 return def.promise();
 
11483                         handleInfo: function(elem, info) {
 
11484                                 elem.type = 'IMAGE';
 
11485                                 elem.src = info.src;
 
11486                                 elem.href = info.src;
 
11487                                 if (RESUtils.pageType() === 'linklist') {
 
11488                                         $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
11490                                 elem.onerror = function() {
 
11491                                     if (this.src.match(/\.jpg/)) {
 
11492                                         this.src = this.src.slice(0, elem.src.length - 3) + 'png';
 
11493                                     } else if (this.src.match(/\.png/)) {
 
11494                                         this.src = this.src.slice(0, elem.src.length - 3) + 'gif';
 
11497                                 return $.Deferred().resolve(elem).promise();
 
11502                         detect: function(elem) {
 
11503                                 var href = elem.href.toLowerCase();
 
11504                                 return href.indexOf('picsarus.com') !== -1 && href.substring(-1) !== '+';
 
11506                         handleLink: function(elem) {
 
11507                                 var hashRe = /^https?:\/\/(?:i\.|edge\.|www\.)*picsarus\.com\/(?:r\/[\w]+\/)?([\w]{6,})(\..+)?$/i;
 
11508                                 var def = $.Deferred();
 
11509                                 var groups = hashRe.exec(elem.href);
 
11511                                         def.resolve(elem, {
 
11512                                                 src: 'http://www.picsarus.com/'+groups[1]+'.jpg'
 
11517                                 return def.promise();
 
11519                         handleInfo: function(elem, info) {
 
11520                                 elem.type = 'IMAGE';
 
11521                                 elem.src = info.src;
 
11522                                 elem.href = info.src;
 
11523                                 if (RESUtils.pageType() === 'linklist') {
 
11524                                         $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
11526                                 return $.Deferred().resolve(elem).promise();
 
11531                         detect: function(elem) {
 
11532                                 return elem.href.toLowerCase().indexOf('snag.gy/') !== -1;
 
11534                         handleLink: function(elem) {
 
11535                                 var def = $.Deferred();
 
11536                                 var href = elem.href;
 
11537                                 var extensions = ['.jpg','.png','.gif'];
 
11538                                 if (href.indexOf('i.snag') === -1) href = href.replace('snag.gy', 'i.snag.gy');
 
11539                                 if (extensions.indexOf(href.substr(-4)) === -1) href = href+'.jpg';
 
11540                                 def.resolve(elem, {src: href});
 
11541                                 return def.promise();
 
11543                         handleInfo: function(elem, info) {
 
11544                                 elem.type = 'IMAGE';
 
11545                                 elem.src = info.src;
 
11546                                 elem.href = info.src;
 
11547                                 if (RESUtils.pageType() === 'linklist') {
 
11548                                         $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
11550                                 return $.Deferred().resolve(elem).promise();
 
11555                         detect: function(elem) {
 
11556                                 var href = elem.href.toLowerCase();
 
11557                                 return href.indexOf('picshd.com/') !== -1;
 
11559                         handleLink: function(elem) {
 
11560                                 var def = $.Deferred();
 
11561                                 var hashRe = /^https?:\/\/(?:i\.|edge\.|www\.)*picshd\.com\/([\w]{5,})(\..+)?$/i;
 
11562                                 var groups = hashRe.exec(elem.href);
 
11564                                         def.resolve(elem, 'http://i.picshd.com/'+groups[1]+'.jpg');
 
11568                                 return def.promise();
 
11570                         handleInfo: function(elem, info) {
 
11571                                 elem.type = 'IMAGE';
 
11574                                 if (RESUtils.pageType() === 'linklist') {
 
11575                                         $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
11577                                 return $.Deferred().resolve(elem).promise();
 
11583                         detect: function(elem) {
 
11584                                 var href = elem.href.toLowerCase();
 
11585                                 return href.indexOf('min.us') !== -1 && href.indexOf('blog.') === -1;
 
11587                         handleLink: function(elem) {
 
11588                                 var def = $.Deferred();
 
11589                                 var hashRe = /^http:\/\/min\.us\/([\w]+)(?:#[\d+])?$/i;
 
11590                                 var href = elem.href.split('?')[0];
 
11591                                 //TODO: just make default run first and remove this
 
11592                                 var getExt = href.split('.');
 
11593                                 var ext = (getExt.length > 1?getExt[getExt.length - 1].toLowerCase():'');
 
11594                                 if (['jpg', 'jpeg', 'png', 'gif'].indexOf(ext) !== -1) {
 
11595                                         var groups = hashRe.exec(href);
 
11596                                         if (groups && !groups[2]) {
 
11597                                                 var hash = groups[1];
 
11598                                                 if (hash.substr(0, 1) === 'm') {
 
11599                                                         var apiURL = 'http://min.us/api/GetItems/' + hash;
 
11600                                                         var calls = modules['showImages'].siteModules['minus'].calls;
 
11601                                                         if (apiURL in calls) {
 
11602                                                                 if (calls[apiURL] != null) {
 
11603                                                                         def.resolve(elem, calls[apiURL]);
 
11608                                                                 GM_xmlhttpRequest({
 
11611                                                                         onload: function(response) {
 
11613                                                                                         var json = JSON.parse(response.responseText);
 
11614                                                                                         modules['showImages'].siteModules['minus'].calls[apiURL] = json;
 
11615                                                                                         def.resolve(elem, json);
 
11617                                                                                         modules['showImages'].siteModules['minus'].calls[apiURL] = null;
 
11621                                                                         onerror: function(response) {
 
11626                                                 } else { // if not 'm', not a gallery, we can't do anything with the API.
 
11635                                 return def.promise();
 
11637                         handleInfo: function(elem, info) {
 
11638                                 var def = $.Deferred();
 
11639                                 //TODO: Handle titles
 
11640                                 //TODO: Handle possibility of flash items
 
11641                                 if ('ITEMS_GALLERY' in info) {
 
11642                                         if (info.ITEMS_GALLERY.length > 1) {
 
11643                                                 elem.type = 'GALLERY';
 
11645                                                         src: info.ITEMS_GALLERY
 
11648                                                 elem.type = 'IMAGE';
 
11649                                                 elem.href = info.ITEMS_GALLERY[0];
 
11650                                                 if (RESUtils.pageType() === 'linklist') {
 
11651                                                         $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
11653                                                 elem.src = info.ITEMS_GALLERY[0];
 
11659                                 return def.promise()
 
11664                         detect: function(elem) {
 
11665                                 var hashRe = /^http:\/\/(?:\w+)\.?flickr\.com\/(?:.*)\/([\d]{10})\/?(?:.*)?$/i;
 
11666                                 var href = elem.href;
 
11667                                 return hashRe.test(href);
 
11669                         handleLink: function(elem) {
 
11670                                 var def = $.Deferred();
 
11671                                 // modules['showImages'].createImageExpando(elem);
 
11672                                 // var selector = '#allsizes-photo > IMG';
 
11673                                 var href = elem.href;
 
11674                                 if (href.indexOf('/sizes') === -1) {
 
11675                                         var inPosition = href.indexOf('/in/');
 
11676                                         var inFragment = '';
 
11677                                         if (inPosition !== -1) {
 
11678                                                 inFragment = href.substring(inPosition);
 
11679                                                 href = href.substring(0, inPosition);
 
11682                                         href += '/sizes/c' + inFragment;
 
11684                                 href = href.replace('/lightbox', '');
 
11685                                 href = 'http://www.flickr.com/services/oembed/?format=json&url=' + href;
 
11686                                 GM_xmlhttpRequest({
 
11689                                         onload: function(response) {
 
11691                                                         var json = JSON.parse(response.responseText);
 
11692                                                         def.resolve(elem, json);
 
11697                                         onerror: function(response) {
 
11701                                 return def.promise();
 
11703                         handleInfo: function(elem, info) {
 
11704                                 var def = $.Deferred();
 
11705                                 var imgRe = /\.(jpg|jpeg|gif|png)/i;
 
11706                                 if ('url' in info) {
 
11707                                         elem.imageTitle = info.title;
 
11708                                         var original_url = elem.href;
 
11709                                         if(imgRe.test(info.url)) {
 
11710                                                 elem.src = info.url;
 
11711                                                 // elem.href = info.url;
 
11713                                                 elem.src = info.thumbnail_url;
 
11714                                                 // elem.href = info.thumbnail_url;
 
11716                                         if (RESUtils.pageType() === 'linklist') {
 
11717                                                 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
11719                                         elem.credits = 'Picture by: <a href="'+info.author_url+'">'+info.author_name+'</a> @ Flickr';
 
11720                                         elem.type = 'IMAGE';
 
11725                                 return def.promise();
 
11730                         detect: function(elem) {
 
11731                                 return elem.href.toLowerCase().indexOf('cloud.steampowered.com') !== -1;
 
11733                         handleLink: function(elem) {
 
11734                                 return $.Deferred().resolve(elem, elem.href).promise();
 
11736                         handleInfo: function(elem, info) {
 
11737                                 elem.type = 'IMAGE';
 
11740                                 if (RESUtils.pageType() === 'linklist') {
 
11741                                         $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
 
11743                                 return $.Deferred().resolve(elem).promise();
 
11748                         matchRe: /^http:\/\/(?:fav\.me\/.*|(?:.+\.)?deviantart\.com\/(?:art\/.*|[^#]*#\/d.*))$/i,
 
11750                         detect: function(elem) {
 
11751                                 return modules['showImages'].siteModules['deviantart'].matchRe.test(elem.href);
 
11753                         handleLink: function(elem) {
 
11754                                 var def = $.Deferred();
 
11755                                 var siteMod = modules['showImages'].siteModules['deviantart'];
 
11756                                 var apiURL = 'http://backend.deviantart.com/oembed?url=' + encodeURIComponent(elem.href);
 
11757                                 if (apiURL in siteMod.calls) {
 
11758                                         if (siteMod.calls[apiURL] != null) {
 
11759                                                 def.resolve(elem, siteMod.calls[apiURL]);
 
11764                                         GM_xmlhttpRequest({
 
11767                                                 // aggressiveCache: true,
 
11768                                                 onload: function(response) {
 
11770                                                                 var json = JSON.parse(response.responseText);
 
11771                                                                 siteMod.calls[apiURL] = json;
 
11772                                                                 def.resolve(elem, json);
 
11774                                                                 siteMod.calls[apiURL] = null;
 
11778                                                 onerror: function(response) {
 
11783                                 return def.promise();
 
11785                         handleInfo: function(elem, info) {
 
11786                                 var def = $.Deferred();
 
11787                                 if ('url' in info) {
 
11788                                         elem.imageTitle = info.title;
 
11789                                         var original_url = elem.href;
 
11790                                         if(['jpg', 'jpeg', 'png', 'gif'].indexOf(info.url) !== -1) {
 
11791                                                 elem.src = info.url;
 
11792                                                 // elem.href = info.url;
 
11794                                                 elem.src = info.thumbnail_url;
 
11795                                                 // elem.href = info.thumbnail_url;
 
11797                                         if (RESUtils.pageType() === 'linklist') {
 
11798                                                 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
11800                                         // elem.credits = 'Original link: <a href="'+original_url+'">'+original_url+'</a><br>Art by: <a href="'+info.author_url+'">'+info.author_name+'</a> @ deviantART';
 
11801                                         elem.credits = 'Art by: <a href="'+info.author_url+'">'+info.author_name+'</a> @ deviantART';
 
11802                                         elem.type = 'IMAGE';
 
11807                                 return def.promise();
 
11812                         APIKey: 'WeJQquHCAasi5EzaN9jMtIZkYzGfESUtEvcYDeSMLICveo3XDq',
 
11813                         matchRE: /^https?:\/\/([a-z0-9\-]+\.tumblr\.com)\/post\/(\d+)(?:\/.*)?$/i,
 
11814                         go: function() { },
 
11815                         detect: function(elem) {
 
11816                                 return modules['showImages'].siteModules['tumblr'].matchRE.test(elem.href);
 
11818                         handleLink: function(elem) {
 
11819                                 var def = $.Deferred();
 
11820                                 var siteMod = modules['showImages'].siteModules['tumblr'];
 
11821                                 var groups = siteMod.matchRE.exec(elem.href);
 
11823                                         var apiURL = 'http://api.tumblr.com/v2/blog/'+groups[1]+'/posts?api_key='+siteMod.APIKey+'&id='+groups[2] + '&filter=raw';
 
11824                                         if (apiURL in siteMod.calls) {
 
11825                                                 if (siteMod.calls[apiURL] != null) {
 
11826                                                         def.resolve(elem, siteMod.calls[apiURL]);
 
11831                                                 GM_xmlhttpRequest({
 
11834                                                         // aggressiveCache: true,
 
11835                                                         onload: function(response) {
 
11837                                                                         var json = JSON.parse(response.responseText);
 
11838                                                                         if ('meta' in json && json.meta.status === 200) {
 
11839                                                                                 siteMod.calls[apiURL] = json;
 
11840                                                                                 def.resolve(elem, json);
 
11842                                                                                 siteMod.calls[apiURL] = null;
 
11846                                                                         siteMod.calls[apiURL] = null;
 
11850                                                         onerror: function(response) {
 
11858                                 return def.promise();
 
11860                         handleInfo: function(elem, info) {
 
11861                                 var def = $.Deferred();
 
11862                                 var original_url = elem.href;
 
11863                                 var post = info.response.posts[0];
 
11864                                 switch (post.type) {
 
11866                                                 if (post.photos.length > 1) {
 
11867                                                         elem.type = 'GALLERY';
 
11868                                                         elem.src = post.photos.map(function(e) {
 
11870                                                                         src: e.original_size.url,
 
11875                                                         elem.type = "IMAGE";
 
11876                                                         elem.src = post.photos[0].original_size.url;
 
11880                                                 elem.type = 'TEXT';
 
11881                                                 elem.imageTitle = post.title;
 
11882                                                 if (post.format === 'markdown') {
 
11883                                                         elem.src = modules['commentPreview'].converter.render(post.body)
 
11884                                                 } else if (post.format === 'html') {
 
11885                                                         elem.src = post.body;
 
11889                                                 return def.reject().promise();
 
11892                                 elem.caption = post.caption;
 
11893                                 if (RESUtils.pageType() === 'linklist') {
 
11894                                         $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
11896                                 elem.credits = 'Posted by: <a href="'+info.response.blog.url+'">'+info.response.blog.name+'</a> @ Tumblr';
 
11898                                 return def.promise()
 
11903                         detect: function(elem) {
 
11904                                 return elem.href.toLowerCase().indexOf('memecrunch.com') !== -1;
 
11906                         handleLink: function(elem) {
 
11907                                 var def = $.Deferred();
 
11908                                 var hashRe = /^http:\/\/memecrunch\.com\/meme\/([0-9A-Z]+)\/([\w\-]+)(\/image\.(png|jpg))?/i;
 
11909                                 var groups = hashRe.exec(elem.href);
 
11910                                 if (groups && typeof groups[1] !== 'undefined') {
 
11911                                         def.resolve(elem, 'http://memecrunch.com/meme/'+groups[1]+'/'+(groups[2]||'null')+'/image.png');
 
11915                                 return def.promise();
 
11917                         handleInfo: function(elem, info) {
 
11918                                 elem.type = 'IMAGE';
 
11921                                 if (RESUtils.pageType() === 'linklist') {
 
11922                                         $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
11924                                 modules['showImages'].createImageExpando(elem);
 
11930                         detect: function(elem) {
 
11931                                 return elem.href.toLowerCase().indexOf('mediacru.sh') !== -1;
 
11933                         handleLink: function(elem) {
 
11934                                 var hashRe = /^https?:\/\/(?:www\.)?mediacru\.sh\/([\w-]+)$/i;
 
11935                                 var def = $.Deferred();
 
11936                                 var groups = hashRe.exec(elem.href);
 
11937                                 if (!groups) return def.reject();
 
11938                                 var siteMod = modules['showImages'].siteModules['mediacrush'];
 
11939                                 var apiURL = 'https://mediacru.sh/' + encodeURIComponent(groups[1]) + '.json';
 
11940                                 if (apiURL in siteMod.calls) {
 
11941                                         if (siteMod.calls[apiURL] != null) {
 
11942                                                 def.resolve(elem, siteMod.calls[apiURL]);
 
11944                                                 siteMod.calls[apiURL] = null;
 
11948                                         GM_xmlhttpRequest({
 
11951                                                 // aggressiveCache: true,
 
11952                                                 onload: function(response) {
 
11954                                                                 var json = JSON.parse(response.responseText);
 
11955                                                                 siteMod.calls[apiURL] = json;
 
11956                                                                 def.resolve(elem, json);
 
11958                                                                 siteMod.calls[apiURL] = null;
 
11962                                                 onerror: function(response) {
 
11967                                 return def.promise();
 
11969                         handleInfo: function(elem, info) {
 
11970                                 var def = $.Deferred()
 
11971                                 // check files to see if video or image
 
11972                                 elem.type = 'IMAGE';
 
11974                                         src: 'http://mediacru.sh/'+info.original,
 
11977                                 if (info.original.indexOf('.gif') !== -1) {
 
11978                                         elem.mediaOptions = {
 
11984                                 for (var i=0, len=info.files.length; i<len; i++) {
 
11985                                         if (info.files[i].type.indexOf('video') !== -1) {
 
11986                                                 elem.type = 'VIDEO';
 
11987                                                 info.files[i].file = 'http://mediacru.sh/' + info.files[i].file;
 
11988                                                 mediaData.sources.push(info.files[i]);
 
11990                                         if (info.files[i].type.indexOf('audio') !== -1) {
 
11991                                                 elem.type = 'AUDIO';
 
11992                                                 info.files[i].file = 'http://mediacru.sh/' + info.files[i].file;
 
11993                                                 mediaData.sources.push(info.files[i]);
 
11996                                 if (elem.type === 'IMAGE') {
 
11997                                         elem.src = mediaData.src;
 
11999                                 $(elem).data('sources', mediaData.sources);
 
12000                                 return $.Deferred().resolve(elem).promise();
 
12007                         urlMod: function(url) {
 
12008                                 return url+'/embed/postcard';
 
12011                         detect: function(elem) {
 
12012                                 return elem.href.toLowerCase().indexOf('vine.co') !== -1;
 
12014                         handleLink: function(elem) {
 
12015                                 var hashRe = /^https?:\/\/(?:www\.)?vine\.co\/v\/(\w{10,12})$/i;
 
12016                                 var def = $.Deferred();
 
12017                                 var groups = hashRe.exec(elem.href);
 
12019                                         def.resolve(elem, {
 
12025                                 return def.promise();
 
12027                         handleInfo: function(elem, info) {
 
12028                                 elem.type = 'NOEMBED';
 
12029                                 elem.src = info.src;
 
12030                                 elem.expandoClass = 'collapsed video';
 
12031                                 elem.siteMod = modules['showImages'].siteModules['vine'];
 
12032                                 return $.Deferred().resolve(elem).promise();
 
12036                         go: function() { },
 
12037                         detect: function(elem) {
 
12038                                 return elem.href.toLowerCase().indexOf('livememe.com') !== -1;
 
12040                         handleLink: function(elem) {
 
12041                                 var def = $.Deferred();
 
12042                                 var hashRe = /^http:\/\/(?:www\.livememe\.com|lvme\.me)\/(?!edit)([\w]+)\/?/i;
 
12043                                 var groups = hashRe.exec(elem.href);
 
12045                                         def.resolve(elem, 'http://www.livememe.com/'+groups[1]+'.jpg');
 
12049                                 return def.promise();
 
12051                         handleInfo: function(elem, info) {
 
12052                                 elem.type = 'IMAGE';
 
12055                                 if (RESUtils.pageType() === 'linklist') {
 
12056                                         $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
 
12058                                 return $.Deferred().resolve(elem).promise();
 
12063                         detect: function(elem) {
 
12064                                 return elem.href.toLowerCase().indexOf('makeameme.org') !== -1;
 
12066                         handleLink: function(elem) {
 
12067                                 var def = $.Deferred()
 
12068                                 var hashRe = /^http:\/\/makeameme\.org\/meme\/([\w-]+)\/?/i;
 
12069                                 var groups = hashRe.exec(elem.href);
 
12071                                         def.resolve(elem, 'http://makeameme.org/media/created/'+groups[1]+'.jpg');
 
12075                                 return def.promise();
 
12077                         handleInfo: function(elem, info) {
 
12078                                 elem.type = 'IMAGE';
 
12081                                 if (RESUtils.pageType() === 'linklist') {
 
12082                                         $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
 
12084                                 return $.Deferred().resolve(elem).promise();
 
12089                         detect: function(elem) {
 
12090                                 return elem.href.toLowerCase().indexOf('memefive.com') !== -1;
 
12092                         handleLink: function(elem) {
 
12093                                 var def = $.Deferred()
 
12094                                 var hashRe = /^http:\/\/(?:www\.)?(?:memefive\.com)\/meme\/([\w]+)\/?/i;
 
12095                                 var altHashRe = /^http:\/\/(?:www\.)?(?:memefive\.com)\/([\w]+)\/?/i;
 
12096                                 var groups = hashRe.exec(elem.href);
 
12098                                         groups = altHashRe.exec(elem.href);
 
12101                                         def.resolve(elem, 'http://memefive.com/memes/'+groups[1]+'.jpg');
 
12105                                 return def.promise();
 
12107                         handleInfo: function(elem, info) {
 
12108                                 elem.type = 'IMAGE';
 
12111                                 if (RESUtils.pageType() === 'linklist') {
 
12112                                         $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
 
12114                                 return $.Deferred().resolve(elem).promise();
 
12118                         go: function() { },
 
12119                         detect: function(elem) {
 
12120                                 return elem.href.toLowerCase().indexOf('.memegen.') !== -1;
 
12122                         handleLink: function(elem) {
 
12123                                 var def = $.Deferred();
 
12124                                 var hashRe = /^http:\/\/((?:www|ar|ru|id|el|pt|tr)\.memegen\.(?:com|de|nl|fr|it|es|se|pl))(\/a)?\/(?:meme|mem|mim)\/([A-Za-z0-9]+)\/?/i;
 
12125                                 var groups = hashRe.exec(elem.href);
 
12127                                         // Animated vs static meme images.
 
12129                                                 def.resolve(elem, 'http://a.memegen.com/' + groups[3] + '.gif');
 
12131                                                 def.resolve(elem, 'http://m.memegen.com/' + groups[3] + '.jpg');
 
12136                                 return def.promise();
 
12138                         handleInfo: function(elem, info) {
 
12139                                 elem.type = 'IMAGE';
 
12142                                 if (RESUtils.pageType() === 'linklist') {
 
12143                                         $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
 
12145                                 return $.Deferred().resolve(elem).promise();
 
12151 modules['showKarma'] = {
 
12152         moduleID: 'showKarma',
 
12153         moduleName: 'Show Comment Karma',
 
12154         category: 'Accounts',
 
12159                         description: 'Separator character between post/comment karma'
 
12164                         description: 'Use commas for large karma numbers'
 
12167         description: 'Shows your comment karma next to your link karma.',
 
12168         isEnabled: function() {
 
12169                 return RESConsole.getModulePrefs(this.moduleID);
 
12172                 /^https?:\/\/([a-z]+)\.reddit\.com\/.*/i
 
12174         isMatchURL: function() {
 
12175                 return RESUtils.isMatchURL(this.moduleID);
 
12178                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
12179                         if (RESUtils.loggedInUser()) {
 
12180                                 RESUtils.getUserInfo(modules['showKarma'].updateKarmaDiv);
 
12184         updateKarmaDiv: function(userInfo) {
 
12185                 var karmaDiv = document.querySelector("#header-bottom-right .userkarma");
 
12186                 if ((typeof karmaDiv !== 'undefined') && (karmaDiv !== null)) {
 
12187                         var linkKarma = karmaDiv.innerHTML;
 
12188                         karmaDiv.title = '';
 
12189                         var commentKarma = userInfo.data.comment_karma;
 
12190                         if (modules['showKarma'].options.useCommas.value) {
 
12191                                 linkKarma = RESUtils.addCommas(linkKarma);
 
12192                                 commentKarma = RESUtils.addCommas(commentKarma);
 
12194                         $(karmaDiv).html("<a title=\"link karma\" href=\"/user/" + RESUtils.loggedInUser() + "/submitted/\">" + linkKarma + "</a> " + modules['showKarma'].options.separator.value + " <a title=\"comment karma\" href=\"/user/" + RESUtils.loggedInUser() + "/comments/\">" + commentKarma + "</a>");
 
12199 modules['hideChildComments'] = {
 
12200         moduleID: 'hideChildComments',
 
12201         moduleName: 'Hide All Child Comments',
 
12202         category: 'Comments',
 
12204                 // any configurable options you have go here...
 
12205                 // options must have a type and a value.. 
 
12206                 // valid types are: text, boolean (if boolean, value must be true or false)
 
12211                         description: 'Automatically hide all but parent comments, or provide a link to hide them all?'
 
12214         description: 'Allows you to hide all comments except for replies to the OP for easier reading.',
 
12215         isEnabled: function() {
 
12216                 return RESConsole.getModulePrefs(this.moduleID);
 
12219                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
 
12220                 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
 
12222         isMatchURL: function() {
 
12223                 return RESUtils.isMatchURL(this.moduleID);
 
12226                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
12227                         var toggleButton = document.createElement('li');
 
12228                         this.toggleAllLink = document.createElement('a');
 
12229                         this.toggleAllLink.textContent = 'hide all child comments';
 
12230                         this.toggleAllLink.setAttribute('action','hide');
 
12231                         this.toggleAllLink.setAttribute('href','javascript:void(0);');
 
12232                         this.toggleAllLink.setAttribute('title','Show only replies to original poster.');
 
12233                         this.toggleAllLink.addEventListener('click', function() {
 
12234                                 modules['hideChildComments'].toggleComments(this.getAttribute('action'));
 
12235                                 if (this.getAttribute('action') === 'hide') {
 
12236                                         this.setAttribute('action','show');
 
12237                                         this.setAttribute('title','Show all comments.');
 
12238                                         this.textContent = 'show all child comments';
 
12240                                         this.setAttribute('action','hide');
 
12241                                         this.setAttribute('title','Show only replies to original poster.');
 
12242                                         this.textContent = 'hide all child comments';
 
12245                         toggleButton.appendChild(this.toggleAllLink);
 
12246                         var commentMenu = document.querySelector('ul.buttons');
 
12248                                 commentMenu.appendChild(toggleButton);
 
12249                                 var rootComments = document.querySelectorAll('div.commentarea > div.sitetable > div.thing > div.child > div.listing');
 
12250                                 for (var i=0, len=rootComments.length; i<len; i++) {
 
12251                                         var toggleButton = document.createElement('li');
 
12252                                         var toggleLink = document.createElement('a');
 
12253                                         toggleLink.textContent = 'hide child comments';
 
12254                                         toggleLink.setAttribute('action','hide');
 
12255                                         toggleLink.setAttribute('href','javascript:void(0);');
 
12256                                         toggleLink.setAttribute('class','toggleChildren');
 
12257                                         // toggleLink.setAttribute('title','Hide child comments.');
 
12258                                         toggleLink.addEventListener('click', function(e) {
 
12259                                                 modules['hideChildComments'].toggleComments(this.getAttribute('action'), this);
 
12260                                                 if (this.getAttribute('action') === 'hide') {
 
12261                                                         this.setAttribute('action','show');
 
12262                                                         // this.setAttribute('title','show child comments.');
 
12263                                                         this.textContent = 'show child comments';
 
12265                                                         this.setAttribute('action','hide');
 
12266                                                         // this.setAttribute('title','hide child comments.');
 
12267                                                         this.textContent = 'hide child comments';
 
12270                                         toggleButton.appendChild(toggleLink);
 
12271                                         var sib = rootComments[i].parentNode.previousSibling;
 
12272                                         if (typeof sib !== 'undefined') {
 
12273                                                 var sibMenu = sib.querySelector('ul.buttons');
 
12274                                                 if (sibMenu) sibMenu.appendChild(toggleButton);
 
12277                                 if (this.options.automatic.value) {
 
12278                                         RESUtils.click(this.toggleAllLink);
 
12283         toggleComments: function(action, obj) {
 
12285                         var thisChildren = $(obj).closest('.thing').children('.child').children('.sitetable')[0];
 
12286                         thisChildren.style.display = (action === 'hide') ? 'none' : 'block';
 
12288                         // toggle all comments
 
12289                         var commentContainers = document.querySelectorAll('div.commentarea > div.sitetable > div.thing');
 
12290                         for (var i=0, len=commentContainers.length; i<len; i++) {
 
12291                                 var thisChildren = commentContainers[i].querySelector('div.child > div.sitetable');
 
12292                                 var thisToggleLink = commentContainers[i].querySelector('a.toggleChildren');
 
12293                                 if (thisToggleLink !== null) {
 
12294                                         if (action === 'hide') {
 
12295                                                 if (thisChildren !== null) {
 
12296                                                         thisChildren.style.display = 'none' 
 
12298                                                 thisToggleLink.textContent = 'show child comments';
 
12299                                                 // thisToggleLink.setAttribute('title','show child comments');
 
12300                                                 thisToggleLink.setAttribute('action','show');
 
12302                                                 if (thisChildren !== null) {
 
12303                                                         thisChildren.style.display = 'block';
 
12305                                                 thisToggleLink.textContent = 'hide child comments';
 
12306                                                 // thisToggleLink.setAttribute('title','hide child comments');
 
12307                                                 thisToggleLink.setAttribute('action','hide');
 
12315 modules['showParent'] = {
 
12316         moduleID: 'showParent',
 
12317         moduleName: 'Show Parent on Hover',
 
12318         category: 'Comments',
 
12323                         description: 'Delay, in milliseconds, before parent hover loads. Default is 500.'
 
12328                         description: 'Delay, in milliseconds, before parent hover fades away after the mouse leaves. Default is 200.'
 
12333                         description: 'Fade animation\'s speed. Default is 0.3, the range is 0-1. Setting the speed to 1 will disable the animation.'
 
12339                                 { name: 'Up', value: 'up' },
 
12340                                 { name: 'Down', value: 'down' }
 
12342                         description: 'Order the parent comments to place the closest parent at the top (down) or at the bottom (up).'
 
12345         description: 'Shows the parent comments when hovering over the "parent" link of a comment.',
 
12346         isEnabled: function() {
 
12347                 return RESConsole.getModulePrefs(this.moduleID);
 
12350                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
 
12351                 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
 
12353         isMatchURL: function() {
 
12354                 return RESUtils.isMatchURL(this.moduleID);
 
12357                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
12358                         if (modules['showParent'].options.direction.value === 'up') {
 
12359                                 document.html.classList.add('res-parents-up');
 
12361                                 document.html.classList.add('res-parents-down');
 
12363                         $('body').on('mouseenter', '.comment .buttons :not(:first-child) .bylink', function(e) {
 
12364                                 RESUtils.hover.begin(this, {
 
12365                                         openDelay: modules['showParent'].options.hoverDelay.value,
 
12366                                         fadeDelay: modules['showParent'].options.fadeDelay.value,
 
12367                                         fadeSpeed: modules['showParent'].options.fadeSpeed.value
 
12368                                 }, modules['showParent'].showCommentHover , {});
 
12371                         $('body').on('click', '#RESHoverContainer .parentCommentWrapper .arrow', modules['showParent'].handleVoteClick);
 
12374         handleVoteClick: function(evt) {
 
12375                 var voteKey = {'-1':'disliked', 0:'unvoted', 1:'liked'};
 
12377                 var id = $(this).parent().parent().attr('data-fullname');
 
12379                 var direction = /(up|down)(mod)?/.exec(this.className);
 
12380                 if (direction) direction = direction[1];
 
12383                 var targetButton = $('.content .thing.id-'+id+' > .midcol').find('.arrow.'+direction + ', .arrow.'+direction+'mod');
 
12384                 if (targetButton.length !== 1) {
 
12385                         console.error("When attempting to find %s arrow for comment %s %d elements were returned", direction, id, targetButton.length);
 
12388                 targetButton.click();
 
12390                 var clickedDir = (direction==='up'?1:-1);
 
12392                 var mid = $(this).parent();
 
12393                 if (mid.hasClass('unvoted')) startDir = 0;
 
12394                 else if (mid.hasClass('likes')) startDir = 1;
 
12395                 else if (mid.hasClass('dislikes')) startDir = -1;
 
12398                 var newDir = clickedDir===startDir ? 0 : clickedDir;
 
12401                 mid.parent().children('.'+voteKey[startDir]).removeClass(voteKey[startDir]).addClass(voteKey[newDir])
 
12402                 mid.find('.up, .upmod')
 
12403                         .toggleClass('upmod', clickedDir === 1)
 
12404                         .toggleClass('up', clickedDir !== 1);
 
12405                 mid.find('.down, .downmod')
 
12406                         .toggleClass('downmod', clickedDir === -1)
 
12407                         .toggleClass('down', clickedDir !== -1);
 
12409         showCommentHover: function(def, base, context) {
 
12410                 var direction = modules['showParent'].options.direction.value;
 
12411                 var thing = $(base);
 
12413                 //If the passed element is not a `.thing` move up to the nearest `.thing`
 
12414                 if (!$(thing).is('.thing')) thing = thing.parents('.thing:first');
 
12415                 var parents = $(thing).parents('.thing').clone();
 
12417                 if (direction === 'up') {
 
12418                         parents = $(parents.get().reverse());
 
12421                 parents.addClass('comment parentComment').removeClass('thing even odd');
 
12422                 parents.children('.child').remove(); // replies and reply edit form
 
12423                 parents.each(function(i) {
 
12424                         //A link to go to the actual comment
 
12425                         var id = $(this).attr('data-fullname');
 
12426                         if (id != undefined) {
 
12428                                 $(this).find('> .entry .tagline').append('<a class="bylink parentlink" href="#'+id+'"">goto comment</a>');
 
12431                 parents.find('.parent').remove();
 
12432                 parents.find('.usertext-body').show(); // contents
 
12433                 parents.find('.flat-list.buttons').remove(); // buttons
 
12434                 parents.find('.usertext-edit').remove();  // edit form
 
12435                 parents.find('.RESUserTag').remove();  // tags
 
12436                 parents.find('.voteWeight').remove();  // tags
 
12437                 parents.find('.entry').removeClass('RES-keyNav-activeElement');
 
12438                 parents.find('.author.userTagged').removeClass('userTagged'); //tags again
 
12439                 parents.find('.collapsed').remove();  //unused collapse view
 
12440                 parents.find('.expand').remove(); //expand vutton
 
12441                 parents.find('form').attr('id', ''); //ID's should be unique
 
12442                 parents.find('.arrow').attr('onclick', ''); //clear the vote handlers
 
12444                 I am stripping out the image viewer stuff for now.
 
12445                 Making the image viewer work here requires some changes that are for another time.
 
12447                 parents.find('.madeVisible, .toggleImage').remove(); //image viewer
 
12448                 parents.find('.keyNavAnnotation').remove();
 
12450                 var container = $('<div class="parentCommentWrapper">');
 
12451                 container.append(parents);
 
12452                 $(parents).slice(0,-1).after('<div class="parentArrow">reply to</div>');
 
12453                 def.resolve("Parents", container);
 
12457 modules['neverEndingReddit'] = {
 
12458         moduleID: 'neverEndingReddit',
 
12459         moduleName: 'Never Ending Reddit',
 
12462                 // any configurable options you have go here...
 
12463                 // options must have a type and a value.. 
 
12464                 returnToPrevPage: {
 
12467                         description: 'Return to the page you were last on when hitting "back" button?'
 
12472                         description: 'Automatically load new page on scroll (if off, you click to load)'
 
12474                 notifyWhenPaused: {
 
12477                         description: 'Show a reminder to unpause Never-Ending Reddit after pausing'
 
12479                 reversePauseIcon: {
 
12482                         description: 'Show "paused" bars icon when auto-load is paused and "play" wedge icon when active'
 
12487                         description: 'After auto-loading a certain number of pages, pause the auto-loader'
 
12488                                 + '<br><br>0 or a negative number means Never-Ending Reddit will only pause when you click'
 
12489                                 + ' the play/pause button in the top right corner.'
 
12495                                 { name: 'Fade', value: 'fade' },
 
12496                                 { name: 'Hide', value: 'hide' },
 
12497                                 { name: 'Do not hide', value: 'none' }
 
12499                         description: 'Fade or completely hide duplicate posts from previous pages.'
 
12502         description: 'Inspired by modules like River of Reddit and Auto Pager - gives you a never ending stream of reddit goodness.',
 
12503         isEnabled: function() {
 
12504                 return RESConsole.getModulePrefs(this.moduleID);
 
12507                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\_\?=]*/i
 
12511         isMatchURL: function() {
 
12512                 return RESUtils.isMatchURL(this.moduleID);
 
12514         beforeLoad: function() {
 
12515                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
12516                         RESUtils.addCSS('#NERModal { display: none; z-index: 999; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: #333; opacity: 0.6; }');
 
12517                         RESUtils.addCSS('#NERContent { display: none; position: fixed; top: 40px; z-index: 1000; width: 720px; background-color: #FFF; color: #000; padding: 10px; font-size: 12px; }');
 
12518                         RESUtils.addCSS('#NERModalClose { position: absolute; top: 3px; right: 3px; }');
 
12519                         RESUtils.addCSS('#NERFail { min-height: 30px; width: 95%; font-size: 14px; border: 1px solid #999; border-radius: 10px; padding: 5px; text-align: center; bgcolor: #f0f3fc; cursor: pointer; }');
 
12520                         RESUtils.addCSS('.NERdupe p.title:after { color: #000; font-size: 10px; content: \' (duplicate from previous page)\'; }');
 
12521                         RESUtils.addCSS('.NERPageMarker { text-align: center; color: #7f7f7f; font-size: 14px; margin-top: 6px; margin-bottom: 6px; font-weight: normal; background-color: #f0f3fc; border: 1px solid #c7c7c7; border-radius: 3px; padding: 3px 0; }');
 
12522                         // hide next/prev page indicators
 
12523                         RESUtils.addCSS('.content p.nextprev { display: none; } ');
 
12524                         switch (this.options.hideDupes.value) {
 
12526                                         RESUtils.addCSS('.NERdupe { opacity: 0.3; }');
 
12529                                         RESUtils.addCSS('.NERdupe { display: none; }');
 
12532                         // set the style for our little loader widget
 
12533                         RESUtils.addCSS('#progressIndicator { width: 95%; min-height: 20px; margin: 0 auto; font-size: 14px; border: 1px solid #999; border-radius: 10px; padding: 10px; text-align: center; background-color: #f0f3fc; cursor: pointer; } ');
 
12534                         RESUtils.addCSS('#progressIndicator h2 { margin-bottom: .5em; }');
 
12535                         RESUtils.addCSS('#progressIndicator .gearIcon { margin-left: 1em; }');
 
12536                         RESUtils.addCSS('#NREMailCount { margin-left: 0; float: left; margin-top: 3px;}');
 
12537                         RESUtils.addCSS('#NREPause { margin-left: 2px; width: 16px; height: 16px; float: left; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); cursor: pointer; }');
 
12538                         RESUtils.addCSS('#NREPause, #NREPause.paused.reversePause { background-position: -16px -192px; }');
 
12539                         RESUtils.addCSS('#NREPause.paused, #NREPause.reversePause {  background-position: 0 -192px; }');
 
12543                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
12545 /*                      if (RESUtils.pageType() !== 'linklist') {
 
12546                                 sessionStorage.NERpageURL = location.href;
 
12548 */                      // modified from a contribution by Peter Siewert, thanks Peter!
 
12549                         if (typeof modules['neverEndingReddit'].dupeHash === 'undefined') modules['neverEndingReddit'].dupeHash = {};
 
12550                         var entries = document.body.querySelectorAll('a.comments');
 
12551                         for (var i = entries.length - 1; i > -1; i--) {
 
12552                                 modules['neverEndingReddit'].dupeHash[entries[i].href] = 1;
 
12555                         this.allLinks = document.body.querySelectorAll('#siteTable div.thing');
 
12557                         // code inspired by River of Reddit, but rewritten from scratch to work across multiple browsers...
 
12558                         // Original River of Reddit author: reddy kapil
 
12559                         // Original link to Chrome extension: https://chrome.google.com/extensions/detail/bjiggjllfebckflfdjbimogjieeghcpp
 
12561                         // store access to the siteTable div since that's where we'll append new data...
 
12562                         var stMultiCheck = document.querySelectorAll('#siteTable');
 
12563                         this.siteTable = stMultiCheck[0];
 
12564                         // stupid sponsored links create a second div with ID of sitetable (bad reddit! you should never have 2 IDs with the same name! naughty, naughty reddit!)
 
12565                         if (stMultiCheck.length === 2) {
 
12566                                 // console.log('skipped first sitetable, stupid reddit.');
 
12567                                 this.siteTable = stMultiCheck[1];
 
12569                         // get the first link to the next page of reddit...
 
12570                         var nextPrevLinks = document.body.querySelectorAll('.content .nextprev a');
 
12571                         if (nextPrevLinks.length > 0) {
 
12572                                 var nextLink = nextPrevLinks[nextPrevLinks.length-1];
 
12574                                         this.nextPageURL = nextLink.getAttribute('href');
 
12575                                         var nextXY=RESUtils.getXYpos(nextLink);
 
12576                                         this.nextPageScrollY = nextXY.y;
 
12578                                 this.attachLoaderWidget();
 
12580                                 //Reset this info if the page is in a new tab
 
12581                                 // wait, this is always  tre... commenting out.
 
12583                                 if (window.history.length) {
 
12584                                         console.log('delete nerpage');
 
12585                                         delete sessionStorage['NERpage'];
 
12587                                 if (this.options.returnToPrevPage.value) {
 
12588                                         // if the user clicks any external links, save that link
 
12589                                         // get all external links and track clicks...
 
12590 /*                                      $('body').on('click', 'a.title[href^="http://"]', function(e) {
 
12591                                                 // if left click and not going to open in a new tab...
 
12592                                                 if ((this.target !== '_blank') && (e.which === 1)) sessionStorage.lastPageURL = this.href;
 
12595                                         this.returnToPrevPageCheck(location.hash);
 
12598                                 // watch for the user scrolling to the bottom of the page.  If they do it, load a new page.
 
12599                                 if (this.options.autoLoad.value) {
 
12600                                         window.addEventListener('scroll', modules['neverEndingReddit'].handleScroll, false);
 
12603                         // check if the user has new mail...
 
12604                         this.navMail = document.body.querySelector('#mail');
 
12605                         this.NREFloat = createElementWithID('div','NREFloat');
 
12606                         this.NREPause = createElementWithID('div','NREPause');
 
12607                         this.NREPause.setAttribute('title','Pause / Restart Never Ending Reddit');
 
12608                         if (this.options.reversePauseIcon.value) this.NREPause.classList.add('reversePause');
 
12609                         this.isPaused = (RESStorage.getItem('RESmodules.neverEndingReddit.isPaused') == true);
 
12610                         if (this.isPaused) this.NREPause.classList.add('paused');
 
12611                         this.NREPause.addEventListener('click',modules['neverEndingReddit'].togglePause, false);
 
12612                         if ((modules['betteReddit'].options.pinHeader.value !== 'userbar') && (modules['betteReddit'].options.pinHeader.value !== 'header')) {
 
12613                                 this.NREMail = createElementWithID('a','NREMail');
 
12614                                 if (modules['betteReddit'].options.pinHeader.value === 'sub') {
 
12615                                         RESUtils.addCSS('#NREFloat { position: fixed; top: 23px; right: 8px; display: none; }');
 
12616                                 } else if (modules['betteReddit'].options.pinHeader.value === 'subanduser') {
 
12617                                         RESUtils.addCSS('#NREFloat { position: fixed; top: 44px; right: 0; display: none; }');
 
12618                                         RESUtils.addCSS('#NREMail { display: none; }');
 
12619                                         RESUtils.addCSS('#NREMailCount { display: none; }');
 
12621                                         RESUtils.addCSS('#NREFloat { position: fixed; top: 10px; right: 10px; display: none; }');
 
12623                                 RESUtils.addCSS('#NREMail { width: 16px; height: 12px; float: left; margin-top: 4px; background: center center no-repeat; }');
 
12624                                 RESUtils.addCSS('#NREMail.nohavemail { background-image: url(http://www.redditstatic.com/mailgray.png); }');
 
12625                                 RESUtils.addCSS('#NREMail.havemail { background-image: url(http://www.redditstatic.com/mail.png); }');
 
12626                                 RESUtils.addCSS('.res-colorblind #NREMail.havemail { background-image: url(http://thumbs.reddit.com/t5_2s10b_5.png); }');
 
12627                                 this.NREFloat.appendChild(this.NREMail);
 
12628                                 this.NREMailCount = createElementWithID('a','NREMailCount');
 
12629                                 this.NREMailCount.display = 'none';
 
12630                                 this.NREMailCount.setAttribute('href',modules['betteReddit'].getInboxLink(true));
 
12631                                 this.NREFloat.appendChild(this.NREMailCount);
 
12632                                 var hasNew = false;
 
12633                                 if ((typeof this.navMail !== 'undefined') && (this.navMail !== null)) {
 
12634                                         hasNew = this.navMail.classList.contains('havemail');
 
12636                                 this.setMailIcon(hasNew);
 
12638                                 this.NREMail = this.navMail;
 
12639                                 RESUtils.addCSS('#NREFloat { position: fixed; top: 30px; right: 8px; display: none; }');
 
12641                         this.NREFloat.appendChild(this.NREPause);
 
12642                         document.body.appendChild(this.NREFloat);
 
12647         togglePause: function() {
 
12648                 modules['neverEndingReddit'].isPaused = !modules['neverEndingReddit'].isPaused;
 
12649                 RESStorage.setItem('RESmodules.neverEndingReddit.isPaused', modules['neverEndingReddit'].isPaused);
 
12650                 if (modules['neverEndingReddit'].isPaused) {
 
12651                         modules['neverEndingReddit'].NREPause.classList.add('paused');
 
12652                         if (modules['neverEndingReddit'].options.notifyWhenPaused.value) {
 
12653                                 var notification = [];
 
12654                                 notification.push('Never-Ending Reddit has been paused. Click the play/pause button to unpause it.');
 
12655                                 notification.push('To hide this message, disable Never-Ending Reddit\'s ' + modules['settingsNavigation'].makeUrlHashLink('neverEndingReddit', 'notifyWhenPaused', 'notifyWhenPaused option <span class="gearIcon" />') + '.');
 
12656                                 notification = notification.join('<br><br>');
 
12657                                 RESUtils.notification({
 
12658                                         moduleID: 'neverEndingReddit',
 
12659                                         message: notification
 
12663                         modules['neverEndingReddit'].NREPause.classList.remove('paused');
 
12664                         modules['neverEndingReddit'].handleScroll();
 
12666                 modules['neverEndingReddit'].setWidgetActionText();
 
12668         returnToPrevPageCheck: function(hash) {
 
12669                 var pageRE = /page=(\d+)/,
 
12670                     match = pageRE.exec(hash);
 
12671                 // Set the current page to page 1...
 
12674                         var backButtonPageNumber = match[1] || 1;
 
12675                         if (backButtonPageNumber > 1) {
 
12676                                 this.attachModalWidget();
 
12677                                 this.currPage = backButtonPageNumber;
 
12678                                 this.loadNewPage(true);
 
12683                 if ((sessionStorage.NERpageURL) && (sessionStorage.NERpageURL != sessionStorage.lastPageURL)) {
 
12684                         var backButtonPageNumber = sessionStorage.getItem('NERpage') || 1;
 
12685                         if (backButtonPageNumber > 1) {
 
12686                                 this.currPage = backButtonPageNumber;
 
12687                                 this.loadNewPage(true);
 
12690                 sessionStorage.lastPageURL = location.href;
 
12693         handleScroll: function(e) {
 
12694                 if (modules['neverEndingReddit'].scrollTimer) clearTimeout(modules['neverEndingReddit'].scrollTimer);
 
12695                 modules['neverEndingReddit'].scrollTimer = setTimeout(modules['neverEndingReddit'].handleScrollAfterTimer, 300);
 
12697         handleScrollAfterTimer: function(e) {
 
12698                 var thisPageNum = 1,
 
12701                 for (var i=0, len=modules['neverEndingReddit'].pageMarkers.length; i<len; i++) {
 
12702                         var thisXY = RESUtils.getXYpos(modules['neverEndingReddit'].pageMarkers[i]);
 
12703                         if (thisXY.y < window.pageYOffset) {
 
12704                                 thisMarker = modules['neverEndingReddit'].pageMarkers[i];
 
12705                                 thisPageNum = thisMarker.getAttribute('id').replace('page-','');
 
12706                                 modules['neverEndingReddit'].currPage = thisPageNum;
 
12708                                         var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
 
12709                                         RESStorage.setItem('RESmodules.neverEndingReddit.lastPage.'+thisPageType, thisMarker.getAttribute('url'));
 
12715                 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
 
12716                 // RESStorage.setItem('RESmodules.neverEndingReddit.lastPage.'+thisPageType, modules['neverEndingReddit'].pageURLs[thisPageNum]);
 
12717                 if (thisPageNum != sessionStorage.NERpage) {
 
12718                         if (thisPageNum > 1) {
 
12719                                 // sessionStorage.NERpageURL = location.href;
 
12720                                 sessionStorage.NERpage = thisPageNum;
 
12721                                 modules['neverEndingReddit'].pastFirstPage = true;
 
12722                                 location.hash = 'page='+thisPageNum;
 
12724                                 if (location.hash.indexOf('page=') !== -1) {
 
12725                                         location.hash = 'page='+thisPageNum;
 
12727                                 delete sessionStorage['NERpage'];
 
12730                 if ((modules['neverEndingReddit'].fromBackButton != true) && (modules['neverEndingReddit'].options.returnToPrevPage.value)) {
 
12731                         for (var i=0, len=modules['neverEndingReddit'].allLinks.length; i<len; i++) {
 
12732                                 if (RESUtils.elementInViewport(modules['neverEndingReddit'].allLinks[i])) {
 
12733                                         var thisClassString = modules['neverEndingReddit'].allLinks[i].getAttribute('class');
 
12734                                         var thisClass = thisClassString.match(/id-t[\d]_[\w]+/);
 
12736                                                 var thisID = thisClass[0];
 
12737                                                 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
 
12738                                                 RESStorage.setItem('RESmodules.neverEndingReddit.lastVisibleIndex.'+thisPageType, thisID);
 
12744                 if ((RESUtils.elementInViewport(modules['neverEndingReddit'].progressIndicator)) && (modules['neverEndingReddit'].fromBackButton != true)) {
 
12745                         if (modules['neverEndingReddit'].isPaused != true) {
 
12746                                 modules['neverEndingReddit'].loadNewPage();
 
12747                                 modules['neverEndingReddit'].pauseAfter(thisPageNum);
 
12750                 if ($(window).scrollTop() > 30) {
 
12751                         modules['neverEndingReddit'].showFloat(true);
 
12753                         modules['neverEndingReddit'].showFloat(false);
 
12756         pauseAfterPages: null,
 
12757         pauseAfter: function(currPageNum) {
 
12758                 if (this.pauseAfterPages === null) {
 
12759                         this.pauseAfterPages = parseInt(modules['neverEndingReddit'].options.pauseAfterEvery.value);
 
12762                 if ((this.pauseAfterPages > 0) && (currPageNum % this.pauseAfterPages === 0)) {
 
12763                         this.togglePause(true);
 
12764                         var notification = [];
 
12765                         notification.push('Time for a break!');
 
12766                         notification.push('Never-Ending Reddit was paused automatically. ' +  modules['settingsNavigation'].makeUrlHashLink('neverEndingReddit', 'pauseAfterEvery', '', 'gearIcon'));
 
12767                         notification = notification.join('<br><br>');
 
12768                         setTimeout(RESUtils.notification.bind(RESUtils, notification, 5000));
 
12771         duplicateCheck: function(newHTML){
 
12772                 var newLinks = newHTML.querySelectorAll('div.link');
 
12773                 for(var i = newLinks.length - 1; i > -1; i--) {
 
12774                         var newLink = newLinks[i];
 
12775                         var thisCommentLink = newLink.querySelector('a.comments').href;
 
12776                         if( modules['neverEndingReddit'].dupeHash[thisCommentLink] ) {
 
12777                           // let's not remove it altogether, but instead dim it...
 
12778                           // newLink.parentElement.removeChild(newLink);
 
12779                           newLink.classList.add('NERdupe');
 
12781                                 modules['neverEndingReddit'].dupeHash[thisCommentLink] = 1;
 
12786         setMailIcon: function(newmail) {
 
12787                 if (RESUtils.loggedInUser() === null) return false;
 
12789                         modules['neverEndingReddit'].hasNewMail = true;
 
12790                         this.NREMail.classList.remove('nohavemail');
 
12791                         this.NREMail.setAttribute('href', modules['betteReddit'].getInboxLink(true));
 
12792                         this.NREMail.setAttribute('title','new mail!');
 
12793                         this.NREMail.classList.add('havemail');
 
12794                         modules['betteReddit'].showUnreadCount();
 
12796                         modules['neverEndingReddit'].hasNewMail = false;
 
12797                         this.NREMail.classList.add('nohavemail');
 
12798                         this.NREMail.setAttribute('href',modules['betteReddit'].getInboxLink(false));
 
12799                         this.NREMail.setAttribute('title','no new mail');
 
12800                         this.NREMail.classList.remove('havemail');
 
12801                         modules['betteReddit'].setUnreadCount(0);
 
12804         attachModalWidget: function() {
 
12805                 this.modalWidget = createElementWithID('div','NERModal');
 
12806                 $(this.modalWidget).html(' ');
 
12807                 this.modalContent = createElementWithID('div','NERContent');
 
12808                 $(this.modalContent).html('<div id="NERModalClose" class="RESCloseButton">×</div>Never Ending Reddit has detected that you are returning from a page that it loaded. Please give us a moment while we reload that content and return you to where you left off.<br><img src="'+RESConsole.loader+'">');
 
12809                 document.body.appendChild(this.modalWidget);
 
12810                 document.body.appendChild(this.modalContent);
 
12811                 $('#NERModalClose').click(function() {
 
12812                         $(modules['neverEndingReddit'].modalWidget).hide();
 
12813                         $(modules['neverEndingReddit'].modalContent).hide();
 
12816         attachLoaderWidget: function() {
 
12817                 // add a widget at the bottom that will be used to detect that we've scrolled to the bottom, and will also serve as a "loading" bar...
 
12818                 this.progressIndicator = document.createElement('div');
 
12819                 this.setWidgetActionText();
 
12820                 this.progressIndicator.id = 'progressIndicator';
 
12821                 this.progressIndicator.className = 'neverEndingReddit';
 
12823                 this.progressIndicator.addEventListener('click', function(e) {
 
12824                         if (e.target.id !== 'NERStaticLink' && !e.target.classList.contains('gearIcon')) {
 
12825                                 e.preventDefault();
 
12826                                 modules['neverEndingReddit'].loadNewPage();
 
12829                 insertAfter(this.siteTable, this.progressIndicator);
 
12831         setWidgetActionText: function () {
 
12832                 $(this.progressIndicator).empty();
 
12833                 $('<h2>Never Ending Reddit</h2>')
 
12834                         .appendTo(this.progressIndicator)
 
12835                         .append(modules['settingsNavigation'].makeUrlHashLink('neverEndingReddit', null, ' ', 'gearIcon'));
 
12837                 var text = "Click to load the next page";
 
12838                 if (this.options.autoLoad.value && !this.isPaused) {
 
12839                         text = "scroll or click to load the next page";
 
12840                 } else if (this.options.autoLoad.value && this.isPaused) {
 
12841                         text = "click to load the next page; or click the 'pause' button in the top right corner"
 
12846                         .appendTo(this.progressIndicator);
 
12848                 var nextpage = $('<a id="NERStaticLink">or open next page</a>')
 
12849                         .attr('href', this.nextPageURL);
 
12850                 $('<p />').append(nextpage)
 
12851                         .append(' (and clear Never-Ending stream)')
 
12852                         .appendTo(this.progressIndicator);
 
12854         loadNewPage: function(fromBackButton, reload) {
 
12855                 var me = modules['neverEndingReddit'];
 
12856                 if (me.isLoading != true) {
 
12857                         me.isLoading = true;
 
12858                         if (fromBackButton) {
 
12859                                 me.fromBackButton = true;
 
12860                                 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
 
12861                                 var savePageURL = me.nextPageURL;
 
12862                                 me.nextPageURL = RESStorage.getItem('RESmodules.neverEndingReddit.lastPage.'+thisPageType);
 
12863                                 if ((me.nextPageURL === 'undefined') || (me.nextPageURL == null)) {
 
12864                                         // something went wrong, probably someone hit refresh. Just revert to the first page...
 
12865                                         modules['neverEndingReddit'].fromBackButton = false;
 
12866                                         me.nextPageURL = savePageURL;
 
12868                                         me.isLoading = false;
 
12871                                 var leftCentered = Math.floor((window.innerWidth - 720) / 2);
 
12872                                 me.modalWidget.style.display = 'block';
 
12873                                 me.modalContent.style.display = 'block';
 
12874                                 me.modalContent.style.left = leftCentered + 'px';
 
12875                                 // remove the progress indicator early, as we don't want the user to scroll past it on accident, loading more content.
 
12876                                 me.progressIndicator.parentNode.removeChild(modules['neverEndingReddit'].progressIndicator);
 
12878                                 me.fromBackButton = false;
 
12882                         me.progressIndicator.removeEventListener('click', modules['neverEndingReddit'].loadNewPage , false);
 
12883                         $(me.progressIndicator).html('<img src="'+RESConsole.loader+'"> Loading next page...');
 
12884                         // as a sanity check, which should NEVER register true, we'll make sure me.nextPageURL is on the same domain we're browsing...
 
12885                         if (me.nextPageURL.indexOf(location.hostname) === -1) {
 
12886                                 console.log('Next page URL mismatch. Something strange may be afoot.')
 
12887                                 me.isLoading = false;
 
12890                         GM_xmlhttpRequest({
 
12892                                 url:    me.nextPageURL,
 
12893                                 onload: function(response) {
 
12894                                         if ((typeof modules['neverEndingReddit'].progressIndicator.parentNode !== 'undefined') && (modules['neverEndingReddit'].progressIndicator.parentNode !== null)) {
 
12895                                                 modules['neverEndingReddit'].progressIndicator.parentNode.removeChild(modules['neverEndingReddit'].progressIndicator);
 
12897                                         // drop the HTML we got back into a div...
 
12898                                         var thisHTML = response.responseText;
 
12899                                         var tempDiv = document.createElement('div');
 
12900                                         // clear out any javascript so we don't render it again...
 
12901                                         $(tempDiv).html(thisHTML.replace(/<script(.|\s)*?\/script>/g, ''));
 
12902                                         // grab the siteTable out of there...
 
12903                                         var newHTML = tempDiv.querySelector('#siteTable');
 
12904                                         // did we find anything?
 
12905                                         if (newHTML !== null) {
 
12906                                                 var stMultiCheck = tempDiv.querySelectorAll('#siteTable');
 
12907                                                 // stupid sponsored links create a second div with ID of sitetable (bad reddit! you should never have 2 IDs with the same name! naughty, naughty reddit!)
 
12908                                                 if (stMultiCheck.length === 2) {
 
12909                                                         // console.log('skipped first sitetable, stupid reddit.');
 
12910                                                         newHTML = stMultiCheck[1];
 
12912                                                 newHTML.setAttribute('ID','siteTable-'+modules['neverEndingReddit'].currPage+1);
 
12913                                                 modules['neverEndingReddit'].duplicateCheck(newHTML);
 
12914                                                 // check for new mail
 
12915                                                 var hasNewMail = tempDiv.querySelector('#mail');
 
12916                                                 if ((typeof hasNewMail !== 'undefined') && (hasNewMail !== null) && (hasNewMail.classList.contains('havemail'))) {
 
12917                                                         modules['neverEndingReddit'].setMailIcon(true);
 
12919                                                         modules['neverEndingReddit'].setMailIcon(false);
 
12921                                                 // load up uppers and downers, if enabled...
 
12922                                                 // maybe not necessary anymore..
 
12924                                                 if ((modules['uppersAndDowners'].isEnabled()) && (RESUtils.pageType() === 'comments')) {
 
12925                                                         modules['uppersAndDowners'].applyUppersAndDownersToComments(modules['neverEndingReddit'].nextPageURL);
 
12928                                                 // get the new nextLink value for the next page...
 
12929                                                 var nextPrevLinks = tempDiv.querySelectorAll('.content .nextprev a');
 
12930                                                 if ((nextPrevLinks) && (nextPrevLinks.length)) {
 
12931                                                         if (isNaN(modules['neverEndingReddit'].currPage)) modules['neverEndingReddit'].currPage = 1;
 
12932                                                         if (!fromBackButton) modules['neverEndingReddit'].currPage++;
 
12933                                                         if ((!(modules['neverEndingReddit'].fromBackButton)) && (modules['neverEndingReddit'].options.returnToPrevPage.value)) {
 
12934                                                                 // modules['neverEndingReddit'].pageURLs[modules['neverEndingReddit'].currPage] = modules['neverEndingReddit'].nextPageURL;
 
12935                                                                 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
 
12936                                                                 RESStorage.setItem('RESmodules.neverEndingReddit.lastPage.'+thisPageType, modules['neverEndingReddit'].nextPageURL);
 
12937                                                                 // location.hash = 'page='+modules['neverEndingReddit'].currPage;
 
12939                                                         var nextLink = nextPrevLinks[nextPrevLinks.length-1];
 
12940                                                         var nextPage = modules['neverEndingReddit'].currPage;
 
12941                                                         var pageMarker = createElementWithID('div','page-'+nextPage);
 
12942                                                         pageMarker.classList.add('NERPageMarker');
 
12943                                                         $(pageMarker).text('Page ' + nextPage);
 
12944                                                         modules['neverEndingReddit'].siteTable.appendChild(pageMarker);
 
12945                                                         modules['neverEndingReddit'].pageMarkers.push(pageMarker);
 
12946                                                         modules['neverEndingReddit'].siteTable.appendChild(newHTML);
 
12947                                                         modules['neverEndingReddit'].isLoading = false;
 
12949                                                                 // console.log(nextLink);
 
12950                                                                 pageMarker.setAttribute('url',me.nextPageURL);
 
12951                                                                 if (nextLink.getAttribute('rel').indexOf('prev') !== -1) {
 
12952                                                                         // remove the progress indicator from the DOM, it needs to go away.
 
12953                                                                         modules['neverEndingReddit'].progressIndicator.style.display = 'none';
 
12954                                                                         var endOfReddit = createElementWithID('div','endOfReddit');
 
12955                                                                         $(endOfReddit).text('You\'ve reached the last page available.  There are no more pages to load.');
 
12956                                                                         modules['neverEndingReddit'].siteTable.appendChild(endOfReddit);
 
12957                                                                         window.removeEventListener('scroll', modules['neverEndingReddit'].handleScroll, false);
 
12959                                                                         // console.log('not over yet');
 
12960                                                                         var prevLink = nextPrevLinks[0];
 
12961                                                                         modules['neverEndingReddit'].nextPageURL = nextLink.getAttribute('href');
 
12962                                                                         modules['neverEndingReddit'].attachLoaderWidget();
 
12965                                                         modules['neverEndingReddit'].allLinks = document.body.querySelectorAll('#siteTable div.thing');
 
12966                                                         if ((fromBackButton) && (modules['neverEndingReddit'].options.returnToPrevPage.value)) {
 
12967                                                                 // TODO: it'd be great to figure out a better way than a timeout, but this
 
12968                                                                 // has considerably helped the accuracy of RES's ability to return you to where
 
12970                                                                 setTimeout(modules['neverEndingReddit'].scrollToLastElement, 4000);
 
12973                                                         // If we're on the reddit-browsing page (/reddits or /subreddits), add +shortcut and -shortcut buttons...
 
12974                                                         if (/^https?:\/\/www\.reddit\.com\/(?:sub)?reddits\/?(?:\?[\w=&]+)*/.test(location.href)) {
 
12975                                                                 modules['subredditManager'].browsingReddits();
 
12978                                                         var noresults = tempDiv.querySelector('#noresults');
 
12979                                                         var noresultsfound = (noresults !== null);
 
12980                                                         modules['neverEndingReddit'].NERFail(noresultsfound);
 
12982                                                 var e = document.createEvent("Events");
 
12983                                                 e.initEvent("neverEndingLoad", true, true);
 
12984                                                 window.dispatchEvent(e);                                        
 
12987                                 onerror: function(err) {
 
12988                                         modules['neverEndingReddit'].NERFail();
 
12992                         // console.log('load new page ignored');
 
12995         scrollToLastElement: function() {
 
12996                 modules['neverEndingReddit'].modalWidget.style.display = 'none';
 
12997                 modules['neverEndingReddit'].modalContent.style.display = 'none';
 
12998                 // window.scrollTo(0,0)
 
12999                 // RESUtils.scrollTo(0,modules['neverEndingReddit'].nextPageScrollY);
 
13000                 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
 
13001                 var lastTopScrolledID = RESStorage.getItem('RESmodules.neverEndingReddit.lastVisibleIndex.'+thisPageType);
 
13002                 var lastTopScrolledEle = document.body.querySelector('.'+lastTopScrolledID);
 
13003                 if (!lastTopScrolledEle) {
 
13004                         var lastTopScrolledEle = newHTML.querySelector('#siteTable div.thing');
 
13006                 var thisXY=RESUtils.getXYpos(lastTopScrolledEle);
 
13007                 RESUtils.scrollTo(0, thisXY.y);
 
13008                 modules['neverEndingReddit'].fromBackButton = false;
 
13010         NERFail: function(noresults) {
 
13011                 modules['neverEndingReddit'].isLoading = false;
 
13012                 var newHTML = createElementWithID('div','NERFail');
 
13014                         $(newHTML).html('Reddit has responded "there doesn\'t seem to be anything here." - this sometimes happens after several pages as votes shuffle posts up and down. You\'ll have to <a href="'+location.href.split('#')[0]+'">start from the beginning.</a>  If you like, you can try loading <a target="_blank" href="'+modules['neverEndingReddit'].nextPageURL+'">the same URL that RES tried to load.</a> If you are interested, there is a <a target="_blank" href="http://www.reddit.com/r/Enhancement/comments/s72xt/never_ending_reddit_and_reddit_barfing_explained/">technical explanation here</a>.');
 
13015                         newHTML.setAttribute('style','cursor: auto !important;');
 
13017                         $(newHTML).text('It appears Reddit is under heavy load or has barfed for some other reason, so Never Ending Reddit couldn\'t load the next page. Click here to try to load the page again.');
 
13018                         newHTML.addEventListener('click', function(e) {
 
13019                                 modules['neverEndingReddit'].attachLoaderWidget();
 
13020                                 modules['neverEndingReddit'].loadNewPage(false, true);
 
13021                                 e.target.parentNode.removeChild(e.target);
 
13022                                 e.target.textContent = 'Loading... or trying, anyway...';
 
13025                 modules['neverEndingReddit'].siteTable.appendChild(newHTML);
 
13026                 modules['neverEndingReddit'].modalWidget.style.display = 'none';
 
13027                 modules['neverEndingReddit'].modalContent.style.display = 'none';
 
13029         showFloat: function(show) {
 
13031                         this.NREFloat.style.display = 'block';
 
13033                         this.NREFloat.style.display = 'none';
 
13038 modules['saveComments'] = {
 
13039         moduleID: 'saveComments',
 
13040         moduleName: 'Save Comments',
 
13041         category: 'Comments',
 
13043                 // any configurable options you have go here...
 
13044                 // options must have a type and a value.. 
 
13045                 // valid types are: text, boolean (if boolean, value must be true or false)
 
13048         description: 'Save Comments saves the full text of comments locally in your browser, unlike Reddit\'s "save" feature, which saves a link to the comment.',
 
13049         isEnabled: function() {
 
13050                 return RESConsole.getModulePrefs(this.moduleID);
 
13053                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
 
13056                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*\/submit\/?/i,
 
13057                 /^https?:\/\/([a-z]+)\.reddit\.com\/submit\/?/i
 
13059         isMatchURL: function() {
 
13060                 return RESUtils.isMatchURL(this.moduleID);
 
13062         beforeLoad: function() {
 
13063                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
13064                         RESUtils.addCSS('.RES-save { cursor: help; }');
 
13068                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
13069                         var currURL = location.href;
 
13070                         var commentsRegex = /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*comments\/[-\w\.\/]*/i;
 
13071                         var savedRegex = /^https?:\/\/([a-z]+)\.reddit\.com\/user\/([-\w]+)\/saved\/?/i;
 
13072                         if (commentsRegex.test(currURL)) {
 
13073                                 // load already-saved comments into memory...
 
13074                                 this.loadSavedComments();
 
13075                                 this.addSaveLinks();
 
13076                                 $('body').on('click', 'li.saveComments', function(e) {
 
13077                                         e.preventDefault();
 
13078                                         modules['saveComments'].saveComment(this, this.getAttribute('saveID'), this.getAttribute('saveLink'), this.getAttribute('saveUser'));
 
13080                                 $('body').on('click', 'li.unsaveComments', function(e) {
 
13081                                         // e.preventDefault();
 
13082                                         var id = this.getAttribute('unsaveID');
 
13083                                         modules['saveComments'].unsaveComment(id, this);
 
13085                         } else if (savedRegex.test(currURL)) {
 
13086                                 // load already-saved comments into memory...
 
13087                                 this.loadSavedComments();
 
13088                                 this.addSavedCommentsTab();
 
13089                                 this.drawSavedComments();
 
13090                                 if (location.hash === '#comments') {
 
13091                                         this.showSavedTab('comments');
 
13094                                 this.addSavedCommentsTab();
 
13096                         // Watch for any future 'reply' forms, or stuff loaded in via "load more comments"...
 
13098                         document.body.addEventListener(
 
13100                                 function( event ) {
 
13101                                         if ((event.target.tagName === 'DIV') && (hasClass(event.target,'thing'))) {
 
13102                                                 modules['saveComments'].addSaveLinks(event.target);
 
13108                         RESUtils.watchForElement('newComments', modules['saveComments'].addSaveLinks);
 
13111         addSaveLinks: function(ele) {
 
13112                 if (!ele) var ele = document.body;
 
13113                 var allComments = ele.querySelectorAll('div.commentarea > div.sitetable > div.thing div.entry div.noncollapsed');
 
13114                 RESUtils.forEachChunked(allComments, 15, 1000, function(comment, i, array) {
 
13115                         modules['saveComments'].addSaveLinkToComment(comment);
 
13118         addSaveLinkToComment: function(commentObj) {
 
13119                 var commentsUL = commentObj.querySelector('ul.flat-list');
 
13120                 var permaLink = commentsUL.querySelector('li.first a.bylink');
 
13121                 if (permaLink !== null) {
 
13122                         // if there's no 'parent' link, then we don't want to put the save link before 'lastchild', we need to move it one to the left..
 
13123                         // note that if the user is not logged in, there is no next link for first level comments... set to null!
 
13124                         if (RESUtils.loggedInUser()) {
 
13125                                 if (permaLink.parentNode.nextSibling !== null) {
 
13126                                         if (typeof permaLink.parentNode.nextSibling.firstChild.getAttribute !== 'undefined') {
 
13127                                                 var nextLink = permaLink.parentNode.nextSibling.firstChild.getAttribute('href');
 
13129                                                 var nextLink = null;
 
13132                                         var nextLink = null;
 
13135                                 var nextLink = null;
 
13137                         var isTopLevel = ((nextLink == null) || (nextLink.indexOf('#') === -1));
 
13138                         var userLink = commentObj.querySelector('a.author');
 
13139                         if (userLink == null) {
 
13140                                 var saveUser = '[deleted]';
 
13142                                 var saveUser = userLink.text;
 
13144                         var saveHREF = permaLink.getAttribute('href');
 
13145                         var splitHref = saveHREF.split('/');
 
13146                         var saveID = splitHref[splitHref.length-1];
 
13147                         var saveLink = document.createElement('li');
 
13148                         if ((typeof this.storedComments !== 'undefined') && (typeof this.storedComments[saveID] !== 'undefined')) {
 
13149                                 $(saveLink).html('<a class="RES-saved" href="/saved#comments">saved-RES</a>');
 
13151                                 $(saveLink).html('<a class="RES-save" href="javascript:void(0);" title="Save using RES - which is local only, but preserves the full text in case someone edits/deletes it">save-RES</a>');
 
13152                                 saveLink.setAttribute('class',  'saveComments');
 
13153                                 saveLink.setAttribute('saveID',saveID);
 
13154                                 saveLink.setAttribute('saveLink',saveHREF);
 
13155                                 saveLink.setAttribute('saveUser',saveUser);
 
13157                         var whereToInsert = commentsUL.lastChild;
 
13158                         if (isTopLevel) whereToInsert = whereToInsert.previousSibling;
 
13159                         commentsUL.insertBefore(saveLink, whereToInsert);
 
13162         loadSavedComments: function() {
 
13163                 // first, check if we're storing saved comments the old way (as an array)...
 
13164                 var thisComments = RESStorage.getItem('RESmodules.saveComments.savedComments');
 
13165                 if (thisComments == null) {
 
13166                         this.storedComments = {};
 
13168                         this.storedComments = safeJSON.parse(thisComments, 'RESmodules.saveComments.savedComments');
 
13169                         // old way of storing saved comments... convert...
 
13170                         if (thisComments.slice(0,1) === '[') {
 
13171                                 var newFormat = {};
 
13172                                 for (var i in this.storedComments) {
 
13173                                         var urlSplit = this.storedComments[i].href.split('/');
 
13174                                         var thisID = urlSplit[urlSplit.length-1];
 
13175                                         newFormat[thisID] = this.storedComments[i];
 
13177                                 this.storedComments = newFormat;
 
13178                                 RESStorage.setItem('RESmodules.saveComments.savedComments',JSON.stringify(newFormat));
 
13182         saveComment: function(obj, id, href, username, comment) {
 
13183                 // reload saved comments in case they've been updated in other tabs (works in all but greasemonkey)
 
13184                 this.loadSavedComments();
 
13185                 // loop through comments and make sure we haven't already saved this one...
 
13186                 if (typeof this.storedComments[id] !== 'undefined') {
 
13187                         alert('comment already saved!');
 
13189                         if (modules['keyboardNav'].isEnabled()) {
 
13190                                 // unfocus it before we save it so we don't save the keyboard annotations...
 
13191                                 modules['keyboardNav'].keyUnfocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
 
13193                         var comment = obj.parentNode.parentNode.querySelector('div.usertext-body > div.md');
 
13194                         if (comment !== null) {
 
13195                                 commentHTML = comment.innerHTML;
 
13196                                 var savedComment = {
 
13198                                         username: username,
 
13199                                         comment: commentHTML,
 
13202                                 this.storedComments[id] = savedComment;
 
13203                                 var unsaveObj = document.createElement('li');
 
13204                                 $(unsaveObj).html('<a href="javascript:void(0);">unsave-RES</a>');
 
13205                                 unsaveObj.setAttribute('unsaveID',id);
 
13206                                 unsaveObj.setAttribute('unsaveLink',href);
 
13207                                 unsaveObj.setAttribute('class','unsaveComments');
 
13209                                 obj.parentNode.replaceChild(unsaveObj, obj);
 
13211                         if (modules['keyboardNav'].isEnabled()) {
 
13212                                 modules['keyboardNav'].keyFocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
 
13214                         if (RESUtils.proEnabled()) {
 
13215                                 // add sync adds/deletes for RES Pro.
 
13216                                 if (typeof this.storedComments.RESPro_add === 'undefined') {
 
13217                                         this.storedComments.RESPro_add = {}
 
13219                                 if (typeof this.storedComments.RESPro_delete === 'undefined') {
 
13220                                         this.storedComments.RESPro_delete = {}
 
13222                                 // add this ID next time we sync...
 
13223                                 this.storedComments.RESPro_add[id] = true;
 
13224                                 // make sure we don't run a delete on this ID next time we sync...
 
13225                                 if (typeof this.storedComments.RESPro_delete[id] !== 'undefined') delete this.storedComments.RESPro_delete[id];
 
13227                         RESStorage.setItem('RESmodules.saveComments.savedComments', JSON.stringify(this.storedComments));
 
13228                         if (RESUtils.proEnabled()) {
 
13229                                 modules['RESPro'].authenticate(function() {
 
13230                                         modules['RESPro'].saveModuleData('saveComments');
 
13235         addSavedCommentsTab: function() {
 
13236                 var mainmenuUL = document.body.querySelector('#header-bottom-left ul.tabmenu');
 
13238                         var savedRegex = /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w]+\/saved\/?/i;
 
13239                         var menuItems = mainmenuUL.querySelectorAll('li');
 
13240                         var thisUser = RESUtils.loggedInUser() || '';
 
13241                         for (var i=0, len=menuItems.length;i<len;i++) {
 
13242                                 var savedLink = menuItems[i].querySelector('a');
 
13243                                 if ((menuItems[i].classList.contains('selected')) && (savedRegex.test(savedLink.href))) {
 
13244                                         menuItems[i].addEventListener('click', function(e) {
 
13245                                                 e.preventDefault();
 
13246                                                 modules['saveComments'].showSavedTab('links');
 
13250                                 if (savedRegex.test(savedLink.href)) {
 
13251                                         $(menuItems[i]).attr('id', 'savedLinksTab');
 
13252                                         savedLink.textContent = 'saved links';
 
13256                         var savedCommentsTab = $('<li id="savedCommentsTab">')
 
13257                                 .html('<a href="javascript:void(0);">saved comments</a>')
 
13258                                 .insertAfter('#savedLinksTab');
 
13259                         if (savedRegex.test(location.href)) {
 
13260                                 $('#savedCommentsTab').click(function(e) {
 
13261                                         e.preventDefault();
 
13262                                         modules['saveComments'].showSavedTab('comments');
 
13265                                 $('#savedCommentsTab').click(function(e) {
 
13266                                         e.preventDefault();
 
13267                                         location.href = location.protocol + '//www.reddit.com/saved/#comments';
 
13272         showSavedTab: function(tab) {
 
13275                                 location.hash = 'links';
 
13276                                 this.savedLinksContent.style.display = 'block';
 
13277                                 this.savedCommentsContent.style.display = 'none';
 
13278                                 $('#savedLinksTab').addClass('selected');
 
13279                                 $('#savedCommentsTab').removeClass('selected');
 
13282                                 location.hash = 'comments';
 
13283                                 this.savedLinksContent.style.display = 'none';
 
13284                                 this.savedCommentsContent.style.display = 'block';
 
13285                                 $('#savedLinksTab').removeClass('selected');
 
13286                                 $('#savedCommentsTab').addClass('selected');
 
13290         drawSavedComments: function() {
 
13291                 RESUtils.addCSS('.savedComment { padding: 5px; font-size: 12px; margin-bottom: 20px; margin-left: 40px; margin-right: 10px; border: 1px solid #CCC; border-radius: 10px; width: auto; } ');
 
13292                 RESUtils.addCSS('.savedCommentHeader { margin-bottom: 8px; }');
 
13293                 RESUtils.addCSS('.savedCommentBody { margin-bottom: 8px; }');
 
13294                 RESUtils.addCSS('#savedLinksList { margin-top: 10px; }');
 
13295                 // css += '.savedCommentFooter {  }';
 
13296                 this.savedLinksContent = document.body.querySelector('BODY > div.content');
 
13297                 this.savedCommentsContent = createElementWithID('div', 'savedLinksList');
 
13298                 this.savedCommentsContent.style.display = 'none';
 
13299                 this.savedCommentsContent.setAttribute('class','sitetable linklisting');
 
13300                 for (var i in this.storedComments) {
 
13301                         if ((i !== 'RESPro_add') && (i !== 'RESPro_delete')) {
 
13302                                 var clearLeft = document.createElement('div');
 
13303                                 clearLeft.setAttribute('class','clearleft');
 
13304                                 var thisComment = document.createElement('div');
 
13305                                 thisComment.classList.add('savedComment');
 
13306                                 thisComment.classList.add('entry');
 
13307                                 // this is all saved locally, but just for safety, we'll clean out any script tags and whatnot...
 
13308                                 var cleanHTML = '<div class="savedCommentHeader">Comment by user: ' + escapeHTML(this.storedComments[i].username) + ' saved on ' + escapeHTML(this.storedComments[i].timeSaved) + '</div>';
 
13309                                 cleanHTML += '<div class="savedCommentBody">' + this.storedComments[i].comment.replace(/<script(.|\s)*?\/script>/g, '') + '</div>';
 
13310                                 cleanHTML += '<div class="savedCommentFooter"><ul class="flat-list buttons"><li><a class="unsaveComment" href="javascript:void(0);">unsave-RES</a></li><li><a href="' + escapeHTML(this.storedComments[i].href) + '">view original</a></li></ul></div>'
 
13311                                 $(thisComment).html(cleanHTML);
 
13312                                 var unsaveLink = thisComment.querySelector('.unsaveComment');
 
13313                                 unsaveLink.setAttribute('unsaveID', i);
 
13314                                 unsaveLink.setAttribute('unsaveLink', this.storedComments[i].href);
 
13315                                 unsaveLink.addEventListener('click', function(e) {
 
13316                                         e.preventDefault();
 
13317                                         modules['saveComments'].unsaveComment(this.getAttribute('unsaveID'));
 
13319                                 this.savedCommentsContent.appendChild(thisComment);
 
13320                                 this.savedCommentsContent.appendChild(clearLeft);
 
13323                 if (this.storedComments.length === 0) {
 
13324                         $(this.savedCommentsContent).html('<li>You have not yet saved any comments.</li>');
 
13326                 insertAfter(this.savedLinksContent, this.savedCommentsContent);
 
13328         unsaveComment: function(id, unsaveLink) {
 
13330                 var newStoredComments = [];
 
13331                 for (var i=0, len=this.storedComments.length;i<len;i++) {
 
13332                         if (this.storedComments[i].href != href) {
 
13333                                 newStoredComments.push(this.storedComments[i]);
 
13335                                 // console.log('found match. deleted comment');
 
13338                 this.storedComments = newStoredComments;
 
13340                 delete this.storedComments[id];
 
13341                 if (RESUtils.proEnabled()) {
 
13342                         // add sync adds/deletes for RES Pro.
 
13343                         if (typeof this.storedComments.RESPro_add === 'undefined') {
 
13344                                 this.storedComments.RESPro_add = {}
 
13346                         if (typeof this.storedComments.RESPro_delete === 'undefined') {
 
13347                                 this.storedComments.RESPro_delete = {}
 
13349                         // delete this ID next time we sync...
 
13350                         this.storedComments.RESPro_delete[id] = true;
 
13351                         // make sure we don't run an add on this ID next time we sync...
 
13352                         if (typeof this.storedComments.RESPro_add[id] !== 'undefined') delete this.storedComments.RESPro_add[id];
 
13354                 RESStorage.setItem('RESmodules.saveComments.savedComments', JSON.stringify(this.storedComments));
 
13355                 if (RESUtils.proEnabled()) {
 
13356                         modules['RESPro'].authenticate(function() {
 
13357                                 modules['RESPro'].saveModuleData('saveComments');
 
13360                 if (typeof this.savedCommentsContent !== 'undefined') {
 
13361                         this.savedCommentsContent.parentNode.removeChild(this.savedCommentsContent);
 
13362                         this.drawSavedComments();
 
13363                         this.showSavedTab('comments');
 
13365                         var commentObj = unsaveLink.parentNode.parentNode;
 
13366                         unsaveLink.parentNode.removeChild(unsaveLink);
 
13367                         this.addSaveLinkToComment(commentObj);
 
13372 modules['userHighlight'] = {
 
13373         moduleID: 'userHighlight',
 
13374         moduleName: 'User Highlighter',
 
13376         description: 'Highlights certain users in comment threads: OP, Admin, Friends, Mod - contributed by MrDerk',
 
13381                         description: 'Highlight OP\'s comments'
 
13386                         description: 'Color to use to highlight OP. Defaults to original text color'
 
13391                         description: 'Color used to highlight OP on hover.'
 
13396                         description: 'Highlight Admin\'s comments'
 
13401                         description: 'Color to use to highlight Admins. Defaults to original text color'
 
13406                         description: 'Color used to highlight Admins on hover.'
 
13411                         description: 'Highlight Friends\' comments'
 
13416                         description: 'Color to use to highlight Friends. Defaults to original text color'
 
13418                 friendColorHover: {
 
13421                         description: 'Color used to highlight Friends on hover.'
 
13426                         description: 'Highlight Mod\'s comments'
 
13431                         description: 'Color to use to highlight Mods. Defaults to original text color'
 
13436                         description: 'Color used to highlight Mods on hover. Defaults to gray.'
 
13438                 highlightFirstCommenter: {
 
13441                         description: 'Highlight the person who has the first comment in a tree, within that tree'
 
13443                 firstCommentColor: {
 
13446                         description: 'Color to use to highlight the first-commenter. Defaults to original text color'
 
13448                 firstCommentColorHover: {
 
13451                         description: 'Color used to highlight the first-commenter on hover.'
 
13456                         description: 'Color for highlighted text.'
 
13458                 autoColorUsernames: {
 
13461                         description: 'Set a unique color for each username'
 
13464         isEnabled: function() {
 
13465                 return RESConsole.getModulePrefs(this.moduleID);
 
13468                 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
 
13470         isMatchURL: function() {
 
13471                 return RESUtils.isMatchURL(this.moduleID);
 
13474                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
13475                         this.findDefaults();
 
13476                         if (this.options.highlightOP.value)             this.doHighlight('submitter');
 
13477                         if (this.options.highlightFriend.value) this.doHighlight('friend');
 
13478                         if (this.options.highlightMod.value)    this.doHighlight('moderator');
 
13479                         if (this.options.highlightAdmin.value)  this.doHighlight('admin');
 
13481                         if (this.options.autoColorUsernames.value) {
 
13482                                 RESUtils.watchForElement('newComments', this.scanPageForNewUsernames);
 
13483                                 RESUtils.watchForElement('siteTable', this.scanPageForNewUsernames);
 
13484                                 this.scanPageForNewUsernames();
 
13487                         if (this.options.highlightFirstCommenter.value) {
 
13488                                 RESUtils.watchForElement('newComments', this.scanPageForFirstComments);
 
13489                                 this.scanPageForFirstComments();
 
13493         findDefaults: function() {
 
13494                 var dummy = $('<div style="height: 0px;" id="dummy" class="tagline">\
 
13495                         <a class="author submitter">submitter</a>\
 
13496                         <a class="author friend">friend</a>\
 
13497                         <a class="author moderator">moderator</a>\
 
13498                         <a class="author admin">admin</a>\
 
13500                 $(document.body).append(dummy);
 
13501                 this.colorTable = {
 
13503                                 default: RESUtils.getComputedStyle('#dummy .author.submitter', 'color'),
 
13504                                 color: this.options.OPColor.value,
 
13505                                 hoverColor: this.options.OPColorHover.value
 
13508                                 default: RESUtils.getComputedStyle('#dummy .author.friend', 'color'),
 
13509                                 color: this.options.friendColor.value,
 
13510                                 hoverColor: this.options.friendColorHover.value
 
13513                                 default: RESUtils.getComputedStyle('#dummy .author.moderator', 'color'),
 
13514                                 color: this.options.modColor.value,
 
13515                                 hoverColor: this.options.modColorHover.value
 
13518                                 default: RESUtils.getComputedStyle('#dummy .author.admin', 'color'),
 
13519                                 color: this.options.adminColor.value,
 
13520                                 hoverColor: this.options.adminColorHover.value
 
13523                                 default: '#5544CC',
 
13524                                 color: modules['userTagger'].options['highlightColor'].value,
 
13525                                 hoverColor: modules['userTagger'].options['highlightColorHover'].value,
 
13528                                 default: '#46B6CC',
 
13529                                 color: this.options.firstCommentColor.value,
 
13530                                 hoverColor: this.options.firstCommentColorHover.value
 
13533                 $('#dummy').detach();
 
13535         scanPageForFirstComments: function (ele) {
 
13537                         ? $(ele).closest('.commentarea > .sitetable > .thing')
 
13538                         : document.body.querySelectorAll('.commentarea > .sitetable > .thing');
 
13541                 RESUtils.forEachChunked(comments, 15, 1000, function(element, i, array) {
 
13544                         for (var i = 0, length = element.classList.length; i < length; i++) {
 
13545                                 idClass = element.classList[i];
 
13546                                 if (idClass.substring(0, 6) === 'id-t1_') break;
 
13549                         if (modules['userHighlight'].firstComments[idClass]) return;
 
13551                         var author = element.querySelector('.author');
 
13552                         if (!author) return;
 
13554                         for (var i = 0, length = author.classList.length; i < length; i++) {
 
13555                                 authorClass = author.classList[i];
 
13556                                 if (authorClass.substring(0, 6) === 'id-t2_') break;
 
13559                         var authorDidReply = element.querySelector('.child .' + authorClass);
 
13560                         if (!authorDidReply) return;
 
13562                         modules['userHighlight'].firstComments[idClass] = true;
 
13563                         modules['userHighlight'].doHighlight('firstComment', authorClass, '.' + idClass);
 
13567         scanPageForNewUsernames: function (ele) {
 
13568                 ele = ele || document.body;
 
13569                 var authors = ele.querySelectorAll('.author');
 
13570                 RESUtils.forEachChunked(authors, 15, 1000, function(element, i, array) {
 
13573                         for (var i = 0, length = element.classList.length; i < length; i++) {
 
13574                                 idClass = element.classList[i];
 
13575                                 if (idClass.substring(0, 6) === 'id-t2_') break;
 
13578                         if (modules['userHighlight'].coloredUsernames[idClass]) return;
 
13581                         var hash = 5381, str = idClass;
 
13582                         for (var i = 0; i < str.length; i++) {
 
13583                                 hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */
 
13586                         var r = (hash & 0xFF0000) >> 16;
 
13587                         var g = (hash & 0x00FF00) >> 8;
 
13588                         var b = hash & 0x0000FF;
 
13589                         var color = "rgb(" + [ r, g, b ].join(',') + ")";
 
13592                         modules['userHighlight'].doTextColor('.' + idClass, color);
 
13595         coloredUsernames: {},
 
13596         highlightUser: function (username) {
 
13597                 var name = 'author[href$="/' + username + '"]';  // yucky, but it'll do
 
13598                 return this.doHighlight('user', name);
 
13600         doHighlight: function(name, selector, container) {
 
13601                 if (selector == undefined) {
 
13604                 if (container == undefined) {
 
13607                 var color, hoverColor;
 
13608                 var color = this.colorTable[name].color;
 
13609                 if (color === 'default') this.colorTable[name].default;
 
13611                 var hoverColor = this.colorTable[name].hoverColor;
 
13612                 if (hoverColor === 'default') hoverColor = '#aaa';
 
13614                         ' + container + ' .author.' + selector + ' { \
 
13615                                 color: ' + this.options.fontColor.value + ' !important; \
 
13616                                 font-weight: bold; \
 
13617                                 padding: 0 2px 0 2px; \
 
13618                                 border-radius: 3px; \
 
13619                                 background-color:' + color + ' !important; \
 
13621                         ' + container + ' .collapsed .author.' + selector + ' { \
 
13622                                 color: white !important; \
 
13623                                 background-color: #AAA !important; \
 
13625                         ' + container + ' .author.' + selector + ':hover {\
 
13626                                 background-color: ' + hoverColor + ' !important; \
 
13627                                 text-decoration: none !important; \
 
13629                 return RESUtils.addCSS(css);
 
13631         doTextColor: function (selector, color) {
 
13633                         .tagline .author' + selector + ' {      \
 
13634                                 color: ' + color + ' !important;        \
 
13637                 return RESUtils.addCSS(css);
 
13641 modules['styleTweaks'] = {
 
13642         moduleID: 'styleTweaks',
 
13643         moduleName: 'Style Tweaks',
 
13645         description: 'Provides a number of style tweaks to the Reddit interface',
 
13650                         description: 'Moves the username navbar to the top (great on netbooks!)'
 
13655                         description: 'Highlights comment boxes for easier reading / placefinding in large threads.'
 
13657                 /* REMOVED for performance reasons...
 
13658                 commentBoxShadows: {
 
13661                         description: 'Drop shadows on comment boxes (turn off for faster performance)'
 
13667                         description: 'Round corners of comment boxes'
 
13669                 commentHoverBorder: {
 
13672                         description: 'Highlight comment box hierarchy on hover (turn off for faster performance)'
 
13677                         description: 'Indent comments by [x] pixels (only enter the number, no \'px\')'
 
13682                         description: 'Show comment continuity lines'
 
13687                         description: 'Enable lightswitch (toggle between light / dark reddit)'
 
13692                                 { name: 'Light', value: 'light' },
 
13693                                 { name: 'Dark', value: 'dark' }
 
13696                         description: 'Light, or dark?'
 
13701                         description: 'Reddit makes it so no links on comment pages appear as "visited" - including user profiles. This option undoes that.'
 
13706                         description: 'Bring back video and text expando buttons for users with compressed link display'
 
13711                         description: 'Hide vote arrows on threads where you cannot vote (e.g. archived due to age)'
 
13713                 colorBlindFriendly: {
 
13716                         description: 'Use colorblind friendly styles when possible'
 
13718                 scrollSubredditDropdown: {
 
13721                         description: 'Scroll the standard subreddit dropdown (useful for pinned header and disabled Subreddit Manager)'
 
13724         isEnabled: function() {
 
13725                 return RESConsole.getModulePrefs(this.moduleID);
 
13728                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
 
13730         isMatchURL: function() {
 
13731                 return RESUtils.isMatchURL(this.moduleID);
 
13733         beforeLoad: function() {
 
13734                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
13735                         if (RESUtils.currentSubreddit()) {
 
13736                                 this.curSubReddit = RESUtils.currentSubreddit().toLowerCase();
 
13739                         this.styleCBName = RESUtils.randomHash();
 
13740                         RESUtils.addCSS('body.res .side .spacer .titlebox div #'+this.styleCBName+':before { display: none !important;  }');
 
13741                         RESUtils.addCSS('body.res .side .spacer .titlebox div #label-'+this.styleCBName+':before { display: none !important; }');
 
13742                         RESUtils.addCSS('body.res .side .spacer .titlebox div #'+this.styleCBName+':after { display: none !important;  }');
 
13743                         RESUtils.addCSS('body.res .side .spacer .titlebox div #label-'+this.styleCBName+':after { display: none !important; }');
 
13745                         // In firefox, we need to style tweet expandos because they can't take advantage of twitter.com's widget.js
 
13746                         if (BrowserDetect.isFirefox()) {
 
13747                                 RESUtils.addCSS('.res blockquote.twitter-tweet { padding: 15px; border-left: 5px solid #ccc; font-size: 14px; line-height: 20px; }');
 
13748                                 RESUtils.addCSS('.res blockquote.twitter-tweet p { margin-bottom: 15px; }');
 
13751                         if (this.options.colorBlindFriendly.value) {
 
13752                                 document.html.classList.add('res-colorblind');
 
13754                         // if night mode is enabled, set a localstorage token so that in the future,
 
13755                         // we can add the res-nightmode class to the page prior to page load.
 
13756                         if (this.options.lightOrDark.value === 'dark') {
 
13757                                 localStorage.setItem('RES_nightMode', true);
 
13760                         // wow, Reddit doesn't define a visited class for any links on comments pages...
 
13761                         // let's put that back if users want it back.
 
13762                         // If not, we still need a visited class for links in comments, like imgur photos for example, or inline image viewer can't make them look different when expanded!
 
13763                         if (this.options.visitedStyle.value) {
 
13764                                 RESUtils.addCSS(".comment a:visited { color:#551a8b }");
 
13766                                 RESUtils.addCSS(".comment .md p > a:visited { color:#551a8b }");
 
13768                         if (this.options.showExpandos.value) {
 
13769                                 RESUtils.addCSS('.compressed .expando-button { display: block !important; }');
 
13771                         if ((this.options.commentBoxes.value) && (RESUtils.pageType() === 'comments')) {
 
13772                                 this.commentBoxes();
 
13774                         if (this.options.hideUnvotable.value) {
 
13775                                 RESUtils.addCSS('.unvoted .arrow[onclick*=unvotable] { visibility: hidden }');
 
13776                                 RESUtils.addCSS('.voted .arrow[onclick*=unvotable] { cursor: normal; }');
 
13781                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
13783                         // get the head ASAP!
 
13784                         this.head = document.getElementsByTagName("head")[0];
 
13786                         // handle night mode scenarios (check if subreddit is compatible, etc)
 
13787                         this.handleNightModeAtStart();
 
13789                         // get rid of antequated option we've removed (err, renamed) due to performance issues.
 
13790                         if (typeof this.options.commentBoxHover !== 'undefined') {
 
13791                                 delete this.options.commentBoxHover;
 
13792                                 RESStorage.setItem('RESoptions.styleTweaks', JSON.stringify(modules['styleTweaks'].options));
 
13794                         if (this.options.lightOrDark.value === 'dark') {
 
13795                                 // still add .res-nightmode to body just in case subreddit stylesheets specified body.res-nightmode instead of just .res-nightmode
 
13796                                 document.body.classList.add('res-nightmode');
 
13798                         if (this.options.navTop.value) {
 
13801                         if (this.options.lightSwitch.value) {
 
13802                                 this.lightSwitch();
 
13804                         if (this.options.colorBlindFriendly.value) {
 
13805                                 var orangered = document.body.querySelector('#mail');
 
13806                                 if ((orangered) && (orangered.classList.contains('havemail'))) {
 
13807                                         orangered.setAttribute('style','background-image: url(http://thumbs.reddit.com/t5_2s10b_5.png); background-position: 0 0;');
 
13810                         if (this.options.scrollSubredditDropdown.value) {
 
13811                                 var calcedHeight = Math.floor(window.innerHeight * 0.95);
 
13812                                 if( $('.drop-choices.srdrop').height() > calcedHeight ) {
 
13813                                         RESUtils.addCSS('.drop-choices.srdrop { \
 
13814                                                 overflow-y:scroll; \
 
13815                                                 max-height:' + calcedHeight + 'px; \
 
13819                         if (this.options.showExpandos.value) {
 
13820                                 RESUtils.addCSS('.compressed .expando-button { display: block !important; }');
 
13821                                 var twitterLinks = document.body.querySelectorAll('.entry > p.title > a.title');
 
13822                                 var isTwitterLink = /twitter.com\/(?:#!\/)?([\w]+)\/(status|statuses)\/([\d]+)/i;
 
13823                                 for (var i=0, len = twitterLinks.length; i<len; i++) {
 
13824                                         var thisHref = twitterLinks[i].getAttribute('href');
 
13825                                         thisHref = thisHref.replace('/#!','');
 
13826                                         if (isTwitterLink.test(thisHref)) {
 
13827                                                 var thisExpandoButton = document.createElement('div');
 
13828                                                 thisExpandoButton.setAttribute('class','expando-button collapsed collapsedExpando selftext twitter');
 
13829                                                 thisExpandoButton.addEventListener('click',modules['styleTweaks'].toggleTweetExpando,false);
 
13830                                                 insertAfter(twitterLinks[i].parentNode, thisExpandoButton);
 
13834                         this.userbarHider();
 
13835                         this.subredditStyles();
 
13838         handleNightModeAtStart: function() {
 
13839                 this.nightModeWhitelist = [];
 
13840                 var getWhitelist = RESStorage.getItem('RESmodules.styleTweaks.nightModeWhitelist');
 
13841                 if (getWhitelist) {
 
13842                         this.nightModeWhitelist = safeJSON.parse(getWhitelist, 'RESmodules.styleTweaks.nightModeWhitelist');
 
13844                 var idx = this.nightModeWhitelist.indexOf(this.curSubReddit);
 
13846                         // go no further. this subreddit is whitelisted.
 
13850                 // check the sidebar for a link [](#/RES_SR_Config/NightModeCompatible) that indicates the sub is night mode compatible.
 
13851                 this.isNightmodeCompatible = (document.querySelector('.side a[href="#/RES_SR_Config/NightModeCompatible"]') !== null);
 
13853                 // if night mode is on and the sub isn't compatible, disable its stylesheet.
 
13854                 if (this.isDark && !this.isNightmodeCompatible) {
 
13855                         this.disableSubredditStyle();
 
13859         toggleTweetExpando: function(e) {
 
13860                 var thisExpando = e.target.nextSibling.nextSibling.nextSibling;
 
13861                 if (e.target.classList.contains('collapsedExpando')) {
 
13862                         $(e.target).removeClass('collapsedExpando collapsed').addClass('expanded');
 
13863                         if (thisExpando.classList.contains('twitterLoaded')) {
 
13864                                 thisExpando.style.display = 'block';
 
13867                         var twitterLink = e.target.previousSibling.querySelector('.title');
 
13868                         if (twitterLink) twitterLink = twitterLink.getAttribute('href').replace('/#!','');
 
13869                         var match = twitterLink.match(/twitter.com\/[^\/]+\/(?:status|statuses)\/([\d]+)/i);
 
13870                         if (match !== null) {
 
13871                                 // var jsonURL = 'http://api.twitter.com/1/statuses/show/'+match[1]+'.json';
 
13872                                 var jsonURL = 'http://api.twitter.com/1/statuses/oembed.json?id='+match[1];
 
13873                                 if (BrowserDetect.isChrome()) {
 
13874                                         // we've got chrome, so we need to hit up the background page to do cross domain XHR
 
13876                                                 requestType: 'loadTweet',
 
13879                                         chrome.extension.sendMessage(thisJSON, function(response) {
 
13880                                                 // send message to background.html 
 
13881                                                 var tweet = response;
 
13882                                                 $(thisExpando).html(tweet.html);
 
13883                                                 thisExpando.style.display = 'block';
 
13884                                                 thisExpando.classList.add('twitterLoaded');
 
13886                                 } else if (BrowserDetect.isSafari()) {
 
13887                                         // we've got safari, so we need to hit up the background page to do cross domain XHR
 
13888                                         modules['styleTweaks'].tweetExpando = thisExpando;
 
13890                                                 requestType: 'loadTweet',
 
13893                                         safari.self.tab.dispatchMessage("loadTweet", thisJSON);
 
13894                                 } else if (BrowserDetect.isOpera()) {
 
13895                                         // we've got opera, so we need to hit up the background page to do cross domain XHR
 
13896                                         modules['styleTweaks'].tweetExpando = thisExpando;
 
13898                                                 requestType: 'loadTweet',
 
13901                                         opera.extension.postMessage(JSON.stringify(thisJSON));
 
13902                                 } else if (BrowserDetect.isFirefox()) {
 
13903                                         // we've got a jetpack extension, hit up the background page...
 
13904                                         // we have to omit the script tag and all of the nice formatting it brings us in Firefox
 
13905                                         // because AMO does not permit externally hosted script tags being pulled in from
 
13906                                         // oEmbed like this...
 
13907                                         jsonURL += '&omit_script=true';
 
13908                                         modules['styleTweaks'].tweetExpando = thisExpando;
 
13910                                                 requestType: 'loadTweet',
 
13913                                         self.postMessage(thisJSON);
 
13915                                         GM_xmlhttpRequest({
 
13918                                                 target: thisExpando,
 
13919                                                 onload: function(response) {
 
13920                                                         var tweet = JSON.parse(response.responseText);
 
13921                                                         $(thisExpando).html('<form class="usertext"><div class="usertext-body"><div class="md"><div><img style="display: block;" src="'+escapeHTML(tweet.user.profile_image_url)+'"></div>' + escapeHTML(tweet.user.screen_name) + ': ' + escapeHTML(tweet.text) + '</div></div></form>');
 
13922                                                         thisExpando.style.display = 'block';
 
13928                         $(e.target).removeClass('expanded').addClass('collapsedExpando').addClass('collapsed');
 
13929                         thisExpando.style.display = 'none';
 
13933         navTop: function() {
 
13934                 RESUtils.addCSS('#header-bottom-right { top: 19px; border-radius: 0 0 0 3px; bottom: auto;  }');
 
13935                 RESUtils.addCSS('.beta-notice { top: 48px; }');
 
13936                 $('body, #header-bottom-right').addClass('res-navTop');
 
13938         userbarHider: function() {
 
13939                 RESUtils.addCSS("#userbarToggle { min-height: 22px; position: absolute; top: auto; bottom: 0; left: -5px; width: 16px; padding-right: 3px; height: 100%; font-size: 15px; border-radius: 4px 0; color: #a1bcd6; display: inline-block; background-color: #dfecf9; border-right: 1px solid #cee3f8; cursor: pointer; text-align: right; line-height: 24px; }");
 
13940                 RESUtils.addCSS("#userbarToggle.userbarShow { min-height: 26px; }");
 
13941                 RESUtils.addCSS("#header-bottom-right .user { margin-left: 16px; }");
 
13942                 // RESUtils.addCSS(".userbarHide { background-position: 0 -137px; }");
 
13943                 RESUtils.addCSS("#userbarToggle.userbarShow { left: -12px; }");
 
13944                 RESUtils.addCSS(".res-navTop #userbarToggle.userbarShow { top: 0; bottom: auto; }");
 
13945                 this.userbar = document.getElementById('header-bottom-right');
 
13946                 if (this.userbar) {
 
13947                         this.userbarToggle = createElementWithID('div','userbarToggle');
 
13948                         $(this.userbarToggle).html('»');
 
13949                         this.userbarToggle.setAttribute('title','Toggle Userbar');
 
13950                         this.userbarToggle.classList.add('userbarHide');
 
13951                         this.userbarToggle.addEventListener('click', function(e) {
 
13952                                 modules['styleTweaks'].toggleUserBar();
 
13954                         this.userbar.insertBefore(this.userbarToggle, this.userbar.firstChild);
 
13955                         // var currHeight = $(this.userbar).height();
 
13956                         // $(this.userbarToggle).css('height',currHeight+'px');
 
13957                         if (RESStorage.getItem('RESmodules.styleTweaks.userbarState') === 'hidden') {
 
13958                                 this.toggleUserBar();
 
13962         toggleUserBar: function() {
 
13963                 var nextEle = this.userbarToggle.nextSibling;
 
13965                 if (this.userbarToggle.classList.contains('userbarHide')) {
 
13966                         this.userbarToggle.classList.remove('userbarHide');
 
13967                         this.userbarToggle.classList.add('userbarShow');
 
13968                         $(this.userbarToggle).html('«');
 
13969                         RESStorage.setItem('RESmodules.styleTweaks.userbarState', 'hidden');
 
13970                         modules['accountSwitcher'].closeAccountMenu();
 
13971                         while ((typeof nextEle !== 'undefined') && (nextEle !== null)) {
 
13972                                 nextEle.style.display = 'none';
 
13973                                 nextEle = nextEle.nextSibling;
 
13977                         this.userbarToggle.classList.remove('userbarShow');
 
13978                         this.userbarToggle.classList.add('userbarHide');
 
13979                         $(this.userbarToggle).html('»');
 
13980                         RESStorage.setItem('RESmodules.styleTweaks.userbarState', 'visible');
 
13981                         while ((typeof nextEle !== 'undefined') && (nextEle !== null)) {
 
13982                         if ((/mail/.test(nextEle.className)) || (nextEle.id === 'openRESPrefs')) {
 
13983                                 nextEle.style.display = 'inline-block';
 
13985                                 nextEle.style.display = 'inline';
 
13987                                 nextEle = nextEle.nextSibling;
 
13991         commentBoxes: function() {
 
13992                 document.html.classList.add('res-commentBoxes');
 
13993                 if (this.options.commentRounded.value) {
 
13994                         document.html.classList.add('res-commentBoxes-rounded');
 
13996                 if (this.options.continuity.value) {
 
13997                         document.html.classList.add('res-continuity');
 
13999                 if (this.options.commentHoverBorder.value) {
 
14000                         document.html.classList.add('res-commentHoverBorder');
 
14002                 if (this.options.commentIndent.value) {
 
14003                         // this should override the default of 10px in commentboxes.css because it's added later.
 
14004                         RESUtils.addCSS('.res-commentBoxes .comment { margin-left:'+this.options.commentIndent.value+'px !important; }');
 
14007         lightSwitch: function() {
 
14008                 RESUtils.addCSS(".lightOn { background-position: 0 -96px; } ");
 
14009                 RESUtils.addCSS(".lightOff { background-position: 0 -108px; } ");
 
14010                 var thisFrag = document.createDocumentFragment();
 
14011                 this.lightSwitch = document.createElement('li');
 
14012                 this.lightSwitch.setAttribute('title',"Toggle night and day");
 
14013                 this.lightSwitch.addEventListener('click', function(e) {
 
14014                         e.preventDefault();
 
14015                         if (modules['styleTweaks'].isDark == true) {
 
14016                                 RESUtils.setOption('styleTweaks','lightOrDark','light');
 
14017                                 modules['styleTweaks'].lightSwitchToggle.classList.remove('enabled');
 
14018                                 modules['styleTweaks'].redditDark(true);
 
14020                                 RESUtils.setOption('styleTweaks','lightOrDark','dark');
 
14021                                 modules['styleTweaks'].lightSwitchToggle.classList.add('enabled');
 
14022                                 modules['styleTweaks'].redditDark();
 
14025                 // this.lightSwitch.setAttribute('id','lightSwitch');
 
14026                 this.lightSwitch.textContent = 'night mode';
 
14027                 this.lightSwitchToggle = createElementWithID('div','lightSwitchToggle','toggleButton');
 
14028                 $(this.lightSwitchToggle).html('<span class="toggleOn">on</span><span class="toggleOff">off</span>');
 
14029                 this.lightSwitch.appendChild(this.lightSwitchToggle);
 
14030                 (this.options.lightOrDark.value === 'dark') ? this.lightSwitchToggle.classList.add('enabled') : this.lightSwitchToggle.classList.remove('enabled');
 
14031                 // thisFrag.appendChild(separator);
 
14032                 thisFrag.appendChild(this.lightSwitch);
 
14033                 // if (RESConsole.RESPrefsLink) insertAfter(RESConsole.RESPrefsLink, thisFrag);
 
14034                 $('#RESDropdownOptions').append(this.lightSwitch);
 
14036         subredditStyles: function() {
 
14037                 if (! RESUtils.currentSubreddit()) return;
 
14038                 this.ignoredSubReddits = [];
 
14039                 var getIgnored = RESStorage.getItem('RESmodules.styleTweaks.ignoredSubredditStyles');
 
14041                         this.ignoredSubReddits = safeJSON.parse(getIgnored, 'RESmodules.styleTweaks.ignoredSubredditStyles');
 
14043                 var subredditTitle = document.querySelector('.titlebox h1');
 
14044                 this.styleToggleContainer = document.createElement('div');
 
14045                 this.styleToggleLabel = document.createElement('label');
 
14046                 this.styleToggleCheckbox = document.createElement('input');
 
14047                 this.styleToggleCheckbox.setAttribute('type','checkbox');
 
14048                 this.styleToggleCheckbox.setAttribute('id',this.styleCBName);
 
14049                 this.styleToggleCheckbox.setAttribute('name',this.styleCBName);
 
14051                 // are we blacklisting, or whitelisting subreddits?  If we're in night mode on a sub that's
 
14052                 // incompatible with it, we want to check the whitelist. Otherwise, check the blacklist.
 
14054                 if ((this.curSubReddit !== null) && (subredditTitle !== null)) {
 
14056                         if (this.isDark && !this.isNightmodeCompatible) {
 
14057                                 var idx = this.nightModeWhitelist.indexOf(this.curSubReddit);
 
14059                                         this.styleToggleCheckbox.checked = true;
 
14062                                 var idx = this.ignoredSubReddits.indexOf(this.curSubReddit);
 
14064                                         this.styleToggleCheckbox.checked = true;
 
14066                                         this.toggleSubredditStyle(false);
 
14069                         this.styleToggleCheckbox.addEventListener('change', function(e) {
 
14070                                 modules['styleTweaks'].toggleSubredditStyle(this.checked);
 
14072                         this.styleToggleContainer.appendChild(this.styleToggleCheckbox);
 
14073                         insertAfter(subredditTitle, this.styleToggleContainer);
 
14075                 this.styleToggleLabel.setAttribute('for',this.styleCBName);
 
14076                 this.styleToggleLabel.setAttribute('id','label-'+this.styleCBName);
 
14077                 this.styleToggleLabel.textContent = 'Use subreddit style ';
 
14078                 this.styleToggleContainer.appendChild(this.styleToggleLabel);
 
14079                 this.setSRStyleToggleVisibility(true); // no source
 
14081         srstyleHideLock: RESUtils.createMultiLock(),
 
14082         setSRStyleToggleVisibility: function (visible, source) {
 
14083                 /// When showing/hiding popups which could overlay the "Use subreddit style" checkbox,
 
14084                 /// set the checkbox's styling to "less visible" or "more visible"
 
14085                 /// @param      visible                 bool    make checkbox "more visible" (true) or less (false)
 
14086                 /// @param      source  string  popup ID, so checkbox stays less visible until that popup's lock is released
 
14087                 var self = modules['styleTweaks']; 
 
14088                 if (!self.styleToggleContainer) return;
 
14090                 if (typeof source !== "undefined") {
 
14092                                 self.srstyleHideLock.unlock(source);
 
14094                                  self.srstyleHideLock.lock(source);
 
14098                 if (visible && self.srstyleHideLock.locked()) {
 
14102                 // great, now people are still finding ways to hide this.. these extra declarations are to try and fight that.
 
14103                 // Sorry, subreddit moderators, but users can disable all subreddit stylesheets if they want - this is a convenience 
 
14104                 // for them and I think taking this functionality away from them is unacceptable.
 
14106                 var zIndex = 'z-index: ' + (visible ? ' 2147483647' : 'auto') + ' !important;';
 
14108                 self.styleToggleContainer.setAttribute( 'style', 'margin: 0 !important; background-color: inherit !important; color: inherit !important; display: block !important; position: relative !important; left: 0 !important; top: 0 !important; max-height: none!important; max-width: none!important; height: auto !important; width: auto !important; visibility: visible !important; overflow: auto !important; text-indent: 0 !important; font-size: 12px !important; float: none !important; opacity: 1 !important;' + zIndex );
 
14109                 self.styleToggleCheckbox.setAttribute( 'style', 'margin: 0 !important; background-color: inherit !important; color: inherit !important; display: inline-block !important; position: relative !important; left: 0 !important; top: 0 !important; max-height: none!important; max-width: none!important; height: auto !important; width: auto !important; visibility: visible !important; overflow: auto !important; text-indent: 0 !important; font-size: 12px !important; float: none !important; opacity: 1 !important;' + zIndex );
 
14110                 self.styleToggleLabel.setAttribute( 'style', 'margin: 0 !important; background-color: inherit !important; color: inherit !important; display: inline-block !important; position: relative !important; left: 0 !important; top: 0 !important; max-height: none!important; max-width: none!important; height: auto !important; width: auto !important; visibility: visible !important; overflow: auto !important; text-indent: 0 !important; font-size: 12px !important; margin-left: 4px !important; float: none !important; opacity: 1 !important;' + zIndex );
 
14112         toggleSubredditStyle: function(toggle, subreddit) {
 
14113                 var togglesr = (subreddit) ? subreddit.toLowerCase() : this.curSubReddit;
 
14115                         this.enableSubredditStyle(subreddit);
 
14117                         this.disableSubredditStyle(subreddit);
 
14120         enableSubredditStyle: function(subreddit) {
 
14121                 var togglesr = (subreddit) ? subreddit.toLowerCase() : this.curSubReddit;
 
14123                 if (this.isDark && !this.isNightmodeCompatible) {
 
14124                         var idx = this.nightModeWhitelist.indexOf(togglesr);
 
14125                         if (idx === -1) this.nightModeWhitelist.push(togglesr); // add if not found
 
14126                         RESStorage.setItem('RESmodules.styleTweaks.nightModeWhitelist',JSON.stringify(this.nightModeWhitelist));
 
14127                 } else if (this.ignoredSubReddits) {
 
14128                         var idx = this.ignoredSubReddits.indexOf(togglesr);
 
14129                         if (idx !== -1) this.ignoredSubReddits.splice(idx, 1); // Remove it if found...
 
14130                         RESStorage.setItem('RESmodules.styleTweaks.ignoredSubredditStyles',JSON.stringify(this.ignoredSubReddits));
 
14133                 var subredditStyleSheet = document.createElement('link');
 
14134                 subredditStyleSheet.setAttribute('title','applied_subreddit_stylesheet');
 
14135                 subredditStyleSheet.setAttribute('rel','stylesheet');
 
14136                 subredditStyleSheet.setAttribute('href','http://www.reddit.com/r/'+togglesr+'/stylesheet.css');
 
14137                 if (!subreddit || (subreddit == this.curSubReddit)) this.head.appendChild(subredditStyleSheet);
 
14139         disableSubredditStyle: function(subreddit) {
 
14140                 var togglesr = (subreddit) ? subreddit.toLowerCase() : this.curSubReddit;
 
14142                 if (this.isDark && !this.isNightmodeCompatible) {
 
14143                         var idx = this.nightModeWhitelist.indexOf(togglesr);
 
14144                         if (idx !== -1) this.nightModeWhitelist.splice(idx, 1); // Remove it if found...
 
14145                         RESStorage.setItem('RESmodules.styleTweaks.nightModeWhitelist',JSON.stringify(this.nightModeWhitelist));
 
14146                 } else if (this.ignoredSubReddits) {
 
14147                         var idx = this.ignoredSubReddits.indexOf(togglesr); // Find the index
 
14148                         if (idx === -1) this.ignoredSubReddits.push(togglesr);
 
14149                         RESStorage.setItem('RESmodules.styleTweaks.ignoredSubredditStyles',JSON.stringify(this.ignoredSubReddits));
 
14152                 var subredditStyleSheet = this.head.querySelector('link[title=applied_subreddit_stylesheet]');
 
14153                 if (!subredditStyleSheet) subredditStyleSheet = this.head.querySelector('style[title=applied_subreddit_stylesheet]');
 
14154                 if ((subredditStyleSheet) && (!subreddit || (subreddit == this.curSubReddit))) {
 
14155                         subredditStyleSheet.parentNode.removeChild(subredditStyleSheet);
 
14158         redditDark: function(off) {
 
14160                         this.isDark = false;
 
14161                         localStorage.removeItem('RES_nightMode');
 
14162                         document.html.classList.remove('res-nightmode');
 
14163                         if (document.body) {
 
14164                                 document.body.classList.remove('res-nightmode');
 
14167                         this.isDark = true;
 
14168                         localStorage.setItem('RES_nightMode', true);
 
14169                         document.html.classList.add('res-nightmode');
 
14170                         if (document.body) {
 
14171                                 document.body.classList.add('res-nightmode');
 
14177 modules['accountSwitcher'] = {
 
14178         moduleID: 'accountSwitcher',
 
14179         moduleName: 'Account Switcher',
 
14180         category: 'Accounts',
 
14184                         addRowText: '+add account',
 
14186                                 { name: 'username', type: 'text' },
 
14187                                 { name: 'password', type: 'password' }
 
14191                                 ['somebodymakethis','SMT','[SMT]'],
 
14192                                 ['pics','pic','[pic]']
 
14195                         description: 'Set your usernames and passwords below. They are only stored in RES preferences.'
 
14200                         description: 'Keep me logged in when I restart my browser.'
 
14202                 showCurrentUserName: {
 
14205                         description: 'Show my current user name in the Account Switcher.'
 
14210                                 { name: 'snoo (alien)', value: 'alien' },
 
14211                                 { name: 'simple arrow', value: 'arrow' }
 
14214                         description: 'Use the "snoo" icon, or older style dropdown?'
 
14217         description: 'Store username/password pairs and switch accounts instantly while browsing Reddit!',
 
14218         isEnabled: function() {
 
14219                 return RESConsole.getModulePrefs(this.moduleID);
 
14222                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
 
14224         isMatchURL: function() {
 
14225                 return RESUtils.isMatchURL(this.moduleID);
 
14227         beforeLoad: function() {
 
14228                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
14229                         RESUtils.addCSS('#header-bottom-right { height: auto; padding: 4px 4px 7px }')
 
14230                         RESUtils.addCSS('#RESAccountSwitcherDropdown { min-width: 110px; width: auto; display: none; position: absolute; z-index: 1000; }');
 
14231                         RESUtils.addCSS('#RESAccountSwitcherDropdown li { height: auto; line-height: 20px; padding: 2px 10px; }');
 
14232                         if (this.options.dropDownStyle.value === 'alien') {
 
14233                                 RESUtils.addCSS('#RESAccountSwitcherIcon { cursor: pointer; margin-left: 3px; display: inline-block; width: 12px; vertical-align: middle; height: 16px; background-repeat: no-repeat; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAPCAYAAAAyPTUwAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkQ3NTExRkExOEYzNTExRTFBNjgzQzhEOUY2QzU2MUNFIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkQ3NTExRkEyOEYzNTExRTFBNjgzQzhEOUY2QzU2MUNFIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RDc1MTFGOUY4RjM1MTFFMUE2ODNDOEQ5RjZDNTYxQ0UiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RDc1MTFGQTA4RjM1MTFFMUE2ODNDOEQ5RjZDNTYxQ0UiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6W3fJJAAAB4ElEQVR42mJgwA4YgdgSiJUUFRXDW1tbL7Kzswsw4VDMBcRXgfgeMzPzJx4eHn4gG0MtSICPjY3NF0jLoCtglJWV1eDm5rZmZWX9k5ZbWGFmYqwhwM3B8Pn7T4bzl6/enzNlQsfrV68+srKxPWHMz89/ZmJiIunn58fA9+YKAwMHHwODlA4Dw4fHDAzPbzD8VLRhWLNuPcOzp0//MEhJSaU/f/HyPxhkyf//3xsEYa+s/f8/nOn//19f/n/98fO/jo5ONwMfH5/S27dvwfL/nt/5//8rhP3/z7f//55cgzD//PkPdK4F2N3x8fFLv3///v/d56//l69a83///v3/V65e8//+k+f///79+7+4uPgAUB0zIywUgNZEZmVlzRMTE2P78OEDA9DTDN++ffs3c+bMglOnTk0HqvkDC5p/L168+P7582cmaWlpBhUVFQZ5eXkGoPUMDx8+BMn/QQ5C1vb29r+HDx/+jwwuXLjwv7e39z8wWHkYkAOdk5OT4cePHygx9OXLF7BzgPpQo05NTS2mp6fnO7LJc+bM+a2np1eKNUFISEg0gEIFHIz//v3X1dWdDU1UYMAMYzg7O8eUlpYmXLly5dtfFm6h40cO3DU2NhYBphOea9euHQOpAQgwAKMW+Z5mJFvIAAAAAElFTkSuQmCC); }');
 
14234                                 RESUtils.addCSS('#RESAccountSwitcherIconOverlay { cursor: pointer; position: absolute; display: none; width: 11px; height: 22px; background-position: 2px 3px; padding-left: 2px; padding-right: 2px; padding-top: 3px; border: 1px solid #369; border-bottom: 1px solid #5f99cf; background-color: #5f99cf; border-radius: 3px 3px 0 0; z-index: 100; background-repeat: no-repeat; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAPCAYAAAAyPTUwAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkQ3NTExRkExOEYzNTExRTFBNjgzQzhEOUY2QzU2MUNFIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkQ3NTExRkEyOEYzNTExRTFBNjgzQzhEOUY2QzU2MUNFIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RDc1MTFGOUY4RjM1MTFFMUE2ODNDOEQ5RjZDNTYxQ0UiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RDc1MTFGQTA4RjM1MTFFMUE2ODNDOEQ5RjZDNTYxQ0UiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6W3fJJAAAB4ElEQVR42mJgwA4YgdgSiJUUFRXDW1tbL7Kzswsw4VDMBcRXgfgeMzPzJx4eHn4gG0MtSICPjY3NF0jLoCtglJWV1eDm5rZmZWX9k5ZbWGFmYqwhwM3B8Pn7T4bzl6/enzNlQsfrV68+srKxPWHMz89/ZmJiIunn58fA9+YKAwMHHwODlA4Dw4fHDAzPbzD8VLRhWLNuPcOzp0//MEhJSaU/f/HyPxhkyf//3xsEYa+s/f8/nOn//19f/n/98fO/jo5ONwMfH5/S27dvwfL/nt/5//8rhP3/z7f//55cgzD//PkPdK4F2N3x8fFLv3///v/d56//l69a83///v3/V65e8//+k+f///79+7+4uPgAUB0zIywUgNZEZmVlzRMTE2P78OEDA9DTDN++ffs3c+bMglOnTk0HqvkDC5p/L168+P7582cmaWlpBhUVFQZ5eXkGoPUMDx8+BMn/QQ5C1vb29r+HDx/+jwwuXLjwv7e39z8wWHkYkAOdk5OT4cePHygx9OXLF7BzgPpQo05NTS2mp6fnO7LJc+bM+a2np1eKNUFISEg0gEIFHIz//v3X1dWdDU1UYMAMYzg7O8eUlpYmXLly5dtfFm6h40cO3DU2NhYBphOea9euHQOpAQgwAKMW+Z5mJFvIAAAAAElFTkSuQmCC); }');
 
14236                                 RESUtils.addCSS('#RESAccountSwitcherIcon { display: inline-block; vertical-align: middle; margin-left: 3px; }');
 
14237                                 RESUtils.addCSS('#RESAccountSwitcherIcon .downArrow { cursor: pointer; margin-top: 2px; display: block; width: 16px; height: 10px; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); background-position: 0 -106px; }');
 
14238                                 RESUtils.addCSS('#RESAccountSwitcherIconOverlay { cursor: pointer; position: absolute; display: none; width: 20px; height: 22px; z-index: 100; border: 1px solid #369; border-bottom: 1px solid #5f99cf; background-color: #5f99cf; border-radius: 3px 3px 0 0; }');
 
14239                                 RESUtils.addCSS('#RESAccountSwitcherIconOverlay .downArrow { margin-top: 6px; margin-left: 3px; display: inline-block; width: 18px; height: 10px; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); background-position: 0 -96px; }');
 
14240                                 // this.alienIMG = '<span class="downArrow"></span>';
 
14242                         // RESUtils.addCSS('#RESAccountSwitcherIconOverlay { display: none; position: absolute; }');
 
14246                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
14247                         this.userLink = document.querySelector('#header-bottom-right > span.user > a');
 
14248                         if (this.userLink) {
 
14249                                 this.userLink.style.marginRight = '2px';
 
14250                                 this.loggedInUser = RESUtils.loggedInUser();
 
14251                                 // var downArrowIMG = 'data:image/gif;base64,R0lGODlhBwAEALMAAAcHBwgICAoKChERETs7Ozo6OkJCQg0NDRoaGhAQEAwMDDIyMv///wAAAAAAAAAAACH5BAEAAAwALAAAAAAHAAQAAAQQ0BSykADsDAUwY4kQfOT4RQA7';
 
14252                                 if (this.options.dropDownStyle.value === 'alien') {
 
14253                                         this.downArrowOverlay = $('<span id="RESAccountSwitcherIconOverlay"></span>');
 
14254                                         this.downArrow = $('<span id="RESAccountSwitcherIcon"></span>');
 
14256                                         this.downArrowOverlay = $('<span id="RESAccountSwitcherIconOverlay"><span class="downArrow"></span></span>');
 
14257                                         this.downArrow = $('<span id="RESAccountSwitcherIcon"><span class="downArrow"></span></span>');
 
14259                                 this.downArrowOverlay.on('click', function() {
 
14260                                         modules['accountSwitcher'].toggleAccountMenu(false);
 
14261                                         modules['accountSwitcher'].manageAccounts();
 
14262                                 }).appendTo(document.body);
 
14264                                 this.downArrow.on('click', function() {
 
14265                                         modules['accountSwitcher'].updateUserDetails();
 
14266                                         modules['accountSwitcher'].toggleAccountMenu(true);
 
14268                                 this.downArrowOverlay.on('mouseleave', function() {
 
14269                                         modules['accountSwitcher'].dropdownTimer = setTimeout(function() {
 
14270                                                 modules['accountSwitcher'].toggleAccountMenu(false);
 
14274                                 // insertAfter(this.userLink, downArrow);
 
14275                                 $(this.userLink).after(this.downArrow);
 
14277                                 this.accountMenu = $('<ul id="RESAccountSwitcherDropdown" class="RESDropdownList"></ul>')
 
14278                                 this.accountMenu.on('mouseenter', function() {
 
14279                                         clearTimeout(modules['accountSwitcher'].dropdownTimer);
 
14281                                 this.accountMenu.on('mouseleave', function() {
 
14282                                         modules['accountSwitcher'].toggleAccountMenu(false);
 
14284                                 // GM_addStyle(css);
 
14285                                 var accounts = this.options.accounts.value;
 
14286                                 if (accounts !== null) {
 
14287                                         var accountCount = 0;
 
14288                                         for (var i=0, len=accounts.length; i<len; i++) {
 
14289                                                 var thisPair = accounts[i],
 
14290                                                     username = thisPair[0];
 
14291                                                 if (!this.loggedInUser || username.toUpperCase() !== this.loggedInUser.toUpperCase() || this.options.showCurrentUserName.value){
 
14293                                                         var $accountLink = $('<li>', { 'class': 'accountName' });
 
14294                                                         // Check if user is logged in before comparing
 
14295                                                         if (this.loggedInUser && username.toUpperCase() === this.loggedInUser.toUpperCase()) {
 
14296                                                                 $accountLink.addClass('active');
 
14300                                                                 .data('username', username)
 
14302                                                                 .css('cursor', 'pointer')
 
14303                                                                 .on('click', function(e) {
 
14304                                                                         e.preventDefault();
 
14305                                                                         modules['accountSwitcher'].switchTo($(this).data('username'));
 
14307                                                                 .appendTo(this.accountMenu);
 
14309                                                         RESUtils.getUserInfo(function (userInfo) {
 
14310                                                                 var userDetails = username;
 
14312                                                                 // Display the karma of the user, if it is already pre-fetched
 
14313                                                                 if (userInfo && !userInfo.error && userInfo.data) {
 
14314                                                                         userDetails = username + ' (' + userInfo.data.link_karma + ' · ' + userInfo.data.comment_karma + ')';
 
14317                                                                 $accountLink.html(userDetails);
 
14318                                                         }, username, false);
 
14321                                         $('<li>', { 'class': 'addAccount' })
 
14322                                                 .text('+ add account')
 
14323                                                 .css('cursor', 'pointer')
 
14324                                                 .on('click', function(e) {
 
14325                                                         e.preventDefault();
 
14326                                                         modules['accountSwitcher'].toggleAccountMenu(false);
 
14327                                                         modules['accountSwitcher'].manageAccounts();
 
14329                                                 .appendTo(this.accountMenu);
 
14331                                 $(document.body).append(this.accountMenu);
 
14335         updateUserDetails: function() {
 
14336                 this.accountMenu.find('.accountName').each(function (index) {
 
14337                         var username = $(this).data('username'),
 
14340                         // Ignore "+ add account"
 
14341                         if (typeof username === 'undefined') {
 
14345                         // Leave a 500 ms delay between requests
 
14346                         setTimeout(function () {
 
14347                                 RESUtils.getUserInfo(function (userInfo) {
 
14348                                         // Fail if retrieving the user's info results in an error (such as a 404)
 
14349                                         if (!userInfo || userInfo.error || !userInfo.data) {
 
14353                                         // Display the karma of the user
 
14354                                         var userDetails = username + ' (' + userInfo.data.link_karma + ' · ' + userInfo.data.comment_karma + ')';
 
14355                                         $(that).html(userDetails);
 
14360         toggleAccountMenu: function(open) {
 
14361                 if ((open) || (! $(modules['accountSwitcher'].accountMenu).is(':visible'))) {
 
14362                         var thisHeight = 18;
 
14363                         if ($('#RESAccountSwitcherDropdown').css('position') !== 'fixed') {
 
14364                                 var thisX = $(modules['accountSwitcher'].userLink).offset().left;
 
14365                                 var thisY = $(modules['accountSwitcher'].userLink).offset().top;
 
14367                                 var thisX = $('#header-bottom-right').position().left + $(modules['accountSwitcher'].userLink).position().left;
 
14368                                 var thisY = $(modules['accountSwitcher'].userLink).position().top;
 
14369                                 if (modules['betteReddit'].options.pinHeader.value === 'subanduser') {
 
14370                                         thisHeight += $('#sr-header-area').height();
 
14371                                 } else if (modules['betteReddit'].options.pinHeader.value === 'header') {
 
14372                                         thisHeight += $('#sr-header-area').height();
 
14375                         $(modules['accountSwitcher'].accountMenu).css({
 
14376                                 top: (thisY + thisHeight) + 'px',
 
14377                                 left: (thisX) + 'px'
 
14379                         $(modules['accountSwitcher'].accountMenu).show();
 
14380                         var thisX = $(modules['accountSwitcher'].downArrow).offset().left;
 
14381                         var thisY = $(modules['accountSwitcher'].downArrow).offset().top;
 
14382                         $(modules['accountSwitcher'].downArrowOverlay).css({
 
14383                                 top: (thisY-4) + 'px',
 
14384                                 left: (thisX-3) + 'px'
 
14387                         $(modules['accountSwitcher'].downArrowOverlay).show();
 
14388                         modules['styleTweaks'].setSRStyleToggleVisibility(false, 'accountSwitcher');
 
14391                         $(modules['accountSwitcher'].accountMenu).hide();
 
14392                         $(modules['accountSwitcher'].downArrowOverlay).hide();
 
14393                         modules['styleTweaks'].setSRStyleToggleVisibility(true, 'accountSwitcher');
 
14396         closeAccountMenu: function() {
 
14397                 // this function basically just exists for other modules to call.
 
14398                 this.accountMenu.hide();
 
14400         switchTo: function(username) {
 
14401                 var accounts = this.options.accounts.value;
 
14404                 if (this.options.keepLoggedIn.value) {
 
14407                 for (var i=0, len=accounts.length; i<len; i++) {
 
14408                         var thisPair = accounts[i];
 
14409                         if (thisPair[0].toUpperCase() === username.toUpperCase()) {
 
14410                                 password = thisPair[1];
 
14414                 var loginUrl = 'https://ssl.reddit.com/api/login';
 
14415                 // unfortunately, due to 3rd party cookie issues, none of the below browsers work with ssl.
 
14416                 if (BrowserDetect.isOpera()) {
 
14417                         loginUrl = 'http://'+location.hostname+'/api/login';
 
14418                 } else if ((BrowserDetect.isChrome()) && (chrome.extension.inIncognitoContext)) {
 
14419                         loginUrl = 'http://'+location.hostname+'/api/login';
 
14420                 } else if (BrowserDetect.isSafari()) {
 
14421                         loginUrl = 'http://'+location.hostname+'/api/login';
 
14424                 // Remove old session cookie
 
14425                 RESUtils.deleteCookie('reddit_session');
 
14427                 GM_xmlhttpRequest({
 
14430                         data: 'user='+encodeURIComponent(username)+'&passwd='+encodeURIComponent(password)+rem,
 
14432                                 "Content-Type": "application/x-www-form-urlencoded"
 
14434                         onload: function(response) {
 
14435                                 var badData = false;
 
14437                                         var data = JSON.parse(response.responseText);
 
14443                                 var error = /WRONG_PASSWORD/;
 
14444                                 var rateLimit = /RATELIMIT/;
 
14446                                         RESUtils.notification({
 
14448                                                 moduleID: 'accountSwitcher',
 
14449                                                 message: 'Could not switch accounts. Reddit may be under heavy load. Please try again in a few moments.'
 
14451                                 } else if (error.test(response.responseText)) {
 
14452                                         alert('Incorrect login and/or password. Please check your configuration.');
 
14453                                 } else if (rateLimit.test(response.responseText)) {
 
14454                                         alert('RATE LIMIT: The Reddit API is seeing too many hits from you too fast, perhaps you keep submitting a wrong password, etc?  Try again in a few minutes.');
 
14461         manageAccounts: function() {
 
14462                 modules['settingsNavigation'].loadSettingsPage('accountSwitcher', 'accounts');
 
14466 modules['filteReddit'] = {
 
14467         moduleID: 'filteReddit',
 
14468         moduleName: 'filteReddit',
 
14469         category: 'Filters',
 
14471                 // any configurable options you have go here...
 
14472                 // options must have a type and a value.. 
 
14473                 // valid types are: text, boolean (if boolean, value must be true or false)
 
14478                         description: 'Filters all links labelled NSFW'
 
14483                         description: 'Show a notification when posts are filtered'
 
14488                         description: 'Add a quick NSFW on/off toggle to the gear menu'
 
14492                         addRowText: '+add filter',
 
14494                                 { name: 'keyword', type: 'text' },
 
14498                                                 { name: 'Everywhere', value: 'everywhere' },
 
14499                                                 { name: 'Everywhere but:', value: 'exclude' },
 
14500                                                 { name: 'Only on:', value: 'include' }
 
14502                                         value: 'everywhere',
 
14503                                         description: 'Apply filter to:'
 
14508                                         source: '/api/search_reddit_names.json?app=res', 
 
14509                                         hintText: 'type a subreddit name',
 
14510                                         onResult: function(response) {
 
14511                                                 var names = response.names;
 
14513                                                 for (var i=0, len=names.length; i<len; i++) {
 
14514                                                         results.push({id: names[i], name: names[i]});
 
14518                                         onCachedResult: function(response) {
 
14519                                                 var names = response.names;
 
14521                                                 for (var i=0, len=names.length; i<len; i++) {
 
14522                                                         results.push({id: names[i], name: names[i]});
 
14527                                 /* { name: 'inclusions', type: 'list', source: location.protocol + '/api/search_reddit_names' } */
 
14531                         description: 'Type in title keywords you want to ignore if they show up in a title'
 
14535                         addRowText: '+add filter',
 
14537                                 { name: 'subreddit', type: 'text' }
 
14541                         description: 'Type in a subreddit you want to ignore (only applies to /r/all or /domain/* urls)'
 
14545                         addRowText: '+add filter',
 
14547                                 { name: 'domain', type: 'text' },
 
14551                                                 { name: 'Everywhere', value: 'everywhere' },
 
14552                                                 { name: 'Everywhere but:', value: 'exclude' },
 
14553                                                 { name: 'Only on:', value: 'include' }
 
14555                                         value: 'everywhere',
 
14556                                         description: 'Apply filter to:'
 
14561                                         source: '/api/search_reddit_names.json?app=res', 
 
14562                                         hintText: 'type a subreddit name',
 
14563                                         onResult: function(response) {
 
14564                                                 var names = response.names;
 
14566                                                 for (var i=0, len=names.length; i<len; i++) {
 
14567                                                         results.push({id: names[i], name: names[i]});
 
14571                                         onCachedResult: function(response) {
 
14572                                                 var names = response.names;
 
14574                                                 for (var i=0, len=names.length; i<len; i++) {
 
14575                                                         results.push({id: names[i], name: names[i]});
 
14580                                 /* { name: 'inclusions', type: 'list', source: location.protocol + '/api/search_reddit_names' } */
 
14584                         description: 'Type in domain keywords you want to ignore. Note that \"reddit\" would ignore \"reddit.com\" and \"fooredditbar.com\"'
 
14588                         addRowText: '+add filter',
 
14590                                 { name: 'keyword', type: 'text' },
 
14594                                                 { name: 'Everywhere', value: 'everywhere' },
 
14595                                                 { name: 'Everywhere but:', value: 'exclude' },
 
14596                                                 { name: 'Only on:', value: 'include' }
 
14598                                         value: 'everywhere',
 
14599                                         description: 'Apply filter to:'
 
14604                                         source: '/api/search_reddit_names.json?app=res', 
 
14605                                         hintText: 'type a subreddit name',
 
14606                                         onResult: function(response) {
 
14607                                                 var names = response.names;
 
14609                                                 for (var i=0, len=names.length; i<len; i++) {
 
14610                                                         results.push({id: names[i], name: names[i]});
 
14614                                         onCachedResult: function(response) {
 
14615                                                 var names = response.names;
 
14617                                                 for (var i=0, len=names.length; i<len; i++) {
 
14618                                                         results.push({id: names[i], name: names[i]});
 
14623                                 /* { name: 'inclusions', type: 'list', source: location.protocol + '/api/search_reddit_names' } */
 
14627                         description: 'Type in keywords you want to ignore if they are contained in link flair'
 
14631                         addRowText: "+add subreddits",
 
14632                         description: "Whitelist subreddits from NSFW filter",
 
14635                                         name: 'subreddits', 
 
14637                                         source: '/api/search_reddit_names.json?app=res', 
 
14638                                         hintText: 'type a subreddit name',
 
14639                                         onResult: function(response) {
 
14640                                                 var names = response.names;
 
14642                                                 for (var i=0, len=names.length; i<len; i++) {
 
14643                                                         results.push({id: names[i], name: names[i]});
 
14647                                         onCachedResult: function(response) {
 
14648                                                 var names = response.names;
 
14650                                                 for (var i=0, len=names.length; i<len; i++) {
 
14651                                                         results.push({id: names[i], name: names[i]});
 
14660                                                 { name: 'Everywhere', value: 'everywhere' },
 
14661                                                 { name: 'When browsing subreddit/multi-subreddit', value: 'visit' }
 
14663                                         value: 'everywhere'
 
14668         description: 'Filter out NSFW content, or links by keyword, domain (use User Tagger to ignore by user) or subreddit (for /r/all or /domain/*).',
 
14669         isEnabled: function() {
 
14670                 return RESConsole.getModulePrefs(this.moduleID);
 
14673                 /^https?:\/\/([a-z]+)\.reddit\.com\/?(?:\??[\w]+=[\w]+&?)*/i,
 
14674                 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\w]+\/?(?:\??[\w]+=[\w]+&?)*$/i
 
14677                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
 
14678                 /^https?:\/\/([a-z]+)\.reddit\.com\/saved\/?/i,
 
14679                 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
 
14681         isMatchURL: function() {
 
14682                 return RESUtils.isMatchURL(this.moduleID);
 
14684         beforeLoad: function() {
 
14685                 if (this.isEnabled()) {
 
14686                         RESUtils.addCSS('.RESFilterToggle { margin-right: 5px; color: white; background-image: url(http://www.redditstatic.com/bg-button-add.png); cursor: pointer; text-align: center; width: 68px; font-weight: bold; font-size: 10px; border: 1px solid #444; padding: 1px 6px; border-radius: 3px 3px 3px 3px;  }');
 
14687                         RESUtils.addCSS('.RESFilterToggle.remove { background-image: url(http://www.redditstatic.com/bg-button-remove.png) }');
 
14688                         RESUtils.addCSS('.RESFiltered { display: none !important; }');
 
14689                         if (this.options.NSFWfilter.value) {
 
14690                                 this.addNSFWFilterStyle();
 
14695                 // shh I'm cheating. This runs the toggle on every single page, bypassing isMatchURL.
 
14696                 if ((this.isEnabled()) && (this.options.NSFWQuickToggle.value)) {
 
14697                         var thisFrag = document.createDocumentFragment();
 
14698                         this.nsfwSwitch = document.createElement('li');
 
14699                         this.nsfwSwitch.setAttribute('title',"Toggle NSFW Filter");
 
14700                         this.nsfwSwitch.addEventListener('click', function(e) {
 
14701                                 e.preventDefault();
 
14702                                 modules['filteReddit'].toggleNsfwFilter();                              
 
14704                         this.nsfwSwitch.textContent = 'nsfw filter';
 
14705                         this.nsfwSwitchToggle = createElementWithID('div','nsfwSwitchToggle','toggleButton');
 
14706                         $(this.nsfwSwitchToggle).html('<span class="toggleOn">on</span><span class="toggleOff">off</span>');
 
14707                         this.nsfwSwitch.appendChild(this.nsfwSwitchToggle);
 
14708                         (this.options.NSFWfilter.value) ? this.nsfwSwitchToggle.classList.add('enabled') : this.nsfwSwitchToggle.classList.remove('enabled');
 
14709                         thisFrag.appendChild(this.nsfwSwitch);
 
14710                         $('#RESDropdownOptions').append(this.nsfwSwitch);
 
14713                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
14714                         this.scanEntries();
 
14715                         RESUtils.watchForElement('siteTable', modules['filteReddit'].scanEntries);
 
14718         toggleNsfwFilter: function(toggle, notify) {
 
14719                 if (toggle !== true && toggle === false || modules['filteReddit'].options.NSFWfilter.value == true) {
 
14720                         modules['filteReddit'].filterNSFW(false);
 
14721                         RESUtils.setOption('filteReddit','NSFWfilter',false);
 
14722                         $(modules['filteReddit'].nsfwSwitchToggle).removeClass('enabled');
 
14724                         modules['filteReddit'].filterNSFW(true);
 
14725                         RESUtils.setOption('filteReddit','NSFWfilter',true);
 
14726                         $(modules['filteReddit'].nsfwSwitchToggle).addClass('enabled');
 
14730                         var onOff = modules['filteReddit'].options.NSFWfilter.value ? 'on' : ' off';
 
14732                         RESUtils.notification({
 
14733                                 header: 'NSFW Filter',
 
14734                                 moduleID: 'filteReddit',
 
14735                                 optionKey: 'NSFWfilter',
 
14736                                 message: 'NSFW Filter has been turned ' + onOff + '.'
 
14740         scanEntries: function(ele) {
 
14741                 var numFiltered = 0;
 
14742                 var numNsfwHidden = 0;
 
14746                         entries = document.querySelectorAll('#siteTable div.thing.link');
 
14748                         entries = ele.querySelectorAll('div.thing.link');
 
14750                 // var RALLre = /\/r\/all\/?(([\w]+)\/)?/i;
 
14751                 // var onRALL = RALLre.exec(location.href);
 
14752                 var filterSubs = (RESUtils.currentSubreddit('all')) || (RESUtils.currentDomain());
 
14753                 for (var i=0, len=entries.length; i<len;i++) {
 
14754                         var postTitle = entries[i].querySelector('.entry a.title').innerHTML;
 
14755                         var postDomain = entries[i].querySelector('.entry span.domain > a').innerHTML.toLowerCase();
 
14756                         var thisSubreddit = entries[i].querySelector('.entry a.subreddit');
 
14757                         var postFlair = entries[i].querySelector('.entry span.linkflairlabel');
 
14758                         if (thisSubreddit !== null) {
 
14759                                 var postSubreddit = thisSubreddit.innerHTML;
 
14761                                 var postSubreddit = false;
 
14763                         var filtered = false;
 
14764                         var currSub = (RESUtils.currentSubreddit()) ? RESUtils.currentSubreddit().toLowerCase() : null;
 
14765                         filtered = modules['filteReddit'].filterTitle(postTitle, postSubreddit || RESUtils.currentSubreddit());
 
14766                         if (!filtered) filtered = modules['filteReddit'].filterDomain(postDomain, postSubreddit || currSub);
 
14767                         if ((!filtered) && (filterSubs) && (postSubreddit)) {
 
14768                                 filtered = modules['filteReddit'].filterSubreddit(postSubreddit);
 
14770                         if ((!filtered) && (postFlair)) {
 
14771                                 filtered = modules['filteReddit'].filterFlair(postFlair.textContent, postSubreddit || RESUtils.currentSubreddit());
 
14774                                 entries[i].classList.add('RESFiltered')
 
14778                         if (entries[i].classList.contains('over18')) {
 
14779                                 if (modules['filteReddit'].allowNSFW(postSubreddit, currSub)) {
 
14780                                         entries[i].classList.add('allowOver18');
 
14781                                 } else if (modules['filteReddit'].options.NSFWfilter.value) {
 
14787                 if ((numFiltered || numNsfwHidden) && modules['filteReddit'].options.notification.value) {
 
14788                         var notification = [];
 
14789                         if (numFiltered) notification.push( numFiltered + ' post(s) hidden by ' + modules['settingsNavigation'].makeUrlHashLink('filteReddit', 'keywords', 'custom filters') + '.');
 
14790                         if (numNsfwHidden) notification.push ( numNsfwHidden + ' post(s) hidden by the ' + modules['settingsNavigation'].makeUrlHashLink('filteReddit', 'NSFWfilter', 'NSFW filter') + '.');
 
14791                         if (numNsfwHidden && modules['filteReddit'].options.NSFWQuickToggle.value) notification.push('You can toggle the nsfw filter in the <span class="gearIcon"></span> menu.');
 
14793                         notification.push("To hide this message, disable the " + modules['settingsNavigation'].makeUrlHashLink('filteReddit', 'notification', 'filteReddit notification') + '.');
 
14794                         var notification = notification.join('<br><br>');
 
14795                         RESUtils.notification({
 
14796                                 header: 'Posts Filtered',
 
14797                                 moduleID: 'filteReddit',
 
14798                                 message: notification
 
14802         addedNSFWFilterStyle: false,
 
14803         addNSFWFilterStyle: function() {
 
14804                 if (this.addedNSFWFilterStyle) return;
 
14805                 this.addedNSFWFilterStyle = true;
 
14807                 RESUtils.addCSS('body:not(.allowOver18) .over18 { display: none !important; }');
 
14808                 RESUtils.addCSS('.thing.over18.allowOver18 { display: block !important; }');
 
14810         filterNSFW: function(filterOn) {
 
14811                 this.addNSFWFilterStyle();
 
14812                 $(document.body).toggleClass('allowOver18');
 
14814         filterTitle: function(title, reddit) {
 
14815                 var reddit = (reddit) ? reddit.toLowerCase() : null;
 
14816                 return this.arrayContainsSubstring(this.options.keywords.value, title.toLowerCase(), reddit);
 
14818         filterDomain: function(domain, reddit) {
 
14819                 var reddit = (reddit) ? reddit.toLowerCase() : null;
 
14820                 var domain = (domain) ? domain.toLowerCase() : null;
 
14821                 return this.arrayContainsSubstring(this.options.domains.value, domain, reddit);
 
14823         filterSubreddit: function(subreddit) {
 
14824                 return this.arrayContainsSubstring(this.options.subreddits.value, subreddit.toLowerCase(), null, true);
 
14826         filterFlair: function(flair, reddit) {
 
14827                 var reddit = (reddit) ? reddit.toLowerCase() : null;
 
14828                 return this.arrayContainsSubstring(this.options.flair.value, flair.toLowerCase(), reddit);
 
14830         allowAllNSFW: null, // lazy loaded with boolean-y value
 
14831         subredditAllowNsfwOption: null, // lazy loaded with function to get a given subreddit's row in this.options.allowNSFW
 
14832         allowNSFW: function (postSubreddit, currSubreddit) {
 
14833                 if (!this.options.allowNSFW.value || !this.options.allowNSFW.value.length) return false;
 
14835                 if (typeof currSubreddit === "undefined") {
 
14836                     currSubreddit = RESUtils.currentSubreddit();
 
14839                 if (!this.subredditAllowNsfwOption) {
 
14840                         this.subredditAllowNsfwOption = RESUtils.indexOptionTable('filteReddit', 'allowNSFW', 0);
 
14843                 if (this.allowAllNsfw ==  null && currSubreddit) {
 
14844                         var optionValue = this.subredditAllowNsfwOption(currSubreddit);
 
14845                         this.allowAllNsfw = (optionValue && optionValue[1] === 'visit') || false;
 
14847                 if (this.allowAllNsfw) {
 
14851                 if (!postSubreddit) postSubreddit = currSubreddit;
 
14852                 if (!postSubreddit) return false;
 
14853                 var optionValue = this.subredditAllowNsfwOption(postSubreddit);
 
14855                         if (optionValue[1] === 'everywhere') {
 
14857                         } else { // optionValue[1] == visit (subreddit or multisubreddit)
 
14858                                 if (RESUtils.inList(postSubreddit, currSubreddit, '+')) {
 
14864         unescapeHTML: function(theString) {
 
14865                 var temp = document.createElement("div");
 
14866                 $(temp).html(theString);
 
14867                 var result = temp.childNodes[0].nodeValue;
 
14868                 temp.removeChild(temp.firstChild);
 
14872         arrayContainsSubstring: function(obj, stringToSearch, reddit, fullmatch) {
 
14873                 if (!obj) return false;
 
14874                 stringToSearch = this.unescapeHTML(stringToSearch);
 
14875                 var i = obj.length;
 
14877                         if ((typeof obj[i] !== 'object') || (obj[i].length < 3)) {
 
14878                                 if (obj[i].length === 1) obj[i] = obj[i][0];
 
14879                                 obj[i] = [obj[i], 'everywhere',''];
 
14881                         var searchString = obj[i][0];
 
14882                         var applyTo = obj[i][1];
 
14883                         var applyList = obj[i][2].toLowerCase().split(',');
 
14884                         var skipCheck = false;
 
14885                         // we also want to know if we should be matching /r/all, because when getting
 
14886                         // listings on /r/all, each post has a subreddit (that does not equal "all")
 
14887                         var checkRAll = ((RESUtils.currentSubreddit() === "all") && (applyList.indexOf("all") !== -1));
 
14890                                         if ((applyList.indexOf(reddit) !== -1) || (checkRAll)) {
 
14895                                         if ((applyList.indexOf(reddit) === -1) && (!checkRAll)) {
 
14900                         // if fullmatch is defined, don't do a substring match... this is used for subreddit matching on /r/all for example
 
14901                         if ((!skipCheck) && (fullmatch) && (obj[i] !== null) && (stringToSearch.toLowerCase() === searchString.toLowerCase())) return true;
 
14902                         if ((!skipCheck) && (!fullmatch) && (obj[i] !== null) && (stringToSearch.indexOf(searchString.toString().toLowerCase()) !== -1)) {
 
14908         toggleFilter: function(e) {
 
14909                 var thisSubreddit = $(e.target).data('subreddit').toLowerCase();
 
14910                 var filteredReddits = modules['filteReddit'].options.subreddits.value || [];
 
14912                 for (var i=0, len=filteredReddits.length; i<len; i++) {
 
14913                         if ((filteredReddits[i]) && (filteredReddits[i][0].toLowerCase() === thisSubreddit)) {
 
14915                                 filteredReddits.splice(i,1);
 
14916                                 e.target.setAttribute('title','Filter this subreddit from /r/all and /domain/*');
 
14917                                 e.target.textContent = '+filter';
 
14918                                 e.target.classList.remove('remove');
 
14923                         var thisObj = [thisSubreddit, 'everywhere',''];
 
14924                         filteredReddits.push(thisObj);
 
14925                         e.target.setAttribute('title','Stop filtering this subreddit from /r/all and /domain/*');
 
14926                         e.target.textContent = '-filter';
 
14927                         e.target.classList.add('remove');
 
14929                 modules['filteReddit'].options.subreddits.value = filteredReddits;
 
14930                 // save change to options...
 
14931                 RESStorage.setItem('RESoptions.filteReddit', JSON.stringify(modules['filteReddit'].options));
 
14935 modules['newCommentCount'] = {
 
14936         moduleID: 'newCommentCount',
 
14937         moduleName: 'New Comment Count',
 
14938         category: 'Comments',
 
14940                 // any configurable options you have go here...
 
14941                 // options must have a type and a value.. 
 
14942                 // valid types are: text, boolean (if boolean, value must be true or false)
 
14947                         description: 'Clean out cached comment counts of pages you haven\'t visited in [x] days - enter a number here only!'
 
14949                 subscriptionLength: {
 
14952                         description: 'Automatically remove thread subscriptions in [x] days - enter a number here only!'
 
14955         description: 'Shows how many new comments there are since your last visit.',
 
14956         isEnabled: function() {
 
14957                 return RESConsole.getModulePrefs(this.moduleID);
 
14960                 /^https?:\/\/([a-z]+)\.reddit\.com\/.*/i
 
14962         isMatchURL: function() {
 
14963                 return RESUtils.isMatchURL(this.moduleID);
 
14965         beforeLoad: function() {
 
14966                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
14967                         RESUtils.addCSS('.newComments { display: inline; color: orangered; }');
 
14968                         RESUtils.addCSS('.RESSubscriptionButton { display: inline-block; margin-left: 15px; padding: 1px 0; text-align: center; width: 78px; font-weight: bold; cursor: pointer; color: #369; border: 1px solid #b6b6b6; border-radius: 3px; }');
 
14969                         RESUtils.addCSS('td .RESSubscriptionButton { margin-left: 0; margin-right: 15px; } ');
 
14970                         RESUtils.addCSS('.RESSubscriptionButton.unsubscribe { color: orangered; }');
 
14971                         RESUtils.addCSS('.RESSubscriptionButton:hover { background-color: #f0f3fc; }');
 
14975                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
14977                         var counts = RESStorage.getItem('RESmodules.newCommentCount.counts');
 
14978                         if (counts == null) counts = '{}';
 
14979                         this.commentCounts = safeJSON.parse(counts, 'RESmodules.newCommentCount.counts');
 
14980                         if (RESUtils.pageType() === 'comments') {
 
14981                                 this.updateCommentCount();
 
14983                                 RESUtils.watchForElement('newCommentsForms', modules['newCommentCount'].updateCommentCountFromMyComment);
 
14984                                 this.addSubscribeLink();
 
14985                         } else if (RESUtils.currentSubreddit('dashboard')) {
 
14986                                 // If we're on the dashboard, add a tab to it...
 
14987                                 // add tab to dashboard
 
14988                                 modules['dashboard'].addTab('newCommentsContents','My Subscriptions');
 
14989                                 // populate the contents of the tab
 
14990                                 var showDiv = $('<div class="show">Show:</div>')
 
14991                                 var subscriptionFilter = $('<select id="subscriptionFilter"><option>subscribed threads</option><option>all threads</option></select>')
 
14992                                 $(showDiv).append(subscriptionFilter);
 
14993                                 $('#newCommentsContents').append(showDiv);
 
14994                                 $('#subscriptionFilter').change(function(){ 
 
14995                                         modules['newCommentCount'].drawSubscriptionsTable();
 
14997                                 var thisTable = $('<table id="newCommentsTable" />');
 
14998                                 $(thisTable).append('<thead><tr><th sort="" class="active">Thread title</th><th sort="subreddit">Subreddit</th><th sort="updateTime">Last Visited</th><th sort="subscriptionDate">Subscription Expires</th><th class="actions">Actions</th></tr></thead><tbody></tbody>');
 
14999                                 $('#newCommentsContents').append(thisTable);
 
15000                                 $('#newCommentsTable thead th').click(function(e) {
 
15001                                         e.preventDefault();
 
15002                                         if ($(this).hasClass('actions')) {
 
15005                                         if ($(this).hasClass('active')) {
 
15006                                                 $(this).toggleClass('descending');
 
15008                                         $(this).addClass('active');
 
15009                                         $(this).siblings().removeClass('active').find('SPAN').remove();
 
15010                                         $(this).find('.sortAsc, .sortDesc').remove();
 
15011                                         ($(e.target).hasClass('descending')) ? $(this).append('<span class="sortDesc" />') : $(this).append('<span class="sortAsc" />');
 
15012                                         modules['newCommentCount'].drawSubscriptionsTable($(e.target).attr('sort'), $(e.target).hasClass('descending'));
 
15014                                 this.drawSubscriptionsTable();
 
15015                                 RESUtils.watchForElement('siteTable', modules['newCommentCount'].processCommentCounts);
 
15017                                 document.body.addEventListener('DOMNodeInserted', function(event) {
 
15018                                         if ((event.target.tagName === 'DIV') && (event.target.getAttribute('id') && event.target.getAttribute('id').indexOf('siteTable') !== -1)) {
 
15019                                                 modules['newCommentCount'].processCommentCounts(event.target);
 
15024                                 this.processCommentCounts();
 
15025                                 RESUtils.watchForElement('siteTable', modules['newCommentCount'].processCommentCounts);
 
15027                                 document.body.addEventListener('DOMNodeInserted', function(event) {
 
15028                                         if ((event.target.tagName === 'DIV') && (event.target.getAttribute('id') && event.target.getAttribute('id').indexOf('siteTable') !== -1)) {
 
15029                                                 modules['newCommentCount'].processCommentCounts(event.target);
 
15034                         this.checkSubscriptions();
 
15037         drawSubscriptionsTable: function(sortMethod, descending) {
 
15038                 var filterType = $('#subscriptionFilter').val();
 
15039                 this.currentSortMethod = sortMethod || this.currentSortMethod;
 
15040                 this.descending = (descending == null) ? this.descending : descending == true;
 
15041                 var thisCounts = [];
 
15042                 for (var i in this.commentCounts) {
 
15043                         this.commentCounts[i].id = i;
 
15044                         // grab the subreddit out of the URL and store it in match[i]
 
15045                         var match = this.commentCounts[i].url.match(RESUtils.matchRE);
 
15047                                 this.commentCounts[i].subreddit = match[1].toLowerCase();
 
15048                                 thisCounts.push(this.commentCounts[i]);
 
15051                 $('#newCommentsTable tbody').html('');
 
15052                 switch (this.currentSortMethod) {
 
15053                         case 'subscriptionDate':
 
15054                                 thisCounts.sort(function(a,b) { 
 
15055                                         return (a.subscriptionDate > b.subscriptionDate) ? 1 : (b.subscriptionDate > a.subscriptionDate) ? -1 : 0;
 
15057                                 if (this.descending) thisCounts.reverse();
 
15060                                 thisCounts.sort(function(a,b) { 
 
15061                                         return (a.updateTime > b.updateTime) ? 1 : (b.updateTime > a.updateTime) ? -1 : 0;
 
15063                                 if (this.descending) thisCounts.reverse();
 
15066                                 thisCounts.sort(function(a,b) { 
 
15067                                         return (a.subreddit > b.subreddit) ? 1 : (b.subreddit > a.subreddit) ? -1 : 0;
 
15069                                 if (this.descending) thisCounts.reverse();
 
15072                                 thisCounts.sort(function(a,b) { 
 
15073                                         return (a.title > b.title) ? 1 : (b.title > a.title) ? -1 : 0;
 
15075                                 if (this.descending) thisCounts.reverse();
 
15079                 for (var i in thisCounts) {
 
15080                         if ((filterType === 'all threads') || ((filterType === 'subscribed threads') && (typeof thisCounts[i].subscriptionDate !== 'undefined'))) {
 
15081                                 var thisTitle = thisCounts[i].title;
 
15082                                 var thisURL = thisCounts[i].url;
 
15083                                 var thisUpdateTime = new Date(thisCounts[i].updateTime);
 
15084                                 // expire time is this.options.subscriptionLength.value days, so: 1000ms * 60s * 60m * 24hr = 86400000
 
15085                                 // then multiply by this.options.subscriptionLength.value
 
15086                                 var thisSubscriptionExpirationDate = (typeof thisCounts[i].subscriptionDate !== 'undefined') ? new Date(thisCounts[i].subscriptionDate + (86400000 * this.options.subscriptionLength.value)) : 0;
 
15087                                 if (thisSubscriptionExpirationDate > 0) {
 
15088                                         var thisExpiresContent = RESUtils.niceDateTime(thisSubscriptionExpirationDate);
 
15089                                         var thisRenewButton = '<span class="RESSubscriptionButton renew" title="renew subscription to this thread" data-threadid="'+thisCounts[i].id+'">renew</span>';
 
15090                                         var thisUnsubButton = '<span class="RESSubscriptionButton unsubscribe" title="unsubscribe from this thread" data-threadid="'+thisCounts[i].id+'">unsubscribe</span>';
 
15091                                         var thisActionContent = thisRenewButton+thisUnsubButton;
 
15094                                         var thisExpiresContent = 'n/a';
 
15095                                         var thisActionContent = '<span class="RESSubscriptionButton subscribe" title="subscribe to this thread" data-threadid="'+thisCounts[i].id+'">subscribe</span>';
 
15097                                 var thisSubreddit = '<a href="/r/'+thisCounts[i].subreddit+'">/r/'+thisCounts[i].subreddit+'</a>';
 
15098                                 var thisROW = $('<tr><td><a href="'+thisURL+'">'+thisTitle+'</a></td><td>'+thisSubreddit+'</td><td>'+RESUtils.niceDateTime(thisUpdateTime)+'</td><td>'+thisExpiresContent+'</td><td>'+thisActionContent+'</td></tr>');
 
15099                                 $(thisROW).find('.renew').click(modules['newCommentCount'].renewSubscriptionButton);
 
15100                                 $(thisROW).find('.unsubscribe').click(modules['newCommentCount'].unsubscribeButton);
 
15101                                 $(thisROW).find('.subscribe').click(modules['newCommentCount'].subscribeButton);
 
15102                                 $('#newCommentsTable tbody').append(thisROW);
 
15107                         if (filterType === 'subscribed threads') {
 
15108                                 $('#newCommentsTable tbody').append('<td colspan="5">You are currently not subscribed to any threads. To subscribe to a thread, click the "subscribe" button found near the top of the comments page.</td>');
 
15110                                 $('#newCommentsTable tbody').append('<td colspan="5">No threads found</td>');
 
15114         renewSubscriptionButton: function(e) {
 
15115                 var thisURL = $(e.target).attr('data-threadid');
 
15116                 modules['newCommentCount'].renewSubscription(thisURL);
 
15117                 RESUtils.notification({
 
15118                         header: 'Subscription Notification',
 
15119                         moduleID: 'newCommentCount',
 
15120                         optionKey: 'subscriptionLength',
 
15121                         message: 'Your subscription has been renewed - it will expire in '+modules['newCommentCount'].options.subscriptionLength.value+' days.'
 
15124         renewSubscription: function(threadid) {
 
15125                 var now = new Date();
 
15126                 modules['newCommentCount'].commentCounts[threadid].subscriptionDate = now.getTime();
 
15127                 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
 
15128                 this.drawSubscriptionsTable();
 
15130         unsubscribeButton: function(e) {
 
15131                 var confirmunsub = window.confirm('Are you sure you want to unsubscribe?');
 
15132                 if (confirmunsub) {
 
15133                         var thisURL = $(e.target).attr('data-threadid');
 
15134                         modules['newCommentCount'].unsubscribe(thisURL);
 
15137         unsubscribe: function(threadid) {
 
15138                 delete modules['newCommentCount'].commentCounts[threadid].subscriptionDate;
 
15139                 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
 
15140                 this.drawSubscriptionsTable();
 
15142         subscribeButton: function(e) {
 
15143                 var thisURL = $(e.target).attr('data-threadid');
 
15144                 modules['newCommentCount'].subscribe(thisURL);
 
15146         subscribe: function(threadid) {
 
15147                 var now = new Date();
 
15148                 modules['newCommentCount'].commentCounts[threadid].subscriptionDate = now.getTime();
 
15149                 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
 
15150                 this.drawSubscriptionsTable();
 
15152         processCommentCounts: function(ele) {
 
15153                 var ele = ele || document.body;
 
15154                 var lastClean = RESStorage.getItem('RESmodules.newCommentCount.lastClean');
 
15155                 var now = new Date();
 
15156                 if (lastClean == null) {
 
15157                         lastClean = now.getTime();
 
15158                         RESStorage.setItem('RESmodules.newCommentCount.lastClean', now.getTime());
 
15160                 // Clean cache every six hours
 
15161                 if ((now.getTime() - lastClean) > 21600000) {
 
15162                         modules['newCommentCount'].cleanCache();
 
15164                 var IDre = /\/r\/[\w]+\/comments\/([\w]+)\//i;
 
15165                 var commentsLinks = ele.querySelectorAll('.sitetable.linklisting div.thing.link a.comments');
 
15166                 for (var i=0, len=commentsLinks.length; i<len;i++) {
 
15167                         var href = commentsLinks[i].getAttribute('href');
 
15168                         var thisCount = commentsLinks[i].innerHTML;
 
15169                         var split = thisCount.split(' ');
 
15170                         thisCount = split[0];
 
15171                         var matches = IDre.exec(href);
 
15173                                 var thisID = matches[1];
 
15174                                 if ((typeof modules['newCommentCount'].commentCounts[thisID] !== 'undefined') && (modules['newCommentCount'].commentCounts[thisID] !== null)) {
 
15175                                         var diff = thisCount - modules['newCommentCount'].commentCounts[thisID].count;
 
15177                                                 var newString = $('<span class="newComments"> ('+diff+' new)</span>');
 
15178                                                 $(commentsLinks[i]).append(newString);
 
15184         updateCommentCountFromMyComment: function() {
 
15185                 modules['newCommentCount'].updateCommentCount(true);
 
15187         updateCommentCount: function(mycomment) {
 
15188                 var thisModule = modules['newCommentCount'];
 
15189                 var IDre = /\/r\/[\w]+\/comments\/([\w]+)\//i;
 
15190                 var matches = IDre.exec(location.href);
 
15192                         if (!thisModule.currentCommentCount) {
 
15193                                 thisModule.currentCommentID = matches[1];
 
15194                                 var thisCount = document.querySelector('#siteTable a.comments');
 
15196                                         var split = thisCount.innerHTML.split(' ');
 
15197                                         thisModule.currentCommentCount = split[0];
 
15198                                         if ((typeof thisModule.commentCounts[thisModule.currentCommentID] !== 'undefined') && (thisModule.commentCounts[thisModule.currentCommentID] !== null)) {
 
15199                                                 var prevCommentCount = thisModule.commentCounts[thisModule.currentCommentID].count;
 
15200                                                 var diff = thisModule.currentCommentCount - prevCommentCount;
 
15201                                                 var newString = $('<span class="newComments"> ('+diff+' new)</span>');
 
15202                                                 if (diff>0) $(thisCount).append(newString);
 
15204                                         if (isNaN(thisModule.currentCommentCount)) thisModule.currentCommentCount = 0;
 
15205                                         if (mycomment) thisModule.currentCommentCount++;
 
15208                                 thisModule.currentCommentCount++;
 
15211                 var now = new Date();
 
15212                 if (typeof thisModule.commentCounts === 'undefined') {
 
15213                         thisModule.commentCounts = {};
 
15215                 if (typeof thisModule.commentCounts[thisModule.currentCommentID] === 'undefined') {
 
15216                         thisModule.commentCounts[thisModule.currentCommentID] = {};
 
15218                 thisModule.commentCounts[thisModule.currentCommentID].count = thisModule.currentCommentCount;
 
15219                 thisModule.commentCounts[thisModule.currentCommentID].url = location.href.replace(location.hash, '');
 
15220                 thisModule.commentCounts[thisModule.currentCommentID].title = document.title;
 
15221                 thisModule.commentCounts[thisModule.currentCommentID].updateTime = now.getTime();
 
15222                 // if (this.currentCommentCount) {
 
15223                 // dumb, but because of Greasemonkey security restrictions we need a window.setTimeout here...
 
15224                 window.setTimeout( function() {
 
15225                         RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
 
15229         cleanCache: function() {
 
15230                 var now = new Date();
 
15231                 for (var i in this.commentCounts) {
 
15232                         if ((this.commentCounts[i] !== null) && ((now.getTime() - this.commentCounts[i].updateTime) > (86400000 * this.options.cleanComments.value))) {
 
15233                                 // this.commentCounts[i] = null;
 
15234                                 delete this.commentCounts[i];
 
15235                         } else if (this.commentCounts[i] == null) {
 
15236                                 delete this.commentCounts[i];
 
15239                 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(this.commentCounts));
 
15240                 RESStorage.setItem('RESmodules.newCommentCount.lastClean', now.getTime());
 
15242         addSubscribeLink: function() {
 
15243                 var commentCount = document.body.querySelector('.commentarea .panestack-title');
 
15244                 if (commentCount) {
 
15245                         this.commentSubToggle = createElementWithID('span','REScommentSubToggle','RESSubscriptionButton');
 
15246                         this.commentSubToggle.addEventListener('click', modules['newCommentCount'].toggleSubscription, false);
 
15247                         commentCount.appendChild(this.commentSubToggle);
 
15248                         if (typeof this.commentCounts[this.currentCommentID].subscriptionDate !== 'undefined') {
 
15249                                 this.commentSubToggle.textContent = 'unsubscribe';
 
15250                                 this.commentSubToggle.setAttribute('title','unsubscribe from thread');
 
15251                                 this.commentSubToggle.classList.add('unsubscribe');
 
15253                                 this.commentSubToggle.textContent = 'subscribe';
 
15254                                 this.commentSubToggle.setAttribute('title','subscribe to this thread to be notified when new comments are posted');
 
15255                                 this.commentSubToggle.classList.remove('unsubscribe');
 
15259         toggleSubscription: function() {
 
15260                 var commentID = modules['newCommentCount'].currentCommentID;
 
15261                 if (typeof modules['newCommentCount'].commentCounts[commentID].subscriptionDate !== 'undefined') {
 
15262                         modules['newCommentCount'].unsubscribeFromThread(commentID);
 
15264                         modules['newCommentCount'].subscribeToThread(commentID);
 
15267         getLatestCommentCounts: function() {
 
15268                 var counts = RESStorage.getItem('RESmodules.newCommentCount.counts');
 
15269                 if (counts == null) {
 
15272                 modules['newCommentCount'].commentCounts = safeJSON.parse(counts, 'RESmodules.newCommentCount.counts');
 
15274         subscribeToThread: function(commentID) {
 
15275                 modules['newCommentCount'].getLatestCommentCounts();
 
15276                 modules['newCommentCount'].commentSubToggle.textContent = 'unsubscribe';
 
15277                 modules['newCommentCount'].commentSubToggle.setAttribute('title','unsubscribe from thread');
 
15278                 modules['newCommentCount'].commentSubToggle.classList.add('unsubscribe');
 
15279                 commentID = commentID || modules['newCommentCount'].currentCommentID;
 
15280                 var now = new Date();
 
15281                 modules['newCommentCount'].commentCounts[commentID].subscriptionDate = now.getTime();
 
15282                 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
 
15283                 RESUtils.notification({ 
 
15284                         header: 'Subscription Notification', 
 
15285                         moduleID: 'newCommentCount',
 
15286                         optionKey: 'subscriptionLength',
 
15287                         message: 'You are now subscribed to this thread for '+modules['newCommentCount'].options.subscriptionLength.value+' days. You will be notified if new comments are posted since your last visit.'  +
 
15288                                 '<br><br><a href="/r/Dashboard#newCommentsContents" target="_blank">Visit your Dashboard</a> to see all your thread subscriptions.'
 
15291         unsubscribeFromThread: function(commentID) {
 
15292                 modules['newCommentCount'].getLatestCommentCounts();
 
15293                 modules['newCommentCount'].commentSubToggle.textContent = 'subscribe';
 
15294                 modules['newCommentCount'].commentSubToggle.setAttribute('title','subscribe to this thread and be notified when new comments are posted');
 
15295                 modules['newCommentCount'].commentSubToggle.classList.remove('unsubscribe');
 
15296                 commentID = commentID || modules['newCommentCount'].currentCommentID;
 
15297                 var now = new Date();
 
15298                 delete modules['newCommentCount'].commentCounts[commentID].subscriptionDate;
 
15299                 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
 
15300                 RESUtils.notification({ 
 
15301                         header: 'Subscription Notification', 
 
15302                         moduleID: 'newCommentCount',
 
15303                         message: 'You are now unsubscribed from this thread.'
 
15306         checkSubscriptions: function() {
 
15307                 if (this.commentCounts) {
 
15308                         var threadsToCheck = [];
 
15309                         for (var i in this.commentCounts) {
 
15310                                 var thisSubscription = this.commentCounts[i];
 
15311                                 if ((thisSubscription) && (typeof thisSubscription.subscriptionDate !== 'undefined')) {
 
15312                                         var lastCheck = parseInt(thisSubscription.lastCheck, 10) || 0;
 
15313                                         var subscriptionDate = parseInt(thisSubscription.subscriptionDate, 10);
 
15314                                         // If it's been subscriptionLength days since we've subscribed, we're going to delete this subscription...
 
15315                                         var now = new Date();
 
15316                                         if ((now.getTime() - subscriptionDate) > (this.options.subscriptionLength.value * 86400000)) {
 
15317                                                 delete this.commentCounts[i].subscriptionDate;
 
15319                                         // if we haven't checked this subscription in 5 minutes, try it again...
 
15320                                         if ((now.getTime() - lastCheck) > 300000) {
 
15321                                                 thisSubscription.lastCheck = now.getTime();
 
15322                                                 this.commentCounts[i] = thisSubscription;
 
15323                                                 // this.checkThread(i);
 
15324                                                 threadsToCheck.push('t3_'+i);
 
15326                                         RESStorage.setItem('RESmodules.newCommentCount.count', JSON.stringify(this.commentCounts));
 
15329                         if (threadsToCheck.length > 0) {
 
15330                                 this.checkThreads(threadsToCheck);
 
15334         checkThreads: function(commentIDs) {
 
15335                 GM_xmlhttpRequest({
 
15337                         url: location.protocol + '//' + location.hostname + '/by_id/' + commentIDs.join(',') + '.json?app=res',
 
15338                         onload: function(response) {
 
15339                                 var now = new Date();
 
15340                                 var commentInfo = JSON.parse(response.responseText);
 
15341                                 if (typeof commentInfo.data !== 'undefined') {
 
15342                                         for (var i=0, len=commentInfo.data.children.length; i<len; i++) {
 
15343                                                 var commentID = commentInfo.data.children[i].data.id;
 
15344                                                 var subObj = modules['newCommentCount'].commentCounts[commentID];
 
15345                                                 if (subObj.count < commentInfo.data.children[i].data.num_comments) {
 
15346                                                         modules['newCommentCount'].commentCounts[commentID].count = commentInfo.data.children[i].data.num_comments;
 
15347                                                         RESUtils.notification({ 
 
15348                                                                 header: 'Subscription Notification', 
 
15349                                                                 moduleID: 'newComments',
 
15350                                                                 message: '<p>New comments posted to thread:</p> <a href="'+subObj.url+'">' + subObj.title + '</a> <p><a class="RESNotificationButtonBlue" href="'+subObj.url+'">view the submission</a></p><div class="clear"></div>'
 
15354                                         RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
 
15361 modules['spamButton'] = {
 
15362         moduleID: 'spamButton',
 
15363         moduleName: 'Spam Button',
 
15364         category: 'Filters',
 
15367         description: 'Adds a Spam button to posts for easy reporting.',
 
15368         isEnabled: function() {
 
15369                 return RESConsole.getModulePrefs(this.moduleID);
 
15372                 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
 
15374         isMatchURL: function() {
 
15375                 return RESUtils.isMatchURL(this.moduleID);
 
15378                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
15379                         // check if the spam button was on by default from an old install of RES.  Per Reddit Admin request, this is being
 
15380                         // disabled by default due to excess misuse, but people who want to purposefully re-enable it may do so.
 
15381                         var reset = RESStorage.getItem('RESmodules.spamButton.reset');
 
15383                                 RESStorage.setItem('RESmodules.spamButton.reset','true');
 
15384                                 RESConsole.enableModule('spamButton', false);
 
15387                         // credit to tico24 for the idea, here: http://userscripts.org/scripts/review/84454
 
15388                         // code adapted for efficiency...
 
15389                         if (RESUtils.loggedInUser() !== RESUtils.currentUserProfile()) {
 
15390                                 RESUtils.watchForElement('siteTable', modules['spamButton'].addSpamButtons);
 
15391                                 this.addSpamButtons();
 
15395         addSpamButtons: function(ele) {
 
15396                 if (ele == null) ele = document;
 
15397                 if ((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments') || (RESUtils.pageType() === 'profile')) {
 
15398                         var allLists = ele.querySelectorAll('#siteTable ul.flat-list.buttons');
 
15399                         for(var i=0, len=allLists.length; i<len; i++)
 
15401                                 var permaLink = allLists[i].childNodes[0].childNodes[0].href;
 
15403                                 var spam = document.createElement('li');
 
15404                                 // insert spam button second to last in the list... this is a bit hacky and assumes singleClick is enabled...
 
15405                                 // it should probably be made smarter later, but there are so many variations of configs, etc, that it's a bit tricky.
 
15406                                 allLists[i].lastChild.parentNode.insertBefore(spam, allLists[i].lastChild);
 
15408                                 // it's faster to figure out the author only if someone actually clicks the link, so we're modifying the code to listen for clicks and not do all that queryselector stuff.
 
15409                                 var a = document.createElement('a');
 
15410                                 a.setAttribute('class', 'option');
 
15411                                 a.setAttribute('title', 'Report this user as a spammer');
 
15412                                 a.addEventListener('click', modules['spamButton'].reportPost, false);
 
15413                                 a.setAttribute('href', 'javascript:void(0)');
 
15414                                 a.textContent= 'rts';
 
15415                                 a.title = "reportthespammers"
 
15416                                 spam.appendChild(a);
 
15420         reportPost: function(e) {
 
15422                 var authorProfileContainer = a.parentNode.parentNode.parentNode;
 
15423                 var authorProfileLink = authorProfileContainer.querySelector('.author');
 
15424                 var href = authorProfileLink.href;
 
15425                 var authorName = authorProfileLink.innerHTML;
 
15426                 a.setAttribute('href', 'http://www.reddit.com/r/reportthespammers/submit?url=' + href + '&title=overview for '+authorName);
 
15427                 a.setAttribute('target', '_blank');
 
15431 modules['commentNavigator'] = {
 
15432         moduleID: 'commentNavigator',
 
15433         moduleName: 'Comment Navigator',
 
15434         category: 'Comments',
 
15435         description: 'Provides a comment navigation tool to easily find comments by OP, mod, etc.',
 
15440                         description: 'Display Comment Navigator by default'
 
15443         isEnabled: function() {
 
15444                 return RESConsole.getModulePrefs(this.moduleID);
 
15447                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
 
15448                 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
 
15450         isMatchURL: function() {
 
15451                 return RESUtils.isMatchURL(this.moduleID);
 
15453         beforeLoad: function() {
 
15454                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
15455                         RESUtils.addCSS('#REScommentNavBox { position: fixed; z-index: 999; right: 10px; top: 46px; width: 265px; border: 1px solid gray; background-color: #fff; opacity: 0.3; padding: 3px; user-select: none; -webkit-user-select: none; -moz-user-select: none; -webkit-transition:opacity 0.5s ease-in; -moz-transition:opacity 0.5s ease-in; -o-transition:opacity 0.5s ease-in; -ms-transition:opacity 0.5s ease-in; -transition:opacity 0.5s ease-in; }');
 
15456                         RESUtils.addCSS('#REScommentNavBox:hover { opacity: 1 }');
 
15457                         RESUtils.addCSS('#REScommentNavToggle { float: left; display: inline; margin-left: 0; width: 100%; }');
 
15458                         RESUtils.addCSS('.commentarea .menuarea { margin-right: 0; }');
 
15459                         RESUtils.addCSS('.menuarea > .spacer { margin-right: 0; }');
 
15460                         RESUtils.addCSS('#commentNavButtons { margin: auto; }');
 
15461                         RESUtils.addCSS('#commentNavUp { margin: auto; cursor: pointer; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); width: 32px; height: 20px; background-position: 0 -224px; }');
 
15462                         RESUtils.addCSS('#commentNavDown { margin: auto; cursor: pointer; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); width: 32px; height: 20px; background-position: 0 -244px; }');
 
15463                         RESUtils.addCSS('#commentNavUp.noNav { background-position: 0 -264px; }');
 
15464                         RESUtils.addCSS('#commentNavDown.noNav { background-position: 0 -284px; }');
 
15465                         RESUtils.addCSS('#commentNavButtons { display: none; margin-left: 12px; text-align: center; user-select: none; -webkit-user-select: none; -moz-user-select: none; }');
 
15466                         RESUtils.addCSS('.commentNavSortType { cursor: pointer; font-weight: bold; float: left; margin-left: 6px; }');
 
15467                         RESUtils.addCSS('#commentNavPostCount { color: #1278d3; }');
 
15468                         RESUtils.addCSS('.noNav #commentNavPostCount { color: #ddd; }');
 
15469                         RESUtils.addCSS('.commentNavSortTypeDisabled { color: #ddd; }');
 
15470                         RESUtils.addCSS('.commentNavSortType:hover { text-decoration: underline; }');
 
15471                         RESUtils.addCSS('#REScommentNavToggle span { float: left; margin-left: 6px; }');
 
15472                         RESUtils.addCSS('.menuarea > .spacer { float: left; }');
 
15476                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
15477                         // draw the commentNav box
 
15478                         this.commentNavBox = createElementWithID('div','REScommentNavBox');
 
15479                         this.commentNavBox.classList.add('RESDialogSmall');
 
15480                         // var commentArea = document.body.querySelector('div.sitetable.nestedlisting');
 
15481                         var commentArea = document.body.querySelector('.commentarea .menuarea');
 
15483                                 this.commentNavToggle = createElementWithID('div','REScommentNavToggle');
 
15484                                 $(this.commentNavToggle).html('<span>navigate by:</span>');
 
15485                                 var sortTypes = ['submitter', 'moderator', 'friend', 'me', 'admin', 'IAmA', 'images', 'popular', 'new'];
 
15486                                 for (var i=0, len=sortTypes.length; i<len; i++) {
 
15487                                         var thisCategory = sortTypes[i];
 
15488                                         // var thisEle = document.createElement('div');
 
15489                                         var thisEle = createElementWithID('div','navigateBy'+thisCategory);
 
15490                                         switch(thisCategory) {
 
15492                                                         thisEle.setAttribute('title','Navigate comments made by the post submitter');
 
15495                                                         thisEle.setAttribute('title','Navigate comments made by moderators');
 
15498                                                         thisEle.setAttribute('title','Navigate comments made by users on your friends list');
 
15501                                                         thisEle.setAttribute('title','Navigate comments made by you');
 
15504                                                         thisEle.setAttribute('title','Navigate comments made by reddit admins');
 
15507                                                         thisEle.setAttribute('title','Navigate through questions that have been answered by the submitter (most useful in /r/IAmA)');
 
15510                                                         thisEle.setAttribute('title','Navigate through comments with images');
 
15513                                                         thisEle.setAttribute('title','Navigate through comments in order of highest vote total');
 
15516                                                         thisEle.setAttribute('title','Navigate through new comments (Reddit Gold users only)');
 
15521                                         thisEle.setAttribute('index',i+1);
 
15522                                         thisEle.classList.add('commentNavSortType');
 
15523                                         thisEle.textContent = thisCategory;
 
15524                                         if (thisCategory === 'new') {
 
15525                                                 var isGold = document.body.querySelector('.gold-accent.comment-visits-box');
 
15527                                                         thisEle.setAttribute('style','color: #9A7D2E;');
 
15529                                                         thisEle.classList.add('commentNavSortTypeDisabled');
 
15532                                         if ((thisCategory !== 'new') || (isGold)) {
 
15533                                                 thisEle.addEventListener('click', function(e) {
 
15534                                                         modules['commentNavigator'].showNavigator(e.target.getAttribute('index'));
 
15537                                         this.commentNavToggle.appendChild(thisEle);
 
15539                                                 var thisDivider = document.createElement('span');
 
15540                                                 thisDivider.textContent = '|';
 
15541                                                 this.commentNavToggle.appendChild(thisDivider);
 
15545                                 // commentArea.insertBefore(this.commentNavToggle,commentArea.firstChild);
 
15546                                 commentArea.appendChild(this.commentNavToggle,commentArea.firstChild);
 
15547                                 if (!(this.options.showByDefault.value)) {
 
15548                                         this.commentNavBox.style.display = 'none';
 
15550                                 var navBoxHTML = ' \
 
15553                                                 <select id="commentNavBy"> \
 
15554                                                         <option name=""></option> \
 
15555                                                         <option name="submitter">submitter</option> \
 
15556                                                         <option name="moderator">moderator</option> \
 
15557                                                         <option name="friend">friend</option> \
 
15558                                                         <option name="me">me</option> \
 
15559                                                         <option name="admin">admin</option> \
 
15560                                                         <option name="IAmA">IAmA</option> \
 
15561                                                         <option name="images">images</option> \
 
15562                                                         <option name="popular">popular</option> \
 
15563                                                         <option name="new">new</option> \
 
15566                                         <div id="commentNavCloseButton" class="RESCloseButton">×</div> \
 
15567                                         <div class="RESDialogContents"> \
 
15568                                                 <div id="commentNavButtons"> \
 
15569                                                         <div id="commentNavUp"></div> <div id="commentNavPostCount"></div> <div id="commentNavDown"></div> \
 
15573                                 $(this.commentNavBox).html(navBoxHTML);
 
15576                                 this.navSelect = this.commentNavBox.querySelector('#commentNavBy');
 
15577                                 this.commentNavPostCount = this.commentNavBox.querySelector('#commentNavPostCount');
 
15578                                 this.commentNavButtons = this.commentNavBox.querySelector('#commentNavButtons');
 
15579                                 this.commentNavCloseButton = this.commentNavBox.querySelector('#commentNavCloseButton');
 
15580                                 this.commentNavCloseButton.addEventListener('click', function(e) {
 
15581                                         modules['commentNavigator'].commentNavBox.style.display = 'none';
 
15583                                 this.commentNavUp = this.commentNavBox.querySelector('#commentNavUp');
 
15584                                 this.commentNavUp.addEventListener('click',modules['commentNavigator'].moveUp, false);
 
15585                                 this.commentNavDown = this.commentNavBox.querySelector('#commentNavDown');
 
15586                                 this.commentNavDown.addEventListener('click',modules['commentNavigator'].moveDown, false);
 
15587                                 this.navSelect.addEventListener('change', modules['commentNavigator'].changeCategory, false);
 
15588                                 document.body.appendChild(this.commentNavBox);
 
15592         changeCategory: function() {
 
15593                 var index = modules['commentNavigator'].navSelect.selectedIndex;
 
15594                 modules['commentNavigator'].currentCategory = modules['commentNavigator'].navSelect.options[index].value;
 
15595                 if (modules['commentNavigator'].currentCategory !== '') {
 
15596                         modules['commentNavigator'].getPostsByCategory(modules['commentNavigator'].currentCategory);
 
15597                         modules['commentNavigator'].commentNavButtons.style.display = 'block';
 
15599                         modules['commentNavigator'].commentNavButtons.style.display = 'none';
 
15602         showNavigator: function(categoryID) {
 
15603                 modules['commentNavigator'].commentNavBox.style.display = 'block';
 
15604                 this.navSelect.selectedIndex = categoryID;
 
15605                 modules['commentNavigator'].changeCategory();
 
15607         getPostsByCategory: function () {
 
15608                 var category = modules['commentNavigator'].currentCategory;
 
15609                 if ((typeof category !== 'undefined') && (category !== '')) {
 
15610                         if (typeof this.posts[category] === 'undefined') {
 
15611                                 switch (category) {
 
15616                                                 this.posts[category] = document.querySelectorAll('.noncollapsed a.author.'+category);
 
15617                                                 this.resetNavigator(category);
 
15620                                                 RESUtils.getUserInfo(function(userInfo) {
 
15621                                                         var myID = 't2_'+userInfo.data.id;
 
15622                                                         modules['commentNavigator'].posts[category] = document.querySelectorAll('.noncollapsed a.author.id-'+myID);
 
15623                                                         modules['commentNavigator'].resetNavigator(category);
 
15627                                                 var submitterPosts = document.querySelectorAll('.noncollapsed a.author.submitter');
 
15628                                                 this.posts[category] = [];
 
15629                                                 for (var i=0, len=submitterPosts.length; i<len; i++) {
 
15630                                                         // go seven parents up to get the proper parent post...
 
15631                                                         var sevenUp = submitterPosts[i].parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
 
15632                                                         if (sevenUp.parentNode.nodeName === 'BODY') {
 
15633                                                                 this.posts[category].push(submitterPosts[i].parentNode.parentNode);
 
15635                                                                 this.posts[category].push(sevenUp);
 
15638                                                 this.resetNavigator(category);
 
15641                                                 var imagePosts = document.querySelectorAll('.toggleImage');
 
15642                                                 this.posts[category] = imagePosts;
 
15643                                                 this.resetNavigator(category);
 
15646                                                 var allComments = document.querySelectorAll('.noncollapsed');
 
15647                                                 var commentsObj = [];
 
15648                                                 for (var i=0, len=allComments.length; i<len; i++) {
 
15649                                                         var thisScore = allComments[i].querySelector('.unvoted');
 
15651                                                                 var scoreSplit = thisScore.innerHTML.split(' ');
 
15652                                                                 var score = scoreSplit[0];
 
15657                                                                 comment: allComments[i],
 
15661                                                 commentsObj.sort(function(a, b) {
 
15662                                                         return parseInt(b.score, 10) - parseInt(a.score, 10);
 
15664                                                 this.posts[category] = [];
 
15665                                                 for (var i=0, len=commentsObj.length; i<len; i++) {
 
15666                                                         this.posts[category][i] = commentsObj[i].comment;
 
15668                                                 this.resetNavigator(category);
 
15671                                                 this.posts[category] = document.querySelectorAll('.new-comment');
 
15672                                                 this.resetNavigator(category);
 
15678         resetNavigator: function(category) {
 
15679                 this.nav[category] = 0;
 
15680                 if (this.posts[category].length) {
 
15681                         modules['commentNavigator'].scrollToNavElement();
 
15682                         modules['commentNavigator'].commentNavUp.classList.remove('noNav');
 
15683                         modules['commentNavigator'].commentNavDown.classList.remove('noNav');
 
15684                         modules['commentNavigator'].commentNavButtons.classList.remove('noNav');
 
15686                         modules['commentNavigator'].commentNavPostCount.textContent = 'none';
 
15687                         modules['commentNavigator'].commentNavUp.classList.add('noNav');
 
15688                         modules['commentNavigator'].commentNavDown.classList.add('noNav');
 
15689                         modules['commentNavigator'].commentNavButtons.classList.add('noNav');
 
15692         moveUp: function() {
 
15693                 var category = modules['commentNavigator'].currentCategory;
 
15694                 if (modules['commentNavigator'].posts[category].length) {
 
15695                         if (modules['commentNavigator'].nav[category] > 0) {
 
15696                                 modules['commentNavigator'].nav[category]--;
 
15698                                 modules['commentNavigator'].nav[category] = modules['commentNavigator'].posts[category].length - 1;
 
15700                         modules['commentNavigator'].scrollToNavElement();
 
15703         moveDown: function() {
 
15704                 var category = modules['commentNavigator'].currentCategory;
 
15705                 if (modules['commentNavigator'].posts[category].length) {
 
15706                         if (modules['commentNavigator'].nav[category] < modules['commentNavigator'].posts[category].length - 1) {
 
15707                                 modules['commentNavigator'].nav[category]++;
 
15709                                 modules['commentNavigator'].nav[category] = 0;
 
15711                         modules['commentNavigator'].scrollToNavElement();
 
15714         scrollToNavElement: function() {
 
15715                 var category = modules['commentNavigator'].currentCategory;
 
15716                 $(modules['commentNavigator'].commentNavPostCount).text(modules['commentNavigator'].nav[category]+1 + '/' + modules['commentNavigator'].posts[category].length);
 
15717                 var thisXY=RESUtils.getXYpos(modules['commentNavigator'].posts[category][modules['commentNavigator'].nav[category]]);
 
15718                 RESUtils.scrollTo(0,thisXY.y);
 
15724 modules['redditProfiles'] = {
 
15725         moduleID: 'redditProfiles',
 
15726         moduleName: 'Reddit Profiles',
 
15730         description: 'Pulls in profiles from redditgifts.com when viewing a user profile.',
 
15731         isEnabled: function() {
 
15732                 return RESConsole.getModulePrefs(this.moduleID);
 
15735                 /^http:\/\/([a-z]+).reddit.com\/user\/[-\w\.]+/i
 
15737         isMatchURL: function() {
 
15738                 return RESUtils.isMatchURL(this.moduleID);
 
15741                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
15742                         RESUtils.addCSS('.redditGiftsProfileField { margin-top: 3px; margin-bottom: 6px; }');
 
15743                         RESUtils.addCSS('.redditGiftsTrophy { margin-right: 4px; }');
 
15744                         var thisCache = RESStorage.getItem('RESmodules.redditProfiles.cache');
 
15745                         if (thisCache == null) {
 
15748                         this.profileCache = safeJSON.parse(thisCache);
 
15749                         if (this.profileCache == null) this.profileCache = {};
 
15750                         var userRE = /\/user\/(\w+)/i;
 
15751                         var match = userRE.exec(location.href);
 
15753                                 var username = match[1];
 
15754                                 this.getProfile(username);
 
15758         getProfile: function(username) {
 
15760                 if ((typeof this.profileCache[username] !== 'undefined') && (this.profileCache[username] !== null)) {
 
15761                         lastCheck = this.profileCache[username].lastCheck;
 
15763                 var now = new Date();
 
15764                 if ((now.getTime() - lastCheck) > 900000) {
 
15765                         var jsonURL = 'http://redditgifts.com/profiles/view-json/'+username+'/';
 
15766                         GM_xmlhttpRequest({
 
15769                                 onload: function(response) {
 
15771                                                 // if it is JSON parseable, it's a profile.
 
15772                                                 var profileData = JSON.parse(response.responseText);
 
15774                                                 // if it is NOT JSON parseable, it's a 404 - user doesn't have a profile.
 
15775                                                 var profileData = {};
 
15777                                         var now = new Date();
 
15778                                         profileData.lastCheck = now.getTime();
 
15779                                         // set the last check time...
 
15780                                         modules['redditProfiles'].profileCache[username] = profileData;
 
15781                                         RESStorage.setItem('RESmodules.redditProfiles.cache', JSON.stringify(modules['redditProfiles'].profileCache));
 
15782                                         modules['redditProfiles'].displayProfile(username, profileData);
 
15786                         this.displayProfile(username, this.profileCache[username]);
 
15789         displayProfile: function(username, profileObject) {
 
15790                 if (typeof profileObject !== 'undefined') {
 
15791                         var firstSpacer = document.querySelector('div.side > div.spacer');
 
15792                         var newSpacer = document.createElement('div');
 
15793                         var profileHTML = '<div class="sidecontentbox profile-area"><a class="helplink" target="_blank" href="http://redditgifts.com">what\'s this?</a><h1>PROFILE</h1><div class="content">';
 
15794                         var profileBody = '';
 
15795                         if (typeof profileObject.body !== 'undefined') {
 
15796                                 profileBody += '<h3><a target="_blank" href="http://redditgifts.com/profiles/view/'+username+'">RedditGifts Profile:</a></h3>';
 
15797                                 profileBody += '<div class="redditGiftsProfileField">'+profileObject.body+'</div>';
 
15799                         if (typeof profileObject.description !== 'undefined') {
 
15800                                 profileBody += '<h3>Description:</h3>';
 
15801                                 profileBody += '<div class="redditGiftsProfileField">'+profileObject.description+'</div>';
 
15803                         if (typeof profileObject.photo !== 'undefined') {
 
15804                                 profileBody += '<h3>Photo:</h3>';
 
15805                                 profileBody += '<div class="redditGiftsProfileField"><a target="_blank" href="'+profileObject.photo.url+'"><img src="'+profileObject.photo_small.url+'" /></a></div>';
 
15807                         if (typeof profileObject.twitter_username !== 'undefined') {
 
15808                                 profileBody += '<h3>Twitter:</h3>';
 
15809                                 profileBody += '<div class="redditGiftsProfileField"><a target="_blank" href="http://twitter.com/'+profileObject.twitter_username+'">@'+profileObject.twitter_username+'</a></div>';
 
15811                         if (typeof profileObject.website !== 'undefined') {
 
15812                                 profileBody += '<h3>Website:</h3>';
 
15813                                 profileBody += '<div class="redditGiftsProfileField"><a target="_blank" href="'+profileObject.website+'">[link]</a></div>';
 
15815                         if (typeof profileObject.trophies !== 'undefined') {
 
15816                                 profileBody += '<h3>RedditGifts Trophies:</h3>';
 
15818                                 var len=profileObject.trophies.length;
 
15819                                 for (var i in profileObject.trophies) {
 
15820                                         var rowNum = parseInt(count/2);
 
15822                                                 profileBody += '<table class="trophy-table"><tbody>';
 
15824                                         // console.log('count: ' + count + ' -- mod: ' + (count%2) + ' len: ' + len);
 
15825                                         // console.log('countmod: ' + ((count%2) === 0));
 
15826                                         if ((count%2) === 1) {
 
15827                                                 profileBody += '<tr>';
 
15829                                         if ((count===len) && ((count%2) === 1)) {
 
15830                                                 profileBody += '<td class="trophy-info" colspan="2">';
 
15832                                                 profileBody += '<td class="trophy-info">';
 
15834                                         profileBody += '<div><img src="'+profileObject.trophies[i].url+'" alt="'+profileObject.trophies[i].title+'" title="'+profileObject.trophies[i].title+'"><br><span class="trophy-name">'+profileObject.trophies[i].title+'</span></div>';
 
15835                                         profileBody += '</td>';
 
15836                                         if (((count%2) === 0) || (count===len)) {
 
15837                                                 profileBody += '</tr>';
 
15842                                         profileBody += '</tbody></table>';
 
15845                         if (profileBody === '') {
 
15846                                 profileBody = 'User has not filled out a profile on <a target="_blank" href="http://redditgifts.com">RedditGifts</a>.';
 
15848                         profileHTML += profileBody + '</div></div>';
 
15849                         $(newSpacer).html(profileHTML);
 
15850                         addClass(newSpacer,'spacer');
 
15851                         insertAfter(firstSpacer,newSpacer);
 
15857 modules['subredditManager'] = {
 
15858         moduleID: 'subredditManager',
 
15859         moduleName: 'Subreddit Manager',
 
15862                 subredditShortcut: {
 
15865                         description: 'Add +shortcut button in subreddit sidebar for easy addition of shortcuts.'
 
15870                         description: 'Show "DASHBOARD" link in subreddit manager'
 
15875                         description: 'Show "ALL" link in subreddit manager'
 
15880                         description: 'show "FRONT" link in subreddit manager'
 
15885                         description: 'Show "RANDOM" link in subreddit manager'
 
15890                         description: 'Show "MYRANDOM" link in subreddit manager (reddit gold only)'
 
15895                         description: 'Show "RANDNSFW" link in subreddit manager'
 
15900                         description: 'Show "FRIENDS" link in subreddit manager'
 
15905                         description: 'Show "MOD" link in subreddit manager'
 
15910                         description: 'Show "MODQUEUE" link in subreddit manager'
 
15915                                 { name: 'Subreddit Name', value: 'displayName' },
 
15916                                 { name: 'Added date', value: 'addedDate' }
 
15918                         value : 'displayName',
 
15919                         description: 'Field to sort subreddit shortcuts by'
 
15921                 sortingDirection: {
 
15924                                 { name: 'Ascending', value: 'asc' },
 
15925                                 { name: 'Descending', value: 'desc' }
 
15928                         description: 'Field to sort subreddit shortcuts by'
 
15931         description: 'Allows you to customize the top bar with your own subreddit shortcuts, including dropdown menus of multi-reddits and more.',
 
15932         isEnabled: function() {
 
15933                 return RESConsole.getModulePrefs(this.moduleID);
 
15936                 /^https?:\/\/([a-z]+)\.reddit\.com\/.*/i
 
15938         isMatchURL: function() {
 
15939                 return RESUtils.isMatchURL(this.moduleID);
 
15941         beforeLoad: function() {
 
15942                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
15943                         RESUtils.addCSS('.srOver { outline: 1px dashed black; }');
 
15944                         RESUtils.addCSS('body { overflow-x: hidden; }');
 
15945                         RESUtils.addCSS('#sr-header-area a { font-size: 100% !important; }');
 
15946                         RESUtils.addCSS('#srList { position: absolute; top: 18px; left: 0; z-index: 9999; display: none; border: 1px solid black; background-color: #FAFAFA; width: auto; overflow-y: auto; }');
 
15947                         RESUtils.addCSS('#srList tr { border-bottom: 1px solid gray; }');
 
15948                         RESUtils.addCSS('#srList thead td { cursor: pointer; }');
 
15949                         RESUtils.addCSS('#srList td { padding: 3px 8px; }');
 
15950                         RESUtils.addCSS('#srList td.RESvisited, #srList td.RESshortcut { text-transform: none; }');
 
15951                         RESUtils.addCSS('#srList td.RESshortcut {cursor: pointer;}');
 
15952                         RESUtils.addCSS('#srList td a { width: 100%; display: block; }');
 
15953                         RESUtils.addCSS('#srList tr:hover { background-color: #eef; }');
 
15954                         RESUtils.addCSS('#srLeftContainer, #RESStaticShortcuts, #RESShortcuts, #srDropdown { display: inline; float: left; position: relative; z-index: 5; }');
 
15955                         RESUtils.addCSS('#editShortcutDialog { display: none; z-index: 999; position: absolute; top: 25px; left: 5px; width: 230px; padding: 10px; background-color: #f0f3fc; border: 1px solid #c7c7c7; border-radius: 3px; font-size: 12px; color: #000; }');
 
15956                         RESUtils.addCSS('#editShortcutDialog h3 { display: inline-block; float: left; font-size: 13px; margin-top: 6px; }');
 
15957                         RESUtils.addCSS('#editShortcutClose { float: right; margin-top: 2px; margin-right: 0; }');
 
15958                         RESUtils.addCSS('#editShortcutDialog label { clear: both; float: left; width: 100px; margin-top: 12px; }');
 
15959                         RESUtils.addCSS('#editShortcutDialog input { float: left; width: 126px; margin-top: 10px; }');
 
15960                         RESUtils.addCSS('#editShortcutDialog input[type=button] { float: right; width: 45px; margin-left: 10px; cursor: pointer; padding: 3px 5px; font-size: 12px; color: #fff; border: 1px solid #636363; border-radius: 3px; background-color: #5cc410; }');                 RESUtils.addCSS( '#srLeftContainer { z-index: 4; padding-left: 4px; margin-right: 6px; }' );                    // RESUtils.addCSS('#RESShortcuts { position: absolute; left: '+ this.srLeftContainerWidth+'px;  z-index: 6; white-space: nowrap; overflow-x: hidden; padding-left: 2px; margin-top: -2px; padding-top: 2px; }');
 
15961                         RESUtils.addCSS( '#srLeftContainer { z-index: 4; padding-left: 4px; margin-right: 6px; }' );
 
15962                         RESUtils.addCSS('#RESShortcutsViewport { width: auto; max-height: 20px; overflow: hidden; } ');
 
15963                         RESUtils.addCSS('#RESShortcuts { z-index: 6; white-space: nowrap; overflow-x: hidden; padding-left: 2px; }');
 
15964                         RESUtils.addCSS('#RESSubredditGroupDropdown { display: none; position: absolute; z-index: 99999; padding: 3px; background-color: #F0F0F0; border-left: 1px solid black; border-right: 1px solid black; border-bottom: 1px solid black; }');
 
15965                         RESUtils.addCSS('#RESSubredditGroupDropdown li { padding-left: 3px; padding-right: 3px; margin-bottom: 2px; }');
 
15966                         RESUtils.addCSS('#RESSubredditGroupDropdown li:hover { background-color: #F0F0FC; }');
 
15968                         RESUtils.addCSS('#RESShortcutsEditContainer { width: 69px; position: absolute; right: 0; top: 0; z-index: 999; background-color: #f0f0f0; height: 16px; user-select: none; -webkit-user-select: none; -moz-user-select: none; }');
 
15969                         RESUtils.addCSS('#RESShortcutsRight { right: 0; }');
 
15970                         RESUtils.addCSS('#RESShortcutsAdd { right: 15px; }');
 
15971                         RESUtils.addCSS('#RESShortcutsLeft { right: 31px; }');
 
15972                         RESUtils.addCSS('#RESShortcutsSort { right: 47px; }');
 
15974                         RESUtils.addCSS('#RESShortcutsSort, #RESShortcutsRight, #RESShortcutsLeft, #RESShortcutsAdd, #RESShortcutsTrash {  width: 16px; cursor: pointer; background: #F0F0F0; font-size: 20px; color: #369; height: 18px; line-height: 15px; position: absolute; top: 0; z-index: 999; background-color: #f0f0f0; user-select: none; -webkit-user-select: none; -moz-user-select: none;  } ');
 
15975                         RESUtils.addCSS('#RESShortcutsSort { font-size: 14px; }')
 
15976                         RESUtils.addCSS('#RESShortcutsTrash { display: none; font-size: 17px; width: 16px; cursor: pointer; right: 15px; height: 16px; position: absolute; top: 0; z-index: 1000; user-select: none; -webkit-user-select: none; -moz-user-select: none; }');
 
15977                         RESUtils.addCSS('.srSep { margin-left: 6px; }');
 
15978                         RESUtils.addCSS('.RESshortcutside { margin-right: 5px; margin-top: 2px; color: white; background-image: url(http://www.redditstatic.com/bg-button-add.png); cursor: pointer; text-align: center; width: 68px; font-weight: bold; font-size: 10px; border: 1px solid #444; padding: 1px 6px; border-radius: 3px 3px 3px 3px; }');
 
15979                         RESUtils.addCSS('.RESshortcutside.remove { background-image: url(http://www.redditstatic.com/bg-button-remove.png) }');
 
15980                         RESUtils.addCSS('.RESshortcutside:hover { background-color: #f0f0ff; }');
 
15981                         // RESUtils.addCSS('h1.redditname > a { float: left; }');
 
15982                         RESUtils.addCSS('h1.redditname { overflow: auto; }');
 
15983                         RESUtils.addCSS('.sortAsc, .sortDesc { float: right; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); width: 12px; height: 12px; background-repeat: no-repeat; }');
 
15984                         RESUtils.addCSS('.sortAsc { background-position: 0 -149px; }');
 
15985                         RESUtils.addCSS('.sortDesc { background-position: -12px -149px; }');
 
15986                         RESUtils.addCSS('#RESShortcutsAddFormContainer { display: none; position: absolute; width: 290px; padding: 2px; right: 0; top: 21px; z-index: 10000; background-color: #f0f3fc; border: 1px solid #c7c7c7; border-radius: 3px; font-size: 12px; color: #000; }');
 
15987                         RESUtils.addCSS('#RESShortcutsAddFormContainer  a { font-weight: bold; }');
 
15988                         RESUtils.addCSS('#newShortcut { width: 130px; }');
 
15989                         RESUtils.addCSS('#displayName { width: 130px; }');
 
15990                         RESUtils.addCSS('#shortCutsAddForm { padding: 5px; }');
 
15991                         RESUtils.addCSS('#shortCutsAddForm div { font-size: 10px; margin-bottom: 10px; }');
 
15992                         RESUtils.addCSS('#shortCutsAddForm label { display: inline-block; width: 100px; }');
 
15993                         RESUtils.addCSS('#shortCutsAddForm input[type=text] { width: 170px; margin-bottom: 6px; }');
 
15994                         RESUtils.addCSS('#addSubreddit { float: right; cursor: pointer; padding: 3px 5px; font-size: 12px; color: #fff; border: 1px solid #636363; border-radius: 3px; background-color: #5cc410; }');
 
15995                         RESUtils.addCSS('.RESShortcutsCurrentSub { color:orangered!important; font-weight:bold; }');
 
15996                         RESUtils.addCSS('.RESShortcutsCurrentSub:visited { color:orangered!important; font-weight:bold; }');
 
15997                         RESUtils.addCSS('#srLeftContainer, #RESShortcutsViewport, #RESShortcutsEditContainer{max-height:18px;}');
 
15999                         // this shows the sr-header-area that we hid while rendering it (to curb opera's glitchy "jumping")...
 
16000                         if (BrowserDetect.isOpera()) {
 
16001                                 RESUtils.addCSS('#sr-header-area { display: block !important; }');
 
16006                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
16008                         if (this.options.linkMyRandom.value) {
 
16009                                 var originalMyRandom = document.querySelector('#sr-header-area a[href$="/r/myrandom/"]')
 
16010                                 if (originalMyRandom) {
 
16011                                         this.myRandomEnabled = true;
 
16012                                         if (originalMyRandom.classList.contains('gold')) {
 
16013                                                 this.myRandomGold = true;
 
16018                         this.manageSubreddits();
 
16019                         if (RESUtils.currentSubreddit() !== null) {
 
16020                                 this.setLastViewtime();
 
16024         manageSubreddits: function() {
 
16025                 // This is the init function for Manage Subreddits - it'll get your preferences and redraw the top bar.
 
16026                 this.redrawSubredditBar();
 
16027                 // Listen for subscriptions / unsubscriptions from reddits so we know to reload the JSON string...
 
16028                 // also, add a +/- shortcut button...
 
16029                 if ((RESUtils.currentSubreddit()) && (this.options.subredditShortcut.value == true)) {
 
16030                         var subButtons = document.querySelectorAll('.fancy-toggle-button');
 
16031                         // for (var h=0, len=currentSubreddits.length; h<len; h++) {
 
16032                         for (var h=0, len=subButtons.length; h<len; h++) {
 
16033                                 var subButton = subButtons[h];
 
16034                                 if ((RESUtils.currentSubreddit().indexOf('+') === -1) && (RESUtils.currentSubreddit() !== 'mod')) {
 
16035                                         var thisSubredditFragment = RESUtils.currentSubreddit();
 
16036                                         var isMulti = false;
 
16037                                 } else if ($(subButton).parent().hasClass('subButtons')) {
 
16038                                         var isMulti = true;
 
16039                                         var thisSubredditFragment = $(subButton).parent().parent().find('a.title').text();
 
16041                                         var isMulti = true;
 
16042                                         var thisSubredditFragment = $(subButton).next().text();
 
16044                                 if ($('#subButtons-'+thisSubredditFragment).length === 0) {
 
16045                                         var subButtonsWrapper = $('<div id="subButtons-'+thisSubredditFragment+'" class="subButtons" style="margin: 0 !important;"></div>');
 
16046                                         $(subButton).wrap(subButtonsWrapper);
 
16047                                         // move this wrapper to the end (after any icons that may exist...)
 
16049                                                 var theWrap = $(subButton).parent();
 
16050                                                 $(theWrap).appendTo($(theWrap).parent());
 
16053                                 subButton.addEventListener('click', function() {
 
16054                                         // reset the last checked time for the subreddit list so that we refresh it anew no matter what.
 
16055                                         RESStorage.setItem('RESmodules.subredditManager.subreddits.lastCheck.'+RESUtils.loggedInUser(),0);
 
16057                                 var theSC = document.createElement('span');
 
16058                                 theSC.setAttribute('style','display: inline-block !important;');
 
16059                                 theSC.setAttribute('class','RESshortcut RESshortcutside');
 
16060                                 theSC.setAttribute('data-subreddit',thisSubredditFragment);
 
16062                                 for (var i=0, sublen=modules['subredditManager'].mySubredditShortcuts.length; i<sublen; i++) {
 
16063                                         if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() === thisSubredditFragment.toLowerCase()) {
 
16069                                         theSC.textContent = '-shortcut';
 
16070                                         theSC.setAttribute('title','Remove this subreddit from your shortcut bar');
 
16071                                         theSC.classList.add('remove');
 
16073                                         theSC.textContent = '+shortcut';
 
16074                                         theSC.setAttribute('title','Add this subreddit to your shortcut bar');
 
16076                                 theSC.addEventListener('click', modules['subredditManager'].toggleSubredditShortcut, false);
 
16077                                 // subButton.parentNode.insertBefore(theSC, subButton);
 
16078                                 // theSubredditLink.appendChild(theSC);
 
16079                                 $('#subButtons-'+thisSubredditFragment).append(theSC);
 
16080                                 var next = $('#subButtons-'+thisSubredditFragment).next();
 
16081                                 if ($(next).hasClass('title') && (! $('#subButtons-'+thisSubredditFragment).hasClass('swapped'))) {
 
16082                                         $('#subButtons-'+thisSubredditFragment).before($(next));
 
16083                                         $('#subButtons-'+thisSubredditFragment).addClass('swapped');
 
16088                 // If we're on the reddit-browsing page (/reddits or /subreddits), add +shortcut and -shortcut buttons...
 
16089                 if (/^https?:\/\/www\.reddit\.com\/(?:sub)?reddits\/?(?:\?[\w=&]+)*/.test(location.href)) {
 
16090                         this.browsingReddits();
 
16093         browsingReddits: function() {
 
16094                 $('.subreddit').each(function () {
 
16095                         // Skip subreddit links that already have a shortcut button
 
16096                         if (typeof $(this).data('hasShortcutButton') !== 'undefined' && $(this).data('hasShortcutButton')) {
 
16100                         // Otherwise, indicate that this link now has a shortcut button
 
16101                         $(this).data('hasShortcutButton', true);
 
16103                         var subreddit = $(this).find('a.title').attr('href').match(/^https?:\/\/(?:[a-z]+).reddit.com\/r\/([\w]+).*/i)[1],
 
16104                                 $theSC = $('<span>')
 
16105                                         .css({ display: 'inline-block', 'margin-right': '0'})
 
16106                                         .addClass('RESshortcut RESshortcutside')
 
16107                                         .data('subreddit', subreddit),
 
16108                             isShortcut = false;
 
16110                         for (var j = 0, shortcutsLength = modules['subredditManager'].mySubredditShortcuts.length; j < shortcutsLength; j++) {
 
16111                                 if (modules['subredditManager'].mySubredditShortcuts[j].subreddit === subreddit) {
 
16119                                         .attr('title', 'Remove this subreddit from your shortcut bar')
 
16121                                         .addClass('remove');
 
16124                                         .attr('title', 'Add this subreddit to your shortcut bar')
 
16126                                         .removeClass('remove');
 
16130                                 .on('click', modules['subredditManager'].toggleSubredditShortcut)
 
16131                                 .appendTo($(this).find('.midcol'));
 
16134         redrawShortcuts: function() {
 
16135                 this.shortCutsContainer.textContent = '';
 
16136                 // Try Refresh subreddit shortcuts
 
16137                 if (this.mySubredditShortcuts.length === 0) {
 
16138                     this.getLatestShortcuts();
 
16140                 if (this.mySubredditShortcuts.length > 0) {
 
16141                         // go through the list of shortcuts and print them out...
 
16142                         for (var i = 0, len = this.mySubredditShortcuts.length; i < len; i++) {
 
16143                                 if (typeof this.mySubredditShortcuts[i] === 'string') {
 
16144                                         this.mySubredditShortcuts[i] = {
 
16145                                                 subreddit: this.mySubredditShortcuts[i],
 
16146                                                 displayName: this.mySubredditShortcuts[i],
 
16147                                                 addedDate: new Date()
 
16151                                 var thisShortCut = document.createElement('a');
 
16152                                 thisShortCut.setAttribute('draggable', 'true');
 
16153                                 thisShortCut.setAttribute('orderIndex', i);
 
16154                                 thisShortCut.setAttribute('data-subreddit', this.mySubredditShortcuts[i].subreddit);
 
16155                                 thisShortCut.classList.add('subbarlink');
 
16157                                 if ((RESUtils.currentSubreddit() !== null) && (RESUtils.currentSubreddit().toLowerCase() === this.mySubredditShortcuts[i].subreddit.toLowerCase())) {
 
16158                                         thisShortCut.classList.add('RESShortcutsCurrentSub');
 
16161                                 thisShortCut.setAttribute('href','/r/'+this.mySubredditShortcuts[i].subreddit);
 
16162                                 thisShortCut.textContent = this.mySubredditShortcuts[i].displayName;
 
16163                                 thisShortCut.addEventListener('click', function(e) {
 
16164                                         if (e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey) {
 
16165                                                 // open in new tab, let the browser handle it
 
16168                                                 e.preventDefault();
 
16169                                                 // use to open links in new tabs... work on this later...
 
16170                                                 modules['subredditManager'].clickedShortcut = e.target.getAttribute('href');
 
16171                                                 if (typeof modules['subredditManager'].clickTimer === 'undefined') {
 
16172                                                         modules['subredditManager'].clickTimer = setTimeout(modules['subredditManager'].followSubredditShortcut, 300);
 
16177                                 thisShortCut.addEventListener('dblclick', function(e) {
 
16178                                         e.preventDefault();
 
16179                                         clearTimeout(modules['subredditManager'].clickTimer);
 
16180                                         delete modules['subredditManager'].clickTimer;
 
16181                                         modules['subredditManager'].editSubredditShortcut(e.target);
 
16184                                 thisShortCut.addEventListener('mouseover', function(e) {
 
16185                                         clearTimeout(modules['subredditManager'].hideSubredditGroupDropdownTimer);
 
16186                                         if ((typeof e.target.getAttribute !== 'undefined') && (e.target.getAttribute('href').indexOf('+') !== -1)) {
 
16187                                                 var subreddits = e.target.getAttribute('href').replace('/r/','').split('+');
 
16188                                                 modules['subredditManager'].showSubredditGroupDropdown(subreddits, e.target);
 
16192                                 thisShortCut.addEventListener('mouseout', function(e) {
 
16193                                         modules['subredditManager'].hideSubredditGroupDropdownTimer = setTimeout(function() {
 
16194                                                 modules['subredditManager'].hideSubredditGroupDropdown();
 
16198                                 thisShortCut.addEventListener('dragstart', modules['subredditManager'].subredditDragStart, false);
 
16199                                 thisShortCut.addEventListener('dragenter', modules['subredditManager'].subredditDragEnter, false)
 
16200                                 thisShortCut.addEventListener('dragover', modules['subredditManager'].subredditDragOver, false);
 
16201                                 thisShortCut.addEventListener('dragleave', modules['subredditManager'].subredditDragLeave, false);
 
16202                                 thisShortCut.addEventListener('drop', modules['subredditManager'].subredditDrop, false);
 
16203                                 thisShortCut.addEventListener('dragend', modules['subredditManager'].subredditDragEnd, false);
 
16204                                 this.shortCutsContainer.appendChild(thisShortCut);
 
16207                                         var sep = document.createElement('span');
 
16208                                         sep.setAttribute('class', 'separator');
 
16209                                         sep.textContent = '-';
 
16210                                         this.shortCutsContainer.appendChild(sep);
 
16213                         if (this.mySubredditShortcuts.length === 0) {
 
16214                                 this.shortCutsContainer.style.textTransform = 'none';
 
16215                                 this.shortCutsContainer.textContent = 'add shortcuts from the my subreddits menu at left or click the button by the subreddit name, drag and drop to sort';
 
16217                                 this.shortCutsContainer.style.textTransform = '';
 
16220                         this.shortCutsContainer.style.textTransform = 'none';
 
16221                         this.shortCutsContainer.textContent = 'add shortcuts from the my subreddits menu at left or click the button by the subreddit name, drag and drop to sort';
 
16222                         this.mySubredditShortcuts = [];
 
16225         showSubredditGroupDropdown: function(subreddits, obj) {
 
16226                 if (typeof this.subredditGroupDropdown === 'undefined') {
 
16227                         this.subredditGroupDropdown = createElementWithID('div', 'RESSubredditGroupDropdown');
 
16228                         this.subredditGroupDropdownUL = document.createElement('ul');
 
16229                         this.subredditGroupDropdown.appendChild(this.subredditGroupDropdownUL);
 
16230                         document.body.appendChild(this.subredditGroupDropdown);
 
16232                         this.subredditGroupDropdown.addEventListener('mouseout', function(e) {
 
16233                                 modules['subredditManager'].hideSubredditGroupDropdownTimer = setTimeout(function() {
 
16234                                         modules['subredditManager'].hideSubredditGroupDropdown();
 
16238                         this.subredditGroupDropdown.addEventListener('mouseover', function(e) {
 
16239                                 clearTimeout(modules['subredditManager'].hideSubredditGroupDropdownTimer);
 
16242                 this.groupDropdownVisible = true;
 
16245                         $(this.subredditGroupDropdownUL).html('');
 
16247                         for (var i=0, len=subreddits.length; i<len; i++) {
 
16248                                 var thisLI = $('<li><a href="/r/'+subreddits[i]+'">'+subreddits[i]+'</a></li>');
 
16249                                 $(this.subredditGroupDropdownUL).append(thisLI);
 
16252                         var thisXY = RESUtils.getXYpos(obj);
 
16253                         this.subredditGroupDropdown.style.top = (thisXY.y + 16) + 'px';
 
16254                         // if fixed, override y to just be the height of the subreddit bar...
 
16255                         // this.subredditGroupDropdown.style.position = 'fixed';
 
16256                         // this.subredditGroupDropdown.style.top = '20px';
 
16257                         this.subredditGroupDropdown.style.left = thisXY.x + 'px';
 
16258                         this.subredditGroupDropdown.style.display = 'block';
 
16260                         modules['styleTweaks'].setSRStyleToggleVisibility(false, "subredditGroupDropdown");
 
16263         hideSubredditGroupDropdown: function() {
 
16264                 delete modules['subredditManager'].hideSubredditGroupDropdownTimer;
 
16265                 if (this.subredditGroupDropdown) {
 
16266                         this.subredditGroupDropdown.style.display = 'none';
 
16267                         modules['styleTweaks'].setSRStyleToggleVisibility(true, "subredditGroupDropdown")
 
16270         editSubredditShortcut: function(ele) {
 
16271                 var subreddit = ele.getAttribute('href').slice(3);
 
16274                 for (var i=0, len=modules['subredditManager'].mySubredditShortcuts.length; i<len; i++) {
 
16275                         if (modules['subredditManager'].mySubredditShortcuts[i].subreddit == subreddit) {
 
16281                 if (typeof this.editShortcutDialog === 'undefined') {
 
16282                         this.editShortcutDialog = createElementWithID('div','editShortcutDialog');
 
16283                         document.body.appendChild(this.editShortcutDialog);
 
16286                 var thisForm = '<form name="editSubredditShortcut"> \
 
16287                                     <h3>Edit Shortcut</h3> \
 
16288                                     <div id="editShortcutClose" class="RESCloseButton">×</div> \
 
16289                                     <label for="subreddit">Subreddit:</label> \
 
16290                                     <input type="text" name="subreddit" value="' + subreddit + '" id="shortcut-subreddit"> \
 
16292                                     <label for="displayName">Display Name:</label> \
 
16293                                     <input type="text" name="displayName" value="' + ele.textContent + '" id="shortcut-displayname"> \
 
16294                                     <input type="hidden" name="idx" value="' + idx + '"> \
 
16295                                     <input type="button" name="shortcut-save" value="save" id="shortcut-save"> \
 
16297                 $(this.editShortcutDialog).html(thisForm);
 
16299                 this.subredditInput = this.editShortcutDialog.querySelector('input[name=subreddit]');
 
16300                 this.displayNameInput = this.editShortcutDialog.querySelector('input[name=displayName]');
 
16302                 this.subredditForm = this.editShortcutDialog.querySelector('FORM');
 
16303                 this.subredditForm.addEventListener('submit', function(e) {
 
16304                         e.preventDefault();
 
16307                 this.saveButton = this.editShortcutDialog.querySelector('input[name=shortcut-save]');
 
16308                 this.saveButton.addEventListener('click', function(e) {
 
16309                         var idx = modules['subredditManager'].editShortcutDialog.querySelector('input[name=idx]').value;
 
16310                         var subreddit = modules['subredditManager'].editShortcutDialog.querySelector('input[name=subreddit]').value;
 
16311                         var displayName = modules['subredditManager'].editShortcutDialog.querySelector('input[name=displayName]').value;
 
16313                         if ((subreddit === '') || (displayName === '')) {
 
16314                                 // modules['subredditManager'].mySubredditShortcuts.splice(idx,1);
 
16315                                 subreddit = modules['subredditManager'].mySubredditShortcuts[idx].subreddit;
 
16316                                 modules['subredditManager'].removeSubredditShortcut(subreddit);
 
16318                                 if (RESUtils.proEnabled()) {
 
16319                                         // store a delete for the old subreddit, and an add for the new.
 
16320                                         var oldsubreddit = modules['subredditManager'].mySubredditShortcuts[idx].subreddit;
 
16321                                         if (typeof modules['subredditManager'].RESPro === 'undefined') {
 
16322                                                 if (RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.RESPro.'+RESUtils.loggedInUser()) !== null) {
 
16323                                                         var temp = safeJSON.parse(RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.RESPro.'+RESUtils.loggedInUser()), 'RESmodules.subredditManager.subredditShortcuts.RESPro.'+RESUtils.loggedInUser());
 
16325                                                         var temp = { add: {}, del: {} };
 
16327                                                 modules['subredditManager'].RESPro = temp;
 
16329                                         if (typeof modules['subredditManager'].RESPro.add === 'undefined') {
 
16330                                                 modules['subredditManager'].RESPro.add = {}
 
16332                                         if (typeof modules['subredditManager'].RESPro.del === 'undefined') {
 
16333                                                 modules['subredditManager'].RESPro.del = {}
 
16335                                         // add modules['subredditManager'] new subreddit next time we sync...
 
16336                                         modules['subredditManager'].RESPro.add[subreddit] = true;
 
16337                                         // delete the old one
 
16338                                         modules['subredditManager'].RESPro.del[oldsubreddit] = true;
 
16339                                         // make sure we don't run an add on the old subreddit next time we sync...
 
16340                                         if (typeof modules['subredditManager'].RESPro.add[oldsubreddit] !== 'undefined') delete modules['subredditManager'].RESPro.add[oldsubreddit];
 
16341                                         // make sure we don't run a delete on the new subreddit next time we sync...
 
16342                                         if (typeof modules['subredditManager'].RESPro.del[subreddit] !== 'undefined') delete modules['subredditManager'].RESPro.del[subreddit];
 
16343                                         RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.RESPro.'+RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].RESPro));
 
16345                                 modules['subredditManager'].mySubredditShortcuts[idx] = {
 
16346                                         subreddit: subreddit,
 
16347                                         displayName: displayName,
 
16348                                         addedDate: new Date()
 
16351                                 modules['subredditManager'].saveLatestShortcuts();
 
16353                                 if (RESUtils.proEnabled()) {
 
16354                                         modules['RESPro'].saveModuleData('subredditManager');
 
16358                         modules['subredditManager'].editShortcutDialog.style.display = 'none';
 
16359                         modules['subredditManager'].redrawShortcuts();
 
16360                         modules['subredditManager'].populateSubredditDropdown();
 
16363                 // handle enter and escape keys in the dialog box...
 
16364                 this.subredditInput.addEventListener('keyup', function(e) {
 
16365                         if (e.keyCode === 27) {
 
16366                                 modules['subredditManager'].editShortcutDialog.style.display = 'none';
 
16367                                 modules['subredditManager'].editShortcutDialog.blur();
 
16368                         } else if (e.keyCode === 13) {
 
16369                                 RESUtils.click(modules['subredditManager'].saveButton);
 
16372                 this.displayNameInput.addEventListener('keyup', function(e) {
 
16373                         if (e.keyCode === 27) {
 
16374                                 modules['subredditManager'].editShortcutDialog.style.display = 'none';
 
16375                                 modules['subredditManager'].editShortcutDialog.blur();
 
16376                         } else if (e.keyCode === 13) {
 
16377                                 RESUtils.click(modules['subredditManager'].saveButton);
 
16381                 var cancelButton = this.editShortcutDialog.querySelector('#editShortcutClose');
 
16382                 cancelButton.addEventListener('click', function(e) {
 
16383                         modules['subredditManager'].editShortcutDialog.style.display = 'none';
 
16386                 this.editShortcutDialog.style.display = 'block';
 
16387                 var thisLeft = Math.min(RESUtils.mouseX, window.innerWidth - 300);
 
16388                 this.editShortcutDialog.style.left = thisLeft + 'px';
 
16390                 setTimeout(function() {
 
16391                         modules['subredditManager'].subredditInput.focus()
 
16394         followSubredditShortcut: function() {
 
16395                 if (BrowserDetect.isFirefox()) {
 
16396                         // stupid firefox... sigh...
 
16397                         location.href = location.protocol + '//' + location.hostname + modules['subredditManager'].clickedShortcut;
 
16399                         location.href = modules['subredditManager'].clickedShortcut;
 
16402         subredditDragStart: function(e) {
 
16403                 clearTimeout(modules['subredditManager'].clickTimer);
 
16404                 // Target (this) element is the source node.
 
16405                 this.style.opacity = '0.4';
 
16406                 modules['subredditManager'].shortCutsTrash.style.display = 'block';
 
16407                 modules['subredditManager'].dragSrcEl = this;
 
16409                 e.dataTransfer.effectAllowed = 'move';
 
16410                 // because Safari is stupid, we have to do this.
 
16411                 modules['subredditManager'].srDataTransfer = this.getAttribute('orderIndex') + ',' + $(this).data('subreddit');
 
16413         subredditDragEnter: function(e) {
 
16414                 this.classList.add('srOver');
 
16417         subredditDragOver: function(e) {
 
16418                 if (e.preventDefault) {
 
16419                         e.preventDefault(); // Necessary. Allows us to drop.
 
16422                 // See the section on the DataTransfer object.
 
16423                 e.dataTransfer.dropEffect = 'move';  
 
16426         subredditDragLeave: function(e) {
 
16427                 this.classList.remove('srOver');
 
16430         subredditDrop: function(e) {
 
16431                 // this/e.target is current target element.
 
16432                 if (e.stopPropagation) {
 
16433                         e.stopPropagation(); // Stops some browsers from redirecting.
 
16436                 // Stops other browsers from redirecting.
 
16437                 e.preventDefault();
 
16439                 modules['subredditManager'].shortCutsTrash.style.display = 'none';
 
16440                 // Don't do anything if dropping the same column we're dragging.
 
16441                 if (modules['subredditManager'].dragSrcEl !== this) {
 
16442                         if (e.target.getAttribute('id') !== 'RESShortcutsTrash') {
 
16443                                 // get the order index of the src and destination to swap...
 
16444                                 // var theData = e.dataTransfer.getData('text/html').split(',');
 
16445                                 var theData = modules['subredditManager'].srDataTransfer.split(',');
 
16446                                 var srcOrderIndex = parseInt(theData[0], 10);
 
16447                                 var srcSubreddit = modules['subredditManager'].mySubredditShortcuts[srcOrderIndex];
 
16448                                 var destOrderIndex = parseInt(this.getAttribute('orderIndex'), 10);
 
16449                                 var destSubreddit = modules['subredditManager'].mySubredditShortcuts[destOrderIndex];
 
16450                                 var rearranged = [];
 
16451                                 var rearrangedI = 0;
 
16453                                 for (var i = 0, len = modules['subredditManager'].mySubredditShortcuts.length; i < len; i++) {
 
16454                                         if ((i !== srcOrderIndex) && (i !== destOrderIndex)) {
 
16455                                                 rearranged[rearrangedI] = modules['subredditManager'].mySubredditShortcuts[i];
 
16457                                         } else if (i === destOrderIndex) {
 
16458                                                 if (destOrderIndex > srcOrderIndex) {
 
16459                                                         // if dragging right, order dest first, src next.
 
16460                                                         rearranged[rearrangedI] = destSubreddit;
 
16462                                                         rearranged[rearrangedI] = srcSubreddit;
 
16465                                                         // if dragging left, order src first, dest next.
 
16466                                                         rearranged[rearrangedI] = srcSubreddit;
 
16468                                                         rearranged[rearrangedI] = destSubreddit;
 
16474                                 // save the updated order...
 
16475                                 modules['subredditManager'].mySubredditShortcuts = rearranged;
 
16476                                 modules['subredditManager'].saveLatestShortcuts();
 
16477                                 // redraw the shortcut bar...
 
16478                                 modules['subredditManager'].redrawShortcuts();
 
16479                                 this.classList.remove('srOver');
 
16481                                 var theData = modules['subredditManager'].srDataTransfer.split(',');
 
16482                                 var srcOrderIndex = parseInt(theData[0], 10);
 
16483                                 var srcSubreddit = theData[1];
 
16484                                 modules['subredditManager'].removeSubredditShortcut(srcSubreddit);
 
16489         subredditDragEnd: function(e) {
 
16490                 modules['subredditManager'].shortCutsTrash.style.display = 'none';
 
16491                 this.style.opacity = '1';
 
16494         redrawSubredditBar: function() {
 
16495                 this.headerContents = document.querySelector('#sr-header-area');
 
16496                 if (this.headerContents) {
 
16497                         // for opera, because it renders progressively and makes it look "glitchy", hide the header bar, then show it all at once with CSS.
 
16498                         // if (BrowserDetect.isOpera()) this.headerContents.style.display = 'none';
 
16499                         // Clear out the existing stuff in the top bar first, we'll replace it with our own stuff.
 
16500                         $(this.headerContents).html('');
 
16502                         this.srLeftContainer = createElementWithID('div','srLeftContainer');
 
16503                         this.srLeftContainer.setAttribute('class','sr-bar');
 
16505                         this.srDropdown = createElementWithID('div','srDropdown');
 
16506                         this.srDropdownContainer = createElementWithID('div','srDropdownContainer');
 
16507                         $(this.srDropdownContainer).html('<a href="javascript:void(0)">My Subreddits</a>');
 
16508                         this.srDropdownContainer.addEventListener('click',modules['subredditManager'].toggleSubredditDropdown, false);
 
16509                         this.srDropdown.appendChild(this.srDropdownContainer);
 
16511                         this.srList = createElementWithID('table','srList');
 
16512                         var maxHeight = $(window).height() - 40;
 
16513                         $(this.srList).css('max-height', maxHeight + 'px');
 
16514                         // this.srDropdownContainer.appendChild(this.srList);
 
16515                         document.body.appendChild(this.srList);
 
16517                         this.srLeftContainer.appendChild(this.srDropdown);
 
16518                         var sep = document.createElement('span');
 
16519                         sep.setAttribute('class','srSep');
 
16520                         sep.textContent = '|';  
 
16521                         this.srLeftContainer.appendChild(sep);
 
16523                         // now put in the shortcuts...
 
16524                         this.staticShortCutsContainer = document.createElement('div');
 
16525                         this.staticShortCutsContainer.setAttribute('id','RESStaticShortcuts');
 
16526                         /* this probably isn't the best way to give the option, since the mechanic is drag/drop for other stuff..  but it's much easier for now... */
 
16527                         $(this.staticShortCutsContainer).html('');
 
16528                         var specialButtonSelected = {};
 
16529                         var subLower = (RESUtils.currentSubreddit()) ? RESUtils.currentSubreddit().toLowerCase() : 'home';
 
16530                         specialButtonSelected[subLower] = 'RESShortcutsCurrentSub';
 
16532                         var shortCutsHTML = '';
 
16534                         if (this.options.linkDashboard.value) shortCutsHTML += '<span class="separator">-</span><a id="RESDashboardLink" class="subbarlink '+specialButtonSelected['dashboard']+'" href="/r/Dashboard/">DASHBOARD</a>';
 
16535                         if (this.options.linkFront.value) shortCutsHTML += '<span class="separator">-</span><a class="subbarlink '+specialButtonSelected['home']+'" href="/">FRONT</a>';
 
16536                         if (this.options.linkAll.value) shortCutsHTML += '<span class="separator">-</span><a class="subbarlink '+specialButtonSelected['all']+'" href="/r/all/">ALL</a>';
 
16537                         if (this.options.linkRandom.value) shortCutsHTML += '<span class="separator">-</span><a class="subbarlink" href="/r/random/">RANDOM</a>';
 
16538                         if (this.options.linkMyRandom.value && this.myRandomEnabled) shortCutsHTML += '<span class="separator">-</span><a class="subbarlink '+(this.myRandomGold ? 'gold' : '') + '" href="/r/myrandom/">MYRANDOM</a>';
 
16539                         if (this.options.linkRandNSFW.value) shortCutsHTML += '<span class="separator over18">-</span><a class="subbarlink over18" href="/r/randnsfw/">RANDNSFW</a>';
 
16541                         if (RESUtils.loggedInUser()) {
 
16542                                 if (this.options.linkFriends.value) shortCutsHTML += '<span class="separator">-</span><a class="subbarlink '+specialButtonSelected['friends']+'" href="/r/friends/">FRIENDS</a>';
 
16544                                 var modmail = document.getElementById('modmail');
 
16546                                         if (this.options.linkMod.value) shortCutsHTML += '<span class="separator">-</span><a class=" '+specialButtonSelected['mod']+'" href="/r/mod/">MOD</a>';
 
16547                                         if (this.options.linkModqueue.value) shortCutsHTML += '<span class="separator">-</span><a class="subbarlink" href="/r/mod/about/modqueue">MODQUEUE</a>';
 
16550                         $(this.staticShortCutsContainer).append(shortCutsHTML);
 
16552                         this.srLeftContainer.appendChild(this.staticShortCutsContainer);
 
16553                         this.srLeftContainer.appendChild(sep);
 
16554                         this.headerContents.appendChild(this.srLeftContainer);                  
 
16556                         this.shortCutsViewport = document.createElement('div');
 
16557                         this.shortCutsViewport.setAttribute('id','RESShortcutsViewport');
 
16558                         this.headerContents.appendChild(this.shortCutsViewport);
 
16560                         this.shortCutsContainer = document.createElement('div');
 
16561                         this.shortCutsContainer.setAttribute('id','RESShortcuts');
 
16562                         this.shortCutsContainer.setAttribute('class','sr-bar');
 
16563                         this.shortCutsViewport.appendChild(this.shortCutsContainer);
 
16565                         this.shortCutsEditContainer = document.createElement('div');
 
16566                         this.shortCutsEditContainer.setAttribute('id','RESShortcutsEditContainer');
 
16567                         this.headerContents.appendChild(this.shortCutsEditContainer);
 
16569                         // Add shortcut sorting arrow
 
16570                         this.sortShortcutsButton = document.createElement('div');
 
16571                         this.sortShortcutsButton.setAttribute('id','RESShortcutsSort');
 
16572                         this.sortShortcutsButton.setAttribute('title','sort subreddit shortcuts');
 
16573                         this.sortShortcutsButton.innerHTML = '↑↓';
 
16574                         this.sortShortcutsButton.addEventListener('click', modules['subredditManager'].showSortMenu, false);
 
16575                         this.shortCutsEditContainer.appendChild(this.sortShortcutsButton);
 
16577                         // add right scroll arrow...
 
16578                         this.shortCutsRight = document.createElement('div');
 
16579                         this.shortCutsRight.setAttribute('id','RESShortcutsRight');
 
16580                         this.shortCutsRight.textContent = '>';
 
16581                         this.shortCutsRight.addEventListener('click', function(e) {
 
16582                                 modules['subredditManager'].containerWidth = modules['subredditManager'].shortCutsContainer.offsetWidth;
 
16583                                 var marginLeft = modules['subredditManager'].shortCutsContainer.firstChild.style.marginLeft;
 
16584                                 marginLeft = parseInt(marginLeft.replace('px', ''), 10);
 
16586                                 if (isNaN(marginLeft)) marginLeft = 0;
 
16588                                 var shiftWidth = $('#RESShortcutsViewport').width() - 80;
 
16589                                 if (modules['subredditManager'].containerWidth > (shiftWidth)) {
 
16590                                         marginLeft -= shiftWidth;
 
16591                                         modules['subredditManager'].shortCutsContainer.firstChild.style.marginLeft = marginLeft + 'px';
 
16594                         this.shortCutsEditContainer.appendChild(this.shortCutsRight);
 
16596                         // add an "add shortcut" button...
 
16597                         this.shortCutsAdd = document.createElement('div');
 
16598                         this.shortCutsAdd.setAttribute('id','RESShortcutsAdd');
 
16599                         this.shortCutsAdd.textContent = '+';
 
16600                         this.shortCutsAdd.title = 'add shortcut';
 
16601                         this.shortCutsAddFormContainer = document.createElement('div');
 
16602                         this.shortCutsAddFormContainer.setAttribute('id','RESShortcutsAddFormContainer');
 
16603                         this.shortCutsAddFormContainer.style.display = 'none';
 
16605                                 <form id="shortCutsAddForm"> \
 
16606                                         <div>Add shortcut or multi-reddit (i.e. foo+bar+baz):</div> \
 
16607                                         <label for="newShortcut">Subreddit:</label> <input type="text" id="newShortcut"><br> \
 
16608                                         <label for="displayName">Display Name:</label> <input type="text" id="displayName"><br> \
 
16609                                         <input type="submit" name="submit" value="add" id="addSubreddit"> \
 
16610                                         <div style="clear: both; float: right; margin-top: 5px;"><a style="font-size: 9px;" href="/subreddits/">Edit frontpage subscriptions</a></div> \
 
16613                         $(this.shortCutsAddFormContainer).html(thisForm);
 
16614                         this.shortCutsAddFormField = this.shortCutsAddFormContainer.querySelector('#newShortcut');
 
16615                         this.shortCutsAddFormFieldDisplayName = this.shortCutsAddFormContainer.querySelector('#displayName');
 
16617                         modules['subredditManager'].shortCutsAddFormField.addEventListener('keyup', function(e) {
 
16618                                 if (e.keyCode === 27) {
 
16619                                         modules['subredditManager'].shortCutsAddFormContainer.style.display = 'none';
 
16620                                         modules['subredditManager'].shortCutsAddFormField.blur();
 
16624                         modules['subredditManager'].shortCutsAddFormFieldDisplayName.addEventListener('keyup', function(e) {
 
16625                                 if (e.keyCode === 27) {
 
16626                                         modules['subredditManager'].shortCutsAddFormContainer.style.display = 'none';
 
16627                                         modules['subredditManager'].shortCutsAddFormFieldDisplayName.blur();
 
16631                         // add the "add shortcut" form...
 
16632                         this.shortCutsAddForm = this.shortCutsAddFormContainer.querySelector('#shortCutsAddForm');
 
16633                         this.shortCutsAddForm.addEventListener('submit', function(e) {
 
16634                                 e.preventDefault();
 
16635                                 var subreddit = modules['subredditManager'].shortCutsAddFormField.value;
 
16636                                 var displayname = modules['subredditManager'].shortCutsAddFormFieldDisplayName.value;
 
16637                                 if (displayname === '') displayname = subreddit;
 
16639                                 var r_match_regex = /^(\/r\/|r\/)(.*)/i;
 
16640                                 if(r_match_regex.test(subreddit)) {
 
16641                                         subreddit = subreddit.match(r_match_regex)[2];
 
16644                                 modules['subredditManager'].shortCutsAddFormField.value = '';
 
16645                                 modules['subredditManager'].shortCutsAddFormFieldDisplayName.value = '';
 
16646                                 modules['subredditManager'].shortCutsAddFormContainer.style.display = 'none';
 
16649                                         modules['subredditManager'].addSubredditShortcut(subreddit, displayname);
 
16652                         this.shortCutsAdd.addEventListener('click', function(e) {
 
16653                                 if (modules['subredditManager'].shortCutsAddFormContainer.style.display === 'none') {
 
16654                                         modules['subredditManager'].shortCutsAddFormContainer.style.display = 'block';
 
16655                                         modules['subredditManager'].shortCutsAddFormField.focus();
 
16657                                         modules['subredditManager'].shortCutsAddFormContainer.style.display = 'none';
 
16658                                         modules['subredditManager'].shortCutsAddFormField.blur();
 
16661                         this.shortCutsEditContainer.appendChild(this.shortCutsAdd);
 
16662                         document.body.appendChild(this.shortCutsAddFormContainer);
 
16664                         // add the "trash bin"...
 
16665                         this.shortCutsTrash = document.createElement('div');
 
16666                         this.shortCutsTrash.setAttribute('id','RESShortcutsTrash');
 
16667                         this.shortCutsTrash.textContent = '×';
 
16668                         this.shortCutsTrash.addEventListener('dragenter', modules['subredditManager'].subredditDragEnter, false)
 
16669                         this.shortCutsTrash.addEventListener('dragleave', modules['subredditManager'].subredditDragLeave, false);
 
16670                         this.shortCutsTrash.addEventListener('dragover', modules['subredditManager'].subredditDragOver, false);
 
16671                         this.shortCutsTrash.addEventListener('drop', modules['subredditManager'].subredditDrop, false);
 
16672                         this.shortCutsEditContainer.appendChild(this.shortCutsTrash);
 
16674                         // add left scroll arrow...
 
16675                         this.shortCutsLeft = document.createElement('div');
 
16676                         this.shortCutsLeft.setAttribute('id','RESShortcutsLeft');
 
16677                         this.shortCutsLeft.textContent = '<';
 
16678                         this.shortCutsLeft.addEventListener('click', function(e) {
 
16679                                 var marginLeft = modules['subredditManager'].shortCutsContainer.firstChild.style.marginLeft;
 
16680                                 marginLeft = parseInt(marginLeft.replace('px', ''), 10);
 
16682                                 if (isNaN(marginLeft)) marginLeft = 0;
 
16684                                 var shiftWidth = $('#RESShortcutsViewport').width() - 80;
 
16685                                 marginLeft += shiftWidth;
 
16686                                 if (marginLeft <= 0) {
 
16687                                         modules['subredditManager'].shortCutsContainer.firstChild.style.marginLeft = marginLeft + 'px';
 
16690                         this.shortCutsEditContainer.appendChild(this.shortCutsLeft);
 
16692                         this.redrawShortcuts();
 
16695         showSortMenu: function() {
 
16696                 // Add shortcut sorting menu if it doesn't exist in the DOM yet...
 
16697                 if (!modules['subredditManager'].sortMenu) {
 
16698                         modules['subredditManager'].sortMenu = 
 
16699                                 $('<div id="sort-menu" class="drop-choices">' +
 
16700                                 '<p> sort by:</p>' +
 
16701                                 '<a class="choice" data-field="displayName" href="javascript:void(0);">display name</a>' +
 
16702                                 '<a class="choice" data-field="addedDate" href="javascript:void(0);">added date</a>' +
 
16705                         $(modules['subredditManager'].sortMenu).find('a').click(modules['subredditManager'].sortShortcuts);
 
16707                         $(document.body).append(modules['subredditManager'].sortMenu);
 
16709                 var menu = modules['subredditManager'].sortMenu;
 
16710                 if ($(menu).is(':visible')) {
 
16714                 var thisXY = $(modules['subredditManager'].sortShortcutsButton).offset();
 
16715                 thisXY.left = thisXY.left - $(menu).width() + $(modules['subredditManager'].sortShortcutsButton).width();
 
16716                 var thisHeight = $(modules['subredditManager'].sortShortcutsButton).height();
 
16719                         top: thisXY.top + thisHeight,
 
16723         hideSortMenu: function() {
 
16724                 var menu = modules['subredditManager'].sortMenu;
 
16727         sortShortcuts: function(e) {
 
16728                 modules['subredditManager'].hideSortMenu();
 
16730                 var sortingField = $(this).data('field');
 
16731                 var asc = ! modules['subredditManager'].currentSort;
 
16732                 // toggle sort method...
 
16733                 modules['subredditManager'].currentSort = !modules['subredditManager'].currentSort;
 
16734                 // Make sure we have a valid list of shortucts
 
16735                 if (!modules['subredditManager'].mySubredditShortcuts) {
 
16736                     modules['subredditManager'].getLatestShortcuts();
 
16739                 modules['subredditManager'].mySubredditShortcuts = modules['subredditManager'].mySubredditShortcuts.sort(function(a, b) {
 
16740                     // var sortingField = field; // modules['subredditManager'].options.sortingField.value;
 
16741                     // var asc = order === 'asc'; // (modules['subredditManager'].options.sortingDirection.value === 'asc');
 
16742                     var aField = a[sortingField];
 
16743                     var bField = b[sortingField];
 
16744                     if (typeof aField === 'string' && typeof bField === 'string') {
 
16745                         aField = aField.toLowerCase();
 
16746                         bField = bField.toLowerCase();
 
16749                     if (aField === bField) {
 
16751                 } else if (aField > bField) {
 
16752                     return (asc) ? 1 : -1;
 
16754                     return (asc) ? -1 : 1;
 
16758             // Save shortcuts sort order
 
16759             modules['subredditManager'].saveLatestShortcuts();
 
16761             // Refresh shortcuts
 
16762             modules['subredditManager'].redrawShortcuts();
 
16764         toggleSubredditDropdown: function(e) {
 
16765                 e.stopPropagation();
 
16766                 if (modules['subredditManager'].srList.style.display === 'block') {
 
16767                         modules['subredditManager'].srList.style.display = 'none';
 
16768                         document.body.removeEventListener('click',modules['subredditManager'].toggleSubredditDropdown, false);
 
16770                         if (RESUtils.loggedInUser()) {
 
16771                                 $(modules['subredditManager'].srList).html('<tr><td width="360">Loading subreddits (may take a moment)...<div id="subredditPagesLoaded"></div></td></tr>');
 
16772                                 if (!modules['subredditManager'].subredditPagesLoaded) {
 
16773                                         modules['subredditManager'].subredditPagesLoaded = modules['subredditManager'].srList.querySelector('#subredditPagesLoaded');
 
16775                                 modules['subredditManager'].srList.style.display = 'block';
 
16776                                 modules['subredditManager'].getSubreddits();
 
16778                                 $(modules['subredditManager'].srList).html('<tr><td width="360">You must be logged in to load your own list of subreddits. <a style="display: inline; float: left;" href="/subreddits/">browse them all</a></td></tr>');
 
16779                                 modules['subredditManager'].srList.style.display = 'block';
 
16781                         modules['subredditManager'].srList.addEventListener('click',modules['subredditManager'].stopDropDownPropagation, false);
 
16782                         document.body.addEventListener('click',modules['subredditManager'].toggleSubredditDropdown, false);
 
16785         stopDropDownPropagation: function(e) {
 
16786                 e.stopPropagation();
 
16790         mySubredditShortcuts: [
 
16792         getSubredditJSON: function(after) {
 
16793                 var jsonURL = location.protocol + '//' + location.hostname + '/subreddits/mine/.json?app=res';
 
16794                 if (after) jsonURL += '&after='+after;
 
16795                 GM_xmlhttpRequest({
 
16798                         onload: function(response) {
 
16799                                 var thisResponse = JSON.parse(response.responseText);
 
16800                                 if ((typeof thisResponse.data !== 'undefined') && (typeof thisResponse.data.children !== 'undefined')) {
 
16801                                         if (modules['subredditManager'].subredditPagesLoaded.innerHTML === '') {
 
16802                                                 modules['subredditManager'].subredditPagesLoaded.textContent = 'Pages loaded: 1';
 
16804                                                 var pages = modules['subredditManager'].subredditPagesLoaded.innerHTML.match(/:\ ([\d]+)/);
 
16805                                                 modules['subredditManager'].subredditPagesLoaded.textContent = 'Pages loaded: ' + (parseInt(pages[1], 10)+1);
 
16808                                         var now = new Date();
 
16809                                         RESStorage.setItem('RESmodules.subredditManager.subreddits.lastCheck.'+RESUtils.loggedInUser(),now.getTime());
 
16811                                         var subreddits = thisResponse.data.children;
 
16812                                         for (var i = 0, len = subreddits.length; i < len; i++) {
 
16814                                                         display_name: subreddits[i].data.display_name,
 
16815                                                         url: subreddits[i].data.url,
 
16816                                                         over18: subreddits[i].data.over18,
 
16817                                                         id: subreddits[i].data.id,
 
16818                                                         created: subreddits[i].data.created,
 
16819                                                         description: subreddits[i].data.description
 
16821                                                 modules['subredditManager'].mySubreddits.push(srObj);
 
16824                                         if (thisResponse.data.after) {
 
16825                                                 modules['subredditManager'].getSubredditJSON(thisResponse.data.after);
 
16827                                                 modules['subredditManager'].mySubreddits.sort(function(a, b) {
 
16828                                                         var adisp = a.display_name.toLowerCase();
 
16829                                                         var bdisp = b.display_name.toLowerCase();
 
16830                                                         if (adisp > bdisp) return 1;
 
16831                                                         if (adisp == bdisp) return 0;
 
16835                                                 RESStorage.setItem('RESmodules.subredditManager.subreddits.' + RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].mySubreddits));
 
16836                                                 this.gettingSubreddits = false;
 
16837                                                 modules['subredditManager'].populateSubredditDropdown();
 
16840                                         // User is probably not logged in.. no subreddits found.
 
16841                                         modules['subredditManager'].populateSubredditDropdown(null, true);
 
16847         getSubreddits: function() {
 
16848                 modules['subredditManager'].mySubreddits = [];
 
16849                 var lastCheck = parseInt(RESStorage.getItem('RESmodules.subredditManager.subreddits.lastCheck.'+RESUtils.loggedInUser()), 10) || 0;
 
16850                 var now = new Date();
 
16851                 var check = RESStorage.getItem('RESmodules.subredditManager.subreddits.'+RESUtils.loggedInUser());
 
16853                 // 86400000 = 1 day
 
16854                 if (((now.getTime() - lastCheck) > 86400000) || !check || (check.length === 0)) {
 
16855                         if (!this.gettingSubreddits) {
 
16856                                 this.gettingSubreddits = true;
 
16857                                 this.getSubredditJSON();
 
16860                         modules['subredditManager'].mySubreddits = safeJSON.parse(check, 'RESmodules.subredditManager.subreddits.'+RESUtils.loggedInUser());
 
16861                         this.populateSubredditDropdown();
 
16864         // if badJSON is true, then getSubredditJSON ran into an error...
 
16865         populateSubredditDropdown: function(sortBy, badJSON) {
 
16866                 modules['subredditManager'].sortBy = sortBy || 'subreddit';
 
16867                 $(modules['subredditManager'].srList).html('');
 
16868                 // NOTE WE NEED TO CHECK LAST TIME THEY UPDATED THEIR SUBREDDIT LIST AND REPOPULATE...
 
16870                 var theHead = document.createElement('thead');
 
16871                 var theRow = document.createElement('tr');
 
16873                 modules['subredditManager'].srHeader = document.createElement('td');
 
16874                 modules['subredditManager'].srHeader.addEventListener('click', function() {
 
16875                         if (modules['subredditManager'].sortBy === 'subreddit') {
 
16876                                 modules['subredditManager'].populateSubredditDropdown('subredditDesc');
 
16878                                 modules['subredditManager'].populateSubredditDropdown('subreddit');
 
16881                 modules['subredditManager'].srHeader.textContent = 'subreddit';
 
16882                 modules['subredditManager'].srHeader.setAttribute('width','200');
 
16884                 modules['subredditManager'].lvHeader = document.createElement('td');
 
16885                 modules['subredditManager'].lvHeader.addEventListener('click', function() {
 
16886                         if (modules['subredditManager'].sortBy === 'lastVisited') {
 
16887                                 modules['subredditManager'].populateSubredditDropdown('lastVisitedAsc');
 
16889                                 modules['subredditManager'].populateSubredditDropdown('lastVisited');
 
16892                 modules['subredditManager'].lvHeader.textContent = 'Last Visited';
 
16893                 modules['subredditManager'].lvHeader.setAttribute('width','120');
 
16895                 var scHeader = document.createElement('td');
 
16896                 $(scHeader).width(50);
 
16897                 $(scHeader).html('<a style="float: right;" href="/subreddits/">View all »</a>');
 
16898                 theRow.appendChild(modules['subredditManager'].srHeader);
 
16899                 theRow.appendChild(modules['subredditManager'].lvHeader);
 
16900                 theRow.appendChild(scHeader);
 
16901                 theHead.appendChild(theRow);
 
16902                 modules['subredditManager'].srList.appendChild(theHead);
 
16904                 var theBody = document.createElement('tbody');
 
16906                         var subredditCount = modules['subredditManager'].mySubreddits.length;
 
16908                         if (typeof this.subredditsLastViewed === 'undefined') {
 
16909                                 var check = RESStorage.getItem('RESmodules.subredditManager.subredditsLastViewed.'+RESUtils.loggedInUser());
 
16911                                         this.subredditsLastViewed = safeJSON.parse(check, 'RESmodules.subredditManager.subredditsLastViewed.'+RESUtils.loggedInUser());
 
16913                                         this.subredditsLastViewed = {};
 
16917                         // copy modules['subredditManager'].mySubreddits to a placeholder array so we can sort without modifying it...
 
16918                         var sortableSubreddits = modules['subredditManager'].mySubreddits;
 
16919                         if (sortBy === 'lastVisited') {
 
16920                                 $(modules['subredditManager'].lvHeader).html('Last Visited <div class="sortAsc"></div>');
 
16921                                 modules['subredditManager'].srHeader.textContent = 'subreddit';
 
16923                                 sortableSubreddits.sort(function(a, b) {
 
16924                                         var adisp = a.display_name.toLowerCase();
 
16925                                         var bdisp = b.display_name.toLowerCase();
 
16927                                         (typeof modules['subredditManager'].subredditsLastViewed[adisp] === 'undefined') ? alv = 0 : alv = parseInt(modules['subredditManager'].subredditsLastViewed[adisp].last_visited, 10);
 
16928                                         (typeof modules['subredditManager'].subredditsLastViewed[bdisp] === 'undefined') ? blv = 0 : blv = parseInt(modules['subredditManager'].subredditsLastViewed[bdisp].last_visited, 10);
 
16930                                         if (alv < blv) return 1;
 
16932                                                 if (adisp > bdisp) return 1;
 
16937                         } else if (sortBy === 'lastVisitedAsc') {
 
16938                                 $(modules['subredditManager'].lvHeader).html('Last Visited <div class="sortDesc"></div>');
 
16939                                 modules['subredditManager'].srHeader.textContent = 'subreddit';
 
16941                                 sortableSubreddits.sort(function(a, b) {
 
16942                                         var adisp = a.display_name.toLowerCase();
 
16943                                         var bdisp = b.display_name.toLowerCase();
 
16945                                         (typeof modules['subredditManager'].subredditsLastViewed[adisp] === 'undefined') ? alv = 0 : alv = parseInt(modules['subredditManager'].subredditsLastViewed[adisp].last_visited, 10);
 
16946                                         (typeof modules['subredditManager'].subredditsLastViewed[bdisp] === 'undefined') ? blv = 0 : blv = parseInt(modules['subredditManager'].subredditsLastViewed[bdisp].last_visited, 10);
 
16948                                         if (alv > blv) return 1;
 
16950                                                 if (adisp > bdisp) return 1;
 
16955                         } else if (sortBy === 'subredditDesc') {
 
16956                                 modules['subredditManager'].lvHeader.textContent = 'Last Visited';
 
16957                                 $(modules['subredditManager'].srHeader).html('subreddit <div class="sortDesc"></div>');
 
16959                                 sortableSubreddits.sort(function(a,b) {
 
16960                                         var adisp = a.display_name.toLowerCase();
 
16961                                         var bdisp = b.display_name.toLowerCase();
 
16963                                         if (adisp < bdisp) return 1;
 
16964                                         if (adisp == bdisp) return 0;
 
16968                                 modules['subredditManager'].lvHeader.textContent = 'Last Visited';
 
16969                                 $(modules['subredditManager'].srHeader).html('subreddit <div class="sortAsc"></div>');
 
16971                                 sortableSubreddits.sort(function(a,b) {
 
16972                                         var adisp = a.display_name.toLowerCase();
 
16973                                         var bdisp = b.display_name.toLowerCase();
 
16975                                         if (adisp > bdisp) return 1;
 
16976                                         if (adisp == bdisp) return 0;
 
16980                         for (var i=0; i<subredditCount; i++) {
 
16981                                 var dateString = 'Never';
 
16982                                 var thisReddit = sortableSubreddits[i].display_name.toLowerCase();
 
16983                                 if (typeof this.subredditsLastViewed[thisReddit] !== 'undefined') {
 
16984                                         var ts = parseInt(this.subredditsLastViewed[thisReddit].last_visited, 10);
 
16985                                         var dateVisited = new Date(ts);
 
16986                                         dateString = RESUtils.niceDate(dateVisited);
 
16989                                 var theRow = document.createElement('tr');
 
16990                                 var theSR = document.createElement('td');
 
16991                                 $(theSR).html('<a href="'+escapeHTML(modules['subredditManager'].mySubreddits[i].url)+'">'+escapeHTML(modules['subredditManager'].mySubreddits[i].display_name)+'</a>');
 
16992                                 theRow.appendChild(theSR);
 
16994                                 var theLV = document.createElement('td');
 
16995                                 theLV.textContent = dateString;
 
16996                                 theLV.setAttribute('class','RESvisited');
 
16997                                 theRow.appendChild(theLV);
 
16999                                 var theSC = document.createElement('td');
 
17000                                 theSC.setAttribute('class','RESshortcut');
 
17001                                 theSC.setAttribute('data-subreddit',modules['subredditManager'].mySubreddits[i].display_name);
 
17004                                 for (var j = 0, len = modules['subredditManager'].mySubredditShortcuts.length; j < len; j++) {
 
17005                                         if (modules['subredditManager'].mySubredditShortcuts[j].subreddit === modules['subredditManager'].mySubreddits[i].display_name) {
 
17012                                         theSC.addEventListener('click', function(e) {
 
17013                                                 if (e.stopPropagation) {
 
17014                                                         e.stopPropagation(); // Stops from triggering the click on the bigger box, which toggles this window closed...
 
17017                                                 var subreddit = $(e.target).data('subreddit');
 
17018                                                 modules['subredditManager'].removeSubredditShortcut(subreddit);
 
17021                                         theSC.textContent = '-shortcut';
 
17023                                         theSC.addEventListener('click', function(e) {
 
17024                                                 if (e.stopPropagation) {
 
17025                                                         e.stopPropagation(); // Stops from triggering the click on the bigger box, which toggles this window closed...
 
17028                                                 var subreddit = $(e.target).data('subreddit');
 
17029                                                 modules['subredditManager'].addSubredditShortcut(subreddit);
 
17032                                         theSC.textContent = '+shortcut';
 
17035                                 theRow.appendChild(theSC);
 
17036                                 theBody.appendChild(theRow);
 
17039                         var theTD = document.createElement('td');
 
17040                         theTD.textContent = 'There was an error getting your subreddits. You may have third party cookies disabled by your browser. For this function to work, you\'ll need to add an exception for cookies from reddit.com';
 
17041                         theTD.setAttribute('colspan','3');
 
17043                         var theRow = document.createElement('tr');
 
17044                         theRow.appendChild(theTD);
 
17045                         theBody.appendChild(theRow);
 
17048                 modules['subredditManager'].srList.appendChild(theBody);
 
17050         toggleSubredditShortcut: function(e) {
 
17051                 e.stopPropagation(); // Stops from triggering the click on the bigger box, which toggles this window closed...
 
17053                 var isShortcut = false;
 
17054                 for (var i = 0, len = modules['subredditManager'].mySubredditShortcuts.length; i < len; i++) {
 
17055                         if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() === $(this).data('subreddit').toLowerCase()) {
 
17062                         modules['subredditManager'].removeSubredditShortcut($(this).data('subreddit'));
 
17064                                 .attr('title', 'Add this subreddit to your shortcut bar')
 
17066                                 .removeClass('remove');
 
17068                         modules['subredditManager'].addSubredditShortcut($(this).data('subreddit'));
 
17070                                 .attr('title', 'Remove this subreddit from your shortcut bar')
 
17072                                 .addClass('remove');
 
17075                 modules['subredditManager'].redrawShortcuts();
 
17077         getLatestShortcuts: function() {
 
17078                 // re-retreive the latest data to ensure we're not losing info between tab changes...
 
17079                 var shortCuts = RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.' + RESUtils.loggedInUser());
 
17084                 this.mySubredditShortcuts = safeJSON.parse(shortCuts, 'RESmodules.subredditManager.subredditShortcuts.' + RESUtils.loggedInUser());
 
17087         // JSON specification doesn't specify what to do with dates - so unstringify here
 
17088         parseDates: function () {
 
17089             for (var i = 0, len = this.mySubredditShortcuts.length; i < len; i++) {
 
17090                 this.mySubredditShortcuts[i].addedDate = this.mySubredditShortcuts[i].addedDate
 
17091                         ? new Date(this.mySubredditShortcuts[i].addedDate)
 
17095         saveLatestShortcuts: function() {
 
17096                 // Retreive the latest data to ensure we're not losing info
 
17097                 if (!modules['subredditManager'].mySubredditShortcuts) {
 
17098                         modules['subredditManager'].mySubredditShortcuts = [];
 
17101                 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.' + RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].mySubredditShortcuts));
 
17103         addSubredditShortcut: function(subreddit, displayname) {
 
17104                 modules['subredditManager'].getLatestShortcuts();
 
17107                 for (var i = 0, len=modules['subredditManager'].mySubredditShortcuts.length; i < len; i++) {
 
17108                         if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() === subreddit.toLowerCase()) {
 
17115                         alert('Whoops, you already have a shortcut for that subreddit');
 
17117                         displayname = displayname || subreddit;
 
17118                         var subredditObj = {
 
17119                                 subreddit: subreddit,
 
17120                                 displayName: displayname.toLowerCase(),
 
17121                                 addedDate: new Date()
 
17124                         modules['subredditManager'].mySubredditShortcuts.push(subredditObj);
 
17125                         if (RESUtils.proEnabled()) {
 
17126                                 if (typeof modules['subredditManager'].RESPro === 'undefined') {
 
17127                                         if (RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser())) {
 
17128                                                 var temp = safeJSON.parse(RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser()), 'RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser());
 
17130                                                 var temp = { add: {}, del: {} };
 
17133                                         modules['subredditManager'].RESPro = temp;
 
17136                                 if (typeof modules['subredditManager'].RESPro.add === 'undefined') {
 
17137                                         modules['subredditManager'].RESPro.add = {}
 
17140                                 if (typeof modules['subredditManager'].RESPro.del === 'undefined') {
 
17141                                         modules['subredditManager'].RESPro.del = {}
 
17144                                 // add this subreddit next time we sync...
 
17145                                 modules['subredditManager'].RESPro.add[subreddit] = true;
 
17147                                 // make sure we don't run a delete on this subreddit next time we sync...
 
17148                                 if (typeof modules['subredditManager'].RESPro.del[subreddit] !== 'undefined') delete modules['subredditManager'].RESPro.del[subreddit];
 
17150                                 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.RESPro.'+RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].RESPro));
 
17153                         modules['subredditManager'].saveLatestShortcuts();
 
17154                         modules['subredditManager'].redrawShortcuts();
 
17155                         modules['subredditManager'].populateSubredditDropdown();
 
17157                         if (RESUtils.proEnabled()) {
 
17158                                 modules['RESPro'].saveModuleData('subredditManager');
 
17161                         RESUtils.notification({ 
 
17162                                 moduleID: 'subredditManager',
 
17163                                 message: 'Subreddit shortcut added. You can edit by double clicking, or trash by dragging to the trash can.'
 
17167         removeSubredditShortcut: function(subreddit) {
 
17168                 this.getLatestShortcuts();
 
17171                 for (var i = 0, len = modules['subredditManager'].mySubredditShortcuts.length; i < len; i++) {
 
17172                         if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() === subreddit.toLowerCase()) {
 
17179                         modules['subredditManager'].mySubredditShortcuts.splice(idx, 1);
 
17181                         if (RESUtils.proEnabled()) {
 
17182                                 if (typeof modules['subredditManager'].RESPro === 'undefined') {
 
17183                                         if (RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser())) {
 
17184                                                 var temp = safeJSON.parse(RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser()), 'RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser());
 
17186                                                 var temp = { add: {}, del: {} };
 
17189                                         modules['subredditManager'].RESPro = temp;
 
17191                                 if (typeof modules['subredditManager'].RESPro.add === 'undefined') {
 
17192                                         modules['subredditManager'].RESPro.add = {}
 
17194                                 if (typeof modules['subredditManager'].RESPro.del === 'undefined') {
 
17195                                         modules['subredditManager'].RESPro.del = {}
 
17198                                 // delete this subreddit next time we sync...
 
17199                                 modules['subredditManager'].RESPro.del[subreddit] = true;
 
17201                                 // make sure we don't run an add on this subreddit
 
17202                                 if (typeof modules['subredditManager'].RESPro.add[subreddit] !== 'undefined') delete modules['subredditManager'].RESPro.add[subreddit];
 
17204                                 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].RESPro));
 
17207                         modules['subredditManager'].saveLatestShortcuts();
 
17208                         modules['subredditManager'].redrawShortcuts();
 
17209                         modules['subredditManager'].populateSubredditDropdown();
 
17211                         if (RESUtils.proEnabled()) {
 
17212                                 modules['RESPro'].saveModuleData('subredditManager');
 
17216         setLastViewtime: function() {
 
17217                 var check = RESStorage.getItem('RESmodules.subredditManager.subredditsLastViewed.' + RESUtils.loggedInUser());
 
17220                         this.subredditsLastViewed = {};
 
17222                         this.subredditsLastViewed = safeJSON.parse(check, 'RESmodules.subredditManager.subredditsLastViewed.' + RESUtils.loggedInUser());
 
17225                 var now = new Date();
 
17226                 var thisReddit = RESUtils.currentSubreddit().toLowerCase();
 
17227                 this.subredditsLastViewed[thisReddit] = {
 
17228                         last_visited: now.getTime()
 
17231                 RESStorage.setItem('RESmodules.subredditManager.subredditsLastViewed.' + RESUtils.loggedInUser(), JSON.stringify(this.subredditsLastViewed));
 
17233         subscribeToSubreddit: function(subredditName, subscribe) {
 
17234                 // subredditName should look like t5_123asd
 
17235                 subscribe = subscribe !== false; // default to true
 
17236                 var userHash = RESUtils.loggedInUserHash();
 
17238                 var formData = new FormData();
 
17239                 formData.append('sr', subredditName);
 
17240                 formData.append('action', subscribe ? 'sub' : 'unsub');
 
17241                 formData.append('uh', userHash);
 
17243                 GM_xmlhttpRequest({
 
17245                         url:    location.protocol + "//"+location.hostname+"/api/subscribe?app=res",
 
17251 }; // note: you NEED this semicolon at the end!
 
17253 // RES Pro needs some work still... not ready yet.
 
17255 modules['RESPro'] = {
 
17256         moduleID: 'RESPro',
 
17257         moduleName: 'RES Pro',
 
17258         category: 'Pro Features',
 
17260                 // any configurable options you have go here...
 
17261                 // options must have a type and a value.. 
 
17262                 // valid types are: text, boolean (if boolean, value must be true or false)
 
17267                         description: 'Your RES Pro username'
 
17272                         description: 'Your RES Pro password'
 
17277                                 { name: 'Hourly', value: '3600000' },
 
17278                                 { name: 'Daily', value: '86400000' },
 
17279                                 { name: 'Manual Only', value: '-1' }
 
17282                         description: 'How often should RES automatically sync settings?'
 
17285         description: 'RES Pro allows you to sync settings and data to a server. It requires an account, which you can sign up for <a href="http://reddit.honestbleeps.com/register.php">here</a>',
 
17286         isEnabled: function() {
 
17287                 return RESConsole.getModulePrefs(this.moduleID);
 
17290                 /^https?:\/\/([a-z]+)\.reddit\.com\/?/i,
 
17291                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+/i
 
17293         isMatchURL: function() {
 
17294                 return RESUtils.isMatchURL(this.moduleID);
 
17297                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
17299                         // if we haven't synced in more than our settings, and settings != manual, sync!
 
17300                         if (this.options.syncFrequency.value > 0) {
 
17301                                 var lastSync = parseInt(RESStorage.getItem('RESmodules.RESPro.lastSync')) || 0;
 
17302                                 var now = new Date();
 
17303                                 if ((now.getTime() - lastSync) > this.options.syncFrequency.value) {
 
17304                                         this.authenticate(this.autoSync);
 
17310         autoSync: function() {
 
17311                 modules['RESPro'].authenticate(modules['RESPro'].savePrefs);
 
17313                 // modules['RESPro'].authenticate(function() {
 
17314                 //      modules['RESPro'].saveModuleData('saveComments');
 
17317         saveModuleData: function(module) {
 
17320                                 // THIS IS NOT READY YET!  We need to merge votes on the backend.. hard stuff...
 
17321                                 // in this case, we want to send the JSON from RESmodules.userTagger.tags;
 
17322                                 var tags = RESStorage.getItem('RESmodules.userTagger.tags');
 
17323                                 GM_xmlhttpRequest({
 
17325                                         url:    'http://reddit.honestbleeps.com/RESsync.php',
 
17326                                         data: 'action=PUT&type=module_data&module='+module+'&data='+tags,
 
17328                                                 "Content-Type": "application/x-www-form-urlencoded"
 
17330                                         onload: function(response) {
 
17331                                                 var resp = JSON.parse(response.responseText);
 
17332                                                 // console.log(resp);
 
17333                                                 if (resp.success) {
 
17334                                                         if (RESConsole.proUserTaggerSaveButton) RESConsole.proUserTaggerSaveButton.textContent = 'Saved!';
 
17336                                                         alert(response.responseText);
 
17341                         case 'saveComments':
 
17342                                 var savedComments = RESStorage.getItem('RESmodules.saveComments.savedComments');
 
17343                                 GM_xmlhttpRequest({
 
17345                                         url:    'http://reddit.honestbleeps.com/RESsync.php',
 
17346                                         data: 'action=PUT&type=module_data&module='+module+'&data='+savedComments,
 
17348                                                 "Content-Type": "application/x-www-form-urlencoded"
 
17350                                         onload: function(response) {
 
17351                                                 // console.log(response.responseText);
 
17352                                                 var resp = JSON.parse(response.responseText);
 
17353                                                 if (resp.success) {
 
17354                                                         if (RESConsole.proSaveCommentsSaveButton) RESConsole.proSaveCommentsSaveButton.textContent = 'Saved!';
 
17355                                                         var thisComments = safeJSON.parse(savedComments);
 
17356                                                         delete thisComments.RESPro_add;
 
17357                                                         delete thisComments.RESPro_delete;
 
17358                                                         thisComments = JSON.stringify(thisComments);
 
17359                                                         RESStorage.setItem('RESmodules.saveComments.savedComments',thisComments);
 
17360                                                         RESUtils.notification({
 
17361                                                                 header: 'RES Pro Notification', 
 
17362                                                                 message: 'Saved comments synced to server'
 
17365                                                         alert(response.responseText);
 
17370                         case 'subredditManager':
 
17371                                 var subredditManagerData = {};
 
17372                                 subredditManagerData.RESPro = {};
 
17374                                 for (var key in RESStorage) {
 
17375                                         // console.log(key);
 
17376                                         if (key.indexOf('RESmodules.subredditManager') !== -1) {
 
17377                                                 var keySplit = key.split('.');
 
17378                                                 var username = keySplit[keySplit.length-1];
 
17379                                                 if ((keySplit.indexOf('subredditsLastViewed') === -1) && (keySplit.indexOf('subreddits') === -1)) {
 
17380                                                         // console.log(key);
 
17381                                                         (keySplit.indexOf('RESPro') !== -1) ? subredditManagerData.RESPro[username] = JSON.parse(RESStorage[key]) : subredditManagerData[username] = JSON.parse(RESStorage[key]);
 
17382                                                         // if (key.indexOf('RESPro') === -1) console.log(username + ' -- ' + RESStorage[key]);
 
17383                                                         if (key.indexOf('RESPro') !== -1) RESStorage.removeItem('RESmodules.subredditManager.subredditShortcuts.RESPro.'+username);
 
17387                                 var stringData = JSON.stringify(subredditManagerData);
 
17388                                 stringData = encodeURIComponent(stringData);
 
17389                                 GM_xmlhttpRequest({
 
17391                                         url:    'http://reddit.honestbleeps.com/RESsync.php',
 
17392                                         data: 'action=PUT&type=module_data&module='+module+'&data='+stringData,
 
17394                                                 "Content-Type": "application/x-www-form-urlencoded"
 
17396                                         onload: function(response) {
 
17397                                                 console.log(response.responseText);
 
17398                                                 var resp = JSON.parse(response.responseText);
 
17399                                                 if (resp.success) {
 
17400                                                         if (RESConsole.proSubredditManagerSaveButton) RESConsole.proSubredditManagerSaveButton.textContent = 'Saved!';
 
17401                                                         RESUtils.notification({
 
17402                                                                 header: 'RES Pro Notification', 
 
17403                                                                 message: 'Subreddit shortcuts synced to server'
 
17406                                                         alert(response.responseText);
 
17412                                 console.log('invalid module specified: ' + module);
 
17416         getModuleData: function(module) {
 
17418                         case 'saveComments':
 
17419                                 if (RESConsole.proSaveCommentsGetButton) RESConsole.proSaveCommentsGetButton.textContent = 'Loading...';
 
17420                                 GM_xmlhttpRequest({
 
17422                                         url:    'http://reddit.honestbleeps.com/RESsync.php',
 
17423                                         data: 'action=GET&type=module_data&module='+module,
 
17425                                                 "Content-Type": "application/x-www-form-urlencoded"
 
17427                                         onload: function(response) {
 
17428                                                 var resp = JSON.parse(response.responseText);
 
17429                                                 if (resp.success) {
 
17430                                                         var serverResponse = JSON.parse(response.responseText);
 
17431                                                         var serverData = serverResponse.data;
 
17432                                                         currentData = safeJSON.parse(RESStorage.getItem('RESmodules.saveComments.savedComments'), 'RESmodules.saveComments.savedComments');
 
17433                                                         for (var attrname in serverData) {
 
17434                                                                 if (typeof currentData[attrname] === 'undefined') {
 
17435                                                                         currentData[attrname] = serverData[attrname];
 
17438                                                         // console.log(JSON.stringify(prefsData));
 
17439                                                         RESStorage.setItem('RESmodules.saveComments.savedComments', JSON.stringify(currentData));
 
17440                                                         if (RESConsole.proSaveCommentsGetButton) RESConsole.proSaveCommentsGetButton.textContent = 'Saved Comments Loaded!';
 
17442                                                         alert(response.responseText);
 
17447                         case 'subredditManager':
 
17448                                 if (RESConsole.proSubredditManagerGetButton) RESConsole.proSubredditManagerGetButton.textContent = 'Loading...';
 
17449                                 GM_xmlhttpRequest({
 
17451                                         url:    'http://reddit.honestbleeps.com/RESsync.php',
 
17452                                         data: 'action=GET&type=module_data&module='+module,
 
17454                                                 "Content-Type": "application/x-www-form-urlencoded"
 
17456                                         onload: function(response) {
 
17457                                                 var resp = JSON.parse(response.responseText);
 
17458                                                 if (resp.success) {
 
17459                                                         var serverResponse = JSON.parse(response.responseText);
 
17460                                                         var serverData = serverResponse.data;
 
17461                                                         for (var username in serverResponse.data) {
 
17462                                                                 var newSubredditData = serverResponse.data[username];
 
17463                                                                 var oldSubredditData = safeJSON.parse(RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.'+username), 'RESmodules.subredditManager.subredditShortcuts.'+username);
 
17464                                                                 if (oldSubredditData == null) oldSubredditData = [];
 
17465                                                                 for (var newidx in newSubredditData) {
 
17466                                                                         var exists = false;
 
17467                                                                         for (var oldidx in oldSubredditData) {
 
17468                                                                                 if (oldSubredditData[oldidx].subreddit == newSubredditData[newidx].subreddit) {
 
17469                                                                                         oldSubredditData[oldidx].displayName = newSubredditData[newidx].displayName;
 
17475                                                                                 oldSubredditData.push(newSubredditData[newidx]);
 
17478                                                                 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.'+username,JSON.stringify(oldSubredditData));
 
17481                                                         alert(response.responseText);
 
17487                                 console.log('invalid module specified: ' + module);
 
17491         savePrefs: function() {
 
17492                 // (typeof unsafeWindow !== 'undefined') ? ls = unsafeWindow.localStorage : ls = localStorage;
 
17493                 if (RESConsole.proSaveButton) RESConsole.proSaveButton.textContent = 'Saving...';
 
17494                 var RESOptions = {};
 
17495                 // for (var i = 0, len=ls.length; i < len; i++) {
 
17496                 for(var i in RESStorage) {
 
17497                         if ((typeof RESStorage.getItem(i) !== 'function') && (typeof RESStorage.getItem(i) !== 'undefined')) {
 
17498                                 var keySplit = i.split('.');
 
17500                                         var keyRoot = keySplit[0];
 
17503                                                         var thisNode = keySplit[1];
 
17504                                                         if (thisNode === 'modulePrefs') {
 
17505                                                                 RESOptions[thisNode] = safeJSON.parse(RESStorage.getItem(i), i);
 
17509                                                         var thisModule = keySplit[1];
 
17510                                                         if (thisModule !== 'accountSwitcher') {
 
17511                                                                 RESOptions[thisModule] = safeJSON.parse(RESStorage.getItem(i), i);
 
17515                                                         //console.log('Not currently handling keys with root: ' + keyRoot);
 
17521                 // Post options blob.
 
17522                 var RESOptionsString = JSON.stringify(RESOptions);
 
17523                 GM_xmlhttpRequest({
 
17525                         url:    'http://reddit.honestbleeps.com/RESsync.php',
 
17526                         data: 'action=PUT&type=all_options&data='+RESOptionsString,
 
17528                                 "Content-Type": "application/x-www-form-urlencoded"
 
17530                         onload: function(response) {
 
17531                                 var resp = JSON.parse(response.responseText);
 
17532                                 // console.log(resp);
 
17533                                 if (resp.success) {
 
17534                                         var now = new Date();
 
17535                                         RESStorage.setItem('RESmodules.RESPro.lastSync',now.getTime());
 
17536                                         if (RESConsole.proSaveButton) RESConsole.proSaveButton.textContent = 'Saved.';
 
17537                                         RESUtils.notification({
 
17538                                                 header: 'RES Pro Notification',
 
17539                                                 message: 'RES Pro - module options saved to server.'
 
17542                                         alert(response.responseText);
 
17547         getPrefs: function() {
 
17548                 console.log('get prefs called');
 
17549                 if (RESConsole.proGetButton) RESConsole.proGetButton.textContent = 'Loading...';
 
17550                 GM_xmlhttpRequest({
 
17552                         url:    'http://reddit.honestbleeps.com/RESsync.php',
 
17553                         data: 'action=GET&type=all_options',
 
17555                                 "Content-Type": "application/x-www-form-urlencoded"
 
17557                         onload: function(response) {
 
17558                                 var resp = JSON.parse(response.responseText);
 
17559                                 if (resp.success) {
 
17560                                         var modulePrefs = JSON.parse(response.responseText);
 
17561                                         var prefsData = modulePrefs.data;
 
17562                                         //console.log('prefsData:');
 
17563                                         //console.log(prefsData);
 
17564                                         for (var thisModule in prefsData){
 
17565                                                 if (thisModule === 'modulePrefs') {
 
17566                                                         var thisOptions = prefsData[thisModule];
 
17567                                                         RESStorage.setItem('RES.modulePrefs',JSON.stringify(thisOptions));
 
17569                                                         var thisOptions = prefsData[thisModule];
 
17570                                                         RESStorage.setItem('RESoptions.'+thisModule,JSON.stringify(thisOptions));
 
17573                                         if (RESConsole.proGetButton) RESConsole.proGetButton.textContent = 'Preferences Loaded!';
 
17574                                         RESUtils.notification({
 
17575                                                 header: 'RES Pro Notification',
 
17576                                                 message: 'Module options loaded.'
 
17578                                         // console.log(response.responseText);
 
17580                                         alert(response.responseText);
 
17585         configure: function() {
 
17586                 if (!RESConsole.isOpen) RESConsole.open();
 
17587                 RESConsole.menuClick(document.getElementById('Menu-'+this.category));
 
17588                 RESConsole.drawConfigOptions('RESPro');
 
17590         authenticate: function(callback) {
 
17591                 if (! this.isEnabled()) {
 
17593                 } else if ((modules['RESPro'].options.username.value === '') || (modules['RESPro'].options.password.value === '')) {
 
17594                         modules['RESPro'].configure();
 
17595                 } else if (RESStorage.getItem('RESmodules.RESPro.lastAuthFailed') !== 'true') {
 
17596                         if (typeof modules['RESPro'].lastAuthFailed === 'undefined') {
 
17597                                 GM_xmlhttpRequest({
 
17599                                         url:    'http://reddit.honestbleeps.com/RESlogin.php',
 
17600                                         data: 'uname='+modules['RESPro'].options.username.value+'&pwd='+modules['RESPro'].options.password.value,
 
17602                                                 "Content-Type": "application/x-www-form-urlencoded"
 
17604                                         onload: function(response) {
 
17605                                                 var resp = JSON.parse(response.responseText);
 
17606                                                 if (resp.success) {
 
17607                                                         // RESConsole.proAuthButton.textContent = 'Authenticated!';
 
17608                                                         RESStorage.removeItem('RESmodules.RESPro.lastAuthFailed');
 
17613                                                         // RESConsole.proAuthButton.textContent = 'Authentication failed.';
 
17614                                                         modules['RESPro'].lastAuthFailed = true;
 
17615                                                         RESStorage.setItem('RESmodules.RESPro.lastAuthFailed','true');
 
17616                                                         RESUtils.notification({
 
17617                                                                 header: 'RES Pro Notification', 
 
17618                                                                 message: 'Authentication failed - check your username and password.'
 
17628 modules['RESTips'] = {
 
17629         moduleID: 'RESTips',
 
17630         moduleName: 'RES Tips and Tricks',
 
17633                 // any configurable options you have go here...
 
17634                 // options must have a type and a value.. 
 
17635                 // valid types are: text, boolean (if boolean, value must be true or false)
 
17640                         description: 'Show a random tip once every 24 hours.'
 
17643         description: 'Adds tips/tricks help to RES console',
 
17644         isEnabled: function() {
 
17645                 return RESConsole.getModulePrefs(this.moduleID);
 
17648                 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
 
17650         isMatchURL: function() {
 
17651                 return RESUtils.isMatchURL(this.moduleID);
 
17653         beforeLoad: function() {
 
17654                 if (this.isEnabled() && this.isMatchURL()) {
 
17655                         RESUtils.addCSS('.res-help { cursor: help; }');
 
17656                         RESUtils.addCSS('.res-help #resHelp { cursor: default; }');
 
17660                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
17661                         this.menuItem = createElementWithID('li','RESTipsMenuItem');
 
17662                         this.menuItem.textContent = 'tips & tricks';
 
17663                         this.menuItem.addEventListener('click', function(e) {
 
17664                                 modules['RESTips'].randomTip();
 
17667                         $('#RESDropdownOptions').append(this.menuItem);
 
17669                         if (this.options.dailyTip.value) {
 
17674                         guiders.createGuider({
 
17675                           attachTo: '#RESSettingsButton',
 
17676                           // buttons: [{name: "Next"}],
 
17677                           description: "Guiders are a user interface design pattern for introducing features of software. This dialog box, for example, is the first in a series of guiders that together make up a guide.",
 
17682                           title: "Welcome to Guiders.js!"
 
17686                         setTimeout(function() {
 
17687                                 guiders.createGuider({
 
17688                                           attachTo: "#RESSettingsButton",
 
17689                                           buttons: [{name: "Close"},
 
17691                                           description: "This is just some sorta test guider, here... woop woop.",
 
17694                                           // offset: { left: -200, top: 120 },
 
17696                                           title: "Guiders are typically attached to an element on the page."
 
17698                                 guiders.createGuider({
 
17699                                           attachTo: "a.toggleImage:first",
 
17700                                           buttons: [{name: "Close"},
 
17702                                           description: "An example of an image expando",
 
17705                                           // offset: { left: -200, top: 120 },
 
17707                                           title: "Guiders are typically attached to an element on the page."
 
17713         dailyTip: function() {
 
17714                 var lastCheck = parseInt(RESStorage.getItem('RESLastToolTip'), 10) || 0;
 
17715                 var now = new Date();
 
17716                 // 86400000 = 1 day
 
17717                 if ((now.getTime() - lastCheck) > 86400000) {
 
17718                         // mark off that we've displayed a new tooltip
 
17719                         RESStorage.setItem('RESLastToolTip',now.getTime());
 
17720                         if (lastCheck === 0) {
 
17723                                 setTimeout(function() {
 
17724                                         modules['RESTips'].randomTip();
 
17729         randomTip: function() {
 
17730                 this.currTip = Math.floor(Math.random()*this.tips.length);
 
17731                 this.showTip(this.currTip);
 
17733         disableDailyTipsCheckbox: function(e) {
 
17734                 modules['RESTips'].options.dailyTip.value = e.target.checked;
 
17735                 RESStorage.setItem('RESoptions.RESTips', JSON.stringify(modules['RESTips'].options));
 
17737         nextTip: function() {
 
17738                 if (typeof this.currTip === 'undefined') this.currTip = 0;
 
17739                 modules['RESTips'].nextPrevTip(1);
 
17741         prevTip: function() {
 
17742                 if (typeof this.currTip === 'undefined') this.currTip = 0;
 
17743                 modules['RESTips'].nextPrevTip(-1);
 
17745         nextPrevTip: function(idx) {
 
17746                 if (typeof this.currTip === 'undefined') this.currTip = 0;
 
17747                 // if (idx<0) guiders.hideAll();
 
17749                 this.currTip += idx;
 
17750                 if (this.currTip < 0) {
 
17751                         this.currTip = this.tips.length-1;
 
17752                 } else if (this.currTip >= this.tips.length) {
 
17755                 this.showTip(this.currTip);
 
17757         generateContent: function(help, elem) {
 
17758                 var description = []
 
17760                 if (help.message) description.push(help.message);
 
17762                 if (help.keyboard) {
 
17764                         // TODO: microtemplate
 
17765                         var disabled = !modules['keyboardNav'].isEnabled();
 
17766                         description.push('<h2 class="keyboardNav' + (disabled ? 'keyboardNavDisabled' : '') + '">');
 
17767                         description.push('Keyboard Navigation' + (disabled ? ' (disabled)' : ''));
 
17768                         description.push('</h2>');
 
17770                         var keyboardTable = RESUtils.generateTable(help.keyboard, this.generateContentKeyboard, elem);
 
17771                         if (keyboardTable) description.push(keyboardTable);
 
17775                         description.push('<h2 class="settingsPointer">');
 
17776                         description.push('<span class="gearIcon"></span> RES Settings');
 
17777                         description.push('</h2>');
 
17779                         var optionTable = RESUtils.generateTable(help.option, this.generateContentOption, elem);
 
17780                         if (optionTable) description.push(optionTable);
 
17783                 description = description.join("\n");
 
17784                 return description;
 
17786         generateContentKeyboard: function (keyboardNavOption, index, array, elem) {
 
17787                 var keyCode = modules['keyboardNav'].getNiceKeyCode(keyboardNavOption);
 
17788                 if (!keyCode) return;
 
17790                 var description = [];
 
17791                 description.push('<tr>');
 
17792                 description.push('<td><code>' + keyCode.toLowerCase() + '</code></td>');
 
17793                 description.push('<td>' + keyboardNavOption + '</td>');
 
17794                 description.push('</tr><tr>');
 
17795                 description.push('<td> </td>'); // for styling
 
17796                 description.push('<td>' + modules['keyboardNav'].options[keyboardNavOption].description + '</td>');
 
17797                 description.push('</tr>');
 
17799                 return description;
 
17801         generateContentOption: function (option, index, array, elem) {
 
17802                 var module = modules[option.moduleID];
 
17803                 if (!module) return;
 
17805                 var description = [];
 
17807                 description.push("<tr>");
 
17808                 description.push("<td>" + module.category + '</td>');
 
17810                 description.push('<td>');
 
17811                 description.push(modules['settingsNavigation'].makeUrlHashLink(option.moduleID, null, module.moduleName));
 
17812                 description.push('</td>');
 
17814                 description.push('<td>');
 
17815                 description.push(option.key
 
17816                         ?  modules['settingsNavigation'].makeUrlHashLink(option.moduleID, option.key)
 
17818                 description.push('</td>');
 
17820                 if (module.options[option.key]) {
 
17821                         description.push('</tr><tr>');
 
17822                         description.push('<td colspan="3">' + module.options[option.key].description + '</td>');
 
17824                 description.push("</tr>");
 
17826                 return description;
 
17829                 message: "Roll over the gear icon <span class='gearIcon'></span> and click 'settings console' to explore the RES settings.  You can enable, disable or change just about anything you like/dislike about RES!<br><br>Once you've opened the console once, this message will not appear again.",
 
17830                 attachTo: "#openRESPrefs",
 
17835                         message: 'Welcome to RES. You can turn modules on and off, and configure settings for the modules using the gear icon link at the top right. For feature requests, or just help getting a question answered, be sure to subscribe to <a href="http://reddit.com/r/Enhancement">/r/Enhancement</a>.',
 
17836                         attachTo: "#openRESPrefs",
 
17840                         message: "Click the tag icon next to a user to tag that user with any name you like - you can also color code the tag.",
 
17841                         attachTo: ".RESUserTagImage:visible",
 
17843                         option: { moduleID: 'userTagger' }
 
17846                         message: "Don't forget to subscribe to <a href=\"http://reddit.com/r/Enhancement\">/r/Enhancement</a> to keep up to date on the latest versions of RES or suggest features! For bug reports, submit to <a href=\"http://reddit.com/r/RESIssues\">/r/RESIssues</a>" 
 
17849                         message: "Don't want to see posts containing certain keywords? Want to filter out certain subreddits from /r/all? Try the filteReddit module!" ,
 
17850                         option: { moduleID: 'filteReddit' }
 
17853                         message: "Keyboard Navigation is one of the most underutilized features in RES. You should try it!" ,
 
17854                         option: { moduleID: 'keyboardNav' },
 
17855                         keyboard: 'toggleHelp'
 
17858                         message: "Did you know you can configure the appearance of a number of things in RES? For example: keyboard navigation lets you configure the look of the 'selected' box, and commentBoxes lets you configure the borders / shadows." ,
 
17859                         option: [ { moduleID: 'keyboardNav', key: 'focusBGColor' }, { moduleID: 'styleTweaks', key: 'commentBoxes' }]
 
17863                         message: "Do you subscribe to a ton of reddits? Give the subreddit tagger a try, it can make your homepage a bit more readable." ,
 
17864                         option: { moduleID: 'subRedditTagger' }
 
17867                         message: "If you haven't tried it yet, Keyboard Navigation is great. Just hit ? while browsing for instructions." ,
 
17868                         option: { moduleID: 'keyboardNav' },
 
17869                         keyboard: 'toggleHelp'
 
17872                         message: "Roll over a user's name to get information about them such as their karma, and how long they've been a reddit user." ,
 
17873                         option: { moduleID: 'userTagger', key: 'hoverInfo' }
 
17876                         message: "Hover over the 'parent' link in comments pages to see the text of the parent being referred to." ,
 
17877                         option: { moduleID: 'showParent' }
 
17880                         message: "You can configure the color and style of the User Highlighter module if you want to change how the highlights look." ,
 
17881                         option: { moduleID: 'userHighlight' }
 
17884                         message: "Not a fan of how comments pages look? You can change the appearance in the Style Tweaks module" ,
 
17885                         option: { moduleID: 'styleTweaks' }
 
17888                         message: "Don't like the style in a certain subreddit? RES gives you a checkbox to disable styles individually - check the right sidebar!" 
 
17891                         message: "Looking for posts by submitter, post with photos, or posts in IAmA form? Try out the comment navigator." 
 
17894                         message: "Have you seen the <a href=\"http://www.reddit.com/r/Dashboard\">RES Dashboard</a>? It allows you to do all sorts of great stuff, like keep track of lower traffic subreddits, and manage your <a href=\"/r/Dashboard#userTaggerContents\">user tags</a> and <a href=\"/r/Dashboard#newCommentsContents\">thread subscriptions</a>!",
 
17895                         options: { moduleID: 'dashboard' }
 
17898                         message: "Sick of seeing these tips?  They only show up once every 24 hours, but you can disable that in the RES Tips and Tricks preferences.", 
 
17899                         option: { moduleID: 'RESTips' }
 
17902                         message: "Did you know that there is now a 'keep me logged in' option in the Account Switcher? Turn it on if you want to stay logged in to Reddit when using the switcher!",
 
17903                         option: { moduleID: 'accountSwitcher', key: 'keepLoggedIn' }
 
17906                         message: "See that little [vw] next to users you've voted on?  That's their vote weight - it moves up and down as you vote the same user up / down.",
 
17907                         option: { moduleID: 'userTagger', key: 'vwTooltip' }
 
17911                 // array of guiders will go here... and we will add a "tour" button somewhere to start the tour...
 
17913         initTips: function() {
 
17914                 $('body').on('click', '#disableDailyTipsCheckbox', modules['RESTips'].disableDailyTipsCheckbox);
 
17915                 // create the special "you have never visited the console" guider...
 
17916                 this.createGuider(0, 'console');
 
17917                 for (var i=0, len=this.tips.length; i<len; i++) {
 
17918                         this.createGuider(i);
 
17921         createGuider: function(i, special) {
 
17922                 if (special === 'console') {
 
17923                         var thisID = special;
 
17924                         var thisTip = this.consoleTip;
 
17926                         var thisID = "tip"+i;
 
17927                         var thisTip = this.tips[i];
 
17929                 var description = modules['RESTips'].generateContent(thisTip);
 
17930                 var attachTo = thisTip.attachTo;
 
17931                 var nextidx = ((parseInt(i+1, 10)) >= len) ? 0 : (parseInt(i+1, 10));
 
17932                 var nextID = "tip"+nextidx;
 
17933                 var thisChecked = (modules['RESTips'].options.dailyTip.value) ? 'checked="checked"' : '';
 
17935                         attachTo: attachTo,
 
17938                                 onclick: modules['RESTips'].prevTip
 
17942                                 onclick: modules['RESTips'].nextTip
 
17945                         description: description,
 
17946                         buttonCustomHTML: "<label class='stopper'> <input type='checkbox' name='disableDailyTipsCheckbox' id='disableDailyTipsCheckbox' "+thisChecked+" />Show these tips once every 24 hours</label>",
 
17949                         onShow: modules['RESTips'].onShow,
 
17950                         onHide: modules['RESTips'].onHide,
 
17951                         position: this.tips[i].position,
 
17953                         title: "RES Tips and Tricks"
 
17955                 if (special === 'console') {
 
17956                         delete guiderObj.buttonCustomHTML;
 
17957                         delete guiderObj.next;
 
17958                         delete guiderObj.buttons;
 
17960                         guiderObj.title = "RES is extremely configurable";
 
17964                 guiders.createGuider(guiderObj);
 
17966         showTip: function(idx, special) {
 
17967                 if (typeof this.tipsInitialized === 'undefined') {
 
17969                         this.tipsInitialized = true;
 
17972                         guiders.show('tip'+idx);
 
17974                         guiders.show('console');
 
17977         onShow: function() {
 
17978                 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'tipstricks');
 
17980         onHide: function() {
 
17981                 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'tipstricks');
 
17986 modules['settingsNavigation'] = {
 
17987         moduleID: 'settingsNavigation',
 
17988         moduleName: 'RES Settings Navigation',
 
17990         description: 'Helping you get around the RES Settings Console with greater ease',
 
17994         isEnabled: function() {
 
17995                 // return RESConsole.getModulePrefs(this.moduleID);
 
17999                 /^https?:\/\/([-\w\.]+\.)?reddit\.com\/[-\w\.\/]*/i
 
18001         isMatchURL: function() {
 
18002                 return RESUtils.isMatchURL(this.moduleID);
 
18004         beforeLoad: function() {
 
18005                 RESUtils.addCSS('#RESSearchMenuItem { \
 
18009                         width: 21px;height: 21px;       \
 
18010                         border: 1px #c9def2 solid;      \
 
18011                         border-radius: 3px;     \
 
18012                         background: transparent center center no-repeat; \
 
18013                         background-image: ' + this.searchButtonIcon + '; \
 
18015                 RESUtils.addCSS('li:hover > #RESSearchMenuItem { \
 
18016                         border-color: #369;     \
 
18017                         background-image: ' + this.searchButtonIconHover + '; \
 
18021                 RESUtils.addCSS(modules['settingsNavigation'].css);
 
18022                 this.menuItem = createElementWithID('i','RESSearchMenuItem');
 
18023                 this.menuItem.setAttribute('title', 'search settings');
 
18024                 this.menuItem.addEventListener('click', function(e) {
 
18025                         modules['settingsNavigation'].showSearch()
 
18027                 RESConsole.settingsButton.appendChild(this.menuItem);
 
18029                 if (!(this.isEnabled() && this.isMatchURL())) return;
 
18031                 window.addEventListener('hashchange', modules['settingsNavigation'].onHashChange);
 
18032                 window.addEventListener('popstate', modules['settingsNavigation'].onPopState);
 
18033                 setTimeout(modules['settingsNavigation'].onHashChange, 300); // for initial pageload; wait until after RES has completed loading
 
18037         searchButtonIcon: "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAEJGlDQ1BJQ0MgUHJvZmlsZQAAOBGFVd9v21QUPolvUqQWPyBYR4eKxa9VU1u5GxqtxgZJk6XtShal6dgqJOQ6N4mpGwfb6baqT3uBNwb8AUDZAw9IPCENBmJ72fbAtElThyqqSUh76MQPISbtBVXhu3ZiJ1PEXPX6yznfOec7517bRD1fabWaGVWIlquunc8klZOnFpSeTYrSs9RLA9Sr6U4tkcvNEi7BFffO6+EdigjL7ZHu/k72I796i9zRiSJPwG4VHX0Z+AxRzNRrtksUvwf7+Gm3BtzzHPDTNgQCqwKXfZwSeNHHJz1OIT8JjtAq6xWtCLwGPLzYZi+3YV8DGMiT4VVuG7oiZpGzrZJhcs/hL49xtzH/Dy6bdfTsXYNY+5yluWO4D4neK/ZUvok/17X0HPBLsF+vuUlhfwX4j/rSfAJ4H1H0qZJ9dN7nR19frRTeBt4Fe9FwpwtN+2p1MXscGLHR9SXrmMgjONd1ZxKzpBeA71b4tNhj6JGoyFNp4GHgwUp9qplfmnFW5oTdy7NamcwCI49kv6fN5IAHgD+0rbyoBc3SOjczohbyS1drbq6pQdqumllRC/0ymTtej8gpbbuVwpQfyw66dqEZyxZKxtHpJn+tZnpnEdrYBbueF9qQn93S7HQGGHnYP7w6L+YGHNtd1FJitqPAR+hERCNOFi1i1alKO6RQnjKUxL1GNjwlMsiEhcPLYTEiT9ISbN15OY/jx4SMshe9LaJRpTvHr3C/ybFYP1PZAfwfYrPsMBtnE6SwN9ib7AhLwTrBDgUKcm06FSrTfSj187xPdVQWOk5Q8vxAfSiIUc7Z7xr6zY/+hpqwSyv0I0/QMTRb7RMgBxNodTfSPqdraz/sDjzKBrv4zu2+a2t0/HHzjd2Lbcc2sG7GtsL42K+xLfxtUgI7YHqKlqHK8HbCCXgjHT1cAdMlDetv4FnQ2lLasaOl6vmB0CMmwT/IPszSueHQqv6i/qluqF+oF9TfO2qEGTumJH0qfSv9KH0nfS/9TIp0Wboi/SRdlb6RLgU5u++9nyXYe69fYRPdil1o1WufNSdTTsp75BfllPy8/LI8G7AUuV8ek6fkvfDsCfbNDP0dvRh0CrNqTbV7LfEEGDQPJQadBtfGVMWEq3QWWdufk6ZSNsjG2PQjp3ZcnOWWing6noonSInvi0/Ex+IzAreevPhe+CawpgP1/pMTMDo64G0sTCXIM+KdOnFWRfQKdJvQzV1+Bt8OokmrdtY2yhVX2a+qrykJfMq4Ml3VR4cVzTQVz+UoNne4vcKLoyS+gyKO6EHe+75Fdt0Mbe5bRIf/wjvrVmhbqBN97RD1vxrahvBOfOYzoosH9bq94uejSOQGkVM6sN/7HelL4t10t9F4gPdVzydEOx83Gv+uNxo7XyL/FtFl8z9ZAHF4bBsrEwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAONJREFUOBGlkz0KwkAQRo2ICt5IsBE9gI1dwAOk8AqCgufQPo2CYGlhIVh4Aj2ClYVg4hvdCclmI8gOPHbn78vsLgnSNK35WN2nWXq9BRoVEwyIj6ANO4ghgbLJHVjM8BM4wwGesIYm2LU1O9ClSCwCzfXZi8gkF9NcSWBB0dVRGBPbOOLOSww4qJA38d3vbanqEabEA5Mbsj4gNH42vvgFxxTIJYpd4AgvcbAVdKDQ8/lKflaz12ds4e+hBxFsIYQ7fM1W/OHPyYktIZuiagLVt9cxgRucNPGvgPZlq/e/4C3wBoAXSrzY2Qd2AAAAAElFTkSuQmCC')",
 
18038         searchButtonIconHover: "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAEJGlDQ1BJQ0MgUHJvZmlsZQAAOBGFVd9v21QUPolvUqQWPyBYR4eKxa9VU1u5GxqtxgZJk6XtShal6dgqJOQ6N4mpGwfb6baqT3uBNwb8AUDZAw9IPCENBmJ72fbAtElThyqqSUh76MQPISbtBVXhu3ZiJ1PEXPX6yznfOec7517bRD1fabWaGVWIlquunc8klZOnFpSeTYrSs9RLA9Sr6U4tkcvNEi7BFffO6+EdigjL7ZHu/k72I796i9zRiSJPwG4VHX0Z+AxRzNRrtksUvwf7+Gm3BtzzHPDTNgQCqwKXfZwSeNHHJz1OIT8JjtAq6xWtCLwGPLzYZi+3YV8DGMiT4VVuG7oiZpGzrZJhcs/hL49xtzH/Dy6bdfTsXYNY+5yluWO4D4neK/ZUvok/17X0HPBLsF+vuUlhfwX4j/rSfAJ4H1H0qZJ9dN7nR19frRTeBt4Fe9FwpwtN+2p1MXscGLHR9SXrmMgjONd1ZxKzpBeA71b4tNhj6JGoyFNp4GHgwUp9qplfmnFW5oTdy7NamcwCI49kv6fN5IAHgD+0rbyoBc3SOjczohbyS1drbq6pQdqumllRC/0ymTtej8gpbbuVwpQfyw66dqEZyxZKxtHpJn+tZnpnEdrYBbueF9qQn93S7HQGGHnYP7w6L+YGHNtd1FJitqPAR+hERCNOFi1i1alKO6RQnjKUxL1GNjwlMsiEhcPLYTEiT9ISbN15OY/jx4SMshe9LaJRpTvHr3C/ybFYP1PZAfwfYrPsMBtnE6SwN9ib7AhLwTrBDgUKcm06FSrTfSj187xPdVQWOk5Q8vxAfSiIUc7Z7xr6zY/+hpqwSyv0I0/QMTRb7RMgBxNodTfSPqdraz/sDjzKBrv4zu2+a2t0/HHzjd2Lbcc2sG7GtsL42K+xLfxtUgI7YHqKlqHK8HbCCXgjHT1cAdMlDetv4FnQ2lLasaOl6vmB0CMmwT/IPszSueHQqv6i/qluqF+oF9TfO2qEGTumJH0qfSv9KH0nfS/9TIp0Wboi/SRdlb6RLgU5u++9nyXYe69fYRPdil1o1WufNSdTTsp75BfllPy8/LI8G7AUuV8ek6fkvfDsCfbNDP0dvRh0CrNqTbV7LfEEGDQPJQadBtfGVMWEq3QWWdufk6ZSNsjG2PQjp3ZcnOWWing6noonSInvi0/Ex+IzAreevPhe+CawpgP1/pMTMDo64G0sTCXIM+KdOnFWRfQKdJvQzV1+Bt8OokmrdtY2yhVX2a+qrykJfMq4Ml3VR4cVzTQVz+UoNne4vcKLoyS+gyKO6EHe+75Fdt0Mbe5bRIf/wjvrVmhbqBN97RD1vxrahvBOfOYzoosH9bq94uejSOQGkVM6sN/7HelL4t10t9F4gPdVzydEOx83Gv+uNxo7XyL/FtFl8z9ZAHF4bBsrEwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAPBJREFUOBGlkTEKAjEQRTciIth4HkvRA9jYCR7AwisICvZiZ6X9NlvYW1gIFp5AT2Eh6PpGMjLuBkUy8JmZ5P+fZOLyPE9iohIjFm20QTV0A+dch/UeqIMtSHnqg1wOmYEFjAkQ8hHswA1sQM3ytC6KWxBlqqM3IUna9GIy1DWbiwYziGdLkJpIQVZclz40REbgnKhMSB/+b+sKSZ8wpnb+9C71FQwsV+uPJ3iBDFFOO4E9uPt+TW6oUHPJwJvINy7BCvTBAohpBpoqfnFt861GOPUmc8vTd7L3O5it3OaCwUHZfxmoyObQN9r9n3W0wRPmWv0jZnGemgAAAABJRU5ErkJggg==')",
 
18039         consoleTip: function() {
 
18040                 // first, ensure that we've at least run dailyTip once (so RES first-run has happened)...
 
18041                 var lastToolTip = RESStorage.getItem('RESLastToolTip');
 
18043                         // if yes, see if the user has ever opened the settings console.
 
18044                         var hasOpenedConsole = RESStorage.getItem('RESConsole.hasOpenedConsole');
 
18045                         if (!hasOpenedConsole) {
 
18046                                 // if no, nag them once daily that the console exists until they use it.  Once it's been opened, this check will never run again.
 
18047                                 var lastCheckDailyTip = parseInt(RESStorage.getItem('RESLastToolTip'), 10) || 0;
 
18048                                 var now = new Date();
 
18049                                 // 86400000 = 1 day - remind users once a day if they've never opened the settings that they should check out the console sometime...
 
18050                                 var lastCheck = parseInt(RESStorage.getItem('RESConsole.hasOpenedCheck'), 10) || 0;
 
18051                                 if (((now.getTime() - lastCheckDailyTip) > 1000) && ((now.getTime() - lastCheck) > 86400000)) {
 
18052                                         RESStorage.setItem('RESConsole.hasOpenedCheck', now.getTime());
 
18053                                         modules['RESTips'].showTip(0,'console');
 
18058         makeUrlHashLink: function (moduleID, optionKey, displayText, cssClass) {
 
18059                 if (!displayText) {
 
18061                                 displayText = optionKey;
 
18062                         } else if (modules[moduleID]) {
 
18063                                 displayText = modules[moduleID].moduleName;
 
18064                         } else if (moduleID) {
 
18065                                 displayText = moduleID;
 
18067                                 displayText = 'Settings';
 
18071                 var hash = modules['settingsNavigation'].makeUrlHash(moduleID, optionKey);
 
18072                 var link = ['<a ', 'class="', cssClass || '', '" ', 'href="', hash, '"', '>', displayText, '</a>'].join('');
 
18075         makeUrlHash: function(moduleID, optionKey) {
 
18076                 var hashComponents = ['#!settings']
 
18079                         hashComponents.push(moduleID);
 
18082                 if (moduleID && optionKey) {
 
18083                         hashComponents.push(optionKey);
 
18086                 var hash = hashComponents.join('/');
 
18089         setUrlHash: function(moduleID, optionKey) {
 
18090                 var titleComponents = ['RES Settings'];
 
18093                         var module = modules[moduleID];
 
18094                         var moduleName = module && module.moduleName || moduleID;
 
18095                         titleComponents.push(moduleName);
 
18098                                 titleComponents.push(optionKey);
 
18102                 var hash = this.makeUrlHash(moduleID, optionKey);
 
18103                 var title = titleComponents.join(' - ');
 
18105                 if (window.location.hash != hash) {
 
18106                         window.history.pushState(hash, title, hash);
 
18109         resetUrlHash: function() {
 
18110                 window.location.hash = "";
 
18112         onHashChange: function (event) {
 
18113                 var hash = window.location.hash;
 
18114                 if (hash.substring(0, 10) !== '#!settings') return;
 
18116                 var params = hash.match(/\/[\w\s]+/g);
 
18117                 if (params && params[0]) {
 
18118                         var moduleID = params[0].substring(1);
 
18120                 if (params && params[1]) {
 
18121                         var optionKey = params[1].substring(1);
 
18124                 modules['settingsNavigation'].loadSettingsPage(moduleID, optionKey);
 
18126         onPopState: function (event) {
 
18127                 var state = typeof event.state === "string" && event.state.split('/');
 
18128                 if (!state || state[0] !== '#!settings') {
 
18129                         if (RESConsole.isOpen) {
 
18130                                 RESConsole.close();
 
18135                 var moduleID = state[1];
 
18136                 var optionKey = state[2];
 
18138                 modules['settingsNavigation'].loadSettingsPage(moduleID, optionKey);
 
18140         loadSettingsPage: function(moduleID, optionKey, optionValue) {
 
18141                 if (moduleID && modules.hasOwnProperty(moduleID)) {
 
18142                         var module = modules[moduleID];
 
18145                         var category = module.category;
 
18149                 RESConsole.open(module && module.moduleID);
 
18151                         if (optionKey && module.options.hasOwnProperty(optionKey)) {
 
18152                                 var optionsPanel = $(RESConsole.RESConsoleContent);
 
18153                                 var optionElement = optionsPanel.find('label[for="' + optionKey + '"]');
 
18154                                 var optionParent = optionElement.parent();
 
18155                                 optionParent.addClass('highlight');
 
18156                                 if (optionElement.length) {
 
18157                                         var configPanel = $(RESConsole.RESConsoleConfigPanel);
 
18158                                         var offset = optionElement.offset().top - configPanel.offset().top;
 
18159                                         optionsPanel.scrollTop(offset);
 
18165                                         this.search(optionKey);
 
18172         search: function(query) {
 
18173                 RESConsole.openCategoryPanel('About RES');
 
18174                 modules['settingsNavigation'].drawSearchResults(query);
 
18175                 modules['settingsNavigation'].getSearchResults(query);
 
18176                 modules['settingsNavigation'].setUrlHash('search', query);
 
18178         showSearch: function () {
 
18179                 RESConsole.hidePrefsDropdown();
 
18180                 modules['settingsNavigation'].drawSearchResults();
 
18181                 $('#SearchRES-input').focus();
 
18183         doneSearch: function (query, results) {
 
18184                 modules['settingsNavigation'].drawSearchResults(query, results);
 
18186         getSearchResults: function (query) {
 
18187                 if (!(query && query.toString().length)) {
 
18188                         modules['settingsNavigation'].doneSearch(query, []);
 
18191                 var queryTerms = modules['settingsNavigation'].prepareSearchText(query, true).split(' ');
 
18195                 for (var moduleKey in modules) {
 
18196                         if (!modules.hasOwnProperty(moduleKey)) continue;
 
18197                         var module = modules[moduleKey];
 
18200                         var searchString = module.moduleID + module.moduleName + module.category + module.description;
 
18201                         searchString = modules['settingsNavigation'].prepareSearchText(searchString, false);
 
18202                         var matches = modules['settingsNavigation'].searchMatches(queryTerms, searchString);
 
18204                                 var result = modules['settingsNavigation'].makeModuleSearchResult(moduleKey);
 
18205                                 result.rank = matches;
 
18206                                 results.push(result);
 
18210                         var options = module.options;
 
18212                         for (var optionKey in options) {
 
18213                                 if (!options.hasOwnProperty(optionKey)) continue;
 
18214                                 var option = options[optionKey];
 
18216                                 var searchString = module.moduleID + module.moduleName + module.category + optionKey + option.description;
 
18217                                 searchString = modules['settingsNavigation'].prepareSearchText(searchString, false);
 
18218                                 var matches = modules['settingsNavigation'].searchMatches(queryTerms, searchString);
 
18220                                         var result = modules['settingsNavigation'].makeOptionSearchResult(moduleKey, optionKey);
 
18221                                         result.rank = matches;
 
18222                                         results.push(result);
 
18227                 results.sort(function(a, b) {
 
18228                         var comparison = b.rank - a.rank;
 
18231                         if (comparison === 0) {
 
18233                                         a.title < b.title ? -1
 
18234                                         : a.title > b.title ? 1
 
18239                         if (comparison === 0) {
 
18241                                         a.description < b.description ? -1
 
18242                                         : a.description > b.description ? 1
 
18250                 modules['settingsNavigation'].doneSearch(query, results);
 
18253         searchMatches: function(needles, haystack) {
 
18254                 if (!(haystack && haystack.length)) 
 
18257                 var numMatches = 0;
 
18258                 for (var i = 0; i < needles.length; i++) {
 
18259                         if (haystack.indexOf(needles[i]) !== -1)
 
18265         prepareSearchText: function (text, preserveSpaces) {
 
18266                 if (typeof text === "undefined" || text === null) {
 
18270                 var replaceSpacesWith = !!preserveSpaces ? ' ' : ''
 
18271                 return text.toString().toLowerCase()    
 
18272                         .replace(/[,\/]/g,replaceSpacesWith).replace(/\s+/g, replaceSpacesWith);
 
18274         makeOptionSearchResult: function (moduleKey, optionKey) {
 
18275                 var module = modules[moduleKey];
 
18276                 var option = module.options[optionKey];
 
18279                 result.type = 'option';
 
18280                 result.breadcrumb = ['Settings', 
 
18282                         module.moduleName + ' (' + module.moduleID + ')'
 
18284                 result.title = optionKey;
 
18285                 result.description = option.description;
 
18286                 result.moduleID = moduleKey;
 
18287                 result.optionKey = optionKey;
 
18291         makeModuleSearchResult: function (moduleKey) {
 
18292                 var module = modules[moduleKey];
 
18295                 result.type = 'module';
 
18296                 result.breadcrumb = ['Settings', 
 
18298                         '(' + module.moduleID + ')'
 
18300                 result.title = module.moduleName;
 
18301                 result.description = module.description;
 
18302                 result.moduleID = moduleKey;
 
18307         onSearchResultSelected: function(result) {
 
18308                 if (!result) return;
 
18310                 switch (result.type) {
 
18312                                 modules['settingsNavigation'].loadSettingsPage(result.moduleID);
 
18315                                 modules['settingsNavigation'].loadSettingsPage(result.moduleID, result.optionKey);
 
18318                                 alert('Could not load search result');
 
18322         // ---------- View ------
 
18324                 #SearchRES #SearchRES-results-container { \
 
18327                 #SearchRES #SearchRES-results-container + #SearchRES-boilerplate { margin-top: 1em; border-top: 1px black solid; padding-top: 1em; }    \
 
18329                         margin-top: 1.5em; \
 
18331                 #SearchRES-results { \
 
18333                 #SearchRES-results li { \
 
18334                         list-style-type: none; \
 
18335                         border-bottom: 1px dashed #ccc; \
 
18337                         margin-left: 0px; \
 
18338                         padding-left: 10px; \
 
18339                         padding-top: 24px; \
 
18340                         padding-bottom: 24px; \
 
18342                 #SearchRES-results li:hover { \
 
18343                         background-color: #FAFAFF; \
 
18345                 .SearchRES-result-title { \
 
18346                         margin-bottom: 12px; \
 
18347                         font-weight: bold; \
 
18350                 .SearchRES-breadcrumb { \
 
18351                         font-weight: normal; \
 
18354                 .SearchRES-result-copybutton {\
 
18360                         background: no-repeat center center;    \
 
18361                         background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAWCAYAAADeiIy1AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA+5pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ1dWlkOjY1RTYzOTA2ODZDRjExREJBNkUyRDg4N0NFQUNCNDA3IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkUwRjM3M0QxMDY5NTExRTI5OUZEQTZGODg4RDc1ODdCIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkUwRjM3M0QwMDY5NTExRTI5OUZEQTZGODg4RDc1ODdCIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowMTgwMTE3NDA3MjA2ODExODA4M0ZFMkJBM0M1RUU2NSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowNjgwMTE3NDA3MjA2ODExODA4M0U3NkRBMDNEMDVDMSIvPiA8ZGM6dGl0bGU+IDxyZGY6QWx0PiA8cmRmOmxpIHhtbDpsYW5nPSJ4LWRlZmF1bHQiPmdseXBoaWNvbnM8L3JkZjpsaT4gPC9yZGY6QWx0PiA8L2RjOnRpdGxlPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pn00ay4AAAEISURBVHjavJWBDYQgDEXhcgMwwm2gIzgCIzjCjeAIjsAIN8KN4Ai6wW3AtTm4EChYkdjkm2javLa2IKy1olZgL9CD5XsShI8PSF8B8pqvAqEWUJ8FYemglQhEmfg/gA0uhvLHVo5EUtmAf3ZgCoNB74xvLkEVgnwlJtOep8vSVihM9veRAKiDFdhCK/VdECal9JBONLSkIhzVBpWUW4cT1giSDEMMmqP+GjdxA2OPiuMdgxb3bQozOr2wBMhSGTFAppQYBZoqDtWR4ZuA1MFromf60gt7AKZyp0oo6UD4ImuWEJYbB6Dbi28BYsXfQJsLMBUQH7Nx/HWDU4B3le9cfCWtHAjqK8AAypyhqqqagq4AAAAASUVORK5CYII=");    \
 
18364                 #SearchRES-results li:hover .SearchRES-result-copybutton { display: block; }    \
 
18365                 #SearchRES-input-submit { \
 
18366                         margin-left: 8px; \
 
18368                 #SearchRES-input { \
 
18373                 #SearchRES-input-container { \
 
18375                         margin-left: 3em; \
 
18379         searchPanelHtml: '\
 
18380                 <h3>Search RES Settings Console</h3> \
 
18381                 <div id="SearchRES-results-container"> \
 
18382                         <h4>Results for: <span id="SearchRES-query"></span></h4> \
 
18383                         <ul id="SearchRES-results"></ul> \
 
18384                         <p id="SearchRES-results-none">No results found</p> \
 
18386                 <div id="SearchRES-boilerplate">\
 
18387                 <p>You can search for RES options by module name, option name, and description. For example, try searching for "daily trick" in one of the following ways:</p>\
 
18389                         <li>type <code>daily trick</code> in the search box above and click the button</li> \
 
18390                         <li>press <code>.</code> to open the RES console, type in <code>search <em>daily trick</em></code>, and press Enter</li> \
 
18394         renderSearchPanel: function() {
 
18395                 var searchPanel = $('<div />').html(modules['settingsNavigation'].searchPanelHtml);
 
18396                  searchPanel.delegate('#SearchRES-results-container .SearchRES-result-item', 'click', modules['settingsNavigation'].handleSearchResultClick);
 
18398                  modules['settingsNavigation'].searchPanel = searchPanel;
 
18399                 return searchPanel;
 
18402         renderSearchForm: function() {
 
18403                 var RESSearchContainer = createElementWithID('form', 'SearchRES-input-container');
 
18405                 var RESSearchBox = createElementWithID('input', 'SearchRES-input');
 
18406                 RESSearchBox.setAttribute('type', 'text');
 
18407                 RESSearchBox.setAttribute('placeholder', 'search RES settings');
 
18409                 var RESSearchButton = createElementWithID('input', 'SearchRES-input-submit');
 
18410                 RESSearchButton.classList.add('blueButton');
 
18411                 RESSearchButton.setAttribute('type', 'submit');
 
18412                 RESSearchButton.setAttribute('value', 'search');
 
18414                 RESSearchContainer.appendChild(RESSearchBox);
 
18415                 RESSearchContainer.appendChild(RESSearchButton);
 
18417                 RESSearchContainer.addEventListener('submit', function (e) {
 
18418                         e.preventDefault();
 
18419                         modules['settingsNavigation'].search(RESSearchBox.value);
 
18424                 searchForm = RESSearchContainer;
 
18425                 return RESSearchContainer;
 
18427         drawSearchResultsPage: function() {
 
18428                 if (!RESConsole.isOpen) {
 
18432                 if (!$('#SearchRES').is(':visible')) {
 
18433                         RESConsole.openCategoryPanel('About RES');
 
18435                         // Open "Search RES" page
 
18436                         $('#Button-SearchRES', this.RESConsoleContent).trigger('click', { duration: 0 });
 
18439         drawSearchResults: function (query, results) {
 
18440                 modules['settingsNavigation'].drawSearchResultsPage();
 
18442                 var resultsContainer = $('#SearchRES-results-container', modules['settingsNavigation'].searchPanel);
 
18444                 if (!(query && query.length)) {
 
18445                         resultsContainer.hide();
 
18449                 resultsContainer.show();
 
18450                 resultsContainer.find('#SearchRES-query').text(query);
 
18451                 $("#SearchRES-input", modules['settingsNavigation'].searchForm).val(query);
 
18453                 if (!(results && results.length)) {
 
18454                         resultsContainer.find('#SearchRES-results-none').show();
 
18455                         resultsContainer.find('#SearchRES-results').hide();
 
18457                         resultsContainer.find('#SearchRES-results-none').hide();
 
18458                         var resultsList = $('#SearchRES-results', resultsContainer).show();
 
18460                         resultsList.empty();
 
18461                         for (var i = 0; i < results.length; i++) {
 
18462                                 var result = results[i];
 
18464                                 var element = modules['settingsNavigation'].drawSearchResultItem(result);
 
18465                                 resultsList.append(element);
 
18469         drawSearchResultItem: function(result) {
 
18470                 var element = $('<li>');
 
18471                 element.addClass('SearchRES-result-item')
 
18472                         .data('SearchRES-result', result);
 
18474                 $('<span>', { class: 'SearchRES-result-copybutton'})
 
18476                         .attr('title', 'copy this for a comment')
 
18478                 var breadcrumb = $('<span>', {class: 'SearchRES-breadcrumb'})
 
18479                         .text(result.breadcrumb + ' > ');
 
18480                 $('<div>', {class: 'SearchRES-result-title'})
 
18481                         .append(breadcrumb)
 
18482                         .append(result.title)
 
18483                         .appendTo(element);
 
18484                 $('<div>', {class: 'SearchRES-result-description'})
 
18486                         .html(result.description);
 
18490         handleSearchResultClick: function (e) {
 
18491                 var element = $(this);
 
18492                 var result = element.data('SearchRES-result');
 
18493                 if ($(e.target).is('.SearchRES-result-copybutton')) {
 
18494                         modules['settingsNavigation'].onSearchResultCopy(result, element);
 
18496                         modules['settingsNavigation'].onSearchResultSelected(result);
 
18498                 e.preventDefault();
 
18500         onSearchResultCopy: function(result, element) {
 
18501                 var markdown = modules['settingsNavigation'].makeOptionSearchResultLink(result);
 
18502                 alert('<textarea rows="5" cols="50">' + markdown + '</textarea>');
 
18504         makeOptionSearchResultLink: function (result) {
 
18505                 var url = document.location.pathname + 
 
18506                         modules['settingsNavigation'].makeUrlHash(result.moduleID, result.optionKey);
 
18510                         '[' + result.title + '](' + url + ')',
 
18512                         result.description,
 
18523 modules['dashboard'] = {
 
18524         moduleID: 'dashboard',
 
18525         moduleName: 'RES Dashboard',
 
18531                         description: 'Number of posts to show by default in each widget'
 
18536                                 { name: 'hot', value: 'hot' },
 
18537                                 { name: 'new', value: 'new' },
 
18538                                 { name: 'controversial', value: 'controversial' },
 
18539                                 { name: 'top', value: 'top' }
 
18542                         description: 'Default sort method for new widgets'
 
18544                 dashboardShortcut: {
 
18547                         description: 'Show +dashboard shortcut in sidebar for easy addition of dashboard widgets.'
 
18552                         description: 'How many user tags to show per page. (enter zero to show all on one page)'
 
18555         description: 'The RES Dashboard is home to a number of features including widgets and other useful tools',
 
18556         isEnabled: function() {
 
18557                 return RESConsole.getModulePrefs(this.moduleID);
 
18560                 /^https?:\/\/([-\w\.]+\.)?reddit\.com\/[-\w\.\/]*/i
 
18562         isMatchURL: function() {
 
18563                 return RESUtils.isMatchURL(this.moduleID);
 
18566                 if (this.isEnabled()) {
 
18567                         this.getLatestWidgets();
 
18568                         RESUtils.addCSS('.RESDashboardToggle { margin-right: 5px; color: white; background-image: url(http://www.redditstatic.com/bg-button-add.png); cursor: pointer; text-align: center; width: 68px; font-weight: bold; font-size: 10px; border: 1px solid #444; padding: 1px 6px; border-radius: 3px 3px 3px 3px;  }');
 
18569                         RESUtils.addCSS('.RESDashboardToggle.remove { background-image: url(http://www.redditstatic.com/bg-button-remove.png) }');
 
18570                         if (this.isMatchURL()) {
 
18571                                 $('#RESDropdownOptions').prepend('<li id="DashboardLink"><a href="/r/Dashboard">my dashboard</a></li>');
 
18572                                 if (RESUtils.currentSubreddit()) {
 
18573                                         RESUtils.addCSS('.RESDashboardToggle {}');
 
18574                                         // one more safety check... not sure how people's widgets[] arrays are breaking.
 
18575                                         if (!(this.widgets instanceof Array)) {
 
18578                                         if (RESUtils.currentSubreddit('dashboard')) {
 
18579                                                 $('#noresults, #header-bottom-left .tabmenu:not(".viewimages")').hide();
 
18580                                                 $('#header-bottom-left .redditname a:first').text('My Dashboard');
 
18581                                                 this.drawDashboard();
 
18583                                         if (this.options.dashboardShortcut.value == true) this.addDashboardShortcuts();
 
18588         getLatestWidgets: function() {
 
18590                         this.widgets = JSON.parse(RESStorage.getItem('RESmodules.dashboard.' + RESUtils.loggedInUser())) || [];
 
18595         loader: 'data:image/gif;base64,R0lGODlhEAAQAPQAAP///2+NyPb3+7zK5e3w95as1rPD4W+NyKC02oOdz8/Z7Nnh8HqVzMbS6XGOyI2l06m73gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFdyAgAgIJIeWoAkRCCMdBkKtIHIngyMKsErPBYbADpkSCwhDmQCBethRB6Vj4kFCkQPG4IlWDgrNRIwnO4UKBXDufzQvDMaoSDBgFb886MiQadgNABAokfCwzBA8LCg0Egl8jAggGAA1kBIA1BAYzlyILczULC2UhACH5BAkKAAAALAAAAAAQABAAAAV2ICACAmlAZTmOREEIyUEQjLKKxPHADhEvqxlgcGgkGI1DYSVAIAWMx+lwSKkICJ0QsHi9RgKBwnVTiRQQgwF4I4UFDQQEwi6/3YSGWRRmjhEETAJfIgMFCnAKM0KDV4EEEAQLiF18TAYNXDaSe3x6mjidN1s3IQAh+QQJCgAAACwAAAAAEAAQAAAFeCAgAgLZDGU5jgRECEUiCI+yioSDwDJyLKsXoHFQxBSHAoAAFBhqtMJg8DgQBgfrEsJAEAg4YhZIEiwgKtHiMBgtpg3wbUZXGO7kOb1MUKRFMysCChAoggJCIg0GC2aNe4gqQldfL4l/Ag1AXySJgn5LcoE3QXI3IQAh+QQJCgAAACwAAAAAEAAQAAAFdiAgAgLZNGU5joQhCEjxIssqEo8bC9BRjy9Ag7GILQ4QEoE0gBAEBcOpcBA0DoxSK/e8LRIHn+i1cK0IyKdg0VAoljYIg+GgnRrwVS/8IAkICyosBIQpBAMoKy9dImxPhS+GKkFrkX+TigtLlIyKXUF+NjagNiEAIfkECQoAAAAsAAAAABAAEAAABWwgIAICaRhlOY4EIgjH8R7LKhKHGwsMvb4AAy3WODBIBBKCsYA9TjuhDNDKEVSERezQEL0WrhXucRUQGuik7bFlngzqVW9LMl9XWvLdjFaJtDFqZ1cEZUB0dUgvL3dgP4WJZn4jkomWNpSTIyEAIfkECQoAAAAsAAAAABAAEAAABX4gIAICuSxlOY6CIgiD8RrEKgqGOwxwUrMlAoSwIzAGpJpgoSDAGifDY5kopBYDlEpAQBwevxfBtRIUGi8xwWkDNBCIwmC9Vq0aiQQDQuK+VgQPDXV9hCJjBwcFYU5pLwwHXQcMKSmNLQcIAExlbH8JBwttaX0ABAcNbWVbKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICSRBlOY7CIghN8zbEKsKoIjdFzZaEgUBHKChMJtRwcWpAWoWnifm6ESAMhO8lQK0EEAV3rFopIBCEcGwDKAqPh4HUrY4ICHH1dSoTFgcHUiZjBhAJB2AHDykpKAwHAwdzf19KkASIPl9cDgcnDkdtNwiMJCshACH5BAkKAAAALAAAAAAQABAAAAV3ICACAkkQZTmOAiosiyAoxCq+KPxCNVsSMRgBsiClWrLTSWFoIQZHl6pleBh6suxKMIhlvzbAwkBWfFWrBQTxNLq2RG2yhSUkDs2b63AYDAoJXAcFRwADeAkJDX0AQCsEfAQMDAIPBz0rCgcxky0JRWE1AmwpKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICKZzkqJ4nQZxLqZKv4NqNLKK2/Q4Ek4lFXChsg5ypJjs1II3gEDUSRInEGYAw6B6zM4JhrDAtEosVkLUtHA7RHaHAGJQEjsODcEg0FBAFVgkQJQ1pAwcDDw8KcFtSInwJAowCCA6RIwqZAgkPNgVpWndjdyohACH5BAkKAAAALAAAAAAQABAAAAV5ICACAimc5KieLEuUKvm2xAKLqDCfC2GaO9eL0LABWTiBYmA06W6kHgvCqEJiAIJiu3gcvgUsscHUERm+kaCxyxa+zRPk0SgJEgfIvbAdIAQLCAYlCj4DBw0IBQsMCjIqBAcPAooCBg9pKgsJLwUFOhCZKyQDA3YqIQAh+QQJCgAAACwAAAAAEAAQAAAFdSAgAgIpnOSonmxbqiThCrJKEHFbo8JxDDOZYFFb+A41E4H4OhkOipXwBElYITDAckFEOBgMQ3arkMkUBdxIUGZpEb7kaQBRlASPg0FQQHAbEEMGDSVEAA1QBhAED1E0NgwFAooCDWljaQIQCE5qMHcNhCkjIQAh+QQJCgAAACwAAAAAEAAQAAAFeSAgAgIpnOSoLgxxvqgKLEcCC65KEAByKK8cSpA4DAiHQ/DkKhGKh4ZCtCyZGo6F6iYYPAqFgYy02xkSaLEMV34tELyRYNEsCQyHlvWkGCzsPgMCEAY7Cg04Uk48LAsDhRA8MVQPEF0GAgqYYwSRlycNcWskCkApIyEAOwAAAAAAAAAAAA==',
 
18596         drawDashboard: function() {
 
18597                 // this first line hides the "you need RES 4.0+ to view the dashboard" link
 
18598                 RESUtils.addCSS('.id-t3_qi5iy {display: none;}');
 
18599                 RESUtils.addCSS('.RESDashboardComponent { position: relative; border: 1px solid #ccc; border-radius: 3px 3px 3px 3px; overflow: hidden; margin-bottom: 10px; }');
 
18600                 RESUtils.addCSS('.RESDashboardComponentHeader { box-sizing: border-box; padding: 5px 0 8px 0; background-color: #f0f3fc; overflow: hidden; }');
 
18601                 RESUtils.addCSS('.RESDashboardComponentScrim { position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 5; display: none; }');
 
18602                 RESUtils.addCSS('.RESDashboardComponentLoader { box-sizing: border-box; position: absolute; background-color: #f2f9ff; border: 1px solid #b9d7f4; border-radius: 3px 3px 3px 3px; width: 314px; height: 40px; left: 50%; top: 50%; margin-left: -167px; margin-top: -20px; text-align: center; padding-top: 11px; }');
 
18603                 RESUtils.addCSS('.RESDashboardComponentLoader span { position: relative; top: -6px; left: 5px; } ');
 
18604                 RESUtils.addCSS('.RESDashboardComponentContainer { padding: 10px 15px 0 15px; min-height: 100px; }');
 
18605                 RESUtils.addCSS('.RESDashboardComponentContainer.minimized { display: none; }');
 
18606                 RESUtils.addCSS('.RESDashboardComponent a.widgetPath, .addNewWidget, .editWidget { display: inline-block; margin-left: 0; margin-top: 7px; color: #000; font-weight: bold; }');
 
18607                 RESUtils.addCSS('.editWidget { float: left; margin-right: 10px; } ');
 
18608                 RESUtils.addCSS('.RESDashboardComponent a.widgetPath { margin-left: 15px; vertical-align: top; width: 120px; overflow: hidden; text-overflow: ellipsis; }');
 
18609                 RESUtils.addCSS('#RESDashboardAddComponent, #RESDashboardEditComponent { box-sizing: border-box; padding: 5px 8px 5px 8px; vertical-align: middle; background-color: #cee3f8; border: 1px solid #369;}');
 
18610                 RESUtils.addCSS('#RESDashboardEditComponent { display: none; position: absolute; }');
 
18611                 // RESUtils.addCSS('#RESDashboardComponentScrim, #RESDashboardComponentLoader { background-color: #ccc; opacity: 0.3; border: 1px solid red; display: none; }');
 
18612                 RESUtils.addCSS('#addRedditFormContainer, #addMailWidgetContainer, #addUserFormContainer { display: none; }');
 
18613                 RESUtils.addCSS('#addWidgetButtons, #addRedditFormContainer, #addMailWidgetContainer, #addUserFormContainer, #editRedditFormContainer { width: auto; min-width: 550px; height: 28px; float: right; text-align: right; }');
 
18614                 RESUtils.addCSS('#editRedditFormContainer { width: auto; }');
 
18615                 RESUtils.addCSS('#addUserForm, #addRedditForm { display: inline-block }');
 
18616                 RESUtils.addCSS('#addUser { width: 200px; height: 24px; }');
 
18617                 RESUtils.addCSS('#addRedditFormContainer ul.token-input-list-facebook, #editRedditFormContainer ul.token-input-list-facebook { float: left; }');
 
18618                 RESUtils.addCSS('#addReddit { width: 115px; background-color: #fff; border: 1px solid #96bfe8; margin-left: 6px; margin-right: 6px; padding: 1px 2px 1px 2px; }');
 
18619                 RESUtils.addCSS('#addRedditDisplayName, #editRedditDisplayName { width: 140px; height: 24px; background-color: #fff; border: 1px solid #96bfe8; margin-left: 6px; margin-right: 6px; padding: 1px 2px 1px 2px; }');
 
18620                 RESUtils.addCSS('#editReddit { width: 5px; } ');
 
18621                 RESUtils.addCSS('.addButton, .updateButton { cursor: pointer; display: inline-block; width: auto; padding: 3px 5px; font-size: 11px; color: #fff; border: 1px solid #636363; border-radius: 3px; background-color: #5cc410; margin-top: 3px; margin-left: 5px; }');
 
18622                 RESUtils.addCSS('.cancelButton { width: 50px; text-align: center; cursor: pointer; display: inline-block; padding: 3px 5px; font-size: 11px; color: #fff; border: 1px solid #636363; border-radius: 3px; background-color: #D02020; margin-top: 3px; margin-left: 5px; }');
 
18623                 RESUtils.addCSS('.backToWidgetTypes { display: inline-block; vertical-align: top; margin-top: 8px; font-weight: bold; color: #000; cursor: pointer; }');
 
18624                 RESUtils.addCSS('.RESDashboardComponentHeader ul { font-family: Verdana; font-size: 13px; box-sizing: border-box; line-height: 22px; display: inline-block; margin-top: 2px; }');
 
18625                 RESUtils.addCSS('.RESDashboardComponentHeader ul li { box-sizing: border-box; vertical-align: middle; height: 24px; display: inline-block; cursor: pointer; padding: 0 6px; border: 1px solid #c7c7c7; background-color: #fff; color: #6c6c6c; border-radius: 3px 3px 3px 3px; }');
 
18626                 RESUtils.addCSS('.RESDashboardComponentHeader .editButton { display: inline-block; padding: 0; width: 24px; -moz-box-sizing: border-box; vertical-align: middle; margin-left: 10px; } ');
 
18627                 RESUtils.addCSS('.RESDashboardComponent.minimized ul li { display: none; }');
 
18628                 RESUtils.addCSS('.RESDashboardComponent.minimized li.RESClose, .RESDashboardComponent.minimized li.minimize { display: inline-block; }');
 
18629                 RESUtils.addCSS('ul.widgetSortButtons li { margin-right: 10px; }');
 
18630                 RESUtils.addCSS('.RESDashboardComponentHeader ul li.active, .RESDashboardComponentHeader ul li:hover { background-color: #a6ccf1; color: #fff; border-color: #699dcf; }');
 
18631                 RESUtils.addCSS('ul.widgetStateButtons li { margin-right: 5px; }');
 
18632                 RESUtils.addCSS('ul.widgetStateButtons li:last-child { margin-right: 0; }');
 
18633                 RESUtils.addCSS('ul.widgetStateButtons li.disabled { background-color: #ddd; }');
 
18634                 RESUtils.addCSS('ul.widgetStateButtons li.disabled:hover { cursor: auto; background-color: #ddd; color: #6c6c6c; border: 1px solid #c7c7c7; }');
 
18635                 RESUtils.addCSS('ul.widgetSortButtons { margin-left: 10px; }');
 
18636                 RESUtils.addCSS('ul.widgetStateButtons { float: right; margin-right: 8px; }');
 
18637                 RESUtils.addCSS('ul.widgetStateButtons li.updateTime { cursor: auto; background: none; border: none; color: #afafaf; font-size: 9px; padding-right: 0; }');
 
18638                 RESUtils.addCSS('ul.widgetStateButtons li.minimize, ul.widgetStateButtons li.close { font-size: 24px; }');
 
18639                 RESUtils.addCSS('.minimized ul.widgetStateButtons li.minimize { font-size: 14px; }');
 
18640                 RESUtils.addCSS('ul.widgetStateButtons li.refresh { margin-left: 3px; width: 24px; position:relative; padding: 0; }');
 
18641                 RESUtils.addCSS('ul.widgetStateButtons li.refresh div { height: 16px; width: 16px; position: absolute; left: 4px; top: 4px; background-image: url(\'http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png\'); background-repeat: no-repeat; background-position: -16px -209px; }');
 
18642                 RESUtils.addCSS('#userTaggerContents .show { display: inline-block; }');
 
18643                 RESUtils.addCSS('#tagPageControls { display: inline-block; position: relative; top: 9px;}');
 
18645                 var dbLinks = $('span.redditname a');
 
18646                 if ($(dbLinks).length > 1) {
 
18647                         $(dbLinks[0]).addClass('active');
 
18650                 // add each subreddit widget...
 
18651                 // add the "add widget" form...
 
18652                 this.attachContainer();
 
18653                 this.attachAddComponent();
 
18654                 this.attachEditComponent();
 
18655                 this.initUpdateQueue();
 
18657         initUpdateQueue: function() {
 
18658                 modules['dashboard'].updateQueue = [];
 
18659                 for (var i in this.widgets) if (this.widgets[i]) this.addWidget(this.widgets[i]);
 
18660                 setTimeout(function () {
 
18661                         $('#RESDashboard').dragsort({ 
 
18662                                 dragSelector: "div.RESDashboardComponentHeader",
 
18663                                 dragSelectorExclude: 'a, li, li.refreshAll, li.refresh > div, .editButton',
 
18664                                 dragEnd: modules['dashboard'].saveOrder,
 
18665                                 placeHolderTemplate: "<div class='placeHolder'><div></div></div>"
 
18669         addToUpdateQueue: function(updateFunction) {
 
18670                 modules['dashboard'].updateQueue.push(updateFunction);
 
18671                 if (!modules['dashboard'].updateQueueTimer) {
 
18672                         modules['dashboard'].updateQueueTimer = setInterval(modules['dashboard'].processUpdateQueue, 2000);
 
18673                         setTimeout(modules['dashboard'].processUpdateQueue, 100);
 
18676         processUpdateQueue: function() {
 
18677                 var thisUpdate = modules['dashboard'].updateQueue.pop();
 
18679                 if (modules['dashboard'].updateQueue.length < 1) {
 
18680                         clearInterval(modules['dashboard'].updateQueueTimer);
 
18681                         delete modules['dashboard'].updateQueueTimer;
 
18684         saveOrder: function() {
 
18685                 var data = $("#siteTable li.RESDashboardComponent").map(function() { return $(this).attr("id"); }).get();
 
18688                 for (var i=0, len=modules['dashboard'].widgets.length; i<len; i++) {
 
18689                         var newIndex = data.indexOf(modules['dashboard'].widgets[i].basePath.replace(/(\/|\+)/g, '_'));
 
18690                         newOrder[newIndex] = modules['dashboard'].widgets[i];
 
18692                 modules['dashboard'].widgets = newOrder;
 
18694                 RESStorage.setItem('RESmodules.dashboard.' + RESUtils.loggedInUser(), JSON.stringify(modules['dashboard'].widgets));
 
18696         attachContainer: function() {
 
18697                 this.siteTable = $('#siteTable.linklisting');
 
18698                 $(this.siteTable).append('<div id="dashboardContents" class="dashboardPane" />');
 
18699                 if ((location.hash !== '') && (location.hash !== '#dashboardContents')) {
 
18700                         $('span.redditname a').removeClass('active');
 
18701                         var activeTabID = location.hash.replace('#','#tab-');
 
18702                         $(activeTabID).addClass('active');
 
18703                         $('.dashboardPane').hide();
 
18704                         $(location.hash).show();
 
18706                         $('#userTaggerContents').hide();
 
18708                 $('span.redditname a:first').click(function(e) {
 
18709                         e.preventDefault();
 
18710                         location.hash = 'dashboardContents';
 
18711                         $('span.redditname a').removeClass('active');
 
18712                         $(this).addClass('active');
 
18713                         $('.dashboardPane').hide();
 
18714                         $('#dashboardContents').show();
 
18717         attachEditComponent: function() {
 
18718                 this.dashboardContents = $('#dashboardContents');
 
18719                 this.dashboardEditComponent = $('<div id="RESDashboardEditComponent" class="RESDashboardComponent" />');
 
18720                 $(this.dashboardEditComponent).html(' \
 
18721                         <div class="editWidget">Edit widget</div> \
 
18722                         <div id="editRedditFormContainer" class="editRedditForm"> \
 
18723                                 <form id="editRedditForm"><input type="text" id="editReddit"><input type="text" id="editRedditDisplayName" placeholder="display name (e.g. stuff)"><input type="submit" class="updateButton" value="save changes"> <input type="cancel" class="cancelButton" value="cancel"></form> \
 
18726                 var thisEle = $(this.dashboardEditComponent).find('#editReddit');
 
18728                 $(this.dashboardEditComponent).find('#editRedditForm').submit(
 
18730                                 e.preventDefault();
 
18731                                 var thisBasePath = $('#editReddit').val();
 
18732                                 if (thisBasePath !== '') {
 
18733                                         if (thisBasePath.indexOf(',') !== -1) {
 
18734                                                 thisBasePath = thisBasePath.replace(/\,/g,'+');
 
18736                                         modules['dashboard'].widgetBeingEdited.formerBasePath = modules['dashboard'].widgetBeingEdited.basePath;
 
18737                                         modules['dashboard'].widgetBeingEdited.basePath = '/r/'+thisBasePath;
 
18738                                         modules['dashboard'].widgetBeingEdited.displayName = $('#editRedditDisplayName').val();
 
18739                                         modules['dashboard'].widgetBeingEdited.update();
 
18740                                         $('#editReddit').tokenInput('clear');
 
18741                                         $('#RESDashboardEditComponent').fadeOut(function() {
 
18742                                                 $('#editReddit').blur();
 
18744                                         modules['dashboard'].widgetBeingEdited.widgetEle.find('.widgetPath').text(modules['dashboard'].widgetBeingEdited.displayName).attr('title','/r/'+thisBasePath);
 
18745                                         modules['dashboard'].updateWidget();
 
18749                 $(this.dashboardEditComponent).find('.cancelButton').click(
 
18751                                         $('#editReddit').tokenInput('clear');
 
18752                                         $('#RESDashboardEditComponent').fadeOut(function() {
 
18753                                                 $('#editReddit').blur();
 
18757                 $(document.body).append(this.dashboardEditComponent);
 
18759         showEditForm: function() {
 
18760                 var basePath = modules['dashboard'].widgetBeingEdited.basePath;
 
18761                 var widgetEle = modules['dashboard'].widgetBeingEdited.widgetEle;
 
18762                 $('#editRedditDisplayName').val(modules['dashboard'].widgetBeingEdited.displayName);
 
18763                 var eleTop = $(widgetEle).position().top;
 
18764                 var eleWidth = $(widgetEle).width();
 
18765                 $('#RESDashboardEditComponent').css('top',eleTop+'px').css('left','5px').css('width',(eleWidth+2)+'px').fadeIn('fast');
 
18766                 basePath = basePath.replace(/^\/r\//,'');
 
18768                 var reddits = basePath.split('+');
 
18769                 for (var i=0, len=reddits.length; i<len; i++) {
 
18775                 if (typeof modules['dashboard'].firstEdit === 'undefined') {
 
18776                         $('#editReddit').tokenInput('/api/search_reddit_names.json?app=res', {
 
18778                                 queryParam: "query",
 
18780                                 allowFreeTagging: true,
 
18782                                 onResult: function(response) {
 
18783                                                         var names = response.names;
 
18785                                                         for (var i=0, len=names.length; i<len; i++) {
 
18786                                                                 results.push({id: names[i], name: names[i]});
 
18788                                                         if (names.length === 0) {
 
18789                                                                 var failedQueryValue = $('#token-input-editReddit').val();
 
18790                                                                 results.push({id: failedQueryValue, name: failedQueryValue, failedResult: true});
 
18794                                 onCachedResult: function(response) {
 
18795                                                         var names = response.names;
 
18797                                                         for (var i=0, len=names.length; i<len; i++) {
 
18798                                                                 results.push({id: names[i], name: names[i]});
 
18800                                                         if (names.length === 0) {
 
18801                                                                 var failedQueryValue = $('#token-input-editReddit').val();
 
18802                                                                 results.push({id: failedQueryValue, name: failedQueryValue, failedResult: true});
 
18806                                 prePopulate: prepop,
 
18807                                 searchingText: 'Searching for matching reddits - may take a few seconds...',
 
18808                                 hintText: 'Type one or more subreddits for which to create a widget.',
 
18809                                 resultsFormatter: function(item) { 
 
18810                                         var thisDesc = item.name;
 
18811                                         if (item['failedResult']) thisDesc += ' - [this subreddit may not exist, ensure proper spelling]';
 
18812                                         return "<li>" + thisDesc + "</li>" 
 
18815                         modules['dashboard'].firstEdit = true;
 
18817                         $('#editReddit').tokenInput('clear');
 
18818                         for (var i=0, len=prepop.length; i<len; i++) {
 
18819                                 $('#editReddit').tokenInput('add', prepop[i]);
 
18823         attachAddComponent: function() {
 
18824                 this.dashboardContents = $('#dashboardContents');
 
18825                 this.dashboardAddComponent = $('<div id="RESDashboardAddComponent" class="RESDashboardComponent" />');
 
18826                 $(this.dashboardAddComponent).html(' \
 
18827                         <div class="addNewWidget">Add a new widget</div> \
 
18828                         <div id="addWidgetButtons"> \
 
18829                                 <div class="addButton" id="addMailWidget">+mail widget</div> \
 
18830                                 <div class="addButton" id="addUserWidget">+user widget</div> \
 
18831                                 <div class="addButton" id="addRedditWidget">+subreddit widget</div> \
 
18833                         <div id="addMailWidgetContainer"> \
 
18834                                 <div class="backToWidgetTypes">« back</div> \
 
18835                                 <div class="addButton widgetShortcut" widgetPath="/message/inbox/">+inbox</div> \
 
18836                                 <div class="addButton widgetShortcut" widgetPath="/message/unread/">+unread</div> \
 
18837                                 <div class="addButton widgetShortcut" widgetPath="/message/messages/">+messages</div> \
 
18838                                 <div class="addButton widgetShortcut" widgetPath="/message/comments/">+comment replies</div> \
 
18839                                 <div class="addButton widgetShortcut" widgetPath="/message/selfreply/">+post replies</div> \
 
18841                         <div id="addUserFormContainer" class="addUserForm"> \
 
18842                                 <div class="backToWidgetTypes">« back</div> \
 
18843                                 <form id="addUserForm"><input type="text" id="addUser"><input type="submit" class="addButton" value="+add"></form> \
 
18845                         <div id="addRedditFormContainer" class="addRedditForm"> \
 
18846                                 <div class="backToWidgetTypes">« back</div> \
 
18847                                 <form id="addRedditForm"><input type="text" id="addReddit"><input type="text" id="addRedditDisplayName" placeholder="display name (e.g. stuff)"><input type="submit" class="addButton" value="+add"></form> \
 
18850                 $(this.dashboardAddComponent).find('.backToWidgetTypes').click(function(e) {
 
18851                         $(this).parent().fadeOut(function() {
 
18852                                 $('#addWidgetButtons').fadeIn();
 
18855                 $(this.dashboardAddComponent).find('.widgetShortcut').click(function(e) {
 
18856                         var thisBasePath = $(this).attr('widgetPath');
 
18857                         modules['dashboard'].addWidget({
 
18858                                 basePath: thisBasePath
 
18860                         $('#addMailWidgetContainer').fadeOut(function() {
 
18861                                 $('#addWidgetButtons').fadeIn();
 
18864                 $(this.dashboardAddComponent).find('#addRedditWidget').click(function(e) {
 
18865                         $('#addWidgetButtons').fadeOut(function() {
 
18866                                 $('#addRedditFormContainer').fadeIn(function() {
 
18867                                         $('#token-input-addReddit').focus();
 
18871                 $(this.dashboardAddComponent).find('#addMailWidget').click(function(e) {
 
18872                         $('#addWidgetButtons').fadeOut(function() {
 
18873                                 $('#addMailWidgetContainer').fadeIn();
 
18876                 $(this.dashboardAddComponent).find('#addUserWidget').click(function(e) {
 
18877                         $('#addWidgetButtons').fadeOut(function() {
 
18878                                 $('#addUserFormContainer').fadeIn();
 
18881                 var thisEle = $(this.dashboardAddComponent).find('#addReddit');
 
18882                 $(thisEle).tokenInput('/api/search_reddit_names.json?app=res', {
 
18884                         queryParam: "query",
 
18886                         allowFreeTagging: true,
 
18888                         onResult: function(response) {
 
18889                                                 var names = response.names;
 
18891                                                 for (var i=0, len=names.length; i<len; i++) {
 
18892                                                         results.push({id: names[i], name: names[i]});
 
18894                                                 if (names.length === 0) {
 
18895                                                         var failedQueryValue = $('#token-input-addReddit').val();
 
18896                                                         results.push({id: failedQueryValue, name: failedQueryValue, failedResult: true});
 
18900                         onCachedResult: function(response) {
 
18901                                                 var names = response.names;
 
18903                                                 for (var i=0, len=names.length; i<len; i++) {
 
18904                                                         results.push({id: names[i], name: names[i]});
 
18906                                                 if (names.length === 0) {
 
18907                                                         var failedQueryValue = $('#token-input-addReddit').val();
 
18908                                                         results.push({id: failedQueryValue, name: failedQueryValue, failedResult: true});
 
18912                         /* prePopulate: prepop, */
 
18913                         searchingText: 'Searching for matching reddits - may take a few seconds...',
 
18914                         hintText: 'Type one or more subreddits for which to create a widget.',
 
18915                         resultsFormatter: function(item) { 
 
18916                                 var thisDesc = item.name;
 
18917                                 if (item['failedResult']) thisDesc += ' - [this subreddit may not exist, ensure proper spelling]';
 
18918                                 return "<li>" + thisDesc + "</li>" 
 
18922                 $(this.dashboardAddComponent).find('#addRedditForm').submit(
 
18924                                 e.preventDefault();
 
18925                                 var thisBasePath = $('#addReddit').val();
 
18926                                 if (thisBasePath !== '') {
 
18927                                         if (thisBasePath.indexOf(',') !== -1) {
 
18928                                                 thisBasePath = thisBasePath.replace(/\,/g,'+');
 
18930                                         var thisDisplayName = ($('#addRedditDisplayName').val()) ? $('#addRedditDisplayName').val() : thisBasePath;
 
18931                                         modules['dashboard'].addWidget({
 
18932                                                 basePath: thisBasePath,
 
18933                                                 displayName: thisDisplayName
 
18935                                         // $('#addReddit').val('').blur();
 
18936                                         $('#addReddit').tokenInput('clear');
 
18937                                         $('#addRedditFormContainer').fadeOut(function() {
 
18938                                                 $('#addReddit').blur();
 
18939                                                 $('#addWidgetButtons').fadeIn();
 
18944                 $(this.dashboardAddComponent).find('#addUserForm').submit(
 
18946                                 e.preventDefault();
 
18947                                 var thisBasePath = '/user/'+$('#addUser').val();
 
18948                                 modules['dashboard'].addWidget({
 
18949                                         basePath: thisBasePath
 
18951                                 $('#addUser').val('').blur();
 
18952                                 $('#addUserFormContainer').fadeOut(function() {
 
18953                                         $('#addWidgetButtons').fadeIn();
 
18958                 $(this.dashboardContents).append(this.dashboardAddComponent);
 
18959                 this.dashboardUL = $('<ul id="RESDashboard"></ul>');
 
18960                 $(this.dashboardContents).append(this.dashboardUL);
 
18962         addWidget: function(optionsObject, isNew) {
 
18963                 if (optionsObject.basePath.slice(0,1) !== '/') optionsObject.basePath = '/r/'+optionsObject.basePath;
 
18965                 for (var i=0, len=this.widgets.length; i<len; i++) {
 
18966                         if (this.widgets[i].basePath == optionsObject.basePath) {
 
18971                 // hide any shortcut button for this widget, since it exists... wait a second, though, or it causes rendering stupidity.
 
18972                 setTimeout(function() {
 
18973                         $('.widgetShortcut[widgetPath="'+optionsObject.basePath+'"]').hide();
 
18975                 if (exists && isNew) {
 
18976                         alert('A widget for '+optionsObject.basePath+' already exists!');
 
18978                         var thisWidget = new this.widgetObject(optionsObject);
 
18980                         modules['dashboard'].saveWidget(thisWidget.optionsObject());
 
18983         removeWidget: function(optionsObject) {
 
18984                 this.getLatestWidgets();
 
18985                 var exists = false;
 
18986                 for (var i=0, len=modules['dashboard'].widgets.length; i<len; i++) {
 
18987                         if (modules['dashboard'].widgets[i].basePath == optionsObject.basePath) {
 
18989                                 $('#'+modules['dashboard'].widgets[i].basePath.replace(/\/|\+/g,'_')).fadeOut('slow', function(ele) {
 
18992                                 modules['dashboard'].widgets.splice(i,1);
 
18993                                 // show any shortcut button for this widget, since we've now deleted it...
 
18994                                 setTimeout(function() {
 
18995                                         $('.widgetShortcut[widgetPath="'+optionsObject.basePath+'"]').show();
 
19001                         RESUtils.notification({
 
19002                                 moduleID: 'dashboard',
 
19004                                 message: 'The widget you just tried to remove does not seem to exist.'
 
19007                 RESStorage.setItem('RESmodules.dashboard.' + RESUtils.loggedInUser(), JSON.stringify(modules['dashboard'].widgets));
 
19009         saveWidget: function(optionsObject, init) {
 
19010                 this.getLatestWidgets();
 
19011                 var exists = false;
 
19012                 for (var i=0, len=modules['dashboard'].widgets.length; i<len; i++) {
 
19013                         if (modules['dashboard'].widgets[i].basePath == optionsObject.basePath) {
 
19015                                 modules['dashboard'].widgets[i] = optionsObject;
 
19018                 if (!exists) modules['dashboard'].widgets.push(optionsObject);
 
19019                 RESStorage.setItem('RESmodules.dashboard.' + RESUtils.loggedInUser(), JSON.stringify(modules['dashboard'].widgets));
 
19021         updateWidget: function() {
 
19022                 this.getLatestWidgets();
 
19023                 var exists = false;
 
19024                 for (var i=0, len=modules['dashboard'].widgets.length; i<len; i++) {
 
19025                         if (modules['dashboard'].widgets[i].basePath == modules['dashboard'].widgetBeingEdited.formerBasePath) {
 
19027                                 delete modules['dashboard'].widgetBeingEdited.formerBasePath;
 
19028                                 modules['dashboard'].widgets[i] = modules['dashboard'].widgetBeingEdited.optionsObject();
 
19031                 RESStorage.setItem('RESmodules.dashboard.' + RESUtils.loggedInUser(), JSON.stringify(modules['dashboard'].widgets));
 
19033         widgetObject: function(widgetOptions) {
 
19034                 var thisWidget = this; // keep a reference because the this keyword can mean different things in different scopes...
 
19035                 thisWidget.basePath = widgetOptions.basePath;
 
19036                 if ((typeof widgetOptions.displayName === 'undefined') || (widgetOptions.displayName === null)) {
 
19037                         widgetOptions.displayName = thisWidget.basePath;
 
19039                 thisWidget.displayName = widgetOptions.displayName;
 
19040                 thisWidget.numPosts = widgetOptions.numPosts || modules['dashboard'].options.defaultPosts.value;
 
19041                 thisWidget.sortBy = widgetOptions.sortBy || modules['dashboard'].options.defaultSort.value;
 
19042                 thisWidget.minimized = widgetOptions.minimized || false;
 
19043                 thisWidget.widgetEle = $('<li class="RESDashboardComponent" id="'+thisWidget.basePath.replace(/\/|\+/g,'_')+'"><div class="RESDashboardComponentScrim"><div class="RESDashboardComponentLoader"><img id="dashboardLoader" src="'+modules['dashboard'].loader+'"><span>querying the server. one moment please.</span></div></div></li>');
 
19044                 var editButtonHTML = (thisWidget.basePath.indexOf('/r/') === -1) ? '' : '<div class="editButton" title="edit"></div>';
 
19045                 thisWidget.header = $('<div class="RESDashboardComponentHeader"><a class="widgetPath" title="'+thisWidget.basePath+'" href="'+thisWidget.basePath+'">'+thisWidget.displayName+'</a></div>');
 
19046                 thisWidget.sortControls = $('<ul class="widgetSortButtons"><li sort="hot">hot</li><li sort="new">new</li><li sort="controversial">controversial</li><li sort="top">top</li></ul>');
 
19047                 // return an optionsObject, which is what we'll store in the modules['dashboard'].widgets array.
 
19048                 thisWidget.optionsObject = function() {
 
19050                                 basePath: thisWidget.basePath,
 
19051                                 displayName: thisWidget.displayName,
 
19052                                 numPosts: thisWidget.numPosts,
 
19053                                 sortBy: thisWidget.sortBy,
 
19054                                 minimized: thisWidget.minimized 
 
19057                 // set the sort by properly...
 
19058                 $(thisWidget.sortControls).find('li[sort='+thisWidget.sortBy+']').addClass('active');
 
19059                 $(thisWidget.sortControls).find('li').click(function(e) {
 
19060                         thisWidget.sortChange($(e.target).attr('sort'));
 
19062                 $(thisWidget.header).append(thisWidget.sortControls);
 
19063                 if ((thisWidget.basePath.indexOf('/r/') !== 0) && (thisWidget.basePath.indexOf('/user/') !== 0)) {
 
19064                         setTimeout(function() {
 
19065                                 $(thisWidget.sortControls).hide();
 
19068                 thisWidget.stateControls = $('<ul class="widgetStateButtons"><li class="updateTime"></li><li action="refresh" class="refresh"><div action="refresh"></div></li><li action="refreshAll" class="refreshAll">Refresh All</li><li action="addRow">+row</li><li action="subRow">-row</li><li action="edit" class="editButton"></li><li action="minimize" class="minimize">-</li><li action="delete" class="RESClose">×</li></ul>');
 
19069                 $(thisWidget.stateControls).find('li').click(function (e) {
 
19070                         switch ($(e.target).attr('action')) {
 
19072                                         thisWidget.update();
 
19075                                         $('li[action="refresh"]').click();
 
19078                                         if (thisWidget.numPosts === 10) break;
 
19079                                         thisWidget.numPosts++;
 
19080                                         if (thisWidget.numPosts === 10) $(thisWidget.stateControls).find('li[action=addRow]').addClass('disabled');
 
19081                                         $(thisWidget.stateControls).find('li[action=subRow]').removeClass('disabled');
 
19082                                         modules['dashboard'].saveWidget(thisWidget.optionsObject());
 
19083                                         thisWidget.update();
 
19086                                         if (thisWidget.numPosts === 0) break;
 
19087                                         thisWidget.numPosts--;
 
19088                                         if (thisWidget.numPosts === 1) $(thisWidget.stateControls).find('li[action=subRow]').addClass('disabled');
 
19089                                         $(thisWidget.stateControls).find('li[action=addRow]').removeClass('disabled');
 
19090                                         modules['dashboard'].saveWidget(thisWidget.optionsObject());
 
19091                                         thisWidget.update();
 
19094                                         $(thisWidget.widgetEle).toggleClass('minimized');
 
19095                                         if ($(thisWidget.widgetEle).hasClass('minimized')) {
 
19096                                                 $(e.target).text('+');
 
19097                                                 thisWidget.minimized = true;
 
19099                                                 $(e.target).text('-');
 
19100                                                 thisWidget.minimized = false;
 
19101                                                 thisWidget.update();
 
19103                                         $(thisWidget.contents).parent().slideToggle();
 
19104                                         modules['dashboard'].saveWidget(thisWidget.optionsObject());
 
19107                                         modules['dashboard'].removeWidget(thisWidget.optionsObject());
 
19111                 $(thisWidget.header).append(thisWidget.stateControls);
 
19112                 thisWidget.sortChange = function(sortBy) {
 
19113                         thisWidget.sortBy = sortBy;
 
19114                         $(thisWidget.header).find('ul.widgetSortButtons li').removeClass('active');
 
19115                         $(thisWidget.header).find('ul.widgetSortButtons li[sort='+sortBy+']').addClass('active');
 
19116                         thisWidget.update();
 
19117                         modules['dashboard'].saveWidget(thisWidget.optionsObject());
 
19119                 thisWidget.edit = function(e) {
 
19120                         modules['dashboard'].widgetBeingEdited = thisWidget;
 
19121                         modules['dashboard'].showEditForm();
 
19123                 $(thisWidget.header).find('.editButton').click(thisWidget.edit);
 
19124                 thisWidget.update = function() {
 
19125                         if (thisWidget.basePath.match(/\/user\//)) {
 
19126                                 thisWidget.sortPath = (thisWidget.sortBy === 'hot') ? '/' : '?sort='+thisWidget.sortBy;
 
19127                         } else if (thisWidget.basePath.match(/\/r\//)) {
 
19128                                 thisWidget.sortPath = (thisWidget.sortBy === 'hot') ? '/' : '/'+thisWidget.sortBy+'/';
 
19130                                 thisWidget.sortPath = '';
 
19132                         thisWidget.url = location.protocol + '//' + location.hostname + '/' + thisWidget.basePath + thisWidget.sortPath;
 
19133                         $(thisWidget.contents).fadeTo('fast',0.25);
 
19134                         $(thisWidget.scrim).fadeIn();
 
19136                                 url: thisWidget.url,
 
19138                                         limit: thisWidget.numPosts
 
19140                                 success: thisWidget.populate,
 
19141                                 error: thisWidget.error
 
19144                 thisWidget.container = $('<div class="RESDashboardComponentContainer"><div class="RESDashboardComponentContents"></div></div>');
 
19145                 if (thisWidget.minimized) {
 
19146                         $(thisWidget.container).addClass('minimized');
 
19147                         $(thisWidget.stateControls).find('li.minimize').addClass('minimized').text('+');
 
19149                 thisWidget.scrim = $(thisWidget.widgetEle).find('.RESDashboardComponentScrim');
 
19150                 thisWidget.contents = $(thisWidget.container).find('.RESDashboardComponentContents');
 
19151                 thisWidget.init = function() {
 
19152                         if (RESUtils.currentSubreddit('dashboard')) {
 
19154                                 if (!thisWidget.minimized) modules['dashboard'].addToUpdateQueue(thisWidget.update);
 
19157                 thisWidget.draw = function() {
 
19158                         $(thisWidget.widgetEle).append(thisWidget.header);
 
19159                         $(thisWidget.widgetEle).append(thisWidget.container);
 
19160                         if (thisWidget.minimized) $(thisWidget.widgetEle).addClass('minimized');
 
19161                         modules['dashboard'].dashboardUL.prepend(thisWidget.widgetEle);
 
19162                         // $(thisWidget.scrim).fadeIn();
 
19164                 thisWidget.populate = function(response) {
 
19165                         var widgetContent = $(response).find('#siteTable');
 
19166                         $(widgetContent).attr('id','siteTable_'+thisWidget.basePath.replace(/\/|\+/g,'_'));
 
19167                         if (widgetContent.length === 2) widgetContent = widgetContent[1];
 
19168                         $(widgetContent).attr('url',thisWidget.url+'?limit='+thisWidget.numPosts);
 
19169                         if ((widgetContent) && ($(widgetContent).html() !== '')) {
 
19170                                 // widgetContent will contain HTML from Reddit's page load. No XSS here or you'd already be hit, can't call escapeHTML on this either and wouldn't help anyhow.
 
19171                                 $(thisWidget.contents).html(widgetContent);
 
19172                                 $(thisWidget.contents).fadeTo('fast',1);
 
19173                                 $(thisWidget.scrim).fadeOut(function(e) {
 
19174                                         $(this).hide(); // make sure it is hidden in case the element isn't visible due to being on a different dashboard tab
 
19176                                 $(thisWidget.stateControls).find('.updateTime').text('updated: '+RESUtils.niceDateTime());
 
19178                                 if (thisWidget.url.indexOf('/message/') !== -1) {
 
19179                                         $(thisWidget.contents).html('<div class="widgetNoMail">No messages were found.</div>');
 
19181                                         $(thisWidget.contents).html('<div class="error">There were no results returned for this widget. If you made a typo, simply close the widget to delete it. If reddit is just under heavy load, try clicking refresh in a few moments.</div>');
 
19183                                 $(thisWidget.contents).fadeTo('fast',1);
 
19184                                 $(thisWidget.scrim).fadeOut();
 
19185                                 $(thisWidget.stateControls).find('.updateTime').text('updated: '+RESUtils.niceDateTime());
 
19187                         // now run watcher functions from other modules on this content...
 
19188                         RESUtils.watchers.siteTable.forEach(function(callback) {
 
19189                                 if (callback) callback(widgetContent[0]);
 
19193                 thisWidget.error = function(xhr, err) {
 
19194                         // alert('There was an error loading data for this widget. Did you type a bad path, perhaps? Removing this widget automatically.');
 
19195                         // modules['dashboard'].removeWidget(thisWidget.optionsObject());
 
19196                         if (xhr.status === 404) {
 
19197                                 $(thisWidget.contents).html('<div class="error">This widget received a 404 not found error. You may have made a typo when adding it.</div>');
 
19199                                 $(thisWidget.contents).html('<div class="error">There was an error loading data for this widget. Reddit may be under heavy load, or you may have provided an invalid path.</div>');
 
19201                         $(thisWidget.scrim).fadeOut();
 
19202                         $(thisWidget.contents).fadeTo('fast',1);
 
19205         addDashboardShortcuts: function() {
 
19206                 var subButtons = document.querySelectorAll('.fancy-toggle-button');
 
19207                 for (var h=0, len=subButtons.length; h<len; h++) {
 
19208                         var subButton = subButtons[h];
 
19209                         if ((RESUtils.currentSubreddit().indexOf('+') === -1) && (RESUtils.currentSubreddit() !== 'mod')) {
 
19210                                 var thisSubredditFragment = RESUtils.currentSubreddit();
 
19211                                 var isMulti = false;
 
19212                         } else if ($(subButton).parent().hasClass('subButtons')) {
 
19213                                 var isMulti = true;
 
19214                                 var thisSubredditFragment = $(subButton).parent().parent().find('a.title').text();
 
19216                                 var isMulti = true;
 
19217                                 var thisSubredditFragment = $(subButton).next().text();
 
19219                         if (! ($('#subButtons-'+thisSubredditFragment).length>0)) {
 
19220                                 var subButtonsWrapper = $('<div id="subButtons-'+thisSubredditFragment+'" class="subButtons" style="margin: 0 !important;"></div>');
 
19221                                 $(subButton).wrap(subButtonsWrapper);
 
19222                                 // move this wrapper to the end (after any icons that may exist...)
 
19224                                         var theWrap = $(subButton).parent();
 
19225                                         $(theWrap).appendTo($(theWrap).parent());
 
19228                         var dashboardToggle = document.createElement('span');
 
19229                         dashboardToggle.setAttribute('class','REStoggle RESDashboardToggle');
 
19230                         dashboardToggle.setAttribute('data-subreddit',thisSubredditFragment);
 
19232                         for (var i=0, sublen=this.widgets.length; i<sublen; i++) {
 
19233                                 if ((this.widgets[i]) && (this.widgets[i].basePath.toLowerCase() === '/r/'+thisSubredditFragment.toLowerCase())) {
 
19239                                 dashboardToggle.textContent = '-dashboard';
 
19240                                 dashboardToggle.setAttribute('title','Remove this subreddit from your dashboard');
 
19241                                 dashboardToggle.classList.add('remove');
 
19243                                 dashboardToggle.textContent = '+dashboard';
 
19244                                 dashboardToggle.setAttribute('title','Add this subreddit to your dashboard');
 
19246                         dashboardToggle.setAttribute('data-subreddit',thisSubredditFragment)
 
19247                         dashboardToggle.addEventListener('click', modules['dashboard'].toggleDashboard, false);
 
19248                         $('#subButtons-'+thisSubredditFragment).append(dashboardToggle);
 
19249                         var next = $('#subButtons-'+thisSubredditFragment).next();
 
19250                         if ($(next).hasClass('title') && (! $('#subButtons-'+thisSubredditFragment).hasClass('swapped'))) {
 
19251                                 $('#subButtons-'+thisSubredditFragment).before($(next));
 
19252                                 $('#subButtons-'+thisSubredditFragment).addClass('swapped');
 
19256         toggleDashboard: function(e) {
 
19257                 var thisBasePath = '/r/'+$(e.target).data('subreddit');
 
19258                 if (e.target.classList.contains('remove')) {
 
19259                         modules['dashboard'].removeWidget({
 
19260                                 basePath: thisBasePath
 
19262                         e.target.textContent = '+dashboard';
 
19263                         e.target.classList.remove('remove');
 
19265                         modules['dashboard'].addWidget({
 
19266                                 basePath: thisBasePath
 
19268                         e.target.textContent = '-dashboard';
 
19269                         RESUtils.notification({ 
 
19270                                 header: 'Dashboard Notification', 
 
19271                                 moduleID: 'dashboard',
 
19272                                 message: 'Dashboard widget added for '+thisBasePath+' <p><a class="RESNotificationButtonBlue" href="/r/Dashboard">view the dashboard</a></p><div class="clear"></div>'
 
19274                         e.target.classList.add('remove');
 
19277         addTab: function(tabID, tabName) {
 
19278                 $('#siteTable.linklisting').append('<div id="'+tabID+'" class="dashboardPane" />');
 
19279                 $('span.redditname').append('<a id="tab-'+tabID+'" class="dashboardTab" title="'+tabName+'">'+tabName+'</a>');
 
19280                 $('#tab-'+tabID).click(function(e) {
 
19281                         location.hash = tabID;
 
19282                         $('span.redditname a').removeClass('active');
 
19283                         $(this).addClass('active');
 
19284                         $('.dashboardPane').hide();
 
19285                         $('#'+tabID).show();
 
19290 modules['subredditInfo'] = {
 
19291         moduleID: 'subredditInfo',
 
19292         moduleName: 'Subreddit Info',
 
19298                         description: 'Delay, in milliseconds, before hover tooltip loads. Default is 800.'
 
19303                         description: 'Delay, in milliseconds, before hover tooltip fades away. Default is 200.'
 
19308                         description: 'Fade animation\'s speed. Default is 0.3, the range is 0-1. Setting the speed to 1 will disable the animation.'
 
19313                         description: 'Show date (subreddit created...) in US format (i.e. 08-31-2010)'
 
19316         description: 'Adds a hover tooltip to subreddits',
 
19317         isEnabled: function() {
 
19318                 return RESConsole.getModulePrefs(this.moduleID);
 
19321                 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
 
19323         isMatchURL: function() {
 
19324                 return RESUtils.isMatchURL(this.moduleID);
 
19326         beforeLoad: function() {
 
19327                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
19329                         css += '.subredditInfoToolTip .subredditLabel { float: left; width: 140px; margin-bottom: 12px; }';
 
19330                         css += '.subredditInfoToolTip .subredditDetail { float: left; width: 240px; margin-bottom: 12px; }';
 
19331                         css += '.subredditInfoToolTip .blueButton { float: right; margin-left: 8px; }';
 
19332                         css += '.subredditInfoToolTip .redButton { float: right; margin-left: 8px; }';
 
19333                         RESUtils.addCSS(css);
 
19337                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
19338                         // create a cache for subreddit data so we only load it once even if the hover is triggered many times
 
19339                         this.subredditInfoCache = [];
 
19340                         this.srRe = /\/r\/(\w+)(?:\/(new|rising|controversial|top))?\/?$/i;
 
19342                         // get subreddit links and add event listeners...
 
19343                         this.addListeners();
 
19344                         RESUtils.watchForElement('siteTable', modules['subredditInfo'].addListeners);
 
19347         addListeners: function(ele) {
 
19348                 var ele = ele || document.body;
 
19349                 var subredditLinks = document.body.querySelectorAll('.listing-page a.subreddit, .comment .md a[href^="/r/"]');
 
19350                 if (subredditLinks) {
 
19351                                 var len=subredditLinks.length;
 
19352                                 for (var i=0; i<len; i++) {
 
19353                                         var thisSRLink = subredditLinks[i];
 
19354                                         if (this.srRe.test(thisSRLink.href)) {
 
19355                                                 thisSRLink.addEventListener('mouseover', function(e) {
 
19356                                                         RESUtils.hover.begin(e.target, {
 
19358                                                                 openDelay: modules['subredditInfo'].options.hoverDelay.value,
 
19359                                                                 fadeDelay: modules['subredditInfo'].options.fadeDelay.value,
 
19360                                                                 fadeSpeed: modules['subredditInfo'].options.fadeSpeed.value
 
19361                                                         }, modules['subredditInfo'].showSubredditInfo, {});
 
19367         showSubredditInfo: function(def, obj, context) {
 
19368                 var mod = modules['subredditInfo'];
 
19369                 var thisSubreddit = obj.textContent.replace("/r/","");
 
19370                 var header = document.createDocumentFragment();
 
19371                 var link = $('<a href="/r/'+escapeHTML(thisSubreddit)+'">/r/' + escapeHTML(thisSubreddit) + '</a>');
 
19372                 header.appendChild(link[0]);
 
19373                 if (RESUtils.loggedInUser()) {
 
19374                         var subscribeToggle = $('<span />');
 
19376                                 .attr('id', 'RESHoverInfoSubscriptionButton')
 
19377                                 .addClass('RESFilterToggle')
 
19378                                 .css('margin-left', '12px')
 
19380                                 .on('click', modules['subredditInfo'].toggleSubscription);
 
19381                         modules['subredditInfo'].updateToggleButton(subscribeToggle, false);
 
19383                         header.appendChild(subscribeToggle[0]);
 
19386                         <div class="subredditInfoToolTip">\
 
19387                                 <a class="hoverSubreddit" href="/user/'+escapeHTML(thisSubreddit)+'">'+escapeHTML(thisSubreddit)+'</a>:<br>\
 
19388                                 <img src="'+RESConsole.loader+'"> loading...\
 
19390                 def.notify(header, null);
 
19391                 if (typeof mod.subredditInfoCache[thisSubreddit] !== 'undefined') {
 
19392                         mod.writeSubredditInfo(mod.subredditInfoCache[thisSubreddit], def);
 
19394                         GM_xmlhttpRequest({
 
19396                                 url:    location.protocol + "//"+location.hostname+"/r/" + thisSubreddit + "/about.json?app=res",
 
19397                                 onload: function(response) {
 
19398                                         var thisResponse = safeJSON.parse(response.responseText, null, true);
 
19399                                         if (thisResponse) {
 
19400                                                 mod.updateCache(thisSubreddit, thisResponse);
 
19401                                                 mod.writeSubredditInfo(thisResponse, def);
 
19403                                                 mod.writeSubredditInfo({}, def);
 
19409         updateCache: function(subreddit, data) {
 
19410                 subreddit = subreddit.toLowerCase();
 
19412                         data = { data : data };
 
19414                 this.subredditInfoCache = this.subredditInfoCache  || [];
 
19415                 this.subredditInfoCache[subreddit] = $.extend(true, {}, this.subredditInfoCache[subreddit], data);
 
19417         writeSubredditInfo: function(jsonData, deferred) {
 
19418                 if (!jsonData.data) {
 
19419                         var srHTML = '<div class="subredditInfoToolTip">Subreddit not found</div>';
 
19420                         var newBody = $(srHTML);
 
19421                         deferred.resolve(null, newBody)
 
19424                 var utctime = jsonData.data.created_utc;
 
19425                 var d = new Date(utctime * 1000);
 
19427                 jsonData.data.over18 === true ? isOver18 = 'Yes' : isOver18 = 'No';
 
19428                 var srHTML = '<div class="subredditInfoToolTip">';
 
19429                 srHTML += '<div class="subredditLabel">Subreddit created:</div> <div class="subredditDetail">' + RESUtils.niceDate(d, this.options.USDateFormat.value) + ' (' + RESUtils.niceDateDiff(d) + ')</div>';
 
19430                 srHTML += '<div class="subredditLabel">Subscribers:</div> <div class="subredditDetail">' + RESUtils.addCommas(jsonData.data.subscribers) + '</div>';
 
19431                 srHTML += '<div class="subredditLabel">Title:</div> <div class="subredditDetail">' + escapeHTML(jsonData.data.title) + '</div>';
 
19432                 srHTML += '<div class="subredditLabel">Over 18:</div> <div class="subredditDetail">' + escapeHTML(isOver18) + '</div>';
 
19433                 // srHTML += '<div class="subredditLabel">Description:</div> <div class="subredditDetail">' + jsonData.data.description + '</div>';
 
19434                 srHTML += '<div class="clear"></div><div id="subTooltipButtons" class="bottomButtons">';
 
19435                 srHTML += '<div class="clear"></div></div>'; // closes bottomButtons div
 
19436                 srHTML += '</div>';
 
19438                 var newBody = $(srHTML);
 
19439                 // bottom buttons will include: +filter +shortcut +dashboard (maybe sub/unsub too?)
 
19440                 if (modules['subredditManager'].isEnabled()) {
 
19441                         var theSC = document.createElement('span');
 
19442                         theSC.setAttribute('style','display: inline-block !important;');
 
19443                         theSC.setAttribute('class','REStoggle RESshortcut RESshortcutside');
 
19444                         theSC.setAttribute('data-subreddit',jsonData.data.display_name.toLowerCase());
 
19446                         for (var i=0, len=modules['subredditManager'].mySubredditShortcuts.length; i<len; i++) {
 
19447                                 if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() == jsonData.data.display_name.toLowerCase()) {
 
19453                                 theSC.textContent = '-shortcut';
 
19454                                 theSC.setAttribute('title','Remove this subreddit from your shortcut bar');
 
19455                                 theSC.classList.add('remove');
 
19457                                 theSC.textContent = '+shortcut';
 
19458                                 theSC.setAttribute('title','Add this subreddit to your shortcut bar');
 
19460                         theSC.addEventListener('click', modules['subredditManager'].toggleSubredditShortcut, false);
 
19462                         newBody.find('#subTooltipButtons').append(theSC);
 
19464                 if (modules['dashboard'].isEnabled()) {
 
19465                         var dashboardToggle = document.createElement('span');
 
19466                         dashboardToggle.setAttribute('class','RESDashboardToggle');
 
19467                         dashboardToggle.setAttribute('data-subreddit',jsonData.data.display_name.toLowerCase());
 
19469                         for (var i=0, len=modules['dashboard'].widgets.length; i<len; i++) {
 
19470                                 if ((modules['dashboard'].widgets[i]) && (modules['dashboard'].widgets[i].basePath.toLowerCase() === '/r/'+jsonData.data.display_name.toLowerCase())) {
 
19476                                 dashboardToggle.textContent = '-dashboard';
 
19477                                 dashboardToggle.setAttribute('title','Remove this subreddit from your dashboard');
 
19478                                 dashboardToggle.classList.add('remove');
 
19480                                 dashboardToggle.textContent = '+dashboard';
 
19481                                 dashboardToggle.setAttribute('title','Add this subreddit to your dashboard');
 
19483                         dashboardToggle.addEventListener('click', modules['dashboard'].toggleDashboard, false);
 
19484                         newBody.find('#subTooltipButtons').append(dashboardToggle);
 
19486                 if (modules['filteReddit'].isEnabled()) {
 
19487                         var filterToggle = document.createElement('span');
 
19488                         filterToggle.setAttribute('class','RESFilterToggle');
 
19489                         filterToggle.setAttribute('data-subreddit',jsonData.data.display_name.toLowerCase());
 
19491                         var filteredReddits = modules['filteReddit'].options.subreddits.value;
 
19492                         for (var i=0, len=filteredReddits.length; i<len; i++) {
 
19493                                 if ((filteredReddits[i]) && (filteredReddits[i][0].toLowerCase() == jsonData.data.display_name.toLowerCase())) {
 
19499                                 filterToggle.textContent = '-filter';
 
19500                                 filterToggle.setAttribute('title','Stop filtering from /r/all and /domain/*');
 
19501                                 filterToggle.classList.add('remove');
 
19503                                 filterToggle.textContent = '+filter';
 
19504                                 filterToggle.setAttribute('title','Filter this subreddit from /r/all and /domain/*');
 
19506                         filterToggle.addEventListener('click', modules['filteReddit'].toggleFilter, false);
 
19507                         newBody.find('#subTooltipButtons').append(filterToggle);
 
19510                 if (RESUtils.loggedInUser()) {
 
19511                         var subscribed = !!jsonData.data.user_is_subscriber;
 
19513                         var subscribeToggle = $('#RESHoverInfoSubscriptionButton');
 
19514                         subscribeToggle.attr('data-subreddit',jsonData.data.display_name.toLowerCase());
 
19515                         modules['subredditInfo'].updateToggleButton(subscribeToggle, subscribed);
 
19516                         subscribeToggle.fadeIn('fast');
 
19519                 deferred.resolve(null, newBody)
 
19521         updateToggleButton: function(toggleButton, subscribed) {
 
19522                 if (toggleButton instanceof jQuery) toggleButton = toggleButton[0];
 
19523                 var toggleOn = '+subscribe';
 
19524                 var toggleOff = '-unsubscribe';
 
19526                         toggleButton.textContent = toggleOff;
 
19527                         toggleButton.classList.add('remove');
 
19529                         toggleButton.textContent = toggleOn;
 
19530                         toggleButton.classList.remove('remove');
 
19533         toggleSubscription: function(e) {
 
19535                 var subscribeToggle = e.target;
 
19536                 var subreddit = subscribeToggle.getAttribute('data-subreddit').toLowerCase();
 
19537                 var subredditData = modules['subredditInfo'].subredditInfoCache[subreddit].data;
 
19538                 var subscribing = !subredditData.user_is_subscriber;
 
19540                 modules['subredditInfo'].updateToggleButton(subscribeToggle, subscribing);
 
19542                 modules['subredditManager'].subscribeToSubreddit(subredditData.name, subscribing);
 
19543                 modules['subredditInfo'].updateCache(subreddit, { 'user_is_subscriber': subscribing });
 
19545 }; // note: you NEED this semicolon at the end!
 
19550  * CommentHidePersistor - stores hidden comments in localStorage and re-hides
 
19551  * them on reload of the page.
 
19553 m_chp = modules['commentHidePersistor'] = {
 
19554         moduleID: 'commentHidePersistor',
 
19555         moduleName: 'Comment Hide Persistor',
 
19556         category: 'Comments',
 
19557         description: 'Saves the state of hidden comments across page views.',
 
19558         allHiddenThings: {},
 
19561         hiddenThingsKey: window.location.href,
 
19566         isEnabled: function () {
 
19567                 return RESConsole.getModulePrefs(this.moduleID);
 
19570                 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
 
19571                 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
 
19573         isMatchURL: function () {
 
19574                 return RESUtils.isMatchURL(this.moduleID);
 
19577                 if ((this.isEnabled()) && (this.isMatchURL())) {
 
19578                         m_chp.bindToHideLinks();
 
19579                         m_chp.hideHiddenThings();
 
19582         bindToHideLinks: function () {
 
19584                  * For every expand/collapse link, add a click listener that will
 
19585                  * store or remove the comment ID from our list of hidden comments.
 
19587                 $('body').on('click', 'a.expand', function () {
 
19588                         var thing   = $(this).parents('.thing'),
 
19589                             thingId = thing.data('fullname'),
 
19590                             collapsing = !$(this).parent().is('.collapsed');
 
19592                         /* Add our key to pages interacted with, for potential pruning
 
19594                         if (m_chp.hiddenKeys.indexOf(m_chp.hiddenThingsKey) === -1) {
 
19595                                 m_chp.hiddenKeys.push(m_chp.hiddenThingsKey);
 
19599                                 m_chp.addHiddenThing(thingId);
 
19601                                 m_chp.removeHiddenThing(thingId);
 
19605         loadHiddenThings: function () {
 
19606                 var hidePersistorJson = RESStorage.getItem('RESmodules.commentHidePersistor.hidePersistor')
 
19608                 if (hidePersistorJson) {
 
19610                                 m_chp.hidePersistorData = safeJSON.parse(hidePersistorJson)
 
19611                                 m_chp.allHiddenThings = m_chp.hidePersistorData['hiddenThings']
 
19612                                 m_chp.hiddenKeys = m_chp.hidePersistorData['hiddenKeys']
 
19615                                  * Prune allHiddenThings of old content so it doesn't get
 
19618                                 if (m_chp.hiddenKeys.length > m_chp.maxKeys) {
 
19619                                         var pruneStart = m_chp.maxKeys - m_chp.pruneKeysTo,
 
19620                                             newHiddenThings = {},
 
19621                                             newHiddenKeys = [];
 
19623                                         /* Recreate our object as a subset of the original */
 
19624                                         for (var i=pruneStart; i < m_chp.hiddenKeys.length; i++) {
 
19625                                                 var hiddenKey = m_chp.hiddenKeys[i];
 
19626                                                 newHiddenKeys.push(hiddenKey);
 
19627                                                 newHiddenThings[hiddenKey] = m_chp.allHiddenThings[hiddenKey];
 
19629                                         m_chp.allHiddenThings = newHiddenThings;
 
19630                                         m_chp.hiddenKeys = newHiddenKeys;
 
19631                                         m_chp.syncHiddenThings();
 
19634                                 if (typeof m_chp.allHiddenThings[m_chp.hiddenThingsKey] !== 'undefined') {
 
19635                                         m_chp.hiddenThings = m_chp.allHiddenThings[m_chp.hiddenThingsKey];
 
19641         addHiddenThing: function (thingId) {
 
19642                 var i = m_chp.hiddenThings.indexOf(thingId);
 
19644                         m_chp.hiddenThings.push(thingId);
 
19646                 m_chp.syncHiddenThings();
 
19648         removeHiddenThing: function (thingId) {
 
19649                 var i = m_chp.hiddenThings.indexOf(thingId);
 
19651                         m_chp.hiddenThings.splice(i, 1);
 
19653                 m_chp.syncHiddenThings();
 
19655         syncHiddenThings: function () {
 
19656                 var hidePersistorData;
 
19657                 m_chp.allHiddenThings[m_chp.hiddenThingsKey] = m_chp.hiddenThings;
 
19658                 hidePersistorData = {
 
19659                         'hiddenThings': m_chp.allHiddenThings,
 
19660                         'hiddenKeys': m_chp.hiddenKeys
 
19662                 RESStorage.setItem('RESmodules.commentHidePersistor.hidePersistor', JSON.stringify(hidePersistorData));
 
19664         hideHiddenThings: function () {
 
19665                 m_chp.loadHiddenThings();
 
19667                 for(var i=0, il=m_chp.hiddenThings.length; i < il; i++) {
 
19668                         var thingId = m_chp.hiddenThings[i],
 
19669                                 // $hideLink = $('div.id-' + thingId + ':first > div.entry div.noncollapsed a.expand');
 
19670                                 // changed how this is grabbed and clicked due to firefox not working properly with it.
 
19671                                 $hideLink = document.querySelector('div.id-' + thingId + ' > div.entry div.noncollapsed a.expand');
 
19674                                  * Zero-length timeout to defer this action until after the
 
19675                                  * other modules have finished. For some reason without
 
19676                                  * deferring the hide was conflicting with the
 
19677                                  * commentNavToggle width.
 
19679                                 (function ($hideLink) {
 
19680                                         window.setTimeout(function () {
 
19681                                                 // $hideLink.click();
 
19682                                                 RESUtils.click($hideLink);
 
19693 modules['bitcointip'] = {
 
19694         moduleID: 'bitcointip',
 
19695         moduleName: 'bitcointip',
 
19697         disabledByDefault: true,
 
19698         description: 'Send <a href="http://bitcoin.org/" target="_blank">' +
 
19699                 'bitcoin</a> to other redditors  via <a href="/r/bitcointip" ' +
 
19700                 'target="_blank">bitcointip</a>. <br><br>' +
 
19701                 'For more information, visit <a href="/r/bitcointip" ' +
 
19702                 'target="_blank">/r/bitcointip</a>  or <a href="/13iykn" ' +
 
19703                 'target="_blank">read the documentation</a>.',
 
19706                         name: 'Default Tip',
 
19709                         description: 'Default tip amount in the form of ' +
 
19710                                 '"[value] [units]", e.g. "0.01 BTC"'
 
19713                         name: 'Add "tip bitcoins" Button',
 
19716                         description: 'Attach "tip bitcoins" button to comments'
 
19719                         name: 'Hide Bot Verifications',
 
19722                         description: 'Hide bot verifications'
 
19725                         name: 'Tip Status Format',
 
19728                                 { name: 'detailed', value: 'detailed' },
 
19729                                 { name: 'basic', value: 'basic' },
 
19730                                 { name: 'none', value: 'none' }
 
19733                         description: 'Tip status - level of detail'
 
19736                         name: 'Preferred Currency',
 
19739                                 { name: 'BTC', value: 'BTC' },
 
19740                                 { name: 'USD', value: 'USD' },
 
19741                                 { name: 'JPY', value: 'JPY' },
 
19742                                 { name: 'GBP', value: 'GBP' },
 
19743                                 { name: 'EUR', value: 'EUR' }
 
19746                         description: 'Preferred currency units'
 
19749                         name: 'Display Balance',
 
19752                         description: 'Display balance'
 
19755                         name: 'Display Enabled Subreddits',
 
19758                         description: 'Display enabled subreddits'
 
19761                         name: 'Known User Addresses',
 
19763                         addRowText: '+add address',
 
19765                                 {name: 'user', type: 'text'},
 
19766                                 {name: 'address', type: 'text'}
 
19769                                 /* ['skeeto', '1...'] */
 
19771                         description: 'Mapping of usernames to bitcoin addresses'
 
19773                 fetchWalletAddress: {
 
19774                         text: 'Search private messages',
 
19775                         description: "Search private messages for bitcoin wallet associated with the current username." +
 
19776                         "<p>You must be logged in to search.</p>" +
 
19777                         "<p>After clicking the button, you must reload the page to see newly-found addresses.</p>",
 
19779                         callback: null // populated when module loads
 
19782         isEnabled: function() {
 
19783                 return RESConsole.getModulePrefs(this.moduleID);
 
19786                 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
 
19789                 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*\/user\/bitcointip\/?/i
 
19791         isMatchURL: function() {
 
19792                 return RESUtils.isMatchURL(this.moduleID);
 
19794         beforeLoad: function() {
 
19795                 this.options.fetchWalletAddress.callback = this.fetchAddressForCurrentUser.bind(this);
 
19796                 RESUtils.addCSS('.tip-bitcoins { cursor: pointer; }');
 
19797                 RESUtils.addCSS('.tips-enabled-icon { cursor: help; }');
 
19798                 RESUtils.addCSS('#tip-menu { display: none; position: absolute; top: 0; left: 0; }');
 
19799                 // fix weird z-indexing issue caused by reddit's default .dropdown class
 
19800                 RESUtils.addCSS('.tip-wrapper .dropdown { position: static; }');
 
19804                 if (!this.isEnabled() || !this.isMatchURL()) {
 
19808                 if (this.options.status.value === 'basic') {
 
19809                         this.icons.pending = this.icons.completed;
 
19810                         this.icons.reversed = this.icons.completed;
 
19813                 if (this.options.subreddit.value) {
 
19814                         this.attachSubredditIndicator();
 
19817                 if (this.options.balance.value) {
 
19818                         this.attachBalance();
 
19821                 if (RESUtils.currentSubreddit() === 'bitcointip') {
 
19822                         this.injectBotStatus();
 
19825                 if (RESUtils.pageType() === 'comments') {
 
19826                         if (this.options.attachButtons.value) {
 
19827                                 this.attachTipButtons();
 
19828                                 RESUtils.watchForElement('newComments', modules['bitcointip'].attachTipButtons.bind(this));
 
19829                                 this.attachTipMenu();
 
19832                         if (this.options.hide.value) {
 
19833                                 this.hideVerifications();
 
19834                                 RESUtils.watchForElement('newComments', modules['bitcointip'].hideVerifications.bind(this));
 
19838                         if (this.options.status.value !== 'none') {
 
19839                                 this.scanForTips();
 
19840                                 RESUtils.watchForElement('newComments', this.scanForTips.bind(this));
 
19845         save: function save() {
 
19846                 var json = JSON.stringify(this.options);
 
19847                 RESStorage.setItem('RESoptions.bitcoinTip', json);
 
19850         load: function load() {
 
19851                 var json = RESStorage.getItem('RESoptions.bitcoinTip');
 
19853                         this.options = JSON.parse(json);
 
19858         /** Specifies how to find tips. */
 
19859         tipregex: /\+((\/u\/)?bitcointip|bitcoin|tip|btctip|bittip|btc)/i,
 
19860         tipregexFun: /(\+((?!0)(\d{1,4})) (point|internet|upcoin))/i,
 
19862         /** How many milliseconds until the bot is considered down. */
 
19863         botDownThreshold: 15 * 60 * 1000,
 
19865         /** Bitcointip API endpoints. */
 
19867                 gettips: '//bitcointip.net/api/gettips.php',
 
19868                 gettipped: '//bitcointip.net/api/gettipped.php',
 
19869                 subreddits: '//bitcointip.net/api/subreddits.php',
 
19870                 balance: '//bitcointip.net/api/balance.php'
 
19873         /** Encoded tipping icons. */
 
19875                 completed: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAMAAABFNRROAAAAt1BMVEX///8AAAAAyAAAuwAAwQcAvAcAvwAAwQYAyAUAxAUAxwQAwgQAvAMAxQYAvwYAxQYAxwU5yT060j460j871T89wUE9wkFGokdGu0hIzExJl09JmE9JxExJxE1K1U9K1k5Ll09LmVNMmVNM2FBNmlRRx1NSzlRTqlVUslZU1ldVq1hVrFdV2FhWrFhX21pZqlphrWJh3WRotGtrqm1stW91sXd2t3h5t3urz6zA2sHA28HG3sf4+PhvgZhQAAAAEXRSTlMAARweJSYoLTM0O0dMU1dYbkVIv+oAAACKSURBVHjaVc7XEoIwEIXhFRED1tBUxBaPFSyxK3n/5zIBb/yv9pudnVky2Ywxm345MHkVXByllPm4W24qrLbzdo1sLPPRepc+XlnSIAuz9DQYPtXnkLhUF/ysrndV3CYLRpbg2VtpxFMwfRfEl8IghEPUhB9t9lEQoke6FnzONfpU5kEIoKOn/z+/pREPWTic38sAAAAASUVORK5CYII=",
 
19876                 cancelled: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAMAAABFNRROAAAAQlBMVEX///+qAAAAAAC/AADIABSaTU3YMDDcPj7cSEjeUFDiZGTld3fmfHzoiorqkJDqlpbupKTuqqr99fX99/f+/Pz////kWqLlAAAABXRSTlMAAwcIM6KYVMQAAABfSURBVHjaXc7JDsAgCEVRsYpIBzro//9qHyHpond3AgkklIuXPKJcqleIIEB6FwEhQEW0t4rlUtt+22ZTMQ09NqZyiK8BtBCvc9iDWegY526hBVRmdcQ9RgD9f/G+P1+JEwRF2vKhRgAAAABJRU5ErkJggg==",
 
19877                 tipped: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAANCAMAAACq939wAAAA/FBMVEWqVQCqcQCZZgCLXQCdYgDMjACOXACOXwCacQCfcQCqegCwggChggCnfwCwggCthACtgQC9iACleQC+iwDMlgDJlACmfgDDiwDBjADHlQDJnQDFlQDKmAChfRGjgBOmfhKmghKnghOqhBKthxOviROvjB+vjCGvjCOwiRSwihixixWxjSGziBOzkSmzky+0kSa0kiy3jxW8kxS8mCi8n0i8oEq9lBe9mSvOoBbUrTTUrTjbukXcsTDctDrdtj/exHbexXDfwWXfwmvjrBnksRjksx7lrhnqx17qyWTqymrq377rz3nr2qftuiHtvSv67cD67sj+997/+OX///8rcy1sAAAAHXRSTlMDCQoLDRQkKystMDc5Oj0+QUlKS0tMTVFSV15/i6wTI/gAAACWSURBVHjaHcrnAoFQGADQryI7sjNKN0RKZJVNQ8io938YN+f3ASDp1B9NAhD15UzXNH26KhNQXZyDBxZcNlloKadvFIbR56owUOFV9425ai8PrGwZaITQeisXoKHs7k/MP44ZaHYl54U5El8EdmjNEWbEjesf/Ljd9v0S1ETBtD3PNgUxB8nOQIybOGknAKgMy2FsmoIflIEZdK7PshkAAAAASUVORK5CYII=",
 
19878                 pending: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAABWUlEQVR4nI2Sy0sCURTGD6S2jjYFrdy0DNpEhokb8zFm5YyaO6NFYNGqF/0hPZYtR79FUbgw0BFtDKIgUCSpv8Od3XtGJzWDBj64h/l+954XdbtdGhQZkzNUd7ptifiXZygo0Wz0WsWoyHTMj4Wo6nRLQ7KdRuZz15bWSiF0GQOVXJ4hqP/COGDTjEO9SyByIcDHiUXiT+QsAaW1wabgi4KtVxVqM4lQVcFx4RS5tzy0vIgZFDVTnaYkFG6us2lbTyNws4ZAMYizwjk6nQ7KbQOJfMqCRBlERZpWruJYfvYigx02ZfUDHN2e8Pnpy8T+w6G4MIqI8HFH5Ut9SKZQ/jDYPAh4K36EGzGrkwz1avK8+/jn3n2WzaPASsNnQaJpvYG65ixwFV7Dj7iuQcul+Cwvs4Ga1fafOVUcC31Qpio1BJjO0PiNEJPn9osapeyNqLmW/lyj/+7eN1qRZT0kKLSqAAAAAElFTkSuQmCC",
 
19879                 reversed: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAABOklEQVR42p2SvU4CQRSFj+9h7DQ2VmsDCy0/Cw2wsNtuRwixIiQ8CZaWGDBaaAiJyVAg2xi1AaTQ19jyOHeSIbLBxuIkM7Pn23PvnQHJPSngNAYcK9mnPWng/LpaZVoadg9CC+BSDNsg4FcU7bRpNjkslaiAYAfZhL+AuFbjQ6PBYaXCZ6AK4Mj0IMBGH4rptVjkW73Ote9zUS5z2u/zfTzmRBL1XoNniIFjgdaeZ0yjMORNocCZ1nQwYJIk3CrFSatlIGkDM+BEouNMhndRZEyjTof3vZ5Zfy+XfOp2zQ+ND3BMkoWkhE+lxLwHzPN5rjzPTtLZ9fSRzZqPj+22MaeBlesaSIZmp3dhQZXLcaQHcev7sjZnFngBwr17mgNFC+pSRRawZV0dfBFy89ogDYvEbBMav33/ens/XHaDp7U/bFsAAAAASUVORK5CYII="
 
19882         /** Specifies how to display different currencies. */
 
19884                 USD: {unit: 'US$', precision: 2},
 
19885                 BTC: {unit: '฿'},
 
19887                 GBP: {unit: '£', precision: 2},
 
19888                 EUR: {unit: '€', precision: 2},
 
19889                 AUD: {unit: 'A$', precision: 2},
 
19890                 CAD: {unit: 'C$', precision: 2}
 
19893         /** Return a DOM element to separate items in the user bar. */
 
19894         separator: function() {
 
19895                 return $('<span>|</span>').addClass('separator');
 
19898         /** Convert a quantity into a string. */
 
19899         quantityString: function quantityString(object) {
 
19900                 var pref = this.options.currency.value.toUpperCase();
 
19901                 var unit = this.currencies[pref];
 
19902                 var amount = object['amount' + pref] || object['balance' + pref];
 
19903                 if (amount == null) {
 
19904                         amount = object['amountBTC'] || object['balanceBTC'];
 
19905                         unit = this.currencies['BTC'];
 
19907                 if (unit.precision) {
 
19908                         amount = parseFloat(amount).toFixed(unit.precision);
 
19910                 return unit.unit + amount;
 
19913         tipPublicly: function tipPublicly($target) {
 
19915                 if ($target.closest('.link').length > 0) { /* Post */
 
19916                         form = $('.commentarea .usertext:first');
 
19917                 } else { /* Comment */
 
19918                         var replyButton = $target.closest('ul').find('a[onclick*="reply"]');
 
19919                         RESUtils.click(replyButton[0]);
 
19920                         form = $target.closest('.thing').find('FORM.usertext.cloneable:first');
 
19922                 var textarea = form.find('textarea');
 
19923                 if (!textarea.val().match(this.tipregex)) {
 
19924                         textarea.val(textarea.val() + '\n\n+/u/bitcointip ' + this.options.baseTip.value);
 
19925                         RESUtils.setCursorPosition(textarea, 0);
 
19929         tipPrivately: function tipPrivately($target) {
 
19931                 if ($target.closest('.link').length > 0) { /* Post */
 
19932                         form = $('.commentarea .usertext:first');
 
19934                         form = $target.closest('.thing').find(".child .usertext:first");
 
19936                 if (form.length > 0 && form.find('textarea').val()) {
 
19937                         /* Confirm if a comment has been entered. */
 
19938                         if (!confirm('Really leave this page to tip privately?')) {
 
19942                 var user = $target.closest('.thing').find('.author:first').text();
 
19943                 var msg = encodeURIComponent('+/u/bitcointip @' + user + ' ' + this.options.baseTip.value);
 
19944                 var url = '/message/compose?to=bitcointip&subject=Tip&message=' + msg;
 
19945                 window.location = url;
 
19948         attachTipButtons: function attachTipButtons(ele) {
 
19949                 ele = ele || document.body;
 
19951                 if (!module.tipButton) {
 
19952                         module.tipButton = $(
 
19953                                 '<span class="tip-wrapper">' +
 
19954                                         '<div class="dropdown">' +
 
19955                                                 '<a class="tip-bitcoins login-required" title="Click to give a bitcoin tip">bitcointip</a>' +
 
19958                         module.tipButton.bind('click', function(e) {
 
19959                                 modules['bitcointip'].toggleTipMenu(e.target);
 
19964                 /* Add the "tip bitcoins" button after "give gold". */
 
19965                 var allGiveGoldLinks = ele.querySelectorAll('a.give-gold');
 
19966                 RESUtils.forEachChunked(allGiveGoldLinks, 15, 1000, function(giveGold, i, array) {
 
19967                         $(giveGold).parent().after($('<li/>')
 
19968                                 .append(modules['bitcointip'].tipButton.clone(true)));
 
19971                 if (!module.attachedPostTipButton) {
 
19972                         module.attachedPostTipButton = true; // signifies either "attached button" or "decided not to attach button"
 
19974                         if (!RESUtils.isCommentPermalinkPage() && $('.link').length === 1) {
 
19975                                 // Viewing full comments on a submission, so user can comment on post
 
19976                                 $('.link ul.buttons .share').after($('<li/>')
 
19977                                         .append(modules['bitcointip'].tipButton.clone(true)));
 
19983         attachTipMenu: function() {
 
19985                         $('<div id="tip-menu" class="drop-choices">' +
 
19986                                 '<a class="choice tip-publicly" href="javascript:void(0);">tip publicly</a>' +
 
19987                                 '<a class="choice tip-privately" href="javascript:void(0);">tip privately</a>' +
 
19990                 if (modules['settingsNavigation']) { // affordance for userscript mode
 
19991                         this.tipMenu.append(
 
19992                                 modules['settingsNavigation'].makeUrlHashLink(this.moduleID, null,
 
19993                                 '<img src="' + this.icons.tipped + '"> bitcointip', 'choice')
 
19996                 $(document.body).append(this.tipMenu);
 
19998                 this.tipMenu.find('a').click(function(event) {
 
19999                         modules['bitcointip'].toggleTipMenu();
 
20002                 this.tipMenu.find('.tip-publicly').click(function(event) {
 
20003                         event.preventDefault();
 
20004                         modules['bitcointip'].tipPublicly($(modules['bitcointip'].lastToggle));
 
20007                 this.tipMenu.find('.tip-privately').click(function(event) {
 
20008                         event.preventDefault();
 
20009                         modules['bitcointip'].tipPrivately($(modules['bitcointip'].lastToggle));
 
20014         toggleTipMenu: function(ele) {
 
20015                 var tipMenu = modules['bitcointip'].tipMenu;
 
20017                 if (!ele || ele.length === 0) {
 
20022                 var thisXY = $(ele).offset();
 
20023                 var thisHeight = $(ele).height();
 
20024                 // if already visible and we've clicked a different trigger, hide first, then show after the move.
 
20025                 if ((tipMenu.is(':visible')) && (modules['bitcointip'].lastToggle !== ele)) {
 
20029                         top: (thisXY.top+thisHeight)+'px',
 
20030                         left: thisXY.left+'px'
 
20033                 modules['bitcointip'].lastToggle = ele;
 
20036         attachSubredditIndicator: function() {
 
20037                 var subreddit = RESUtils.currentSubreddit();
 
20038                 if (subreddit && this.getAddress(RESUtils.loggedInUser())) {
 
20039                         $.getJSON(this.api.subreddits, function(data) {
 
20040                                 if (data.subreddits.indexOf(subreddit.toLowerCase()) !== -1) {
 
20041                                         $('#header-bottom-right form.logout')
 
20042                                                 .before(this.separator()).prev()
 
20043                                                 .before($('<img/>').attr({
 
20044                                                         'src': this.icons.tipped,
 
20045                                                         'class': 'tips-enabled-icon',
 
20046                                                         'style': 'vertical-align: text-bottom;',
 
20047                                                         'title': 'Tips enabled in this subreddit.'
 
20054         hideVerifications: function hideVerifications(ele) {
 
20055                 ele = ele || document.body;
 
20057                 /* t2_7vw3n is u/bitcointip. */
 
20059                 var botComments = $(ele).find('a.id-t2_7vw3n').closest('.comment');
 
20060                 RESUtils.forEachChunked(botComments, 15, 1000, function(botComment, i, array) {
 
20061                         var $this = $(botComment);
 
20062                         var isTarget = $this.find('form:first').hasClass('border');
 
20063                         if (isTarget) return;
 
20065                         var hasReplies = $this.find('.comment').length > 0;
 
20066                         if (hasReplies) return;
 
20068                         $this.find('.expand').eq(2).click();
 
20072         toggleCurrency: function() {
 
20073                 var units = Object.keys(this.currencies);
 
20074                 var i = (units.indexOf(this.options.currency.value) + 1) % units.length;
 
20075                 this.options.currency.value = units[i];
 
20079         getAddress: function getAddress(user) {
 
20080                 user = user || RESUtils.loggedInUser();
 
20081                 var address = null;
 
20082                 this.options.address.value.forEach(function(row) {
 
20083                         if (row[0] === user) address = row[1];
 
20088         setAddress: function setAddress(user, address) {
 
20089                 user = user || RESUtils.loggedInUser();
 
20091                 this.options.address.value.forEach(function(row) {
 
20092                         if (row[0] === user) {
 
20097                 if (user && !set) {
 
20098                         this.options.address.value.push([user, address]);
 
20104         attachBalance: function attachBalance() {
 
20105                 var user = RESUtils.loggedInUser();
 
20106                 var address = this.getAddress(user);
 
20107                 if (!address) return;
 
20108                 var bitcointip = this;
 
20110                 $.getJSON(this.api.balance, {
 
20113                 }, function (balance) {
 
20114                         if (!('balanceBTC' in balance)) {
 
20115                                 return; /* Probably have the address wrong! */
 
20117                         $('#header-bottom-right form.logout')
 
20118                                 .before(bitcointip.separator()).prev()
 
20119                                 .before($('<a/>').attr({
 
20122                                 }).click(function() {
 
20123                                         bitcointip.toggleCurrency();
 
20124                                         $(this).text(bitcointip.quantityString(balance));
 
20125                                 }).text(bitcointip.quantityString(balance)));
 
20129         fetchAddressForCurrentUser: function () {
 
20130                 var user = RESUtils.loggedInUser();
 
20132                         RESUtils.notification({
 
20133                                 moduleID: 'bitcointip',
 
20134                                 optionKey: 'fetchWalletAddress',
 
20136                                 message: 'Log in, then try again.'
 
20140                 this.fetchAddress(user, function(address) {
 
20142                                 modules['bitcointip'].setAddress(user, address);
 
20143                                 RESUtils.notification({
 
20144                                                 moduleID: 'bitcointip',
 
20145                                                 optionKey: 'address',
 
20146                                                 message: 'Found address ' + address + ' for user ' + user +
 
20147                                                         '<br><br>Your adress will appear in RES settings after you refresh the page.'
 
20150                                 RESUtils.notification({
 
20151                                                 moduleID: 'bitcointip',
 
20153                                                 message: 'Could not find address for user ' + user
 
20158                 RESUtils.notification({
 
20159                                 moduleID: 'bitcointip',
 
20160                                 optionKey: 'fetchWalletAddress',
 
20161                                 message: 'Searching your private messages for a bitcoin wallet address. ' +
 
20162                                                 '<br><br>Reload the page to see if a wallet was found.'
 
20166         fetchAddress: function fetchAddress(user, callback) {
 
20167                 user = user || RESUtils.loggedInUser();
 
20168                 callback = callback || function nop() {};
 
20170                 $.getJSON('/message/messages.json', function(messages) {
 
20171                         /* Search messages for a bitcointip response. */
 
20172                         var address = messages.data.children.filter(function (message) {
 
20173                                 return message.data.author === 'bitcointip';
 
20174                         }).map(function (message) {
 
20175                                 var pattern = /Deposit Address: \| \[\*\*([a-zA-Z0-9]+)\*\*\]/;
 
20176                                 var address = message.data.body.match(pattern);
 
20182                         }).filter(function(x) { return x; })[0]; // Use the most recent
 
20184                                 this.setAddress(user, address);
 
20192         scanForTips: function(ele) {
 
20193                 ele = ele || document.body;
 
20194                 var tips = this.getTips(this.tipregex, ele);
 
20195                 var fun = this.getTips(this.tipregexFun, ele);
 
20196                 var all = $.extend({}, tips, fun);
 
20197                 if (Object.keys(all).length > 0) {
 
20198                         this.attachTipStatuses(all);
 
20199                         this.attachReceiverStatus(this.getTips(/(?:)/, ele));
 
20203         /** Return true if the comment node matches the regex. */
 
20204         commentMatches: function(regex, $e) {
 
20205                 return $e.find('.md:first, .title:first').children().is(function() {
 
20206                         return regex.test($(this).text());
 
20210         /** Find all things matching a regex. */
 
20211         getTips: function getComments(regex, ele) {
 
20213                 var items = $(ele);
 
20214                 if (items.is('.entry')) {
 
20215                         items = items.closest('div.comment, div.self, div.link');
 
20217                         items = items.find('div.comment, div.self, div.link');
 
20220                 items.each(function() {
 
20221                         var $this = $(this);
 
20222                         if (module.commentMatches(regex, $this)) {
 
20223                                 var id = $this.attr('data-fullname');
 
20224                                 tips[id.replace(/^t._/, '')] = $this;
 
20230         attachTipStatuses: function attachTipStatuses(tips) {
 
20231                 var iconStyle = 'vertical-align: text-bottom; margin-left: 8px;';
 
20232                 var icons = this.icons;
 
20233                 var tipIDs = Object.keys(tips);
 
20234                 $.getJSON(this.api.gettips, {
 
20235                         tips: tipIDs.toString()
 
20236                 }, function(response) {
 
20237                         var lastEvaluated = new Date(response.last_evaluated * 1000);
 
20238                         response.tips.forEach(function (tip) {
 
20239                                 var id = tip.fullname.replace(/^t._/, '');
 
20240                                 var tagline = tips[id].find('.tagline').first();
 
20241                                 var icon = $('<a/>').attr({href: tip.tx, target: '_blank'});
 
20242                                 tagline.append(icon.append($('<img/>').attr({
 
20243                                         src: icons[tip.status],
 
20245                                         title: this.quantityString(tip) + ' → ' + tip.receiver +
 
20246                                                 ' (' + tip.status + ')'
 
20248                                 tips[id].attr('id', 't1_' + id); // for later linking
 
20252                         /* Deal with unanswered tips. */
 
20253                         for (var id in tips) {
 
20254                                 if (this.commentMatches(this.tipregexFun, tips[id])) {
 
20255                                         continue; // probably wasn't actually a tip
 
20257                                 var date = tips[id].find('.tagline time:first')
 
20259                                 if (new Date(date) < lastEvaluated) {
 
20260                                         var tagline = tips[id].find('.tagline:first');
 
20261                                         tagline.append($('<img/>').attr({
 
20262                                                 src: icons.cancelled,
 
20264                                                 title: 'This tip is invalid.'
 
20271         attachReceiverStatus: function attachReceiverStatus(things) {
 
20272                 var iconStyle = 'vertical-align: text-bottom; margin-left: 8px;';
 
20273                 var icons = this.icons;
 
20274                 var thingIDs = Object.keys(things);
 
20275                 $.getJSON(this.api.gettipped, {
 
20276                         tipped: thingIDs.toString()
 
20277                 }, function(response) {
 
20278                         response.forEach(function (tipped) {
 
20279                                 var id = tipped.fullname.replace(/^t._/, '');
 
20280                                 var thing = things[id];
 
20281                                 var tagline = thing.find('.tagline').first();
 
20282                                 var plural = tipped.tipQTY > 1;
 
20283                                 var title = this.quantityString(tipped) + ' to ' +
 
20284                                                 thing.find('.author:first').text() + ' for this ';
 
20286                                         title = 'redditors have given ' + title;
 
20288                                         title = 'a redditor has given ' + title;
 
20290                                 if (thing.closest('.link').length === 0) {
 
20291                                         title += 'comment.';
 
20293                                         title += 'submission.';
 
20295                                 var icon = $('<img/>').attr({
 
20300                                 tagline.append(icon);
 
20302                                         tagline.append($('<span/>').text('x' + tipped.tipQTY));
 
20308         injectBotStatus: function injectBotStatus() {
 
20309                 $.getJSON(this.api.gettips, function(response) {
 
20310                         var lastEvaluated = new Date(response.last_evaluated * 1000);
 
20311                         var botStatus = null;
 
20312                         if (Date.now() - lastEvaluated > this.botDownThreshold) {
 
20313                                 botStatus = '<span class="status-down">DOWN</span>';
 
20315                                 botStatus = '<span class="status-up">UP</span>';
 
20317                         $('.side a[href="http://bitcointip.net/status.php"]').html(botStatus);
 
20322 modules['troubleShooter'] = {
 
20323         moduleID: 'troubleShooter',
 
20324         moduleName: 'Troubleshooter',
 
20325         category: 'Troubleshoot',
 
20327                 clearUserInfoCache: {
 
20331                         description: 'Reset the <code>userInfo</code> cache for the currently logged in user. Useful for when link/comment karma appears to have frozen.'
 
20337                         description: 'Reset the \'My Subreddits\' dropdown contents in the event of old/duplicate/missing entries.'
 
20343                         description: 'Remove all entries for users with +1 or -1 vote tallies (only non-tagged users).'
 
20349                         description: 'Warning: This will remove all your RES settings, including tags, saved comments, filters etc!'
 
20352         description: 'Resolve common problems and clean/clear unwanted settings data.' + '<br/><br/>' +
 
20353                                         'Your first line of defence against browser crashes/updates, or potential issues' +
 
20354                                         ' with RES, is a frequent backup.' + '<br/><br/>' +
 
20355                                         'See <a href="http://www.reddit.com/r/Enhancement/wiki/where_is_res_data_stored">here</a>' +
 
20356                                         ' for the location of the RES settings file for your browser/OS.',
 
20357         isEnabled: function () {
 
20358                 return RESConsole.getModulePrefs(this.moduleID);
 
20361                 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
 
20363         isMatchURL: function () {
 
20364                 return RESUtils.isMatchURL(this.moduleID);
 
20366         beforeLoad: function () {
 
20368                 css += 'body:not(.loggedin) #clearUserInfoCache ~ .optionDescription:before, body:not(.loggedin) #clearSubreddits ~ .optionDescription:before';
 
20369                 css += '{content: "Functionality only for logged in users - ";color:#f00;font-weight:bold}';
 
20370                 RESUtils.addCSS(css);
 
20373                 this.options['clearUserInfoCache'].callback = modules['troubleShooter'].clearUICache;
 
20374                 this.options['clearSubreddits'].callback = modules['troubleShooter'].clearSubreddits;
 
20375                 this.options['clearTags'].callback = modules['troubleShooter'].clearTags;
 
20376                 this.options['resetToFactory'].callback = modules['troubleShooter'].resetToFactory;
 
20378         clearUICache: function () {
 
20379                 var user = RESUtils.loggedInUser();
 
20381                         RESStorage.removeItem('RESUtils.userInfoCache.' + user);
 
20382                         RESUtils.notification('Cached info for ' + user + ' was reset.', 2500);
 
20384                         RESUtils.notification('You must be logged in to perform this task.', 2500);
 
20387         clearSubreddits: function () {
 
20388                 var user = RESUtils.loggedInUser();
 
20390                         RESStorage.removeItem('RESmodules.subredditManager.subreddits.' + user);
 
20391                         RESUtils.notification('Subreddits for ' + user + ' were reset.', 2500);
 
20393                         RESUtils.notification('You must be logged in to perform this task.', 2500);
 
20396         clearTags: function () {
 
20397                 var confirm = window.confirm('Are you positive?');
 
20401                             tags = RESStorage.getItem('RESmodules.userTagger.tags');
 
20403                                 tags = JSON.parse(tags);
 
20405                                         if ((tags[i].votes === 1 || tags[i].votes === -1) && !tags[i].hasOwnProperty('tag')) {
 
20410                                 tags = JSON.stringify(tags);
 
20411                                 RESStorage.setItem('RESmodules.userTagger.tags', tags);
 
20412                                 RESUtils.notification(cnt + ' entries removed.', 2500);
 
20415                         RESUtils.notification('No action was taken', 2500);
 
20418         resetToFactory: function () {
 
20419                 var confirm = window.confirm('This will kill all your settings and saved data. Are you sure?');
 
20421                         for (var key in RESStorage) {
 
20422                                 if (key.indexOf('RES') !== -1) {
 
20423                                         RESStorage.removeItem(key);
 
20426                         RESUtils.notification('All settings reset.', 2500);
 
20428                         RESUtils.notification('No action was taken', 2500);
 
20438         * :: Now with support for touch events and multiple instances for 
 
20439         * :: those situations that call for multiple easter eggs!
 
20440         * Code: http://konami-js.googlecode.com/
 
20441         * Examples: http://www.snaptortoise.com/konami-js
 
20442         * Copyright (c) 2009 George Mandis (georgemandis.com, snaptortoise.com)
 
20443         * Version: 1.3.2 (7/02/2010)
 
20444         * Licensed under the GNU General Public License v3
 
20445         * http://www.gnu.org/copyleft/gpl.html
 
20446         * Tested in: Safari 4+, Google Chrome 4+, Firefox 3+, IE7+ and Mobile Safari 2.2.1
 
20448 var Konami = function () {
 
20450                 addEvent: function (obj, type, fn, ref_obj) {
 
20451                         if (obj.addEventListener)
 
20452                                 obj.addEventListener(type, fn, false);
 
20453                         else if (obj.attachEvent) {
 
20455                                 obj["e" + type + fn] = fn;
 
20456                                 obj[type + fn] = function () {
 
20457                                         obj["e" + type + fn](window.event, ref_obj);
 
20460                                 obj.attachEvent("on" + type, obj[type + fn]);
 
20464                 prepattern: "38384040373937396665",
 
20465                 almostThere: false,
 
20466                 pattern: "3838404037393739666513",
 
20467                 load: function (link) {
 
20468                         this.addEvent(document, "keydown", function (e, ref_obj) {
 
20469                                 if (ref_obj) konami = ref_obj; // IE
 
20470                                 konami.input += e ? e.keyCode : event.keyCode;
 
20471                                 if (konami.input.length > konami.pattern.length) konami.input = konami.input.substr((konami.input.length - konami.pattern.length));
 
20472                                 if (konami.input == konami.pattern) {
 
20476                                 } else if ((konami.input == konami.prepattern) || (konami.input.substr(2, konami.input.length) == konami.prepattern)) {
 
20477                                         konami.almostThere = true;
 
20478                                         setTimeout(function () {
 
20479                                                 konami.almostThere = false;
 
20483                         this.iphone.load(link);
 
20485                 code: function (link) {
 
20486                         window.location = link;
 
20496                         keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP", "TAP"],
 
20497                         code: function (link) {
 
20500                         load: function (link) {
 
20501                                 this.orig_keys = this.keys;
 
20502                                 konami.addEvent(document, "touchmove", function (e) {
 
20503                                         if (e.touches.length === 1 && konami.iphone.capture == true) {
 
20504                                                 var touch = e.touches[0];
 
20505                                                 konami.iphone.stop_x = touch.pageX;
 
20506                                                 konami.iphone.stop_y = touch.pageY;
 
20507                                                 konami.iphone.tap = false;
 
20508                                                 konami.iphone.capture = false;
 
20509                                                 konami.iphone.check_direction();
 
20512                                 konami.addEvent(document, "touchend", function (evt) {
 
20513                                         if (konami.iphone.tap == true) konami.iphone.check_direction(link);
 
20515                                 konami.addEvent(document, "touchstart", function (evt) {
 
20516                                         konami.iphone.start_x = evt.changedTouches[0].pageX;
 
20517                                         konami.iphone.start_y = evt.changedTouches[0].pageY;
 
20518                                         konami.iphone.tap = true;
 
20519                                         konami.iphone.capture = true;
 
20522                         check_direction: function (link) {
 
20523                                 x_magnitude = Math.abs(this.start_x - this.stop_x);
 
20524                                 y_magnitude = Math.abs(this.start_y - this.stop_y);
 
20525                                 x = ((this.start_x - this.stop_x) < 0) ? "RIGHT" : "LEFT";
 
20526                                 y = ((this.start_y - this.stop_y) < 0) ? "DOWN" : "UP";
 
20527                                 result = (x_magnitude > y_magnitude) ? x : y;
 
20528                                 result = (this.tap == true) ? "TAP" : result;
 
20530                                 if (result == this.keys[0]) this.keys = this.keys.slice(1, this.keys.length);
 
20531                                 if (this.keys.length === 0) {
 
20532                                         this.keys = this.orig_keys;
 
20541 var _beforeLoadComplete = false;
 
20542 function RESdoBeforeLoad() {
 
20543         if (_beforeLoadComplete) return;
 
20544         _beforeLoadComplete = true;
 
20545         // if (beforeLoadDoneOnce) return;
 
20546         // first, go through each module and set all of the options so that if a module needs to check another module's options, they're ready...
 
20547         // console.log('get options start: ' + Date());
 
20548         for (var thisModuleID in modules) {
 
20549                 if (typeof modules[thisModuleID] === 'object') {
 
20550                         RESUtils.getOptions(thisModuleID);
 
20553         // console.log('get options end: ' + Date());
 
20554         for (var thisModuleID in modules) {
 
20555                 if (typeof modules[thisModuleID] === 'object') {
 
20556                         if (typeof modules[thisModuleID].beforeLoad === 'function') modules[thisModuleID].beforeLoad();
 
20560         GM_addStyle(RESUtils.css);
 
20561         // clear out css cache...
 
20565 function RESInit() {
 
20566         // $.browser shim since jQuery removed it
 
20568                 safari: BrowserDetect.isSafari(),
 
20569                 mozilla: BrowserDetect.isFirefox(),
 
20570                 chrome: BrowserDetect.isChrome(),
 
20571                 opera: BrowserDetect.isOpera()
 
20573         $.fn.safeHtml = function(string) {
 
20574                 if (!string) return '';
 
20575                 else return $(this).html(RESUtils.sanitizeHTML(string));
 
20578         RESUtils.initObservers();
 
20579         localStorageFail = false;
 
20582         $.extend(backup, RESStorage);
 
20583         delete backup.getItem;
 
20584         delete backup.setItem;
 
20585         delete backup.removeItem;
 
20586         console.log(backup);
 
20589         // Check for localStorage functionality...
 
20591                 localStorage.setItem('RES.localStorageTest','test');
 
20592                 // if this is a firefox addon, check for the old lsTest to see if they used to use the Greasemonkey script...
 
20593                 // if so, present them with a notification explaining that they should download a new script so they can
 
20594                 // copy their old settings...
 
20595                 if (BrowserDetect.isFirefox()) {
 
20596                         if ((localStorage.getItem('RES.lsTest') === 'test') && (localStorage.getItem('copyComplete') !== 'true')) {
 
20597                                 RESUtils.notification('<h2>Important Alert for Greasemonkey Users!</h2>Hey! It looks like you have upgraded to RES 4.0, but used to use the Greasemonkey version of RES. You\'re going to see double until you uninstall the Greasemonkey script. However, you should first copy your settings by clicking the blue button. <b>After installing, refresh this page!</b> <a target="_blank" class="RESNotificationButtonBlue" href="http://redditenhancementsuite.com/gmutil/reddit_enhancement_suite.user.js">GM->FF Import Tool</a>', 15000);
 
20598                                 localStorage.removeItem('RES.lsTest');
 
20600                                 // this is the only "old school" DOMNodeInserted event left... note to readers of this source code:
 
20601                                 // it will ONLY ever be added to the DOM in the specific instance of former OLD RES users from Greasemonkey
 
20602                                 // who haven't yet had the chance to copy their settings to the XPI version of RES.  Once they've completed
 
20603                                 // that, this eventlistener will never be added again, nor will it be added for those who are not in this
 
20604                                 // odd/small subset of people.
 
20605                                 document.body.addEventListener('DOMNodeInserted', function(event) {
 
20606                                         if ((event.target.tagName === 'DIV') && (event.target.getAttribute('id') && event.target.getAttribute('id').indexOf('copyToSimpleStorage') !== -1)) {
 
20613                 localStorageFail = true;
 
20616         document.body.classList.add('res','res-v430');
 
20618         if (localStorageFail) {
 
20619                 RESFail = "Sorry, but localStorage seems inaccessible. Reddit Enhancement Suite can't work without it. \n\n";
 
20620                 if (BrowserDetect.isSafari()) {
 
20621                         RESFail += 'Since you\'re using Safari, it might be that you\'re in private browsing mode, which unfortunately is incompatible with RES until Safari provides a way to allow extensions localStorage access.';
 
20622                 } else if (BrowserDetect.isChrome()) {
 
20623                         RESFail += 'Since you\'re using Chrome, you might just need to go to your extensions settings and check the "Allow in Incognito" box.';
 
20624                 } else if (BrowserDetect.isOpera()) {
 
20625                         RESFail += 'Since you\'re using Opera, you might just need to go to your extensions settings and click the gear icon, then click "privacy" and check the box that says "allow interaction with private tabs".';
 
20627                         RESFail += 'Since it looks like you\'re using Firefox, you probably need to go to about:config and ensure that dom.storage.enabled is set to true, and that dom.storage.default_quota is set to a number above zero (i.e. 5120, the normal default)".';
 
20629                 var userMenu = document.querySelector('#header-bottom-right');
 
20631                         var preferencesUL = userMenu.querySelector('UL');
 
20632                         var separator = document.createElement('span');
 
20633                         separator.setAttribute('class','separator');
 
20634                         separator.textContent = '|';
 
20635                         RESPrefsLink = document.createElement('a');
 
20636                         RESPrefsLink.setAttribute('href','javascript:void(0)');
 
20637                         RESPrefsLink.addEventListener('click', function(e) {
 
20638                                 e.preventDefault();
 
20641                         RESPrefsLink.textContent = '[RES - ERROR]';
 
20642                         RESPrefsLink.setAttribute('style','color: red; font-weight: bold;');
 
20643                         insertAfter(preferencesUL, RESPrefsLink);
 
20644                         insertAfter(preferencesUL, separator);
 
20647                 document.body.addEventListener('mousemove', RESUtils.setMouseXY, false);
 
20648                 // added this if statement because some people's Greasemonkey "include" lines are getting borked or ignored, so they're calling RES on non-reddit pages.
 
20649                 if (location.href.match(/^https?:\/\/([\w]+\.)?reddit\.com/i)) {
 
20650                         RESUtils.firstRun();
 
20651                         RESUtils.checkForUpdate();
 
20652                         // add the config console link...
 
20653                         RESConsole.create();
 
20654                         RESConsole.addConsoleLink();
 
20655                         RESConsole.addConsoleDropdown();
 
20656                         RESUtils.checkIfSubmitting();
 
20657                         // go through each module and run it
 
20658                         for (var thisModuleID in modules) {
 
20659                                 if (typeof modules[thisModuleID] === 'object') {
 
20660                                           // console.log(thisModuleID + ' start: ' + Date());
 
20661                                           // perfTest(thisModuleID+' start');
 
20662                                         modules[thisModuleID].go();
 
20663                                           // perfTest(thisModuleID+' end');
 
20664                                           // console.log(thisModuleID + ' end: ' + Date());
 
20667                         GM_addStyle(RESUtils.css);
 
20668                 //      console.log('end: ' + Date());
 
20670                 if ((location.href.match(/reddit\.honestbleeps\.com\/download/)) || (location.href.match(/redditenhancementsuite\.com\/download/))) {
 
20671                         var installLinks = document.body.querySelectorAll('.install');
 
20672                         for (var i=0, len=installLinks.length;i<len;i++) {
 
20673                                 installLinks[i].classList.add('update');
 
20674                                 installLinks[i].classList.add('res4'); // if update but not RES 4, then FF users == greasemonkey...
 
20675                                 installLinks[i].classList.remove('install');
 
20678                 konami = new Konami();
 
20679                 konami.code = function() {
 
20680                         var baconBit = createElementWithID('div','baconBit');
 
20681                         document.body.appendChild(baconBit);
 
20682                         RESUtils.notification({header: 'RES Easter Eggies!', message: 'Mmm, bacon!'});
 
20683                         setTimeout(function() {
 
20684                                 baconBit.classList.add('makeitrain');
 
20691         RESUtils.postLoad = true;
 
20695 function setUpRESStorage (response) {
 
20696         if (BrowserDetect.isChrome()) {
 
20697                 RESStorage = response;
 
20698                 // we'll set up a method for getItem, but it's not adviseable to use since it's asynchronous...
 
20699                 RESStorage.getItem = function(key) {
 
20700                         if (typeof RESStorage[key] !== 'undefined') return RESStorage[key];
 
20703                 // if the fromBG parameter is true, we've been informed by another tab that this item has updated. We should update the data locally, but not send a background request.
 
20704                 RESStorage.setItem = function(key, value, fromBG) {
 
20705                         //Protect from excessive disk I/O...
 
20706                         if (RESStorage[key] != value) {
 
20707                                 // save it locally in the RESStorage variable, but also write it to the extension's localStorage...
 
20708                                 // It's OK that saving it is asynchronous since we're saving it in this local variable, too...
 
20709                                 RESStorage[key] = value;
 
20711                                         requestType: 'localStorage',
 
20712                                         operation: 'setItem',
 
20717                                         chrome.extension.sendMessage(thisJSON);
 
20721                 RESStorage.removeItem = function(key) {
 
20722                         // delete it locally in the RESStorage variable, but also delete it from the extension's localStorage...
 
20723                         // It's OK that deleting it is asynchronous since we're deleting it in this local variable, too...
 
20724                         delete RESStorage[key];
 
20726                                 requestType: 'localStorage',
 
20727                                 operation: 'removeItem',
 
20730                         chrome.extension.sendMessage(thisJSON);
 
20732                 window.localStorage = RESStorage;
 
20734         } else if (BrowserDetect.isSafari()) {
 
20735                 RESStorage = response;
 
20736                 RESStorage.getItem = function(key) {
 
20737                         if (typeof RESStorage[key] !== 'undefined') return RESStorage[key];
 
20740                 RESStorage.setItem = function(key, value, fromBG) {
 
20741                         //Protect from excessive disk I/O...
 
20742                         if (RESStorage[key] != value) {
 
20743                                 // save it locally in the RESStorage variable, but also write it to the extension's localStorage...
 
20744                                 // It's OK that saving it is asynchronous since we're saving it in this local variable, too...
 
20745                                 RESStorage[key] = value;
 
20747                                         requestType: 'localStorage',
 
20748                                         operation: 'setItem',
 
20753                                         safari.self.tab.dispatchMessage("localStorage", thisJSON);
 
20757                 RESStorage.removeItem = function(key) {
 
20758                         // delete it locally in the RESStorage variable, but also delete it from the extension's localStorage...
 
20759                         // It's OK that deleting it is asynchronous since we're deleting it in this local variable, too...
 
20760                         delete RESStorage[key];
 
20762                                 requestType: 'localStorage',
 
20763                                 operation: 'removeItem',
 
20766                         safari.self.tab.dispatchMessage("localStorage", thisJSON);
 
20768                 window.localStorage = RESStorage;
 
20769         } else if (BrowserDetect.isOpera()) {
 
20770                 RESStorage = response;
 
20771                 RESStorage.getItem = function(key) {
 
20772                         if (typeof RESStorage[key] !== 'undefined') return RESStorage[key];
 
20775                 RESStorage.setItem = function(key, value, fromBG) {
 
20776                         //Protect from excessive disk I/O...
 
20777                         if (RESStorage[key] != value) {
 
20778                                 // save it locally in the RESStorage variable, but also write it to the extension's localStorage...
 
20779                                 // It's OK that saving it is asynchronous since we're saving it in this local variable, too...
 
20780                                 RESStorage[key] = value;
 
20782                                         requestType: 'localStorage',
 
20783                                         operation: 'setItem',
 
20788                                         opera.extension.postMessage(JSON.stringify(thisJSON));
 
20792                 RESStorage.removeItem = function(key) {
 
20793                         // delete it locally in the RESStorage variable, but also delete it from the extension's localStorage...
 
20794                         // It's OK that deleting it is asynchronous since we're deleting it in this local variable, too...
 
20795                         delete RESStorage[key];
 
20797                                 requestType: 'localStorage',
 
20798                                 operation: 'removeItem',
 
20801                         opera.extension.postMessage(JSON.stringify(thisJSON));
 
20803                 window.localStorage = RESStorage;
 
20804         } else if (BrowserDetect.isFirefox()) {
 
20805                 RESStorage = response;
 
20806                 RESStorage.getItem = function(key) {
 
20807                         if (typeof RESStorage[key] !== 'undefined') return RESStorage[key];
 
20810                 RESStorage.setItem = function(key, value, fromBG) {
 
20811                         // save it locally in the RESStorage variable, but also write it to the extension's localStorage...
 
20812                         // It's OK that saving it is asynchronous since we're saving it in this local variable, too...
 
20813                         if (RESStorage[key] != value) {
 
20814                                 RESStorage[key] = value;
 
20816                                         requestType: 'localStorage',
 
20817                                         operation: 'setItem',
 
20822                                         self.postMessage(thisJSON);
 
20826                 RESStorage.removeItem = function(key) {
 
20827                         // delete it locally in the RESStorage variable, but also delete it from the extension's localStorage...
 
20828                         // It's OK that deleting it is asynchronous since we're deleting it in this local variable, too...
 
20829                         delete RESStorage[key];
 
20831                                 requestType: 'localStorage',
 
20832                                 operation: 'removeItem',
 
20835                         self.postMessage(thisJSON);
 
20837                 window.localStorage = RESStorage;
 
20839                 // must be firefox w/greasemonkey...
 
20841                 RESStorage.getItem = function(key) {
 
20842                         if (typeof RESStorage[key] !== 'undefined') return RESStorage[key];
 
20843                         RESStorage[key] = GM_getValue(key);
 
20844                         if (typeof RESStorage[key] === 'undefined') return null;
 
20845                         return GM_getValue(key);
 
20847                 RESStorage.setItem = function(key, value) {
 
20848                         // save it locally in the RESStorage variable, but also write it to the extension's localStorage...
 
20849                         // It's OK that saving it is asynchronous since we're saving it in this local variable, too...
 
20850                         // Wow, GM_setValue doesn't support big integers, so we have to store anything > 2147483647 as a string, so dumb.
 
20851                         if (typeof value !== 'undefined') {
 
20852                                 // if ((typeof value === 'number') && (value > 2147483647)) {
 
20853                                 if (typeof value === 'number') {
 
20854                                         value = value.toString();
 
20856                         //Protect from excessive disk I/O...
 
20857                         if (RESStorage[key] != value) {
 
20858                                         RESStorage[key] = value;
 
20859                                         // because we may want to use jQuery events to call GM_setValue and GM_getValue, we must use this ugly setTimeout hack.
 
20860                                         setTimeout(function() {
 
20861                                                 GM_setValue(key, value);
 
20867                 RESStorage.removeItem = function(key) {
 
20868                         // delete it locally in the RESStorage variable, but also delete it from the extension's localStorage...
 
20869                         // It's OK that deleting it is asynchronous since we're deleting it in this local variable, too...
 
20870                         delete RESStorage[key];
 
20871                         GM_deleteValue(key);
 
20879         // Don't fire the script on the iframe. This annoyingly fires this whole thing twice. Yuck.
 
20880         // Also don't fire it on static.reddit or thumbs.reddit, as those are just images.
 
20881         // Also omit blog and code.reddit
 
20882         if ((typeof RESRunOnce !== 'undefined') || (location.href.match(/\/toolbar\/toolbar\?id/i)) || (location.href.match(/comscore-iframe/i)) || (location.href.match(/static\.reddit/i)) || (location.href.match(/thumbs\.reddit/i)) || (location.href.match(/blog\.reddit/i)) || (location.href.match(/code\.reddit/i)) || (location.href.match(/metareddit\.com/i))) {
 
20887         // call preInit function - work in this function should be kept minimal.  It's for
 
20888         // doing stuff as early as possible prior to pageload, and even prior to the localStorage copy
 
20889         // from the background.
 
20890         // Specifically, this is used to add a class to the document for .res-nightmode, etc, as early
 
20891         // as possible to avoid the flash of unstyled content.
 
20892         RESUtils.preInit();
 
20895         if (BrowserDetect.isChrome()) {
 
20896                 // we've got chrome, get a copy of the background page's localStorage first, so don't init until after.
 
20898                         requestType: 'getLocalStorage'
 
20900                 chrome.extension.sendMessage(thisJSON, function(response) {
 
20901                         // Does RESStorage have actual data in it?  If it doesn't, they're a legacy user, we need to copy 
 
20902                         // old school localStorage from the foreground page to the background page to keep their settings...
 
20903                         if (typeof response.importedFromForeground === 'undefined') {
 
20904                                 // it doesn't exist.. copy it over...
 
20906                                         requestType: 'saveLocalStorage',
 
20909                                 chrome.extension.sendMessage(thisJSON, function(response) {
 
20910                                         setUpRESStorage(response);
 
20913                                 setUpRESStorage(response);
 
20916         } else if (BrowserDetect.isSafari()) {
 
20917                 // we've got safari, get localStorage from background process
 
20919                         requestType: 'getLocalStorage'
 
20921                 safari.self.tab.dispatchMessage("getLocalStorage", thisJSON);
 
20922         } else if (BrowserDetect.isFirefox()) {
 
20923                 // we've got firefox jetpack, get localStorage from background process
 
20925                         requestType: 'getLocalStorage'
 
20927                 self.postMessage(thisJSON);
 
20928         } else if (BrowserDetect.isOpera()) {
 
20929                 // I freaking hate having to use different code that won't run in other browsers to log debugs, so I'm overriding console.log with opera.postError here
 
20930                 // so I don't have to litter my code with different statements for different browsers when debugging.
 
20931                 console.log = opera.postError;
 
20932                 opera.extension.addEventListener( "message", operaMessageHandler, false);       
 
20933                 window.addEventListener("DOMContentLoaded", function(u) {
 
20934                         // we've got opera, let's check for old localStorage...
 
20935                         // RESInit() will be called from operaMessageHandler()
 
20937                                 requestType: 'getLocalStorage'
 
20939                         opera.extension.postMessage(JSON.stringify(thisJSON));
 
20942                 // Check if GM_getValue('importedFromForeground') has been set.. if not, this is an old user using localStorage;
 
20943                 (typeof unsafeWindow !== 'undefined') ? ls = unsafeWindow.localStorage : ls = localStorage;
 
20944                 if (GM_getValue('importedFromForeground') !== 'true') {
 
20945                         // It doesn't exist, so we need to copy localStorage over to GM_setValue storage...
 
20946                         for (var i = 0, len=ls.length; i < len; i++){
 
20947                                 var value = ls.getItem(ls.key(i));
 
20948                                 if (typeof value !== 'undefined') {
 
20949                                         if ((typeof value === 'number') && (value > 2147483647)) {
 
20950                                                 value = value.toString();
 
20953                                                 GM_setValue(ls.key(i), value);
 
20957                         GM_setValue('importedFromForeground','true');
 
20961                 // console.log(GM_listValues());
 
20965 function RESInitReadyCheck() {
 
20966         if ((typeof RESStorage.getItem !== 'function') || (typeof document.body === 'undefined') || (document.body === null)) {
 
20967                 setTimeout(RESInitReadyCheck, 50);
 
20969                 if (BrowserDetect.isFirefox()) {
 
20970                         // firefox addon sdk... we've included jQuery... 
 
20971                         // also, for efficiency, we're going to try using unsafeWindow for "less secure" (but we're not going 2 ways here, so that's OK) but faster DOM node access...
 
20972                         document = unsafeWindow.document;
 
20973                         window = unsafeWindow;
 
20974                         if (typeof $ !== 'function') {
 
20975                                 console.log('Uh oh, something has gone wrong loading jQuery...');
 
20977                 } else if ((typeof unsafeWindow !== 'undefined') && (unsafeWindow.jQuery)) {
 
20978                         // greasemonkey -- should load jquery automatically because of @require line
 
20979                         // in this file's header
 
20980                         if (typeof $ === 'undefined') {
 
20981                                 // greasemonkey-like userscript
 
20982                                 $ = unsafeWindow.jQuery;
 
20985                 } else if (typeof window.jQuery === 'function') {
 
20990                         // chrome and safari...
 
20991                         if (typeof $ !== 'function') {
 
20992                                 console.log('Uh oh, something has gone wrong loading jQuery...');
 
20995                 if (BrowserDetect.isSafari()) {
 
20996                         // since safari's built in extension stylesheets are treated as user stylesheets,
 
20997                         // we can't inject them that way.  That makes them "user stylesheets" which would make
 
20998                         // them require !important everywhere - we don't want that, so we'll inject this way instead.
 
20999                         var loadCSS = function(filename) {
 
21000                                 var linkTag = document.createElement('link');
 
21001                                 linkTag.setAttribute('rel','stylesheet');
 
21002                                 linkTag.href = safari.extension.baseURI+filename;
 
21003                                 document.head.appendChild(linkTag);
 
21006                         // include CSS files, then load scripts.
 
21007                         var cssFiles = ['res.css', 'commentBoxes.css', 'nightmode.css'];
 
21008                         for (var i in cssFiles) {
 
21009                                 loadCSS(cssFiles[i]);
 
21013                 if (BrowserDetect.isOpera()) {
 
21014                         // require.js-like modular injected scripts, code via:
 
21015                         // http://my.opera.com/BS-Harou/blog/2012/08/08/modular-injcted-scripts-in-extensions
 
21016                         // Note: This code requires Opera 12.50 to run!
 
21017                         if (typeof opera.extension.getFile === 'function') {
 
21018                                 var loadCSS = function(filename) {
 
21019                                         var fileObj = opera.extension.getFile(filename);
 
21021                                                 // Read out the File object as a Data URI:
 
21022                                                 var fr = new FileReader();
 
21023                                                 fr.onload = function() {                
 
21024                                                         // Load the library
 
21025                                                         var styleTag = document.createElement("style");
 
21026                                                         styleTag.textContent = fr.result;
 
21027                                                         document.body.appendChild(styleTag);
 
21029                                                 fr.readAsText(fileObj);                                         
 
21033                                 // include CSS files, then load scripts.
 
21034                                 var cssFiles = ['res.css', 'commentBoxes.css', 'nightmode.css'];
 
21035                                 for (var i in cssFiles) {
 
21036                                         loadCSS(cssFiles[i]);
 
21039                                 (function(){var e=opera.extension,t={text:"readAsText",json:"readAsText",dataurl:"readAsDataURL",arraybuffer:"readAsArrayBuffer"};"getFile"in e&&!("getFileData"in e)&&(e.getFileData=function(e,n,r){typeof n==="function"?(r=n,n="text"):(n=n&&n.toLowerCase(),n=n in t?n:"text");if(typeof r!=="function")return;var i=opera.extension.getFile(e);if(i){var s=new FileReader;s.onload=function(e){if(n=="json")try{r(JSON.parse(s.result),i)}catch(e){r(null)}else r(s.result,i)},s.onerror=function(e){r(null)},s[t[n]](i)}else setTimeout(r,0,null,i)})})();var global=this,require=function(){function define(e,t){typeof e==="function"||typeof t!=="function"?define.compiled=typeof e==="undefined"?null:e:(define._wait=!0,require(e,function(e){var n=[].slice.call(arguments,1),r=e.pop();r.cb(t.apply(global,n))}.bind(global,define._store)))}return define.compiled=null,define._store=null,define._wait=!1,function(){function _compile(){define.compiled=null,define._store=arguments[1]._store;with({})eval(arguments[0]);return define._wait?(define._wait=!1,arguments[1]._store.push({cb:arguments[3],path:arguments[4]}),!1):(processData(define.compiled,arguments[1],arguments[2]),!0)}function processData(e,t,n){t.temp[n]&&delete t.temp[n],t.add(e,n);var r=t.path;if(!(r[n]in require._cache)||e&&require._cache[r[n]]===null)require._cache[r[n]]=e;var i=t.temp[n+1];if(i)if(!i.parsedPath[1]){var s=_compile(i.data,t,n+1,i.cb,i.parsedPath[0]);s||(require._cache[i.path]=null)}else processData(i.data,t,n+1)}function wait(e,t){setTimeout(function(){t.apply(global,e)},0)}function compileCB(e,t,n,r){processData(r,t,n),t.length==t.path.length&&e&&wait(t,e)}function parsePath(e){var t=e.split("!");return!t[1]&&e.indexOf(/\.js$/i)===-1&&(t[0]=e+=".js"),t}return function(e,t){var n=[];n.temp=[],n._store=[],n.add=function(r,i){return this.length==i?(this.push(r),!0):(this.temp[i]={data:r,cb:compileCB.bind(global,t,n,i),parsedPath:parsePath(e[i]),path:e[i]},!1)};if(!e.length)return wait(n,t),null;Array.isArray(e)||(e=[e]),n.path=e;for(var r=0,i=e.length;r<i;r++){if(e[r]==="!domReady"){document.readyState==="complete"||document.readyState==="interactive"?processData(document,n,r):document.addEventListener("DOMContentLoaded",function(r){processData(document,n,r),n.length==e.length&&t&&wait(n,t)}.bind(global,r));continue}var s=parsePath(e[r]);if(!s[0]){processData(null,n,r);continue}if(e[r]in require._cache){processData(require._cache[e[r]],n,r);continue}opera.extension.getFileData((require._base||"")+s[0],s[1]||"text",function(r,i,s){if(s)if(!i[1]){if(n.length!=r){if(n.length>r){debugger;alert("oh shit, this shoud not happen!")}processData(s,n,r);return}var o=_compile(s,n,r,compileCB.bind(global,t,n,r),i[0]);if(!o){require._cache[e[r]]=null;return}}else processData(s,n,r);else processData(null,n,r);n.length==e.length&&t&&wait(n,t)}.bind(global,r,s))}if(n.length==e.length)return t&&wait(n,t),n.length==1?n[0]:n}}()}();require._cache={},require._base="/modules/";
 
21041                                 // save Reddit's jQuery, because this script is going to jack it up.
 
21042                                 // now, take the new jQuery in and store it local to RES's scope (it's a var up top)
 
21043                                 var redditJq = window.$;
 
21044                                 require(['jquery-1.9.1.min', 'guiders-1.2.8', /*'tinycon',*/ 'snuownd', 'jquery.dragsort-0.6', 'jquery.tokeninput', 'jquery-fieldselection.min'], function() {
 
21046                                         jQuery = window.jQuery;
 
21047                                         guiders = window.guiders;
 
21048                                         //Tinycon = window.Tinycon;
 
21049                                         SnuOwnd = window.SnuOwnd;
 
21050                                         // now, return the window.$ / window.jQuery back to its original state.
 
21051                                         window.$ = redditJq;
 
21052                                         window.jQuery = redditJq;
 
21059                         $(document).ready(RESInit);
 
21064 window.onload = RESInitReadyCheck();
 
21067 function perfTest(name) {
 
21068         var d = new Date();
 
21069         var diff = d.getTime() - lastPerf;
 
21070         console.log(name+' executed. Diff since last: ' + diff +'ms');
 
21071         lastPerf=d.getTime();