├── extension ├── data │ ├── Icon.png │ ├── images │ │ ├── icon128.png │ │ ├── icon16.png │ │ ├── icon256.png │ │ ├── icon48.png │ │ ├── screenshot-safari-3.2.png │ │ └── logo_vector.svg │ ├── styles │ │ ├── codemirror │ │ │ ├── fullscreen.css │ │ │ ├── matchesonscrollbar.css │ │ │ ├── dialog.css │ │ │ ├── show-hint.css │ │ │ └── codemirror.css │ │ ├── support.css │ │ ├── comment.css │ │ ├── achievements.css │ │ ├── domaintagger.css │ │ ├── notifier.css │ │ ├── config.css │ │ ├── trouble.css │ │ ├── modbar.css │ │ ├── historybutton.css │ │ ├── personalnotes.css │ │ ├── modmatrix.css │ │ ├── queuetools.css │ │ ├── usernotes.css │ │ └── removalreasons.css │ ├── background │ │ ├── options.html │ │ ├── safari.html │ │ └── chrome.js │ ├── tbmoduleinit.js │ ├── modules │ │ ├── devtools.js │ │ ├── bagel.js │ │ ├── support.js │ │ ├── metricstab.js │ │ ├── realtime.js │ │ ├── flyingsnoo.js │ │ ├── newmodmailpro.js │ │ ├── nukecomments.js │ │ ├── trouble.js │ │ ├── banlist.js │ │ ├── achievements.js │ │ └── syntax.js │ └── libs │ │ └── codemirror │ │ ├── addon │ │ ├── fullscreen.js │ │ ├── rulers.js │ │ ├── jump-to-line.js │ │ ├── css-hint.js │ │ ├── matchesonscrollbar.js │ │ ├── panel.js │ │ ├── annotatescrollbar.js │ │ ├── dialog.js │ │ ├── match-highlighter.js │ │ ├── closebrackets.js │ │ ├── searchcursor.js │ │ └── search.js │ │ └── mode │ │ └── yaml.js ├── .chromeignore ├── manifest.json └── Info.plist ├── .gitattributes ├── .gitignore ├── .editorconfig ├── LICENSE ├── package.json ├── gulpfile.js └── README.md /extension/data/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/reddit-moderator-toolbox/master/extension/data/Icon.png -------------------------------------------------------------------------------- /extension/data/images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/reddit-moderator-toolbox/master/extension/data/images/icon128.png -------------------------------------------------------------------------------- /extension/data/images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/reddit-moderator-toolbox/master/extension/data/images/icon16.png -------------------------------------------------------------------------------- /extension/data/images/icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/reddit-moderator-toolbox/master/extension/data/images/icon256.png -------------------------------------------------------------------------------- /extension/data/images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/reddit-moderator-toolbox/master/extension/data/images/icon48.png -------------------------------------------------------------------------------- /extension/.chromeignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.plist 3 | *.zip 4 | *.xpi 5 | Info.plist 6 | 7 | /data/background 8 | /data/Icon.png 9 | /manifest-*.json 10 | -------------------------------------------------------------------------------- /extension/data/images/screenshot-safari-3.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/reddit-moderator-toolbox/master/extension/data/images/screenshot-safari-3.2.png -------------------------------------------------------------------------------- /extension/data/styles/codemirror/fullscreen.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-fullscreen { 2 | position: fixed; 3 | top: 0; left: 0; right: 0; bottom: 0; 4 | height: auto; 5 | z-index: 9; 6 | } 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # JS files must always use LF to prevent issues with libraries and checksums 5 | *.js eol=lf 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /modules/modtalkadvisory.js 2 | *.bak 3 | /.idea 4 | /.settings 5 | /.vscode 6 | /project.json 7 | *.zip 8 | *.xpi 9 | *.nex 10 | /node_modules 11 | extension/data/modules/trollatroll.js 12 | -------------------------------------------------------------------------------- /extension/data/background/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /extension/data/styles/codemirror/matchesonscrollbar.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-search-match { 2 | background: gold; 3 | border-top: 1px solid orange; 4 | border-bottom: 1px solid orange; 5 | -moz-box-sizing: border-box; 6 | box-sizing: border-box; 7 | opacity: .5; 8 | } 9 | -------------------------------------------------------------------------------- /extension/data/styles/support.css: -------------------------------------------------------------------------------- 1 | body.mod-toolbox .tb-action-button.tb-insert-debug { 2 | display: inline-block !important; 3 | box-sizing: border-box !important; 4 | height: 26px !important; 5 | margin: 5px 5px 10px 0 !important; 6 | cursor: pointer; 7 | } 8 | -------------------------------------------------------------------------------- /extension/data/tbmoduleinit.js: -------------------------------------------------------------------------------- 1 | // We have to call this after all of the modules are loaded. 2 | // There's no super-clever way to trigger it automatically 3 | 4 | (function TBObjectInit() { 5 | window.addEventListener("TBModuleLoaded", function () { 6 | $.log("TBModule loaded, getting TBStorage", false, 'TBinit'); 7 | TB.init(); 8 | }); 9 | })(); 10 | -------------------------------------------------------------------------------- /extension/data/modules/devtools.js: -------------------------------------------------------------------------------- 1 | function devtools() { 2 | // Developer Tools, for managing /r/toolbox/w/tbnotes (etc.) 3 | 4 | var self = new TB.Module('Developer Tools'); 5 | self.settings['enabled']['default'] = false; 6 | 7 | self.setting('betamode', false); 8 | self.setting('devmode', true); 9 | 10 | TB.register_module(self); 11 | } 12 | 13 | (function () { 14 | window.addEventListener('TBModuleLoaded', function () { 15 | //devtools(); //disabled 16 | }); 17 | })(); -------------------------------------------------------------------------------- /extension/data/styles/comment.css: -------------------------------------------------------------------------------- 1 | .mod-toolbox #tb-flatview-search { 2 | background-color: #F7FAFD; 3 | color: #696969; 4 | border: 1px solid #B6C9DD; 5 | padding: 3px; 6 | margin: 11px 7 | } 8 | 9 | .mod-toolbox #tb-flatview-search-count { 10 | background-color: #B6C9DD; 11 | padding: 4px; 12 | color: #F7FAFD; 13 | font-weight: bold; 14 | } 15 | 16 | .mod-toolbox #tb-searchuser input[type="submit"] { 17 | padding: 0 !important; 18 | margin-left: 4px; 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | # We use spaces, deal with it. 10 | indent_style = space 11 | indent_size = 4 12 | 13 | end_of_line = lf 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | 21 | # Just in case it causes issues with safari 22 | [*.plist] 23 | indent_style = tab 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014-2017 toolbox development team. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moderator-toolbox-for-reddit", 3 | "version": "3.5.0", 4 | "description": "A set of tools to be used by moderators on reddit in order to make their jobs easier.", 5 | "private": true, 6 | "homepage": "http://www.reddit.com/r/toolbox", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/creesch/reddit-moderator-toolbox.git" 10 | }, 11 | "license": "Apache-2.0", 12 | "devDependencies": { 13 | "del": "^2.2.1", 14 | "gulp": "^3.9.0", 15 | "gulp-zip": "^3.0.2", 16 | "vinyl-paths": "^2.1.0", 17 | "yargs": "^4.3.2", 18 | "fs": "0.0.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /extension/data/styles/codemirror/dialog.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-dialog { 2 | position: absolute; 3 | left: 0; right: 0; 4 | background: inherit; 5 | z-index: 15; 6 | padding: .1em .8em; 7 | overflow: hidden; 8 | color: inherit; 9 | } 10 | 11 | .CodeMirror-dialog-top { 12 | border-bottom: 1px solid #eee; 13 | top: 0; 14 | } 15 | 16 | .CodeMirror-dialog-bottom { 17 | border-top: 1px solid #eee; 18 | bottom: 0; 19 | } 20 | 21 | .CodeMirror-dialog input { 22 | border: none; 23 | outline: none; 24 | background: transparent; 25 | width: 20em; 26 | color: inherit; 27 | font-family: monospace; 28 | } 29 | 30 | .CodeMirror-dialog button { 31 | font-size: 70%; 32 | } 33 | -------------------------------------------------------------------------------- /extension/data/styles/achievements.css: -------------------------------------------------------------------------------- 1 | .tb-settings .achievements p { 2 | padding-top: 0 !important; 3 | } 4 | 5 | .tb-settings .achievements .achievements-list { 6 | max-height: 585px !important; 7 | overflow-y: scroll; 8 | } 9 | 10 | .tb-settings .achievements .achievement { 11 | padding: 4px 4px 3px; 12 | margin-bottom: 4px; 13 | 14 | background-color: rgb(220, 220, 220); 15 | border: 1px solid darkgray; 16 | } 17 | 18 | .tb-settings .achievements .achievement.unlocked { 19 | background-color: white; 20 | } 21 | 22 | .tb-settings .achievements .achievement p { 23 | border: none; 24 | } 25 | 26 | .tb-settings .achievements .achievement p.title { 27 | font-weight: bold; 28 | } 29 | -------------------------------------------------------------------------------- /extension/data/modules/bagel.js: -------------------------------------------------------------------------------- 1 | function bagels() { 2 | var self = new TB.Module('Bagels'); 3 | self.shortname = 'Bagels'; 4 | 5 | //Default settings 6 | self.settings['enabled']['default'] = false; 7 | 8 | self.register_setting('bagelType', { 9 | 'type': 'selector', 10 | 'values': ['Plain', 'Seasame Seed', 'Poppy Seed', 'Onion', 'Everything'], 11 | 'default': 'plain', 12 | 'title': 'Bagel type' 13 | }); 14 | 15 | self.init = function() { 16 | $('body').append(''); 17 | }; 18 | 19 | TB.register_module(self); 20 | } 21 | 22 | (function() { 23 | window.addEventListener("TBModuleLoaded", function () { 24 | //bagels(); //disabled 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /extension/data/styles/codemirror/show-hint.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-hints { 2 | position: absolute; 3 | z-index: 10; 4 | overflow: hidden; 5 | list-style: none; 6 | 7 | margin: 0; 8 | padding: 2px; 9 | 10 | -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 11 | -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 12 | box-shadow: 2px 3px 5px rgba(0,0,0,.2); 13 | border-radius: 3px; 14 | border: 1px solid silver; 15 | 16 | background: white; 17 | font-size: 90%; 18 | font-family: monospace; 19 | 20 | max-height: 20em; 21 | overflow-y: auto; 22 | } 23 | 24 | .CodeMirror-hint { 25 | margin: 0; 26 | padding: 0 4px; 27 | border-radius: 2px; 28 | white-space: pre; 29 | color: black; 30 | cursor: pointer; 31 | } 32 | 33 | li.CodeMirror-hint-active { 34 | background: #08f; 35 | color: white; 36 | } 37 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var zip = require('gulp-zip'); 3 | var exec = require('child_process').exec; 4 | var argv = require('yargs').argv; 5 | var fs = require('fs'); 6 | var vinylPaths = require('vinyl-paths'); 7 | var del = require('del'); 8 | 9 | var src_dir = "extension"; 10 | var dest_dir = "build"; 11 | 12 | // Used when the --post parameter is given to gulp in order to push the xpi to firefox. 13 | var postUrl = "http://localhost:8888/"; 14 | 15 | // Tasks 16 | gulp.task('zip', function() { 17 | console.log(process.cwd()); 18 | 19 | var ignores = fs.readFileSync(src_dir+'/.chromeignore').toString().split("\n"); 20 | for (var i = 0; i < ignores.length; i++) { 21 | if (ignores[i].startsWith("/")) { 22 | ignores[i] = "!"+src_dir+ignores[i]; 23 | } 24 | else { 25 | ignores[i] = "!"+ignores[i]; 26 | } 27 | } 28 | 29 | return gulp.src([src_dir+'/**'].concat(ignores)) 30 | .pipe(zip('chrome-moderator-toolbox.zip')) 31 | .pipe(gulp.dest(dest_dir)); 32 | }); 33 | 34 | 35 | 36 | gulp.task('default', ['zip']); 37 | -------------------------------------------------------------------------------- /extension/data/styles/domaintagger.css: -------------------------------------------------------------------------------- 1 | .add-domain-tag { 2 | margin-left: 2px; 3 | } 4 | 5 | .add-domain-tag:hover { 6 | cursor: pointer; 7 | } 8 | 9 | /* Popup */ 10 | 11 | .tb-popup.dtagger-popup { 12 | max-width: 900px; 13 | text-align: center; 14 | } 15 | .tb-popup.dtagger-popup .dt-popup-content { 16 | display: inline-block; 17 | } 18 | .dtagger-popup .right { 19 | float: right; 20 | } 21 | .dtagger-popup .left { 22 | float: left; 23 | } 24 | .dtagger-popup .status { 25 | display: none; 26 | } 27 | 28 | .dtagger-popup tbody tr { 29 | width: 225px; 30 | vertical-align: top; 31 | border-bottom: 1px solid gray; 32 | display: block; 33 | padding: 5px; 34 | } 35 | 36 | .dtagger-popup th { 37 | padding-right: 10px !important; 38 | } 39 | 40 | .dtagger-popup .dt-popup-color-content { 41 | margin-bottom: 2px; 42 | } 43 | 44 | .dtagger-popup .domain-name { 45 | width: 150px; 46 | margin-left: 2px; 47 | margin-right: 7px; 48 | } 49 | 50 | .dtagger-popup .clear-domain { 51 | margin-left: 4px; 52 | } 53 | 54 | body.mod-toolbox .dtagger-popup .clear-domain:hover { 55 | background-color: #ffd4cf !important; 56 | } 57 | 58 | /* Tag styles */ 59 | 60 | .tb-dt-little-dot { 61 | margin-right: 3px; 62 | font-size: 25px; 63 | line-height: 0; 64 | } 65 | -------------------------------------------------------------------------------- /extension/data/styles/notifier.css: -------------------------------------------------------------------------------- 1 | .mod-toolbox #tb-modmail.nohavemail.custom, 2 | #modmail.nohavemail.custom { 3 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAKCAMAAABcxfTLAAAAulBMVEW+vr7TuIXUwY/a2trew5Thz6LsyX3tz47vizHvmyrv16bwz3Pw16Pw2JLx0XLx2Jvx2Zrx257y0lDy2Zfz01vz14Xz2Inz3bf02Xj03pj04bf04b/0473048b1ji/1oSr14KD14qf14rL23oP234D24Zr243/247D25pj25q/25rn26Zn26tH35Z345qj46Lr468356pP564X57dT67rr679H68dT79IH9+/n9/Pv+/fz/VwD/hgD///8T9eC2AAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB98KBAsZCHPgRWcAAABnSURBVAjXY2DAALa2+qZawkoGUra2YC6DrZiippmCABuQBZZmUNfTNZEQ5WGwhfBtmVSVDUX4GG1hfFZJFSMhbhYYn9mYX8NcjV2WGcIHUjKC2rzilkAWkA/k2lrpcElbAGlmDMcAANrsD7yKGWjgAAAAAElFTkSuQmCC'); 4 | } 5 | 6 | .mod-toolbox #tb-modmail.havemail.custom, 7 | #modmail.havemail.custom { 8 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAKCAMAAABcxfTLAAAAulBMVEW+vr7TuIXUwY/a2trew5Thz6LsyX3tz47vizHvmyrv16bwz3Pw16Pw2JLx0XLx2Jvx2Zrx257y0lDy2Zfz01vz14Xz2Inz3bf02Xj03pj04bf04b/0473048b1ji/1oSr14KD14qf14rL23oP234D24Zr243/247D25pj25q/25rn26Zn26tH35Z345qj46Lr468356pP564X57dT67rr679H68dT79IH9+/n9/Pv+/fz/VwD/hgD///8T9eC2AAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB98KBAsYLSH/oGEAAABtSURBVAjXY7BGBQzW1ra2+qZawkoGUra2IL6tta2YoqaZggAbkAXi21qr6+maSIjygJggvq2cqrKhCB+HLYwvL6liJMTNCePbGPNrmKuxy9pA+EBKRlCbV9wSyALygVxbKx0uaQsgbQOyHwUAABv1HVBj/19nAAAAAElFTkSuQmCC'); 9 | } 10 | 11 | .mod-toolbox #modmail.custom { 12 | background-position: 0 4px; 13 | } 14 | 15 | .mod-toolbox #tb-modmail.custom { 16 | background-position: 0 5px; 17 | } 18 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/addon/fullscreen.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | CodeMirror.defineOption("fullScreen", false, function(cm, val, old) { 15 | if (old == CodeMirror.Init) old = false; 16 | if (!old == !val) return; 17 | if (val) setFullscreen(cm); 18 | else setNormal(cm); 19 | }); 20 | 21 | function setFullscreen(cm) { 22 | var wrap = cm.getWrapperElement(); 23 | cm.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset, 24 | width: wrap.style.width, height: wrap.style.height}; 25 | wrap.style.width = ""; 26 | wrap.style.height = "auto"; 27 | wrap.className += " CodeMirror-fullscreen"; 28 | document.documentElement.style.overflow = "hidden"; 29 | cm.refresh(); 30 | } 31 | 32 | function setNormal(cm) { 33 | var wrap = cm.getWrapperElement(); 34 | wrap.className = wrap.className.replace(/\s*CodeMirror-fullscreen\b/, ""); 35 | document.documentElement.style.overflow = ""; 36 | var info = cm.state.fullScreenRestore; 37 | wrap.style.width = info.width; wrap.style.height = info.height; 38 | window.scrollTo(info.scrollLeft, info.scrollTop); 39 | cm.refresh(); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /extension/data/background/safari.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 48 | 49 | -------------------------------------------------------------------------------- /extension/data/styles/config.css: -------------------------------------------------------------------------------- 1 | /* Usernote types */ 2 | 3 | #tb-config-usernote-types { 4 | margin-bottom: 2px; 5 | } 6 | 7 | #tb-config-usernote-types th { 8 | padding-left: 2px; 9 | font-weight: bold; 10 | } 11 | 12 | #tb-config-usernote-type-list .usernote-type td { 13 | padding-right: 4px; 14 | padding-bottom: 4px; 15 | } 16 | 17 | #tb-config-usernote-type-list .usernote-type input[type="text"] { 18 | width: 200px; 19 | } 20 | 21 | #tb-config-usernote-type-list .usernote-type input.error { 22 | box-shadow: 0 0 3px 0 red; 23 | } 24 | 25 | #tb-config-usernote-type-list .usernote-type .usernote-error { 26 | padding-left: 4px; 27 | } 28 | 29 | .sp-container { 30 | z-index: 9999999999; 31 | } 32 | 33 | /* ban-macro */ 34 | 35 | .tb-page-overlay.tb-config td { 36 | vertical-align: top; 37 | padding: 3px; 38 | } 39 | 40 | 41 | .tb-page-overlay.tb-config td:first-child { 42 | padding-top: 5px; 43 | } 44 | .tb-page-overlay.tb-config td textarea.banMessage { 45 | width: 600px; 46 | height: 100px; 47 | margin: 2px; 48 | } 49 | 50 | .tb-page-overlay.tb-config td input.banNote { 51 | width: 597px; 52 | } 53 | 54 | 55 | /* changing tabs around */ 56 | 57 | .mod-toolbox .tb-config .tb-window-tabs { 58 | display: inline-block; 59 | width: 135px; 60 | height: 100%; 61 | border-bottom: 0; 62 | } 63 | 64 | .mod-toolbox .tb-config .tb-window-tabs a { 65 | box-sizing: border-box !important; 66 | width: 100%; 67 | border-bottom: solid 1px #eef5fd; 68 | } 69 | 70 | .mod-toolbox .tb-config .tb-window-tabs a:nth-last-of-type(1) { 71 | border-bottom: 0; 72 | } 73 | 74 | .mod-toolbox .tb-config .tb-window-tabs-wrapper { 75 | display: inline-block; 76 | vertical-align: top; 77 | width: calc(100% - 135px); 78 | } 79 | -------------------------------------------------------------------------------- /extension/data/styles/trouble.css: -------------------------------------------------------------------------------- 1 | .mod-toolbox.tb-controversy-hl .controversial > .entry{ 2 | box-shadow: -2px 0 rgba(255,0,0,1); 3 | } 4 | 5 | .mod-toolbox.tb-controversy-hl .controversial:not(.link) > .entry{ 6 | background-color: rgba(255,0,0,0.2); 7 | } 8 | .mod-toolbox.tb-controversy-hl .controversial:not(.link) > .RES-keyNav-activeElement.entry{ 9 | box-shadow: inset 0 0 3px 1px rgba(255,0,0,1); 10 | } 11 | 12 | .mod-toolbox .tb-controversy{ 13 | box-shadow: -2px 0 rgba(255,0,0,0.5); 14 | } 15 | 16 | .mod-toolbox .tb-neg:not(.link) > .entry{ 17 | background-color: rgba(0,0,255,0.2); 18 | } 19 | .mod-toolbox .tb-neg:not(.link) > .RES-keyNav-activeElement.entry{ 20 | box-shadow: inset 0 0 3px 1px rgba(0,0,255,1); 21 | } 22 | 23 | .mod-toolbox .tb-neg > .entry{ 24 | box-shadow: -2px 0 rgba(0,0,255,1); 25 | } 26 | 27 | .mod-toolbox .tb-ncontroversy{ 28 | box-shadow: -2px 0 rgba(0,0,255,0.5); 29 | } 30 | 31 | .mod-toolbox.tb-controversy-hl .controversial.tb-neg > .entry{ 32 | box-shadow: -2px 0 rgba(255,0,255,1); 33 | } 34 | 35 | .mod-toolbox.tb-controversy-hl .controversial.tb-neg:not(.link) > .entry{ 36 | background-color: rgba(255,0,255,0.2); 37 | } 38 | 39 | .mod-toolbox.tb-controversy-hl .controversial.tb-neg:not(.link) > .RES-keyNav-activeElement.entry{ 40 | box-shadow: inset 0 0 3px 1px rgba(255,0,255,1); 41 | } 42 | 43 | .mod-toolbox .tb-controversy.tb-ncontroversy{ 44 | box-shadow: -2px 0 rgba(255,0,255,0.5); 45 | } 46 | 47 | .mod-toolbox #tb-trouble-buttons { 48 | margin: 0px 5px 15px; 49 | } 50 | 51 | .mod-toolbox.tb-nchildren .comment.noncollapsed .numchildren{ 52 | display: inline; 53 | } 54 | 55 | .mod-toolbox.tb-nchildrentop .thing:before{ 56 | content: attr(data-nchildren); 57 | } 58 | 59 | .mod-toolbox .thing.collapsed:before{ 60 | position: absolute; 61 | } 62 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/addon/rulers.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | CodeMirror.defineOption("rulers", false, function(cm, val) { 15 | if (cm.state.rulerDiv) { 16 | cm.display.lineSpace.removeChild(cm.state.rulerDiv) 17 | cm.state.rulerDiv = null 18 | cm.off("refresh", drawRulers) 19 | } 20 | if (val && val.length) { 21 | cm.state.rulerDiv = cm.display.lineSpace.insertBefore(document.createElement("div"), cm.display.cursorDiv) 22 | cm.state.rulerDiv.className = "CodeMirror-rulers" 23 | drawRulers(cm) 24 | cm.on("refresh", drawRulers) 25 | } 26 | }); 27 | 28 | function drawRulers(cm) { 29 | cm.state.rulerDiv.textContent = "" 30 | var val = cm.getOption("rulers"); 31 | var cw = cm.defaultCharWidth(); 32 | var left = cm.charCoords(CodeMirror.Pos(cm.firstLine(), 0), "div").left; 33 | cm.state.rulerDiv.style.minHeight = (cm.display.scroller.offsetHeight + 30) + "px"; 34 | for (var i = 0; i < val.length; i++) { 35 | var elt = document.createElement("div"); 36 | elt.className = "CodeMirror-ruler"; 37 | var col, conf = val[i]; 38 | if (typeof conf == "number") { 39 | col = conf; 40 | } else { 41 | col = conf.column; 42 | if (conf.className) elt.className += " " + conf.className; 43 | if (conf.color) elt.style.borderColor = conf.color; 44 | if (conf.lineStyle) elt.style.borderLeftStyle = conf.lineStyle; 45 | if (conf.width) elt.style.borderLeftWidth = conf.width; 46 | } 47 | elt.style.left = (left + col * cw) + "px"; 48 | cm.state.rulerDiv.appendChild(elt) 49 | } 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/addon/jump-to-line.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | // Defines jumpToLine command. Uses dialog.js if present. 5 | 6 | (function(mod) { 7 | if (typeof exports == "object" && typeof module == "object") // CommonJS 8 | mod(require("../../lib/codemirror"), require("../dialog/dialog")); 9 | else if (typeof define == "function" && define.amd) // AMD 10 | define(["../../lib/codemirror", "../dialog/dialog"], mod); 11 | else // Plain browser env 12 | mod(CodeMirror); 13 | })(function(CodeMirror) { 14 | "use strict"; 15 | 16 | function dialog(cm, text, shortText, deflt, f) { 17 | if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true}); 18 | else f(prompt(shortText, deflt)); 19 | } 20 | 21 | var jumpDialog = 22 | 'Jump to line: (Use line:column or scroll% syntax)'; 23 | 24 | function interpretLine(cm, string) { 25 | var num = Number(string) 26 | if (/^[-+]/.test(string)) return cm.getCursor().line + num 27 | else return num - 1 28 | } 29 | 30 | CodeMirror.commands.jumpToLine = function(cm) { 31 | var cur = cm.getCursor(); 32 | dialog(cm, jumpDialog, "Jump to line:", (cur.line + 1) + ":" + cur.ch, function(posStr) { 33 | if (!posStr) return; 34 | 35 | var match; 36 | if (match = /^\s*([\+\-]?\d+)\s*\:\s*(\d+)\s*$/.exec(posStr)) { 37 | cm.setCursor(interpretLine(cm, match[1]), Number(match[2])) 38 | } else if (match = /^\s*([\+\-]?\d+(\.\d+)?)\%\s*/.exec(posStr)) { 39 | var line = Math.round(cm.lineCount() * Number(match[1]) / 100); 40 | if (/^[-+]/.test(match[1])) line = cur.line + line + 1; 41 | cm.setCursor(line - 1, cur.ch); 42 | } else if (match = /^\s*\:?\s*([\+\-]?\d+)\s*/.exec(posStr)) { 43 | cm.setCursor(interpretLine(cm, match[1]), cur.ch); 44 | } 45 | }); 46 | }; 47 | 48 | CodeMirror.keyMap["default"]["Alt-G"] = "jumpToLine"; 49 | }); 50 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/addon/css-hint.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror"), require("../../mode/css/css")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror", "../../mode/css/css"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | var pseudoClasses = {link: 1, visited: 1, active: 1, hover: 1, focus: 1, 15 | "first-letter": 1, "first-line": 1, "first-child": 1, 16 | before: 1, after: 1, lang: 1}; 17 | 18 | CodeMirror.registerHelper("hint", "css", function(cm) { 19 | var cur = cm.getCursor(), token = cm.getTokenAt(cur); 20 | var inner = CodeMirror.innerMode(cm.getMode(), token.state); 21 | if (inner.mode.name != "css") return; 22 | 23 | if (token.type == "keyword" && "!important".indexOf(token.string) == 0) 24 | return {list: ["!important"], from: CodeMirror.Pos(cur.line, token.start), 25 | to: CodeMirror.Pos(cur.line, token.end)}; 26 | 27 | var start = token.start, end = cur.ch, word = token.string.slice(0, end - start); 28 | if (/[^\w$_-]/.test(word)) { 29 | word = ""; start = end = cur.ch; 30 | } 31 | 32 | var spec = CodeMirror.resolveMode("text/css"); 33 | 34 | var result = []; 35 | function add(keywords) { 36 | for (var name in keywords) 37 | if (!word || name.lastIndexOf(word, 0) == 0) 38 | result.push(name); 39 | } 40 | 41 | var st = inner.state.state; 42 | if (st == "pseudo" || token.type == "variable-3") { 43 | add(pseudoClasses); 44 | } else if (st == "block" || st == "maybeprop") { 45 | add(spec.propertyKeywords); 46 | } else if (st == "prop" || st == "parens" || st == "at" || st == "params") { 47 | add(spec.valueKeywords); 48 | add(spec.colorKeywords); 49 | } else if (st == "media" || st == "media_parens") { 50 | add(spec.mediaTypes); 51 | add(spec.mediaFeatures); 52 | } 53 | 54 | if (result.length) return { 55 | list: result, 56 | from: CodeMirror.Pos(cur.line, start), 57 | to: CodeMirror.Pos(cur.line, end) 58 | }; 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /extension/data/styles/modbar.css: -------------------------------------------------------------------------------- 1 | /* console stuff */ 2 | .mod-toolbox .tb-debug-window .CodeMirror { 3 | width: 800px; 4 | height: 600px; 5 | font-family: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace; 6 | } 7 | 8 | .mod-toolbox #tb-toolbar-mysubs:hover, .mod-toolbox #tb-personal-notes-button:hover { 9 | background: #9FBAD6; 10 | 11 | } 12 | 13 | .mod-toolbox #tb-toolbar-mysubs, 14 | .mod-toolbox #tb-personal-notes-button { 15 | background: rgba(199, 214, 230, 0.8); 16 | border-left: 1px dotted #A8B5C2; 17 | border-right: 1px dotted #A8B5C2; 18 | display: inline-block; 19 | vertical-align: top; 20 | height: 22px; 21 | padding: 0 3px; 22 | } 23 | 24 | .mod-toolbox #tb-toolbar-mysubs:hover, 25 | .mod-toolbox #tb-personal-notes-button:hover { 26 | background: #9FBAD6; 27 | } 28 | 29 | .backup-warning { 30 | margin-left: 6px; 31 | color: #EAC117; 32 | } 33 | 34 | .backup-warning.happy { 35 | color: #54c355; 36 | } 37 | 38 | .backup-warning.sad { 39 | color: #FF0000; 40 | } 41 | 42 | .edit-settings { 43 | width: 99%; 44 | font: normal small verdana,arial,helvetica,sans-serif; 45 | -webkit-appearance: textarea; 46 | background-color: white; 47 | border: 1px solid; 48 | -webkit-rtl-ordering: logical; 49 | -webkit-user-select: text; 50 | -webkit-flex-direction: column; 51 | cursor: auto; 52 | padding: 2px !important; 53 | white-space: pre-wrap; 54 | word-wrap: break-word; 55 | background-color: white; 56 | } 57 | 58 | .mod-toolbox span.tb-settings-display { 59 | max-width: 59em; 60 | } 61 | 62 | 63 | /* not really sure if it belongs here, but about tab styling */ 64 | .mod-toolbox .tb-window-tab.about .tb-window-content { 65 | text-align: center; 66 | } 67 | 68 | .mod-toolbox table.tb-about-credits { 69 | 70 | border-collapse: separate; 71 | border-spacing: 10px; 72 | display: inline-block; 73 | } 74 | 75 | .mod-toolbox table.tb-about-credits td { 76 | border-bottom: solid 1px #F3F3F3; 77 | width: 33.3%; 78 | min-width: 140px; 79 | text-align: center; 80 | padding: 2px; 81 | } 82 | 83 | .mod-toolbox .tb-window-tab.about h1#tb-random-about-quote { 84 | font-style: italic; 85 | font-family: non-serif; 86 | font-size: 25px 87 | } 88 | 89 | .mod-toolbox .tb-window-tab.about h3{ 90 | margin-top: 10px; 91 | margin-bottom: 4px; 92 | border-top: solid 1px #DCDCDC; 93 | padding-top: 5px; 94 | } 95 | -------------------------------------------------------------------------------- /extension/data/styles/historybutton.css: -------------------------------------------------------------------------------- 1 | td.url-td { 2 | background-color: #fff; 3 | } 4 | 5 | body.mod-toolbox.mod-toolbox-new-modmail .history-button { 6 | vertical-align: middle; 7 | margin-right: 3px; 8 | margin-left: 2px; 9 | font-size: x-small; 10 | color: rgb(136, 136, 136); 11 | } 12 | 13 | .history-button-popup div.table.domain-table, 14 | .history-button-popup div.table.subreddit-table, 15 | .history-button-popup div.table.comment-table, 16 | .history-button-popup div.table.account-table { 17 | display: inline-block; 18 | width: 310px; 19 | } 20 | .history-button-popup div.tb-popup-content { 21 | min-width: 650px; 22 | } 23 | 24 | .history-button-popup { 25 | padding: 0 !important; 26 | } 27 | 28 | 29 | 30 | .history-button-popup td { 31 | border: 1px dotted lightgray; 32 | border-bottom: none; 33 | word-wrap: break-word; 34 | } 35 | 36 | .history-button-popup th { 37 | border: 1px solid lightgray; 38 | border-top: none; 39 | font-weight: bold; 40 | word-wrap: break-word; 41 | } 42 | 43 | .history-button-popup td.count-td,.history-button-popup td.percentage-td { 44 | min-width: 30px !important; 45 | } 46 | 47 | .history-button-popup td.url-td { 48 | max-width: 186px !important; 49 | word-wrap: break-word; 50 | } 51 | 52 | 53 | .history-button-popup th,.history-button-popup td { 54 | padding: 5px !important; 55 | border-right: none; 56 | } 57 | 58 | .history-button-popup table { 59 | width: 100%; 60 | } 61 | 62 | .history-button .tb-button-active { 63 | background-color: #b1d0ef; 64 | } 65 | 66 | .history-button-popup .table { 67 | margin: 10px; 68 | max-height: 200px; 69 | max-width: 300px; 70 | vertical-align: top; 71 | overflow-x: hidden; 72 | overflow-y: auto; 73 | border-bottom: 1px solid lightgray; 74 | border-top: 1px solid lightgray; 75 | } 76 | 77 | .history-button-popup .submission-markdown-text { 78 | width: 97% !important; 79 | min-height: 100px; 80 | } 81 | 82 | .history-button-popup .submission-markdown { 83 | margin: 0 10px; 84 | } 85 | 86 | .history-button-popup .table .error { 87 | text-align: center; 88 | } 89 | 90 | .mod-toolbox .tb-history-disclaimer { 91 | margin: 5px; 92 | max-width: 500px; 93 | background: #e6e6e6; 94 | padding: 5px; 95 | } 96 | 97 | .mod-toolbox .tb-history-disclaimer strong{ 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /extension/data/background/chrome.js: -------------------------------------------------------------------------------- 1 | function getCookie(tries, callback) { 2 | chrome.cookies.get({url: 'https://www.reddit.com', name: 'token'}, function(rawCookie) { 3 | 4 | // If no cookie is returned it is probably expired and we will need to generate a new one. 5 | // Instead of trying to do the oauth refresh thing ourselves we just do a GET request for modmail. 6 | // We trie this three times, if we don't have a cookie after that the user clearly isn't logged in. 7 | if (!rawCookie && tries < 3) { 8 | $.get('https://mod.reddit.com/mail/all').done(function(data) { 9 | console.log(data); 10 | // Ok we have the data, let's give this a second attempt. 11 | getCookie(tries++, callback); 12 | }); 13 | 14 | } else if (!rawCookie && tries > 2) { 15 | callback('{"ERROR": "user not logged into new modmail."}'); 16 | } else { 17 | 18 | console.log(rawCookie); 19 | // The cookie we grab has a base64 encoded string with data. Sometimes is invalid data at the end. 20 | // This RegExp should take care of that. 21 | const invalidChar = new RegExp('[^A-Za-z0-9+/].*?$'); 22 | const base64Cookie = rawCookie.value.replace(invalidChar, ''); 23 | const tokenData = atob(base64Cookie); 24 | console.log(tokenData); 25 | callback(tokenData); 26 | } 27 | 28 | }); 29 | 30 | } 31 | 32 | chrome.runtime.onMessage.addListener( 33 | function(request, sender, sendResponse) { 34 | 35 | 36 | console.log(request); 37 | 38 | // Request to reload the extension. Let's do so. 39 | if( request.action === "tb-reload" ) 40 | { 41 | console.log('reloading'); 42 | chrome.runtime.reload(); 43 | console.log('reloaded'); 44 | sendResponse(); 45 | } 46 | 47 | // Request to fetch the oauthToken data. 48 | if(request.action === 'oauthToken') { 49 | // This function will fetch the cookie and if there is no cookie attempt to create one by visiting modmail. 50 | getCookie(1, function(tokenData) { 51 | console.log('sending response'); 52 | console.log(tokenData); 53 | sendResponse({oauthToken: tokenData}); 54 | }); 55 | // http://stackoverflow.com/questions/20077487/chrome-extension-message-passing-response-not-sent 56 | return true; 57 | 58 | } 59 | }); 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /extension/data/styles/personalnotes.css: -------------------------------------------------------------------------------- 1 | .mod-toolbox .tb-popup.personal-notes-popup { 2 | max-width: 95%; 3 | } 4 | 5 | /* Don't allow the popup to get bigger than the window */ 6 | .mod-toolbox .tb-popup.personal-notes-popup .tb-popup-content { 7 | max-height: calc(100vh - 95px); /* 95px = modbar + popup header + popup footer + padding */ 8 | } 9 | 10 | /* Box layout */ 11 | .mod-toolbox #tb-personal-notes-listing, 12 | .mod-toolbox #tb-personal-notes-content { 13 | overflow: hidden; 14 | vertical-align: top; 15 | } 16 | .mod-toolbox .tb-popup.personal-notes-popup table { 17 | height: 100%; 18 | position: relative; /* To help with absolute positioning of the sidebar */ 19 | } 20 | 21 | /* Note selection sidebar */ 22 | .mod-toolbox #tb-personal-notes-listing { 23 | height: 100%; 24 | margin-left: -5px; 25 | min-width: 175px; 26 | } 27 | .mod-toolbox .personal-notes-popup input { 28 | width: 100%; 29 | -webkit-box-sizing: border-box; 30 | -moz-box-sizing: border-box; 31 | box-sizing: border-box; 32 | margin-top: 2px; 33 | margin-bottom: 2px; 34 | } 35 | .mod-toolbox #tb-new-personal-note-div { 36 | position: absolute; 37 | bottom: 0; 38 | width: 165px; 39 | height: 65px; 40 | padding: 5px; 41 | background: #C7D6E6; 42 | box-shadow: 0 -1px #FFF; 43 | } 44 | /* Hack to get background on the list */ 45 | .mod-toolbox .tb-popup.personal-notes-popup .tb-popup-content { 46 | box-shadow: inset 175px 0 #C7D6E6; 47 | padding: 0 !important; 48 | height: 100%; 49 | } 50 | /* Actual list */ 51 | .mod-toolbox #tb-personal-notes-list { 52 | width: 175px; 53 | position: absolute; 54 | top: 0; 55 | bottom: 70px; 56 | overflow: auto; 57 | } 58 | .mod-toolbox #tb-personal-notes-ul li { 59 | background: #C7D6E6; 60 | border-bottom: 1px solid #FFF; 61 | padding: 5px 5px 5px 23px; 62 | position: relative; 63 | word-wrap: break-word; 64 | } 65 | .mod-toolbox #tb-personal-notes-ul .tb-personal-notes-active { 66 | background: #9FBAD6 67 | } 68 | .mod-toolbox #tb-personal-notes-ul .tb-personal-notes-active a { 69 | color: #000; 70 | } 71 | .mod-toolbox .tb-personal-note-delete { 72 | display: block; 73 | position: absolute; 74 | left: 5px; 75 | height: 100%; 76 | top: 0; 77 | } 78 | .mod-toolbox .tb-personal-note-delete img { 79 | width: 13px; 80 | opacity: 0.8; 81 | margin-top: 7px; 82 | } 83 | 84 | .mod-toolbox .tb-personal-note-link { 85 | font-size: 14px; 86 | } 87 | 88 | .mod-toolbox .personal-notes-popup { 89 | left : 305px; 90 | bottom : 31px; 91 | display : block; 92 | position : fixed; 93 | } 94 | 95 | 96 | /* Main edit view */ 97 | .mod-toolbox #tb-personal-notes-content { 98 | min-width: 450px; 99 | margin-right: -5px; 100 | padding: 5px; 101 | position: relative; 102 | } 103 | 104 | .mod-toolbox #tb-personal-notes-editarea { 105 | -webkit-box-sizing: border-box; 106 | -moz-box-sizing: border-box; 107 | box-sizing: border-box; 108 | min-height: 100%; 109 | min-width: 100%; 110 | display: none; 111 | } 112 | 113 | /* Save button in footer */ 114 | .mod-toolbox #save-personal-note { 115 | display: none; 116 | width: initial; 117 | } 118 | -------------------------------------------------------------------------------- /extension/data/images/logo_vector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /extension/data/modules/support.js: -------------------------------------------------------------------------------- 1 | function support() { 2 | var self = new TB.Module('Support Module'); 3 | self.shortname = 'support'; 4 | 5 | self.settings['enabled']['default'] = true; 6 | 7 | // How about you don't disable support? No other module should ever do this. Well except for modbar. 8 | self.settings['enabled']['hidden'] = true; // Don't disable it, either! 9 | 10 | self.init = function() { 11 | let $body = $('body'); 12 | const debugTemplate = ` 13 | 14 | --- 15 | ***Toolbox debug information*** 16 | 17 | Info|   18 | ---|--- 19 | *Toolbox version*|{{toolboxVersion}} 20 | *Browser name*|{{browserName}} 21 | *Browser version*|{{browserVersion}} 22 | *Platform information*|{{platformInfo}} 23 | *Beta Mode*|{{betaMode}} 24 | *Debug Mode*|{{debugMode}} 25 | *Compact Mode*|{{compactMode}} 26 | *Advanced Settings*|{{advancedSettings}} 27 | *Cookies Enabled*|{{cookiesEnabled}} 28 | `; 29 | 30 | 31 | const debugInfo = TBUtils.debugInformation(); 32 | const submissionAddition = TBUtils.template(debugTemplate, { 33 | 'toolboxVersion': debugInfo.toolboxVersion, 34 | 'browserName': debugInfo.browser , 35 | 'browserVersion': debugInfo.browserVersion, 36 | 'platformInfo': debugInfo.platformInformation, 37 | 'betaMode': debugInfo.betaMode, 38 | 'debugMode': debugInfo.debugMode, 39 | 'compactMode': debugInfo.compactMode, 40 | 'advancedSettings': debugInfo.advancedSettings, 41 | 'cookiesEnabled': debugInfo.cookiesEnabled 42 | }); 43 | 44 | // If we are on the submit page we add debug information when a user makes a post. 45 | if(location.pathname.match(/\/r\/toolbox\/submit\/?/) || location.pathname.match(/\/r\/tb_dev\/submit\/?/)) { 46 | 47 | let $submissionTextArea = $('.usertext-edit.md-container textarea'); 48 | 49 | $body.on('click', '.submit.content .btn[name="submit"]', function(event) { 50 | // First we stop the action on the button for a bit. 51 | //event.preventDefault(); 52 | let submissionText = $submissionTextArea.val(); 53 | 54 | 55 | 56 | $submissionTextArea.val(submissionText + submissionAddition); 57 | }); 58 | } 59 | 60 | // If we are in the comment section we offer a button to insert the info. 61 | if(location.pathname.match(/\/r\/toolbox\/comments\/?/) || location.pathname.match(/\/r\/tb_dev\/comments\/?/)) { 62 | let $usertextButtons = $body.find('.usertext-edit .usertext-buttons'); 63 | 64 | let $saveButton = $usertextButtons.find('.save'); 65 | let $tbUsertextButtons = $saveButton.parent().find('.tb-usertext-buttons'); 66 | 67 | // This needs to be a div otherwise reddit thinks a save button is clicked. 68 | let $debugInsertButton = $('
').addClass('tb-action-button tb-insert-debug').text('Insert debug info'); 69 | if ($tbUsertextButtons.length) { 70 | $tbUsertextButtons.before($debugInsertButton); 71 | } else { 72 | $saveButton.parent().find('.status').before($('
').addClass('tb-usertext-buttons').append($debugInsertButton)); 73 | } 74 | 75 | $('body').on('click', 'div.tb-insert-debug', function (e) { 76 | self.log('Insert debug clicked!'); 77 | let $commentTextArea = $(this).closest('.usertext-edit.md-container').find('.md textarea'); 78 | let currentComment = $commentTextArea.val(); 79 | 80 | $commentTextArea.val(currentComment + submissionAddition); 81 | 82 | }); 83 | 84 | } 85 | }; 86 | 87 | TB.register_module(self); 88 | } 89 | 90 | (function () { 91 | window.addEventListener("TBModuleLoaded", function () { 92 | support(); 93 | }); 94 | })(); 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![forthebadge](http://forthebadge.com/images/badges/fuck-it-ship-it.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/as-seen-on-tv.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/contains-cat-gifs.svg)](http://forthebadge.com) 2 | 3 | [![forthebadge](http://forthebadge.com/images/badges/kinda-sfw.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/compatibility-betamax.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com) 4 | 5 | toolbox for reddit 6 | ======================== 7 | 8 | Bundled extension of the /r/toolbox moderator tools for reddit.com 9 | 10 | Containing: 11 | - Comments Module: Highlight keywords and hide removed comments. 12 | - Mod button: Adds a button to submissions and comments that allows you to Ban, unban, mod, unmod, approve, unapprove a user from that spot. If a user is banned it will also load the ban reason on the spot. Very handy when someone modmails you asking why they are banned! 13 | - Mod Mail Pro: Filter your modmail, easily compose new modmail for your fellow mods, hide invite spam, much more! 14 | - Moderation log matrix: See who does what in your team, analyze the modlog and output nice statistics. 15 | - Removal Reasons: When removing a submission have a selection of predefined reasons you can select from. Supports removal comments and flairs! 16 | - Toolbar Shortcuts: Put handy shortcuts to your often used pages in the toolbar. 17 | - User Notes: Leave notes about users that other mods can see as well! 18 | - Domain Tagger: A shared feature allowing mod teams to tag domains with colors. Makes it easier to spot spammy domains, approve approved domains, etc. 19 | - Notifications of new (mod)mails, queue items, etc. 20 | - Toolbar with queue counters 21 | - Banlist live search: If you have a big ban list this is a awesome feature, it basically turn the banlist search bar in a live search bar that automatically updates with matchers. 22 | - Trouble Shooter (beta): Highlights and sorts comments in subreddits you moderate to help guide you to potential sources of trouble i.e. controversial and negative score comments. 23 | 24 | Documentation: https://www.reddit.com/r/toolbox/w/docs 25 | 26 | 27 | # Building 28 | 29 | Building is relatively easy through [nodejs](https://nodejs.org/) with gulp. 30 | 31 | Install gulp globally. 32 | 33 | ```sh 34 | $ npm install --global gulp 35 | ``` 36 | 37 | Then navigate to the root of the toolbox folder and install the dependencies 38 | 39 | ```sh 40 | $ npm install 41 | ``` 42 | 43 | To build toolbox now simply run 44 | 45 | ```sh 46 | $ gulp 47 | ``` 48 | 49 | Or if you have followed these steps before and are on windows click the build.bat file. 50 | 51 | This will create a zip file which can be used in both Chrome as well as Firefox versions that support web extensions. 52 | 53 | # Development 54 | 55 | ## Chrome 56 | 57 | - Go to `chrome://extensions`. 58 | - Check the "Developer mode" checkbox if it's not already checked. 59 | - Click the "Load unpacked extension..." button. 60 | - Load the `extension` directory. 61 | 62 | Reload the extension when needed. 63 | 64 | ## Firefox (developer or nightly edition) 65 | 66 | - Go to `about:debugging`. 67 | - Click the "Load Temporary Add-on" button. 68 | - Point to `extension/manifest.json`. 69 | 70 | Reload the addon when needed. 71 | 72 | ### Third party support 73 | 74 | All shared features settings and data are stored in subreddit wikis through versioned json. Third party applications can use this data to hook into toolbox features like usernotes. 75 | 76 | Examples: 77 | 78 | - https://github.com/creesch/reddit-moderator-toolbox/wiki/JSON:-usernotes 79 | - https://github.com/creesch/reddit-moderator-toolbox/wiki/JSON:-toolbox-config 80 | -------------------------------------------------------------------------------- /extension/data/styles/modmatrix.css: -------------------------------------------------------------------------------- 1 | #mod-matrix-wrapper { 2 | position: absolute; 3 | background-color: #fff; 4 | margin-top: 0; 5 | left: 0; 6 | right: 0; 7 | padding-bottom: 100px; 8 | } 9 | 10 | #mod-matrix-wrapper a { 11 | cursor: pointer; 12 | } 13 | 14 | #mod-matrix thead th, #mod-matrix tfoot td, #mod-matrix tbody td.action-total, #mod-matrix tbody td.action-percentage { 15 | font-weight: bold; 16 | } 17 | 18 | #mod-matrix tr { 19 | border-bottom: 1px solid #eee; 20 | } 21 | 22 | #mod-matrix tfoot tr { 23 | border-top: 1px solid #eee; 24 | } 25 | 26 | #mod-matrix td, #mod-matrix th { 27 | padding: 5px; 28 | border-right: 1px solid #eee; 29 | text-align: center; 30 | font-size: x-small; 31 | } 32 | 33 | #mod-matrix td:first-child, #mod-matrix th:first-child { 34 | text-align: left; 35 | } 36 | 37 | #mod-matrix a.modactions { 38 | margin-right: 0; 39 | display: inline-block; 40 | float: none; 41 | } 42 | 43 | #mod-matrix tbody tr:nth-of-type(even) { 44 | background: #fafafa; 45 | } 46 | 47 | #mod-matrix tr:hover, #mod-matrix td.hover { 48 | background: #ffc!important; 49 | } 50 | 51 | #mod-matrix thead th { 52 | position: relative; 53 | cursor: s-resize; 54 | } 55 | 56 | #mod-matrix thead th { 57 | cursor: pointer; 58 | } 59 | 60 | #mod-matrix thead th .sorting-icon { 61 | position: absolute; 62 | left: 50%; 63 | margin-left: -8px; 64 | bottom: -8px; 65 | } 66 | 67 | #mod-matrix .action-number { 68 | color: #000; 69 | } 70 | 71 | #mod-matrix .zero a, #mod-matrix .zero span { 72 | color: #c0c0c0; 73 | } 74 | 75 | #mod-matrix .highlight a, #mod-matrix .highlight span { 76 | color: #f00; 77 | } 78 | 79 | #mod-matrix tr.filtered, #mod-matrix td.filtered, #mod-matrix th.filtered, #mod-matrix tr.hide-zero, #mod-matrix td.hide-zero, #mod-matrix th.hide-zero { 80 | display: none; 81 | } 82 | 83 | #mod-matrix-settings td { 84 | padding: 2px; 85 | vertical-align: top; 86 | line-height: 25px; 87 | } 88 | 89 | #mod-matrix-settings td:first-child { 90 | width: 200px; 91 | } 92 | 93 | #mod-matrix-settings div { 94 | line-height: 1em; 95 | } 96 | 97 | #mod-matrix-settings input[type=submit] { 98 | padding: 1px 6px; 99 | } 100 | 101 | #mod-matrix-statistics { 102 | margin-top: 1em; 103 | } 104 | 105 | #mod-matrix-statistics { 106 | border-top: 1px dotted #000; 107 | padding: 7px; 108 | } 109 | 110 | #mod-matrix-statistics strong { 111 | font-weight: bold; 112 | } 113 | 114 | /* show header labels (thanks to /u/ashishtiwari) */ 115 | #mod-matrix.labels th { 116 | vertical-align: text-bottom; 117 | text-align: center; 118 | text-overflow: ellipsis; 119 | overflow: hidden; 120 | padding-top: 150px; 121 | } 122 | 123 | #mod-matrix.labels a.modactions { 124 | vertical-align: bottom; 125 | width: 16px!important; 126 | height: 16px!important; 127 | } 128 | 129 | #mod-matrix.labels a.modactions:before { 130 | content: attr(title); 131 | position: relative; 132 | bottom: 20px; 133 | display: block; 134 | font-size: 11px; 135 | white-space: nowrap; 136 | font-weight: normal; 137 | color: #000; 138 | -webkit-transform: rotate(-90.0deg); 139 | transform: rotate(-90.0deg); 140 | } 141 | 142 | /* Comment text */ 143 | 144 | .modactionlisting .removed_comment_text { 145 | padding: 6px; 146 | margin: 2px 0; 147 | background-color: rgba(250, 250, 250, 0.8); 148 | border: 1px solid lightgray; 149 | border-radius: 5px; 150 | } 151 | 152 | .modactionlisting .removed_comment_text p:first-child { 153 | margin-top: 0; 154 | } 155 | 156 | .modactionlisting .removed_comment_text p:last-child { 157 | margin-bottom: 0; 158 | } 159 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/addon/matchesonscrollbar.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror"), require("./searchcursor"), require("../scroll/annotatescrollbar")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror", "./searchcursor", "../scroll/annotatescrollbar"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | CodeMirror.defineExtension("showMatchesOnScrollbar", function(query, caseFold, options) { 15 | if (typeof options == "string") options = {className: options}; 16 | if (!options) options = {}; 17 | return new SearchAnnotation(this, query, caseFold, options); 18 | }); 19 | 20 | function SearchAnnotation(cm, query, caseFold, options) { 21 | this.cm = cm; 22 | this.options = options; 23 | var annotateOptions = {listenForChanges: false}; 24 | for (var prop in options) annotateOptions[prop] = options[prop]; 25 | if (!annotateOptions.className) annotateOptions.className = "CodeMirror-search-match"; 26 | this.annotation = cm.annotateScrollbar(annotateOptions); 27 | this.query = query; 28 | this.caseFold = caseFold; 29 | this.gap = {from: cm.firstLine(), to: cm.lastLine() + 1}; 30 | this.matches = []; 31 | this.update = null; 32 | 33 | this.findMatches(); 34 | this.annotation.update(this.matches); 35 | 36 | var self = this; 37 | cm.on("change", this.changeHandler = function(_cm, change) { self.onChange(change); }); 38 | } 39 | 40 | var MAX_MATCHES = 1000; 41 | 42 | SearchAnnotation.prototype.findMatches = function() { 43 | if (!this.gap) return; 44 | for (var i = 0; i < this.matches.length; i++) { 45 | var match = this.matches[i]; 46 | if (match.from.line >= this.gap.to) break; 47 | if (match.to.line >= this.gap.from) this.matches.splice(i--, 1); 48 | } 49 | var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), this.caseFold); 50 | var maxMatches = this.options && this.options.maxMatches || MAX_MATCHES; 51 | while (cursor.findNext()) { 52 | var match = {from: cursor.from(), to: cursor.to()}; 53 | if (match.from.line >= this.gap.to) break; 54 | this.matches.splice(i++, 0, match); 55 | if (this.matches.length > maxMatches) break; 56 | } 57 | this.gap = null; 58 | }; 59 | 60 | function offsetLine(line, changeStart, sizeChange) { 61 | if (line <= changeStart) return line; 62 | return Math.max(changeStart, line + sizeChange); 63 | } 64 | 65 | SearchAnnotation.prototype.onChange = function(change) { 66 | var startLine = change.from.line; 67 | var endLine = CodeMirror.changeEnd(change).line; 68 | var sizeChange = endLine - change.to.line; 69 | if (this.gap) { 70 | this.gap.from = Math.min(offsetLine(this.gap.from, startLine, sizeChange), change.from.line); 71 | this.gap.to = Math.max(offsetLine(this.gap.to, startLine, sizeChange), change.from.line); 72 | } else { 73 | this.gap = {from: change.from.line, to: endLine + 1}; 74 | } 75 | 76 | if (sizeChange) for (var i = 0; i < this.matches.length; i++) { 77 | var match = this.matches[i]; 78 | var newFrom = offsetLine(match.from.line, startLine, sizeChange); 79 | if (newFrom != match.from.line) match.from = CodeMirror.Pos(newFrom, match.from.ch); 80 | var newTo = offsetLine(match.to.line, startLine, sizeChange); 81 | if (newTo != match.to.line) match.to = CodeMirror.Pos(newTo, match.to.ch); 82 | } 83 | clearTimeout(this.update); 84 | var self = this; 85 | this.update = setTimeout(function() { self.updateAfterChange(); }, 250); 86 | }; 87 | 88 | SearchAnnotation.prototype.updateAfterChange = function() { 89 | this.findMatches(); 90 | this.annotation.update(this.matches); 91 | }; 92 | 93 | SearchAnnotation.prototype.clear = function() { 94 | this.cm.off("change", this.changeHandler); 95 | this.annotation.clear(); 96 | }; 97 | }); 98 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/mode/yaml.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | CodeMirror.defineMode("yaml", function() { 15 | 16 | var cons = ['true', 'false', 'on', 'off', 'yes', 'no']; 17 | var keywordRegex = new RegExp("\\b(("+cons.join(")|(")+"))$", 'i'); 18 | 19 | return { 20 | token: function(stream, state) { 21 | var ch = stream.peek(); 22 | var esc = state.escaped; 23 | state.escaped = false; 24 | /* comments */ 25 | if (ch == "#" && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) { 26 | stream.skipToEnd(); 27 | return "comment"; 28 | } 29 | 30 | if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/)) 31 | return "string"; 32 | 33 | if (state.literal && stream.indentation() > state.keyCol) { 34 | stream.skipToEnd(); return "string"; 35 | } else if (state.literal) { state.literal = false; } 36 | if (stream.sol()) { 37 | state.keyCol = 0; 38 | state.pair = false; 39 | state.pairStart = false; 40 | /* document start */ 41 | if(stream.match(/---/)) { return "def"; } 42 | /* document end */ 43 | if (stream.match(/\.\.\./)) { return "def"; } 44 | /* array list item */ 45 | if (stream.match(/\s*-\s+/)) { return 'meta'; } 46 | } 47 | /* inline pairs/lists */ 48 | if (stream.match(/^(\{|\}|\[|\])/)) { 49 | if (ch == '{') 50 | state.inlinePairs++; 51 | else if (ch == '}') 52 | state.inlinePairs--; 53 | else if (ch == '[') 54 | state.inlineList++; 55 | else 56 | state.inlineList--; 57 | return 'meta'; 58 | } 59 | 60 | /* list seperator */ 61 | if (state.inlineList > 0 && !esc && ch == ',') { 62 | stream.next(); 63 | return 'meta'; 64 | } 65 | /* pairs seperator */ 66 | if (state.inlinePairs > 0 && !esc && ch == ',') { 67 | state.keyCol = 0; 68 | state.pair = false; 69 | state.pairStart = false; 70 | stream.next(); 71 | return 'meta'; 72 | } 73 | 74 | /* start of value of a pair */ 75 | if (state.pairStart) { 76 | /* block literals */ 77 | if (stream.match(/^\s*(\||\>)\s*/)) { state.literal = true; return 'meta'; }; 78 | /* references */ 79 | if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) { return 'variable-2'; } 80 | /* numbers */ 81 | if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) { return 'number'; } 82 | if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) { return 'number'; } 83 | /* keywords */ 84 | if (stream.match(keywordRegex)) { return 'keyword'; } 85 | } 86 | 87 | /* pairs (associative arrays) -> key */ 88 | if (!state.pair && stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)) { 89 | state.pair = true; 90 | state.keyCol = stream.indentation(); 91 | return "atom"; 92 | } 93 | if (state.pair && stream.match(/^:\s*/)) { state.pairStart = true; return 'meta'; } 94 | 95 | /* nothing found, continue */ 96 | state.pairStart = false; 97 | state.escaped = (ch == '\\'); 98 | stream.next(); 99 | return null; 100 | }, 101 | startState: function() { 102 | return { 103 | pair: false, 104 | pairStart: false, 105 | keyCol: 0, 106 | inlinePairs: 0, 107 | inlineList: 0, 108 | literal: false, 109 | escaped: false 110 | }; 111 | } 112 | }; 113 | }); 114 | 115 | CodeMirror.defineMIME("text/x-yaml", "yaml"); 116 | 117 | }); 118 | -------------------------------------------------------------------------------- /extension/data/modules/metricstab.js: -------------------------------------------------------------------------------- 1 | function metricstab() { 2 | var self = new TB.Module('Metrics Tab'); 3 | self.shortname = 'Metrics'; 4 | 5 | self.settings['enabled']['default'] = true; 6 | 7 | self.getSectionFromUrl = function getSectionFromUrl(url) { 8 | var regex = new RegExp(/^(http|https):\/\/([a-z]+\.)?reddit\.com\/(user|r)\/([^\/]+)(\/|$)/g); 9 | var matches = regex.exec(url); 10 | 11 | if (matches != null) { 12 | return {section: matches[3], subSection: matches[4]}; 13 | } else { 14 | return null; 15 | } 16 | }; 17 | 18 | self.init = function () { 19 | var page = this.getSectionFromUrl(window.location.href), 20 | $body = $('body'); 21 | 22 | if (page == null) { 23 | return false; 24 | } 25 | 26 | var metrics = { 27 | user: { 28 | //'Observatory': 'http://0bservat0ry.com/reddit/u/{subSection}.html', //Currently offline 29 | 'MetaReddit': 'http://metareddit.com/stalk?user={subSection}', 30 | 'Redective': 'http://www.redective.com/?r=e&a=search&s=user&t=redective&q={subSection}', 31 | 'Hivemind': 'http://www.hivemind.cc/rank/u/{subSection}', 32 | 'Karmawhores': 'http://www.karmawhores.net/user/{subSection}', 33 | //'Karmalb': 'http://www.karmalb.com/user/{subSection}', //Currently offline 34 | 'Karmastats': 'http://reddit.dataoverload.de/karmastats/#{subSection}', 35 | 'RateRedditors': 'http://rateredditors.com/{subSection}', 36 | 'SnoopSnoo': 'http://www.snoopsnoo.com/u/{subSection}', 37 | 'RedditInvestigator': 'http://www.redditinvestigator.com/{subSection}', 38 | 'redditgraphs': 'http://www.roadtolarissa.com/redditgraphs/?{subSection}&PieChart&Number&Submissions' 39 | //'RedditInsight': 'http://www.redditinsight.com/#trackuser', // donno if we want to add ones that don't propagate the user name. 40 | }, 41 | 42 | r: { 43 | //'Observatory': 'http://0bservat0ry.com/reddit/r/{subSection}.html', // Currently offline 44 | 'MetaReddit': 'http://metareddit.com/r/{subSection}', 45 | 'Redective': 'http://www.redective.com/?r=e&a=search&s=subreddit&t=redective&q={subSection}', 46 | 'Hivemind': 'http://www.hivemind.cc/rank/r/{subSection}', 47 | 'RedditMetrics': 'http://redditmetrics.com/r/{subSection}', 48 | 'ExploreReddit': 'http://paulrosenzweig.com/explore-reddit/r/{subSection}' 49 | } 50 | }; 51 | 52 | var header = document.getElementById("header-bottom-left"); 53 | var tabList = header.getElementsByTagName("ul")[0]; 54 | 55 | if (tabList == null) { 56 | return false; 57 | } 58 | $body.append(''); 59 | 60 | var $tabList = $(tabList), 61 | $metricsDropDown = $body.find('#tb-metrics-expand-list ul'); 62 | 63 | $tabList.css('overflow', 'visible'); 64 | 65 | var $listItem = $("
  • metrics
  • "), 66 | $tbMetricsList = $body.find('#tb-metrics-expand-list'); 67 | 68 | 69 | $(tabList).append($listItem); 70 | 71 | var links = metrics[page.section]; 72 | for (var i in links) { 73 | var url = links[i]; 74 | url = url.replace(/\{subSection\}/g, page.subSection); 75 | $metricsDropDown.append('
  • ' + i + '
  • '); 76 | } 77 | 78 | $listItem.on('click', function () { 79 | self.log('metrics tab opened'); 80 | var offset = $(this).offset(), 81 | offsetLeft = offset.left, 82 | offsetTop = (offset.top + 20); 83 | 84 | $body.find('#tb-metrics-expand-list').css({ 85 | "left": offsetLeft + 'px', 86 | "top": offsetTop + 'px' 87 | }); 88 | $tbMetricsList.toggle(); 89 | }); 90 | 91 | $(document).on('click', function (event) { 92 | if (!$(event.target).closest('.tb-metrics').length) { 93 | $tbMetricsList.hide(); 94 | } 95 | }); 96 | 97 | }; 98 | 99 | TB.register_module(self); 100 | } 101 | 102 | (function () { 103 | window.addEventListener("TBModuleLoaded", function () { 104 | metricstab(); 105 | }); 106 | })(); 107 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/addon/panel.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | CodeMirror.defineExtension("addPanel", function(node, options) { 13 | options = options || {}; 14 | 15 | if (!this.state.panels) initPanels(this); 16 | 17 | var info = this.state.panels; 18 | var wrapper = info.wrapper; 19 | var cmWrapper = this.getWrapperElement(); 20 | 21 | if (options.after instanceof Panel && !options.after.cleared) { 22 | wrapper.insertBefore(node, options.before.node.nextSibling); 23 | } else if (options.before instanceof Panel && !options.before.cleared) { 24 | wrapper.insertBefore(node, options.before.node); 25 | } else if (options.replace instanceof Panel && !options.replace.cleared) { 26 | wrapper.insertBefore(node, options.replace.node); 27 | options.replace.clear(); 28 | } else if (options.position == "bottom") { 29 | wrapper.appendChild(node); 30 | } else if (options.position == "before-bottom") { 31 | wrapper.insertBefore(node, cmWrapper.nextSibling); 32 | } else if (options.position == "after-top") { 33 | wrapper.insertBefore(node, cmWrapper); 34 | } else { 35 | wrapper.insertBefore(node, wrapper.firstChild); 36 | } 37 | 38 | var height = (options && options.height) || node.offsetHeight; 39 | this._setSize(null, info.heightLeft -= height); 40 | info.panels++; 41 | return new Panel(this, node, options, height); 42 | }); 43 | 44 | function Panel(cm, node, options, height) { 45 | this.cm = cm; 46 | this.node = node; 47 | this.options = options; 48 | this.height = height; 49 | this.cleared = false; 50 | } 51 | 52 | Panel.prototype.clear = function() { 53 | if (this.cleared) return; 54 | this.cleared = true; 55 | var info = this.cm.state.panels; 56 | this.cm._setSize(null, info.heightLeft += this.height); 57 | info.wrapper.removeChild(this.node); 58 | if (--info.panels == 0) removePanels(this.cm); 59 | }; 60 | 61 | Panel.prototype.changed = function(height) { 62 | var newHeight = height == null ? this.node.offsetHeight : height; 63 | var info = this.cm.state.panels; 64 | this.cm._setSize(null, info.height += (newHeight - this.height)); 65 | this.height = newHeight; 66 | }; 67 | 68 | function initPanels(cm) { 69 | var wrap = cm.getWrapperElement(); 70 | var style = window.getComputedStyle ? window.getComputedStyle(wrap) : wrap.currentStyle; 71 | var height = parseInt(style.height); 72 | var info = cm.state.panels = { 73 | setHeight: wrap.style.height, 74 | heightLeft: height, 75 | panels: 0, 76 | wrapper: document.createElement("div") 77 | }; 78 | wrap.parentNode.insertBefore(info.wrapper, wrap); 79 | var hasFocus = cm.hasFocus(); 80 | info.wrapper.appendChild(wrap); 81 | if (hasFocus) cm.focus(); 82 | 83 | cm._setSize = cm.setSize; 84 | if (height != null) cm.setSize = function(width, newHeight) { 85 | if (newHeight == null) return this._setSize(width, newHeight); 86 | info.setHeight = newHeight; 87 | if (typeof newHeight != "number") { 88 | var px = /^(\d+\.?\d*)px$/.exec(newHeight); 89 | if (px) { 90 | newHeight = Number(px[1]); 91 | } else { 92 | info.wrapper.style.height = newHeight; 93 | newHeight = info.wrapper.offsetHeight; 94 | info.wrapper.style.height = ""; 95 | } 96 | } 97 | cm._setSize(width, info.heightLeft += (newHeight - height)); 98 | height = newHeight; 99 | }; 100 | } 101 | 102 | function removePanels(cm) { 103 | var info = cm.state.panels; 104 | cm.state.panels = null; 105 | 106 | var wrap = cm.getWrapperElement(); 107 | info.wrapper.parentNode.replaceChild(wrap, info.wrapper); 108 | wrap.style.height = info.setHeight; 109 | cm.setSize = cm._setSize; 110 | cm.setSize(); 111 | } 112 | }); 113 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/addon/annotatescrollbar.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | CodeMirror.defineExtension("annotateScrollbar", function(options) { 15 | if (typeof options == "string") options = {className: options}; 16 | return new Annotation(this, options); 17 | }); 18 | 19 | CodeMirror.defineOption("scrollButtonHeight", 0); 20 | 21 | function Annotation(cm, options) { 22 | this.cm = cm; 23 | this.options = options; 24 | this.buttonHeight = options.scrollButtonHeight || cm.getOption("scrollButtonHeight"); 25 | this.annotations = []; 26 | this.doRedraw = this.doUpdate = null; 27 | this.div = cm.getWrapperElement().appendChild(document.createElement("div")); 28 | this.div.style.cssText = "position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none"; 29 | this.computeScale(); 30 | 31 | function scheduleRedraw(delay) { 32 | clearTimeout(self.doRedraw); 33 | self.doRedraw = setTimeout(function() { self.redraw(); }, delay); 34 | } 35 | 36 | var self = this; 37 | cm.on("refresh", this.resizeHandler = function() { 38 | clearTimeout(self.doUpdate); 39 | self.doUpdate = setTimeout(function() { 40 | if (self.computeScale()) scheduleRedraw(20); 41 | }, 100); 42 | }); 43 | cm.on("markerAdded", this.resizeHandler); 44 | cm.on("markerCleared", this.resizeHandler); 45 | if (options.listenForChanges !== false) 46 | cm.on("change", this.changeHandler = function() { 47 | scheduleRedraw(250); 48 | }); 49 | } 50 | 51 | Annotation.prototype.computeScale = function() { 52 | var cm = this.cm; 53 | var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) / 54 | cm.getScrollerElement().scrollHeight 55 | if (hScale != this.hScale) { 56 | this.hScale = hScale; 57 | return true; 58 | } 59 | }; 60 | 61 | Annotation.prototype.update = function(annotations) { 62 | this.annotations = annotations; 63 | this.redraw(); 64 | }; 65 | 66 | Annotation.prototype.redraw = function(compute) { 67 | if (compute !== false) this.computeScale(); 68 | var cm = this.cm, hScale = this.hScale; 69 | 70 | var frag = document.createDocumentFragment(), anns = this.annotations; 71 | 72 | var wrapping = cm.getOption("lineWrapping"); 73 | var singleLineH = wrapping && cm.defaultTextHeight() * 1.5; 74 | var curLine = null, curLineObj = null; 75 | function getY(pos, top) { 76 | if (curLine != pos.line) { 77 | curLine = pos.line; 78 | curLineObj = cm.getLineHandle(curLine); 79 | } 80 | if (wrapping && curLineObj.height > singleLineH) 81 | return cm.charCoords(pos, "local")[top ? "top" : "bottom"]; 82 | var topY = cm.heightAtLine(curLineObj, "local"); 83 | return topY + (top ? 0 : curLineObj.height); 84 | } 85 | 86 | if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) { 87 | var ann = anns[i]; 88 | var top = nextTop || getY(ann.from, true) * hScale; 89 | var bottom = getY(ann.to, false) * hScale; 90 | while (i < anns.length - 1) { 91 | nextTop = getY(anns[i + 1].from, true) * hScale; 92 | if (nextTop > bottom + .9) break; 93 | ann = anns[++i]; 94 | bottom = getY(ann.to, false) * hScale; 95 | } 96 | if (bottom == top) continue; 97 | var height = Math.max(bottom - top, 3); 98 | 99 | var elt = frag.appendChild(document.createElement("div")); 100 | elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: " 101 | + (top + this.buttonHeight) + "px; height: " + height + "px"; 102 | elt.className = this.options.className; 103 | if (ann.id) { 104 | elt.setAttribute("annotation-id", ann.id); 105 | } 106 | } 107 | this.div.textContent = ""; 108 | this.div.appendChild(frag); 109 | }; 110 | 111 | Annotation.prototype.clear = function() { 112 | this.cm.off("refresh", this.resizeHandler); 113 | this.cm.off("markerAdded", this.resizeHandler); 114 | this.cm.off("markerCleared", this.resizeHandler); 115 | if (this.changeHandler) this.cm.off("change", this.changeHandler); 116 | this.div.parentNode.removeChild(this.div); 117 | }; 118 | }); 119 | -------------------------------------------------------------------------------- /extension/data/styles/queuetools.css: -------------------------------------------------------------------------------- 1 | .mod-toolbox .modtools-active { 2 | padding-top: 40px !important; 3 | } 4 | 5 | .mod-toolbox .menuarea.modtools { 6 | background-color: rgba(237, 241, 245, 0.8); 7 | border: 1px solid #B6C9DD; 8 | z-index: 500; 9 | overflow: visible; 10 | } 11 | 12 | .drop-choices.lightdrop { 13 | z-index: 501; 14 | } 15 | 16 | 17 | #modtab-threshold { 18 | width: 20px; 19 | } 20 | 21 | 22 | .mod-toolbox .tb-kitteh { 23 | display: block; 24 | min-width: 330px; 25 | min-height: 351px; 26 | background-image: url("https://b.thumbs.redditmedia.com/7uBqTsML9Y-sksmfvCrKrHvoUANP6ZCOw9UOVIV6aXQ.png"); 27 | background-repeat: no-repeat; 28 | background-position: center center; 29 | text-indent: -1000px; 30 | overflow: hidden; 31 | } 32 | 33 | .mod-toolbox .tb-puppy { 34 | display: block; 35 | min-width: 360px; 36 | min-height: 460px; 37 | background-image: url("https://a.thumbs.redditmedia.com/L8eKqhvo2mcjB0E8t87nOlu1_oMoSQ8F8LzqbFHumK8.png"); 38 | background-repeat: no-repeat; 39 | background-position: center center; 40 | text-indent: -1000px; 41 | overflow: hidden; 42 | } 43 | 44 | .mod-toolbox .tb-begifs { 45 | display: block; 46 | min-width: 360px; 47 | min-height: 460px; 48 | background-image: url("https://creesch.github.io/reddit-moderator-toolbox/hosted_images/begifs.gif"); 49 | background-repeat: no-repeat; 50 | background-position: center center; 51 | text-indent: -1000px; 52 | overflow: hidden; 53 | } 54 | 55 | .mod-toolbox .tb-spiders { 56 | display: block; 57 | min-width: 360px; 58 | min-height: 460px; 59 | background-image: url("https://b.thumbs.redditmedia.com/AgVm8q7yNxjcklDntkpH4hc6UoYrBUcwU8SXcZWgBJA.png"); 60 | background-repeat: no-repeat; 61 | background-position: center center; 62 | text-indent: -1000px; 63 | overflow: hidden; 64 | } 65 | 66 | .mod-toolbox .tb-begifs:after { 67 | content: "baby elephant is pleased!"; 68 | } 69 | 70 | 71 | /* Action reason stuff */ 72 | 73 | .action-reason { 74 | padding: 5px; 75 | font-size: 12px; 76 | background-color: rgba(255, 255, 255, 0.59); 77 | } 78 | 79 | .action-reason a { 80 | font-size: 10px; 81 | } 82 | 83 | 84 | /* Color borders */ 85 | .comment.tb-subreddit-color { 86 | padding-left: 10px; 87 | } 88 | 89 | 90 | /* queue sort counter */ 91 | 92 | a.tb-subreddit-item-count { 93 | background-color: #D56F18; 94 | color: #FFF; 95 | font-size: 11px; 96 | font-weight: bold; 97 | padding: 0px 3px 3px 3px; 98 | margin: 3px; 99 | border-radius: 2px; 100 | display: inline-block; 101 | } 102 | 103 | .subscription-box li { 104 | display: -webkit-flex; 105 | display: -ms-flexbox; 106 | display: flex; 107 | -webkit-flex-direction: row; 108 | -ms-flex-direction: row; 109 | flex-direction: row; 110 | -webkit-flex-wrap: wrap; 111 | -ms-flex-wrap: wrap; 112 | flex-wrap: wrap; 113 | } 114 | 115 | a.tb-subreddit-item-count { 116 | -ms-flex-order: -1; 117 | order: -1; 118 | } 119 | 120 | .mod-toolbox .tb-sort-subs { 121 | float: right; 122 | margin: 4px; 123 | border: 1px solid #B6C9DD; 124 | border-left: 2px solid #B6C9DD; 125 | padding: 3px; 126 | background-color: white; 127 | font-size: 13px; 128 | } 129 | 130 | .mod-toolbox .tb-sort-subs:hover { 131 | background-color: #E2EEFA; 132 | } 133 | 134 | .mod-toolbox .tb-sort-subs img { 135 | margin-bottom: -3px; 136 | margin-right: 3px; 137 | } 138 | 139 | 140 | /* pretty buttons */ 141 | .menuarea.modtools a.pretty-button { 142 | border-radius: 0 !important; 143 | border-width: 1px 1px 1px 2px; 144 | border-color: #778696; 145 | border-style: solid; 146 | padding: 1px 5px; 147 | text-align: center; 148 | font-size: inherit; 149 | } 150 | 151 | .menuarea.modtools .pretty-button.negative { 152 | background: none; 153 | background-color: #EDBDBE; 154 | 155 | } 156 | .menuarea.modtools .pretty-button.positive { 157 | background: none; 158 | background-color: #D1EABF; 159 | } 160 | 161 | .menuarea.modtools .pretty-button.neutral { 162 | background-image: none; 163 | background-color: #D5D5D5; 164 | } 165 | 166 | .menuarea.modtools .pretty-button.negative.pressed { 167 | background: none; 168 | background-color: #886B6C; 169 | 170 | } 171 | .menuarea.modtools .pretty-button.positive.pressed { 172 | background: none; 173 | background-color: #849479; 174 | } 175 | 176 | .menuarea.modtools .pretty-button.neutral.pressed { 177 | background-image: none; 178 | background-color: #9E9C9C; 179 | } 180 | 181 | 182 | /* Highlight for zero-scored posts */ 183 | .tb-zero-highlight { 184 | background: #FFF1C4; 185 | } 186 | -------------------------------------------------------------------------------- /extension/data/modules/realtime.js: -------------------------------------------------------------------------------- 1 | function realtime() { 2 | // =============== 3 | // http://userscripts-mirror.org/scripts/show/129928 4 | // By: /u/DEADBEEF 5 | // =============== 6 | 7 | var self = new TB.Module('Realtime Reddit'); 8 | self.shortname = 'Realtime'; 9 | 10 | self.settings['enabled']['default'] = false; 11 | self.config['betamode'] = true; 12 | 13 | self.init = function () { 14 | 15 | // Don't run if the page we're viewing is paginated or a threaded comments page... or page restrictions. 16 | if (location.search.match(/before|after/) || $('body.comments-page').length || !(TBUtils.isModpage || TBUtils.isCommentsPage || TBUtils.isNewPage || TBUtils.isUserPage)) return; 17 | 18 | // Add checkbox; 19 | $('.tabmenu:first-of-type').append('
  • '); 20 | 21 | var timeout, delay = 5000, 22 | $checkbox = $('.tb-realtime-checkbox'); 23 | 24 | // Add new things 25 | function getNewThings() { 26 | self.log("realtime gettingnewthings"); 27 | 28 | if (!$('#realtime:checked').length) return; 29 | timeout = setTimeout(getNewThings, delay); 30 | 31 | // Don't run when window not visible 32 | if (document.hidden) return; 33 | 34 | // Get first thing 35 | var before = $('#siteTable div.thing:first').attr('data-fullname'), 36 | html = []; 37 | 38 | // Get new things, prepend to page on success 39 | $.get(location.pathname + '.json-html?before=' + before).done(function (response) { 40 | 41 | // Compress the HTML of each returned thing 42 | for (i in response.data) html.push(compressHTML(response.data[i].data.content)); 43 | if (!html.length) return; 44 | 45 | insertHTML(html); 46 | 47 | // Update Ranks on link listings (if applicable) 48 | var n = 1; 49 | $('.rank').each(function () { 50 | this.innerHTML = n++; 51 | this.style.width = '3.30ex'; 52 | this.nextSibling.style.width = '3ex' 53 | }); 54 | 55 | }); 56 | } 57 | 58 | // Insert new things into sitetable. 59 | function insertHTML(html) { 60 | 61 | var height = $sitetable.css('top').slice(0, -2), 62 | things = $(html.join('')) 63 | .find('.child').remove().end() 64 | .prependTo($sitetable) 65 | .each(function () { 66 | height -= this.offsetHeight 67 | }); 68 | 69 | // Scroll new items into view. 70 | $sitetable.stop().css('top', height).animate({top: 0}, 5000); 71 | things.css({opacity: 0.2}).animate({opacity: 1}, 2000, 'linear'); 72 | 73 | // Trim items 74 | $('#siteTable>div.thing:gt(99),#siteTable>.clearleft:gt(99),#siteTable tr.modactions:gt(200)').remove(); 75 | 76 | // Run flowwit callbacks on new things. 77 | if (window.flowwit) for (i in window.flowwit) window.flowwit[i](things.filter('.thing')) 78 | 79 | // Run callbacks for new things 80 | $(document).trigger('new_things_inserted'); 81 | 82 | // Run callbacks for toolbox 83 | setTimeout(function () { 84 | var event = new CustomEvent("TBNewThings"); 85 | window.dispatchEvent(event); 86 | }, 1000); 87 | } 88 | 89 | // Toggle realtime view on/off 90 | $checkbox.on('click', function () { 91 | var $body = $('body'), 92 | siteTableMargin = $body.find('.side').outerWidth() + 10, 93 | $sitetable = $('#siteTable').css({ 94 | 'top': 0, 95 | 'margin-right': siteTableMargin + 'px' 96 | }), 97 | initialPosition = $sitetable.css('position'); 98 | 99 | self.log("realtime checked: " + $checkbox.is(':checked')); 100 | 101 | clearTimeout(timeout); 102 | if ($checkbox.is(':checked')) getNewThings(); 103 | 104 | // Toggle promo box 105 | $('#siteTable_organic,.content>.infobar').css('display', (this.checked ? 'none' : 'block')); 106 | $sitetable.css('position', (this.checked ? 'relative' : initialPosition)); 107 | }); 108 | 109 | // .json-html returns uncompressed html, so we have to compress it manually and replace HTML entities. 110 | function compressHTML(src) { 111 | return src.replace(/(\n+|\s+)?</g, '<') 112 | .replace(/>(\n+|\s+)?/g, '>') 113 | .replace(/&/g, '&') 114 | .replace(/\n/g, '') 115 | .replace(/child" > False/, 'child">'); 116 | } 117 | }; 118 | 119 | TB.register_module(self); 120 | } 121 | 122 | (function() { 123 | window.addEventListener("TBModuleLoaded", function () { 124 | realtime(); 125 | }); 126 | })(); 127 | -------------------------------------------------------------------------------- /extension/data/modules/flyingsnoo.js: -------------------------------------------------------------------------------- 1 | function flyingsnoo() { 2 | // @name Flying Snoo 3 | // @namespace http://reddit.com/user/LowSociety 4 | // @copyright 2014+, LowSociety 5 | 6 | var self = new TB.Module('Userpage'); 7 | self.shortname = 'Userpage'; 8 | 9 | ////Default settings 10 | self.settings['enabled']['default'] = true; 11 | self.settings['enabled']['hidden'] = true; // it's an easter egg. 12 | 13 | self.init = function () { 14 | if (!TB.utils.isUserPage) return; 15 | 16 | $(".profile-page .footer-parent").click(function () { 17 | var background = $(this).css("background"); 18 | 19 | var width = 87, 20 | height = 145; 21 | 22 | // unlock achievement 23 | TB.utils.sendEvent(TB.utils.events.TB_FLY_SNOO); 24 | 25 | var floater = $("
    ").css({ 26 | height: height + "px", 27 | width: width + "px", 28 | background: "url(//c.thumbs.redditmedia.com/7U0yjjycFy9MC5ht.png)", 29 | position: "absolute", 30 | top: $(this).offset().top + "px", 31 | left: (($(window).width() * 0.49) - (width / 2)) + "px", 32 | zIndex: 999 33 | }).appendTo("body"); 34 | 35 | var documentHeight = $(document).height(), 36 | documentWidth = $(document).width(), 37 | iterations = 0, 38 | wind = 0, 39 | oldTop = floater.position().top, 40 | oldLeft = floater.position().left; 41 | 42 | var interval = null; 43 | 44 | var startInterval = function () { 45 | interval = window.setInterval(function () { 46 | var newTop = Math.max(0, (oldTop - ((documentHeight) * 0.001))); 47 | 48 | if (iterations % 50 == 0) { 49 | wind = ((Math.random() * 200) - 100) * 0.02; 50 | } 51 | 52 | newLeft = Math.min(documentWidth - width, Math.max(0, oldLeft + wind)); 53 | 54 | floater.css({ 55 | top: newTop + "px", 56 | left: newLeft + "px" 57 | }); 58 | 59 | iterations++; 60 | if (newTop <= 0) { 61 | window.clearInterval(interval); 62 | interval = null; 63 | } 64 | oldTop = newTop; 65 | oldLeft = newLeft; 66 | }, 50); 67 | }; 68 | 69 | floater.mousedown(function (e) { 70 | if (interval != null) { 71 | window.clearInterval(interval); 72 | interval = null; 73 | } 74 | floater.data("offsetX", e.offsetX); 75 | floater.data("offsetY", e.offsetY); 76 | 77 | var dragEvent = function (e) { 78 | var offsetX = floater.data("offsetX") || 0, 79 | offsetY = floater.data("offsetY") || 0; 80 | oldLeft = (e.pageX - offsetX); 81 | oldTop = (e.pageY - offsetY); 82 | floater.css({ 83 | "left": oldLeft + "px", 84 | "top": oldTop + "px" 85 | }); 86 | }; 87 | 88 | var releaseEvent = function () { 89 | if (interval == null) 90 | startInterval(); 91 | $(document).unbind("mousemove", dragEvent); 92 | $(document).unbind("mouseup", releaseEvent); 93 | }; 94 | 95 | $(document).bind("mousemove", dragEvent).bind("mouseup", releaseEvent); 96 | 97 | }).dblclick(function () { 98 | if (interval != null) { 99 | window.clearInterval(interval); 100 | interval = null; 101 | } 102 | $(this).unbind("mousedown"); 103 | $(this).css("background", "url(//a.thumbs.redditmedia.com/P0tVLpH52smMNXPN.png)"); 104 | interval = window.setInterval(function () { 105 | var newTop = oldTop + Math.max(50, Math.min(10, oldTop * 0.05)); 106 | floater.css({ 107 | "top": newTop + "px" 108 | }); 109 | oldTop = newTop; 110 | if (oldTop + height >= documentHeight) { 111 | window.clearInterval(interval); 112 | interval = null; 113 | floater.css({ 114 | "top": (documentHeight - height) + "px" 115 | }); 116 | floater.css("background", "url(//d.thumbs.redditmedia.com/tIFOjQQ5pPahJKP-.png)"); 117 | 118 | // unlock achievement 119 | TB.utils.sendEvent(TB.utils.events.TB_KILL_SNOO); 120 | } 121 | }, 50); 122 | }); 123 | 124 | $(this).css("background", "transparent"); 125 | startInterval(); 126 | }); 127 | }; 128 | 129 | TB.register_module(self); 130 | } 131 | 132 | (function() { 133 | window.addEventListener("TBModuleLoaded", function () { 134 | flyingsnoo(); 135 | }); 136 | })(); 137 | -------------------------------------------------------------------------------- /extension/data/styles/usernotes.css: -------------------------------------------------------------------------------- 1 | body.mod-toolbox.mod-toolbox-new-modmail .InfoBar__recents.tb-recents .InfoBar__recent { 2 | margin-bottom: 3px; 3 | } 4 | 5 | body.mod-toolbox.mod-toolbox-new-modmail .usernote-button { 6 | vertical-align: middle; 7 | margin-right: 3px; 8 | } 9 | 10 | body.mod-toolbox.mod-toolbox-new-modmail .tb-general-button, 11 | body.mod-toolbox.mod-toolbox-new-modmail li a.tb-general-button, 12 | body.mod-toolbox.mod-toolbox-new-modmail .tb-bracket-button { 13 | display: inline-block; 14 | height: 12px; 15 | line-height: 12px; 16 | font-size: 9px; 17 | padding: 0 2px; 18 | border-width: 0 0 0 3px; 19 | } 20 | body.mod-toolbox.mod-toolbox-new-modmail .tb-popup a { 21 | text-decoration: none; 22 | } 23 | 24 | body.mod-toolbox.mod-toolbox-new-modmail .tb-recents .tb-bracket-button { 25 | border-width: 0 0 0 3px; 26 | padding: 3px; 27 | font-size: 1.2em; 28 | } 29 | 30 | /* Note styling */ 31 | 32 | .usernote-button { 33 | margin-left: 2px; 34 | 35 | font-size: x-small; 36 | color: #888888; 37 | } 38 | 39 | .usernote-button #add-user-tag { 40 | cursor: pointer; 41 | } 42 | 43 | .usernote-button .add-user-tag:hover { 44 | text-decoration: none; 45 | } 46 | 47 | /* Popup styling */ 48 | 49 | .utagger-popup .right { 50 | float: right; 51 | } 52 | 53 | .utagger-popup .left { 54 | float: left; 55 | } 56 | 57 | .utagger-popup .utagger-date { 58 | font-size: 80%; 59 | } 60 | 61 | .utagger-popup .utagger-notes { 62 | width: 100%; 63 | } 64 | 65 | .utagger-popup .utagger-notes td { 66 | padding: 2px !important; 67 | border: solid 1px #C1BFBF; 68 | vertical-align: top; 69 | } 70 | 71 | .utagger-popup .utagger-notes-td1 { 72 | width: 65px; 73 | } 74 | 75 | .utagger-popup .utagger-notes-td3 { 76 | width: 5px; 77 | border: 0; 78 | } 79 | 80 | .utagger-popup .utagger-notes-td3 a { 81 | color: #C92A2A; 82 | font-weight: bold; 83 | } 84 | 85 | .utagger-popup .utagger-note .note-type { 86 | margin-right: 2px; 87 | } 88 | 89 | .utagger-popup .utagger-remove-note { 90 | cursor: pointer; 91 | } 92 | 93 | .utagger-popup .utagger-types { 94 | display: inline-block; 95 | width: 100%; 96 | margin-top: 5px; 97 | margin-bottom: 6px; 98 | border-top: 0; 99 | padding-top: 4px; 100 | } 101 | 102 | .utagger-popup .utagger-type input { 103 | display: none; 104 | } 105 | 106 | .utagger-popup .utagger-type span { 107 | padding: 3px; 108 | border: 1px solid rgb(114, 134, 154); 109 | transition: backgroun-color 0.15s ease-out; 110 | } 111 | 112 | .utagger-popup td:not(:last-child) .utagger-type span { 113 | border-right: none; 114 | } 115 | 116 | .utagger-popup .utagger-type span:hover { 117 | background-color: #e2eefa; 118 | cursor: pointer; 119 | } 120 | .utagger-popup .utagger-type input:checked ~ span { 121 | background-color: #e2eefa; 122 | } 123 | 124 | .utagger-popup .utagger-type span:before { 125 | content: '\2713'; 126 | margin-right: 2px; 127 | opacity: 0.5;} 128 | 129 | .utagger-popup .utagger-type span:hover:before { 130 | opacity: 1; 131 | } 132 | 133 | .utagger-popup .utagger-type input:checked ~ span:before { 134 | opacity: 1; 135 | font-weight: bold; 136 | } 137 | 138 | .utagger-popup .utagger-user-note { 139 | width: 98%; 140 | margin-bottom: 2px; 141 | padding: 2px; 142 | } 143 | 144 | .utagger-popup #utagger-user-note-input.error { 145 | border-color: red; 146 | } 147 | 148 | /*.utagger-popup .utagger-type input,*/ 149 | .utagger-popup .utagger-include-link input { 150 | margin-right: 1px; 151 | vertical-align: sub; 152 | } 153 | 154 | .utagger-popup .utagger-include-link input[disabled] ~ span { 155 | color: slategray; 156 | } 157 | 158 | /* Usernote manager */ 159 | 160 | .mod-toolbox a.tb-un-notedelete img { 161 | width: 13px; 162 | opacity: 0.8; 163 | margin-bottom: -3px; 164 | } 165 | 166 | .mod-toolbox .tb-un-user:nth-child(even) { 167 | background: #F6F9FC; 168 | } 169 | 170 | .mod-toolbox .tb-un-user { 171 | padding: 5px; 172 | margin-bottom: 3px; 173 | border: solid 1px #FAFAFA; 174 | } 175 | 176 | .mod-toolbox .tb-un-note-details { 177 | padding: 3px; 178 | } 179 | 180 | .mod-toolbox .tb-un-user-header { 181 | border-bottom: solid 1px #E7E7E7; 182 | margin-bottom: 3px; 183 | } 184 | 185 | .mod-toolbox .tb-un-user-header .tb-un-delete { 186 | margin-right: 4px; 187 | } 188 | 189 | .mod-toolbox .tb-un-user-header .user { 190 | font-size: 12px; 191 | } 192 | 193 | .mod-toolbox #tb-un-note-content-wrap { 194 | max-width: 115em; 195 | margin: 30px; 196 | } 197 | 198 | .mod-toolbox .tb-un-note-details .note { 199 | margin-left: 4px; 200 | } 201 | 202 | .mod-toolbox .tb-un-note-details .note .note-type { 203 | margin-right: 2px; 204 | } 205 | 206 | .mod-toolbox .tb-un-user-header .mod, 207 | .mod-toolbox .tb-un-note-details .date { 208 | color: #A8A8A8; 209 | } 210 | 211 | .mod-toolbox .tb-un-user-header .tb-un-notedelete { 212 | margin-right: 4px; 213 | } 214 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/addon/dialog.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | // Open simple dialogs on top of an editor. Relies on dialog.css. 5 | 6 | (function(mod) { 7 | if (typeof exports == "object" && typeof module == "object") // CommonJS 8 | mod(require("../../lib/codemirror")); 9 | else if (typeof define == "function" && define.amd) // AMD 10 | define(["../../lib/codemirror"], mod); 11 | else // Plain browser env 12 | mod(CodeMirror); 13 | })(function(CodeMirror) { 14 | function dialogDiv(cm, template, bottom) { 15 | var wrap = cm.getWrapperElement(); 16 | var dialog; 17 | dialog = wrap.appendChild(document.createElement("div")); 18 | if (bottom) 19 | dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom"; 20 | else 21 | dialog.className = "CodeMirror-dialog CodeMirror-dialog-top"; 22 | 23 | if (typeof template == "string") { 24 | dialog.innerHTML = template; 25 | } else { // Assuming it's a detached DOM element. 26 | dialog.appendChild(template); 27 | } 28 | return dialog; 29 | } 30 | 31 | function closeNotification(cm, newVal) { 32 | if (cm.state.currentNotificationClose) 33 | cm.state.currentNotificationClose(); 34 | cm.state.currentNotificationClose = newVal; 35 | } 36 | 37 | CodeMirror.defineExtension("openDialog", function(template, callback, options) { 38 | if (!options) options = {}; 39 | 40 | closeNotification(this, null); 41 | 42 | var dialog = dialogDiv(this, template, options.bottom); 43 | var closed = false, me = this; 44 | function close(newVal) { 45 | if (typeof newVal == 'string') { 46 | inp.value = newVal; 47 | } else { 48 | if (closed) return; 49 | closed = true; 50 | dialog.parentNode.removeChild(dialog); 51 | me.focus(); 52 | 53 | if (options.onClose) options.onClose(dialog); 54 | } 55 | } 56 | 57 | var inp = dialog.getElementsByTagName("input")[0], button; 58 | if (inp) { 59 | inp.focus(); 60 | 61 | if (options.value) { 62 | inp.value = options.value; 63 | if (options.selectValueOnOpen !== false) { 64 | inp.select(); 65 | } 66 | } 67 | 68 | if (options.onInput) 69 | CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);}); 70 | if (options.onKeyUp) 71 | CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);}); 72 | 73 | CodeMirror.on(inp, "keydown", function(e) { 74 | if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; } 75 | if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) { 76 | inp.blur(); 77 | CodeMirror.e_stop(e); 78 | close(); 79 | } 80 | if (e.keyCode == 13) callback(inp.value, e); 81 | }); 82 | 83 | if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close); 84 | } else if (button = dialog.getElementsByTagName("button")[0]) { 85 | CodeMirror.on(button, "click", function() { 86 | close(); 87 | me.focus(); 88 | }); 89 | 90 | if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close); 91 | 92 | button.focus(); 93 | } 94 | return close; 95 | }); 96 | 97 | CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) { 98 | closeNotification(this, null); 99 | var dialog = dialogDiv(this, template, options && options.bottom); 100 | var buttons = dialog.getElementsByTagName("button"); 101 | var closed = false, me = this, blurring = 1; 102 | function close() { 103 | if (closed) return; 104 | closed = true; 105 | dialog.parentNode.removeChild(dialog); 106 | me.focus(); 107 | } 108 | buttons[0].focus(); 109 | for (var i = 0; i < buttons.length; ++i) { 110 | var b = buttons[i]; 111 | (function(callback) { 112 | CodeMirror.on(b, "click", function(e) { 113 | CodeMirror.e_preventDefault(e); 114 | close(); 115 | if (callback) callback(me); 116 | }); 117 | })(callbacks[i]); 118 | CodeMirror.on(b, "blur", function() { 119 | --blurring; 120 | setTimeout(function() { if (blurring <= 0) close(); }, 200); 121 | }); 122 | CodeMirror.on(b, "focus", function() { ++blurring; }); 123 | } 124 | }); 125 | 126 | /* 127 | * openNotification 128 | * Opens a notification, that can be closed with an optional timer 129 | * (default 5000ms timer) and always closes on click. 130 | * 131 | * If a notification is opened while another is opened, it will close the 132 | * currently opened one and open the new one immediately. 133 | */ 134 | CodeMirror.defineExtension("openNotification", function(template, options) { 135 | closeNotification(this, close); 136 | var dialog = dialogDiv(this, template, options && options.bottom); 137 | var closed = false, doneTimer; 138 | var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000; 139 | 140 | function close() { 141 | if (closed) return; 142 | closed = true; 143 | clearTimeout(doneTimer); 144 | dialog.parentNode.removeChild(dialog); 145 | } 146 | 147 | CodeMirror.on(dialog, 'click', function(e) { 148 | CodeMirror.e_preventDefault(e); 149 | close(); 150 | }); 151 | 152 | if (duration) 153 | doneTimer = setTimeout(close, duration); 154 | 155 | return close; 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Moderator toolbox for reddit", 4 | "author": "toolbox team", 5 | "short_name": "toolbox", 6 | "description": "A set of tools to be used by moderators on reddit in order to make their jobs easier.", 7 | "version": "3.6.4", 8 | "options_page": "data/background/options.html", 9 | "applications": { 10 | "gecko": { 11 | "id": "yes@jetpack", 12 | "strict_min_version": "46.0" 13 | } 14 | }, 15 | "permissions": [ 16 | "https://*.reddit.com/", 17 | "http://*.reddit.com/", 18 | "cookies", 19 | "tabs", 20 | "storage", 21 | "unlimitedStorage" 22 | ], 23 | "icons": { 24 | "16": "data/images/icon16.png", 25 | "48": "data/images/icon48.png", 26 | "128": "data/images/icon128.png" 27 | }, 28 | "background": { 29 | "scripts": [ 30 | "data/libs/jquery-3.1.1.js", 31 | "data/background/chrome.js" 32 | ], 33 | "persistent": false 34 | }, 35 | "content_scripts": [ 36 | { 37 | "run_at": "document_end", 38 | "matches": [ 39 | "http://*.reddit.com/*", 40 | "https://*.reddit.com/*" 41 | ], 42 | "css": [ 43 | "data/styles/spectrum.css", 44 | "data/styles/toolbox.css", 45 | "data/styles/support.css", 46 | "data/styles/comment.css", 47 | "data/styles/modmatrix.css", 48 | "data/styles/removalreasons.css", 49 | "data/styles/personalnotes.css", 50 | "data/styles/queuetools.css", 51 | "data/styles/modmailpro.css", 52 | "data/styles/achievements.css", 53 | "data/styles/modbar.css", 54 | "data/styles/historybutton.css", 55 | "data/styles/trouble.css", 56 | "data/styles/notifier.css", 57 | "data/styles/domaintagger.css", 58 | "data/styles/usernotes.css", 59 | "data/styles/config.css", 60 | "data/styles/codemirror/codemirror.css", 61 | "data/styles/codemirror/dialog.css", 62 | "data/styles/codemirror/fullscreen.css", 63 | "data/styles/codemirror/matchesonscrollbar.css", 64 | "data/styles/codemirror/show-hint.css", 65 | "data/styles/codemirror/themes.css" 66 | ], 67 | "js": [ 68 | "data/libs/jquery-3.1.1.js", 69 | "data/libs/jquery_migrate.js", 70 | "data/libs/tbplugins.js", 71 | "data/libs/snuownd.js", 72 | "data/libs/codemirror/codemirror.js", 73 | "data/libs/codemirror/addon/closebrackets.js", 74 | "data/libs/codemirror/addon/css-hint.js", 75 | "data/libs/codemirror/addon/dialog.js", 76 | "data/libs/codemirror/addon/fullscreen.js", 77 | "data/libs/codemirror/addon/jump-to-line.js", 78 | "data/libs/codemirror/addon/matchesonscrollbar.js", 79 | "data/libs/codemirror/addon/match-highlighter.js", 80 | "data/libs/codemirror/addon/annotatescrollbar.js", 81 | "data/libs/codemirror/addon/panel.js", 82 | "data/libs/codemirror/addon/rulers.js", 83 | "data/libs/codemirror/addon/search.js", 84 | "data/libs/codemirror/addon/searchcursor.js", 85 | "data/libs/codemirror/addon/show-hint.js", 86 | "data/libs/codemirror/mode/css.js", 87 | "data/libs/codemirror/mode/markdown.js", 88 | "data/libs/codemirror/mode/yaml.js", 89 | "data/libs/codemirror/mode/javascript.js", 90 | "data/libs/pako.js", 91 | "data/libs/redditapi.js", 92 | "data/libs/spectrum.js", 93 | "data/tbstorage.js", 94 | "data/tbui.js", 95 | "data/tbutils.js", 96 | "data/tbmodule.js", 97 | "data/modules/support.js", 98 | "data/modules/modbar.js", 99 | "data/modules/modbutton.js", 100 | "data/modules/notifier.js", 101 | "data/modules/modmailpro.js", 102 | "data/modules/newmodmailpro.js", 103 | "data/modules/domaintagger.js", 104 | "data/modules/usernotes.js", 105 | "data/modules/metricstab.js", 106 | "data/modules/comment.js", 107 | "data/modules/config.js", 108 | "data/modules/modmatrix.js", 109 | "data/modules/banlist.js", 110 | "data/modules/removalreasons.js", 111 | "data/modules/historybutton.js", 112 | "data/modules/syntax.js", 113 | "data/modules/macros.js", 114 | "data/modules/realtime.js", 115 | "data/modules/nukecomments.js", 116 | "data/modules/personalnotes.js", 117 | "data/modules/betterbuttons.js", 118 | "data/modules/bagel.js", 119 | "data/modules/flyingsnoo.js", 120 | "data/modules/achievements.js", 121 | "data/modules/trouble.js", 122 | "data/modules/queuetools.js", 123 | "data/tbmoduleinit.js" 124 | ] 125 | } 126 | ], 127 | "web_accessible_resources": [ 128 | "data/libs/jquery-2.2.4.js", 129 | "data/libs/snuownd.js", 130 | "data/libs/worker-css.js" 131 | ] 132 | } 133 | -------------------------------------------------------------------------------- /extension/data/modules/newmodmailpro.js: -------------------------------------------------------------------------------- 1 | function newmodmailpro() { 2 | var self = new TB.Module('New Mod Mail Pro'); 3 | self.shortname = 'NewModMail'; 4 | 5 | ////Default settings 6 | self.settings['enabled']['default'] = false; 7 | self.config['betamode'] = false; 8 | 9 | self.register_setting('modmaillink', { 10 | 'type': 'selector', 11 | 'values': ['All modmail', 'New', 'In Progress', 'Archived', 'Highlighted', 'Mod Discussions', 'Notifications'], 12 | 'default': 'all_modmail', 13 | 'title': 'Change the modmail link to open a different modmail view by default.' 14 | }); 15 | 16 | self.register_setting('openmailtab', { 17 | 'type': 'boolean', 18 | 'default': true, 19 | 'title': 'Open modmail in a new tab.' 20 | }); 21 | 22 | self.register_setting('lastreplytypecheck', { 23 | 'type': 'boolean', 24 | 'default': true, 25 | 'title': 'Warns you if you reply as yourself but the last reply type is a private mod note or a "as subreddit" reply. ' 26 | }); 27 | 28 | self.register_setting('modmailnightmode', { 29 | 'type': 'boolean', 30 | 'default': false, 31 | 'title': 'Open modmail in nightmode' 32 | }); 33 | 34 | // All stuff we want to do when we are on new modmail 35 | if (TBUtils.isNewModmail) { 36 | // Add a class to body 37 | var $body = $('body'); 38 | 39 | $body.addClass('tb-new-modmail'); 40 | 41 | // ready some variables. 42 | var modMailNightmode = self.setting('modmailnightmode'), 43 | lastReplyTypeCheck = self.setting('lastreplytypecheck'); 44 | 45 | 46 | if (lastReplyTypeCheck && TBUtils.isNewMMThread) { 47 | $body.on('click', '.ThreadViewerReplyForm__replyButton', function(event) { 48 | 49 | // Get all mod replies and see if they are something we need to warn the user about. 50 | let $lastReply = $body.find('.Thread__messages .Thread__message:has(.m-mod)').last(); 51 | const replyTypeMyself = $body.find('.FancySelect__valueText').text() === 'Reply as myself'; 52 | 53 | // if it finds this the last mod that replied did so with "as subreddit". 54 | if ($lastReply.find('.icon-profile-slash').length && replyTypeMyself) { 55 | if (confirm('The last mod that replied did so as the subreddit, are you sure you want to reply as yourself?')) { 56 | // Ok, do nothing and let the message be posted. 57 | } else { 58 | // Not ok, prevent the button from being clicked. 59 | event.preventDefault(); 60 | } 61 | 62 | } 63 | 64 | // If it finds this class it means the last reply was a private mod note. 65 | if ($lastReply.find('.Thread__messageIsMod').length && replyTypeMyself) { 66 | if (confirm('The last mod that replied did so with a private mod note, are you sure you want to reply as yourself?')) { 67 | // Ok, do nothing and let the message be posted. 68 | } else { 69 | // Not ok, prevent the button from being clicked. 70 | event.preventDefault(); 71 | } 72 | } 73 | 74 | }); 75 | } 76 | 77 | if (modMailNightmode) { 78 | // Let's make sure RES nightmode doesn't mess things up. 79 | $('html, body').removeClass('res-nightmode'); 80 | 81 | // Now enable toolbox nightmode. 82 | // Firefox can't do simple nightmode so we do it like this 83 | if(TBUtils.browser === 'firefox') { 84 | $('html').addClass('tb-nightmode-firefox'); 85 | $('body').addClass('tb-nightmode-firefox'); 86 | } else { 87 | $('html').addClass('tb-nightmode'); 88 | } 89 | } 90 | } 91 | 92 | // Below all stuff we do when we are NOT on new modmail. 93 | if (!TBUtils.isNewModmail) { 94 | 95 | // ready some variables. 96 | var modmailLink = self.setting('modmaillink'), 97 | openMailTab = self.setting('openmailtab'); 98 | 99 | 100 | // Let's mess around with the link to modmail. 101 | var $newModmailLinkElement = $('#new_modmail'), 102 | newModmailBaseUrl = 'https://mod.reddit.com/mail/'; 103 | 104 | // Open modmail in a new tab if the option is selected 105 | if (openMailTab) { 106 | $newModmailLinkElement.attr('target', '_blank'); 107 | } 108 | 109 | // let's replace urls. 110 | switch(modmailLink) { 111 | case 'all_modmail': 112 | $newModmailLinkElement.attr('href', newModmailBaseUrl + 'all'); 113 | 114 | break; 115 | case 'new': 116 | $newModmailLinkElement.attr('href', newModmailBaseUrl + 'new'); 117 | 118 | break; 119 | case 'in_progress': 120 | $newModmailLinkElement.attr('href', newModmailBaseUrl + 'inprogress'); 121 | 122 | break; 123 | case 'archived': 124 | $newModmailLinkElement.attr('href', newModmailBaseUrl + 'archived'); 125 | 126 | break; 127 | case 'highlighted': 128 | $newModmailLinkElement.attr('href', newModmailBaseUrl + 'highlighted'); 129 | 130 | break; 131 | case 'mod_discussions': 132 | $newModmailLinkElement.attr('href', newModmailBaseUrl + 'mod'); 133 | 134 | break; 135 | case 'notifications': 136 | $newModmailLinkElement.attr('href', newModmailBaseUrl + 'notifications'); 137 | 138 | } 139 | 140 | } 141 | 142 | TB.register_module(self); 143 | } 144 | 145 | (function () { 146 | window.addEventListener("TBModuleLoaded", function () { 147 | newmodmailpro(); 148 | }); 149 | })(); 150 | -------------------------------------------------------------------------------- /extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Author 6 | toolbox Team 7 | Builder Version 8 | 10600.8.9 9 | CFBundleDisplayName 10 | Moderator toolbox for reddit 11 | CFBundleIdentifier 12 | com.reddit.toolbox 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleShortVersionString 16 | 3.6.4 17 | CFBundleVersion 18 | 370 19 | Chrome 20 | 21 | Global Page 22 | data/background/safari.html 23 | 24 | Content 25 | 26 | Scripts 27 | 28 | End 29 | 30 | data/libs/jquery-3.1.1.js 31 | data/libs/tbplugins.js 32 | data/libs/snuownd.js 33 | data/libs/codemirror/codemirror.js 34 | data/libs/codemirror/addon/closebrackets.js 35 | data/libs/codemirror/addon/css-hint.js 36 | data/libs/codemirror/addon/dialog.js 37 | data/libs/codemirror/addon/fullscreen.js 38 | data/libs/codemirror/addon/jump-to-line.js 39 | data/libs/codemirror/addon/matchesonscrollbar.js 40 | data/libs/codemirror/addon/match-highlighter.js 41 | data/libs/codemirror/addon/annotatescrollbar.js 42 | data/libs/codemirror/addon/panel.js 43 | data/libs/codemirror/addon/rulers.js 44 | data/libs/codemirror/addon/search.js 45 | data/libs/codemirror/addon/searchcursor.js 46 | data/libs/codemirror/addon/show-hint.js 47 | data/libs/codemirror/mode/css.js 48 | data/libs/codemirror/mode/markdown.js 49 | data/libs/codemirror/mode/yaml.js 50 | data/libs/codemirror/mode/javascript.js 51 | data/libs/pako.js 52 | data/libs/redditapi.js 53 | data/libs/spectrum.js 54 | data/tbstorage.js 55 | data/tbui.js 56 | data/tbutils.js 57 | data/tbmodule.js 58 | data/modules/support.js 59 | data/modules/modbar.js 60 | data/modules/modbutton.js 61 | data/modules/notifier.js 62 | data/modules/modmailpro.js 63 | data/modules/newmodmailpro.js 64 | data/modules/domaintagger.js 65 | data/modules/usernotes.js 66 | data/modules/metricstab.js 67 | data/modules/comment.js 68 | data/modules/config.js 69 | data/modules/modmatrix.js 70 | data/modules/banlist.js 71 | data/modules/removalreasons.js 72 | data/modules/historybutton.js 73 | data/modules/syntax.js 74 | data/modules/macros.js 75 | data/modules/realtime.js 76 | data/modules/nukecomments.js 77 | data/modules/personalnotes.js 78 | data/modules/betterbuttons.js 79 | data/modules/bagel.js 80 | data/modules/flyingsnoo.js 81 | data/modules/achievements.js 82 | data/modules/trouble.js 83 | data/modules/queuetools.js 84 | data/tbmoduleinit.js 85 | 86 | 87 | Stylesheets 88 | 89 | data/styles/spectrum.css 90 | data/styles/toolbox.css 91 | data/styles/support.css 92 | data/styles/comment.css 93 | data/styles/modmatrix.css 94 | data/styles/removalreasons.css 95 | data/styles/personalnotes.css 96 | data/styles/queuetools.css 97 | data/styles/modmailpro.css 98 | data/styles/achievements.css 99 | data/styles/modbar.css 100 | data/styles/historybutton.css 101 | data/styles/trouble.css 102 | data/styles/notifier.css 103 | data/styles/domaintagger.css 104 | data/styles/usernotes.css 105 | data/styles/config.css 106 | data/styles/codemirror/codemirror.css 107 | data/styles/codemirror/dialog.css 108 | data/styles/codemirror/fullscreen.css 109 | data/styles/codemirror/matchesonscrollbar.css 110 | data/styles/codemirror/show-hint.css 111 | data/styles/codemirror/themes.css 112 | 113 | 114 | Description 115 | A set of tools to be used by moderators on reddit in order to make their jobs easier. 116 | DeveloperIdentifier 117 | 42X34WDLP5 118 | ExtensionInfoDictionaryVersion 119 | 1.0 120 | Permissions 121 | 122 | Website Access 123 | 124 | Allowed Domains 125 | 126 | *.reddit.com 127 | 128 | Include Secure Pages 129 | 130 | Level 131 | Some 132 | 133 | 134 | Update Manifest URL 135 | http://creesch.github.io/reddit-moderator-toolbox/update/UpdateManifest.plist 136 | Website 137 | http://reddit.com/r/toolbox 138 | 139 | 140 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/addon/match-highlighter.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | // Highlighting text that matches the selection 5 | // 6 | // Defines an option highlightSelectionMatches, which, when enabled, 7 | // will style strings that match the selection throughout the 8 | // document. 9 | // 10 | // The option can be set to true to simply enable it, or to a 11 | // {minChars, style, wordsOnly, showToken, delay} object to explicitly 12 | // configure it. minChars is the minimum amount of characters that should be 13 | // selected for the behavior to occur, and style is the token style to 14 | // apply to the matches. This will be prefixed by "cm-" to create an 15 | // actual CSS class name. If wordsOnly is enabled, the matches will be 16 | // highlighted only if the selected text is a word. showToken, when enabled, 17 | // will cause the current token to be highlighted when nothing is selected. 18 | // delay is used to specify how much time to wait, in milliseconds, before 19 | // highlighting the matches. If annotateScrollbar is enabled, the occurences 20 | // will be highlighted on the scrollbar via the matchesonscrollbar addon. 21 | 22 | (function(mod) { 23 | if (typeof exports == "object" && typeof module == "object") // CommonJS 24 | mod(require("../../lib/codemirror"), require("./matchesonscrollbar")); 25 | else if (typeof define == "function" && define.amd) // AMD 26 | define(["../../lib/codemirror", "./matchesonscrollbar"], mod); 27 | else // Plain browser env 28 | mod(CodeMirror); 29 | })(function(CodeMirror) { 30 | "use strict"; 31 | 32 | var defaults = { 33 | style: "matchhighlight", 34 | minChars: 2, 35 | delay: 100, 36 | wordsOnly: false, 37 | annotateScrollbar: false, 38 | showToken: false, 39 | trim: true 40 | } 41 | 42 | function State(options) { 43 | this.options = {} 44 | for (var name in defaults) 45 | this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name] 46 | this.overlay = this.timeout = null; 47 | this.matchesonscroll = null; 48 | this.active = false; 49 | } 50 | 51 | CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) { 52 | if (old && old != CodeMirror.Init) { 53 | removeOverlay(cm); 54 | clearTimeout(cm.state.matchHighlighter.timeout); 55 | cm.state.matchHighlighter = null; 56 | cm.off("cursorActivity", cursorActivity); 57 | cm.off("focus", onFocus) 58 | } 59 | if (val) { 60 | var state = cm.state.matchHighlighter = new State(val); 61 | if (cm.hasFocus()) { 62 | state.active = true 63 | highlightMatches(cm) 64 | } else { 65 | cm.on("focus", onFocus) 66 | } 67 | cm.on("cursorActivity", cursorActivity); 68 | } 69 | }); 70 | 71 | function cursorActivity(cm) { 72 | var state = cm.state.matchHighlighter; 73 | if (state.active || cm.hasFocus()) scheduleHighlight(cm, state) 74 | } 75 | 76 | function onFocus(cm) { 77 | var state = cm.state.matchHighlighter 78 | if (!state.active) { 79 | state.active = true 80 | scheduleHighlight(cm, state) 81 | } 82 | } 83 | 84 | function scheduleHighlight(cm, state) { 85 | clearTimeout(state.timeout); 86 | state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay); 87 | } 88 | 89 | function addOverlay(cm, query, hasBoundary, style) { 90 | var state = cm.state.matchHighlighter; 91 | cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); 92 | if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { 93 | var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query; 94 | state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, 95 | {className: "CodeMirror-selection-highlight-scrollbar"}); 96 | } 97 | } 98 | 99 | function removeOverlay(cm) { 100 | var state = cm.state.matchHighlighter; 101 | if (state.overlay) { 102 | cm.removeOverlay(state.overlay); 103 | state.overlay = null; 104 | if (state.matchesonscroll) { 105 | state.matchesonscroll.clear(); 106 | state.matchesonscroll = null; 107 | } 108 | } 109 | } 110 | 111 | function highlightMatches(cm) { 112 | cm.operation(function() { 113 | var state = cm.state.matchHighlighter; 114 | removeOverlay(cm); 115 | if (!cm.somethingSelected() && state.options.showToken) { 116 | var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken; 117 | var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start; 118 | while (start && re.test(line.charAt(start - 1))) --start; 119 | while (end < line.length && re.test(line.charAt(end))) ++end; 120 | if (start < end) 121 | addOverlay(cm, line.slice(start, end), re, state.options.style); 122 | return; 123 | } 124 | var from = cm.getCursor("from"), to = cm.getCursor("to"); 125 | if (from.line != to.line) return; 126 | if (state.options.wordsOnly && !isWord(cm, from, to)) return; 127 | var selection = cm.getRange(from, to) 128 | if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "") 129 | if (selection.length >= state.options.minChars) 130 | addOverlay(cm, selection, false, state.options.style); 131 | }); 132 | } 133 | 134 | function isWord(cm, from, to) { 135 | var str = cm.getRange(from, to); 136 | if (str.match(/^\w+$/) !== null) { 137 | if (from.ch > 0) { 138 | var pos = {line: from.line, ch: from.ch - 1}; 139 | var chr = cm.getRange(pos, from); 140 | if (chr.match(/\W/) === null) return false; 141 | } 142 | if (to.ch < cm.getLine(from.line).length) { 143 | var pos = {line: to.line, ch: to.ch + 1}; 144 | var chr = cm.getRange(to, pos); 145 | if (chr.match(/\W/) === null) return false; 146 | } 147 | return true; 148 | } else return false; 149 | } 150 | 151 | function boundariesAround(stream, re) { 152 | return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) && 153 | (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos))); 154 | } 155 | 156 | function makeOverlay(query, hasBoundary, style) { 157 | return {token: function(stream) { 158 | if (stream.match(query) && 159 | (!hasBoundary || boundariesAround(stream, hasBoundary))) 160 | return style; 161 | stream.next(); 162 | stream.skipTo(query.charAt(0)) || stream.skipToEnd(); 163 | }}; 164 | } 165 | }); 166 | -------------------------------------------------------------------------------- /extension/data/modules/nukecomments.js: -------------------------------------------------------------------------------- 1 | function nukecomments() { 2 | // Adapted from: 3 | // @name Reddit Mod Nuke Userscript 4 | // @author djimbob (dr jimbob) 5 | 6 | var self = new TB.Module('Comment Nuke'); 7 | self.shortname = 'Nuke'; 8 | 9 | ////Default settings 10 | self.settings['enabled']['default'] = false; 11 | self.config['betamode'] = false; 12 | 13 | self.register_setting('hideAfterNuke', { 14 | 'type': 'boolean', 15 | 'default': false, 16 | 'title': 'Hide nuked comments after they are removed' 17 | }); 18 | 19 | self.register_setting('ignoreMods', { 20 | 'type': 'boolean', 21 | 'default': true, 22 | 'title': 'Ignore moderators\' comments when removing' 23 | }); 24 | 25 | self.register_setting('confirmNuke', { 26 | 'type': 'boolean', 27 | 'default': true, 28 | 'title': 'Show a confirmation window before nuking a comment chain' 29 | }); 30 | 31 | // self.register_setting('useImage', { 32 | // 'type': 'boolean', 33 | // 'default': true, 34 | // 'title': 'Use an image button instead of [R]' 35 | // }); 36 | 37 | 38 | self.init = function () { 39 | // // Image or text? 40 | // if (self.setting('useImage')) { 41 | // self.button = $('') 42 | // .attr('title', 'Nuke!') 43 | // .attr('src', TB.ui.iconNuke) 44 | // .prop('outerHTML'); 45 | // } else { 46 | // self.button = '[R]'; 47 | // } 48 | 49 | self.button = 'R'; 50 | 51 | // Mod button clicked 52 | $('body').on('click', '.nuke-button', function (event) { 53 | var $nukeButton = $(event.target); 54 | var $comment = $nukeButton.closest('.comment'); 55 | 56 | var $continue_thread = $comment.find('span.morecomments>a'); 57 | 58 | 59 | var confirmMessage = "Are you sure you want to nuke the following "; 60 | var $delete_button = $comment.find('form.remove-button input[name="spam"][value="False"]~span.option.error a.yes,a[onclick^="return big_mod_action($(this), -1)"]'); 61 | // form input[value="removed"]~span.option.error a.yes -- finds the yes for normal deleting comments. 62 | // a.pretty-button.neutral finds the 'remove' button for flagged comments 63 | confirmMessage += $delete_button.length; 64 | if ($continue_thread.length > 0) { 65 | confirmMessage += "+ comments (more after expanding collapsed threads; there will be a pause before the first deletion to retrieve more comments)?"; 66 | } else { 67 | confirmMessage += " comments?"; 68 | } 69 | 70 | if (!self.setting('confirmNuke') 71 | || confirm(confirmMessage) 72 | ) { 73 | $continue_thread.each(function (idx, $continue_button) { 74 | // wait a bit before each ajax call 75 | setTimeout(function () { 76 | $continue_button.click(); 77 | }, 2000 * idx); 78 | }); 79 | 80 | // wait a bit after last ajax call before deleting 81 | setTimeout(function () { 82 | self.deleteThreadFromComment($comment); 83 | }, 2000 * ($continue_thread.length + ($continue_thread.length ? 1 : 0))); 84 | } 85 | 86 | 87 | return false; // necessary? 88 | }); 89 | 90 | // https://github.com/reddit/reddit/blob/master/r2/r2/public/static/js/jquery.reddit.js#L531 91 | // $(document).on('new_thing', function(e, thing) { 92 | // // This could be useful... 93 | // }); 94 | 95 | // https://github.com/reddit/reddit/blob/master/r2/r2/public/static/js/jquery.reddit.js#L531 96 | // $(document).on('new_things_inserted', function(e, thing) { 97 | // // eh? 98 | // }); 99 | 100 | 101 | // NER support. 102 | window.addEventListener("TBNewThings", function () { 103 | self.run(); 104 | }); 105 | 106 | self.run(); 107 | }; 108 | 109 | self.deleteThreadFromComment = function ($thread_root) { 110 | var ignoreMods = self.setting('ignoreMods'); 111 | 112 | var $removeButtons = $thread_root.find('form input[value="removed"]~span.option.error a.yes,a[onclick^="return big_mod_action($(this), -1)"]'); 113 | TB.ui.longLoadSpinner(true, 'removing comments', 'neutral'); 114 | self.log("Nuking " + $removeButtons.length + " comments"); 115 | 116 | // we need a delay between every single click of >1sec 117 | // this should be re-written to use the API 118 | TB.utils.forEachChunked($removeButtons, 1, 1500, function remove_comment(button, num) { 119 | var msg = 'removing comment ' + (num + 1) + '/' + $removeButtons.length; 120 | TB.ui.textFeedback(msg, 'neutral'); 121 | 122 | if (ignoreMods) { 123 | var $entry = $(button).parents('.entry'), 124 | $author = $entry.find('a.author'); 125 | 126 | if ($author.hasClass('moderator')) { 127 | self.log(" " + (num + 1) + "... ignored"); 128 | return; 129 | } 130 | } 131 | 132 | self.log(" " + (num + 1) + "... removed"); 133 | button.click(); 134 | }, function complete() { 135 | if (self.setting('hideAfterNuke')) { 136 | $thread_root.hide(750); 137 | } 138 | self.log("kill spinner"); 139 | TB.ui.longLoadSpinner(false); 140 | TB.ui.textFeedback('all comments removed', 'positive'); 141 | }); 142 | }; 143 | 144 | 145 | // Add nuke button to all comments 146 | self.processComment = function (comment, num) { 147 | var $comment = $(comment); 148 | if (!$comment.hasClass('nuke-processed')) { 149 | // Add the class so we don't add buttons twice. 150 | $comment.addClass('nuke-processed'); 151 | 152 | 153 | 154 | // Defer info gathering until button is clicked. 155 | // the report button is always visible, so we don't have to do anything special for the big mod action buttons 156 | $comment.find('.userattrs:first') 157 | .after(' ' + self.button + ''); 158 | } 159 | }; 160 | 161 | // need this for RES NER support 162 | self.run = function () { 163 | // Not a mod, don't bother. 164 | if (!TB.utils.isMod || TB.utils.isModQueuePage) { 165 | //self.log('Not a mod of the sub, d\'oh!'); 166 | return; 167 | } 168 | 169 | var $comments = $('div.comment:not(.nuke-processed)'); 170 | TB.utils.forEachChunked($comments, 15, 650, self.processComment); 171 | }; 172 | 173 | TB.register_module(self); 174 | } // nukecomments() wrapper 175 | 176 | (function() { 177 | window.addEventListener("TBModuleLoaded", function () { 178 | nukecomments(); 179 | }); 180 | })(); 181 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/addon/closebrackets.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | var defaults = { 13 | pairs: "()[]{}''\"\"", 14 | triples: "", 15 | explode: "[]{}" 16 | }; 17 | 18 | var Pos = CodeMirror.Pos; 19 | 20 | CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) { 21 | if (old && old != CodeMirror.Init) { 22 | cm.removeKeyMap(keyMap); 23 | cm.state.closeBrackets = null; 24 | } 25 | if (val) { 26 | cm.state.closeBrackets = val; 27 | cm.addKeyMap(keyMap); 28 | } 29 | }); 30 | 31 | function getOption(conf, name) { 32 | if (name == "pairs" && typeof conf == "string") return conf; 33 | if (typeof conf == "object" && conf[name] != null) return conf[name]; 34 | return defaults[name]; 35 | } 36 | 37 | var bind = defaults.pairs + "`"; 38 | var keyMap = {Backspace: handleBackspace, Enter: handleEnter}; 39 | for (var i = 0; i < bind.length; i++) 40 | keyMap["'" + bind.charAt(i) + "'"] = handler(bind.charAt(i)); 41 | 42 | function handler(ch) { 43 | return function(cm) { return handleChar(cm, ch); }; 44 | } 45 | 46 | function getConfig(cm) { 47 | var deflt = cm.state.closeBrackets; 48 | if (!deflt) return null; 49 | var mode = cm.getModeAt(cm.getCursor()); 50 | return mode.closeBrackets || deflt; 51 | } 52 | 53 | function handleBackspace(cm) { 54 | var conf = getConfig(cm); 55 | if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; 56 | 57 | var pairs = getOption(conf, "pairs"); 58 | var ranges = cm.listSelections(); 59 | for (var i = 0; i < ranges.length; i++) { 60 | if (!ranges[i].empty()) return CodeMirror.Pass; 61 | var around = charsAround(cm, ranges[i].head); 62 | if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; 63 | } 64 | for (var i = ranges.length - 1; i >= 0; i--) { 65 | var cur = ranges[i].head; 66 | cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete"); 67 | } 68 | } 69 | 70 | function handleEnter(cm) { 71 | var conf = getConfig(cm); 72 | var explode = conf && getOption(conf, "explode"); 73 | if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass; 74 | 75 | var ranges = cm.listSelections(); 76 | for (var i = 0; i < ranges.length; i++) { 77 | if (!ranges[i].empty()) return CodeMirror.Pass; 78 | var around = charsAround(cm, ranges[i].head); 79 | if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass; 80 | } 81 | cm.operation(function() { 82 | cm.replaceSelection("\n\n", null); 83 | cm.execCommand("goCharLeft"); 84 | ranges = cm.listSelections(); 85 | for (var i = 0; i < ranges.length; i++) { 86 | var line = ranges[i].head.line; 87 | cm.indentLine(line, null, true); 88 | cm.indentLine(line + 1, null, true); 89 | } 90 | }); 91 | } 92 | 93 | function contractSelection(sel) { 94 | var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0; 95 | return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)), 96 | head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))}; 97 | } 98 | 99 | function handleChar(cm, ch) { 100 | var conf = getConfig(cm); 101 | if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; 102 | 103 | var pairs = getOption(conf, "pairs"); 104 | var pos = pairs.indexOf(ch); 105 | if (pos == -1) return CodeMirror.Pass; 106 | var triples = getOption(conf, "triples"); 107 | 108 | var identical = pairs.charAt(pos + 1) == ch; 109 | var ranges = cm.listSelections(); 110 | var opening = pos % 2 == 0; 111 | 112 | var type; 113 | for (var i = 0; i < ranges.length; i++) { 114 | var range = ranges[i], cur = range.head, curType; 115 | var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1)); 116 | if (opening && !range.empty()) { 117 | curType = "surround"; 118 | } else if ((identical || !opening) && next == ch) { 119 | if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch) 120 | curType = "skipThree"; 121 | else 122 | curType = "skip"; 123 | } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 && 124 | cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch && 125 | (cur.ch <= 2 || cm.getRange(Pos(cur.line, cur.ch - 3), Pos(cur.line, cur.ch - 2)) != ch)) { 126 | curType = "addFour"; 127 | } else if (identical) { 128 | if (!CodeMirror.isWordChar(next) && enteringString(cm, cur, ch)) curType = "both"; 129 | else return CodeMirror.Pass; 130 | } else if (opening && (cm.getLine(cur.line).length == cur.ch || 131 | isClosingBracket(next, pairs) || 132 | /\s/.test(next))) { 133 | curType = "both"; 134 | } else { 135 | return CodeMirror.Pass; 136 | } 137 | if (!type) type = curType; 138 | else if (type != curType) return CodeMirror.Pass; 139 | } 140 | 141 | var left = pos % 2 ? pairs.charAt(pos - 1) : ch; 142 | var right = pos % 2 ? ch : pairs.charAt(pos + 1); 143 | cm.operation(function() { 144 | if (type == "skip") { 145 | cm.execCommand("goCharRight"); 146 | } else if (type == "skipThree") { 147 | for (var i = 0; i < 3; i++) 148 | cm.execCommand("goCharRight"); 149 | } else if (type == "surround") { 150 | var sels = cm.getSelections(); 151 | for (var i = 0; i < sels.length; i++) 152 | sels[i] = left + sels[i] + right; 153 | cm.replaceSelections(sels, "around"); 154 | sels = cm.listSelections().slice(); 155 | for (var i = 0; i < sels.length; i++) 156 | sels[i] = contractSelection(sels[i]); 157 | cm.setSelections(sels); 158 | } else if (type == "both") { 159 | cm.replaceSelection(left + right, null); 160 | cm.triggerElectric(left + right); 161 | cm.execCommand("goCharLeft"); 162 | } else if (type == "addFour") { 163 | cm.replaceSelection(left + left + left + left, "before"); 164 | cm.execCommand("goCharRight"); 165 | } 166 | }); 167 | } 168 | 169 | function isClosingBracket(ch, pairs) { 170 | var pos = pairs.lastIndexOf(ch); 171 | return pos > -1 && pos % 2 == 1; 172 | } 173 | 174 | function charsAround(cm, pos) { 175 | var str = cm.getRange(Pos(pos.line, pos.ch - 1), 176 | Pos(pos.line, pos.ch + 1)); 177 | return str.length == 2 ? str : null; 178 | } 179 | 180 | // Project the token type that will exists after the given char is 181 | // typed, and use it to determine whether it would cause the start 182 | // of a string token. 183 | function enteringString(cm, pos, ch) { 184 | var line = cm.getLine(pos.line); 185 | var token = cm.getTokenAt(pos); 186 | if (/\bstring2?\b/.test(token.type)) return false; 187 | var stream = new CodeMirror.StringStream(line.slice(0, pos.ch) + ch + line.slice(pos.ch), 4); 188 | stream.pos = stream.start = token.start; 189 | for (;;) { 190 | var type1 = cm.getMode().token(stream, token.state); 191 | if (stream.pos >= pos.ch + 1) return /\bstring2?\b/.test(type1); 192 | stream.start = stream.pos; 193 | } 194 | } 195 | }); 196 | -------------------------------------------------------------------------------- /extension/data/modules/trouble.js: -------------------------------------------------------------------------------- 1 | function trouble() { 2 | var self = new TB.Module('Trouble Shooter'); 3 | self.shortname = 'Trouble'; 4 | 5 | //Default settings 6 | self.settings['enabled']['default'] = false; 7 | self.config['betamode'] = true; 8 | 9 | self.register_setting('highlightAuto', { 10 | 'type': 'boolean', 11 | 'default': false, 12 | 'title': 'Highlight comments automatically' 13 | }); 14 | 15 | self.register_setting('negHighlightThreshold', { 16 | 'type': 'number', 17 | 'default': 0, 18 | 'title': 'Negative comment highlight score threshold' 19 | }); 20 | 21 | self.register_setting('highlightControversy', { 22 | 'type': 'boolean', 23 | 'default': true, 24 | 'title': 'Highlight controversial comments' 25 | }); 26 | 27 | self.register_setting('expandOnLoad', { 28 | 'type': 'boolean', 29 | 'default': false, 30 | 'title': 'Expand all downvoted/controversial comments on page load' 31 | }); 32 | 33 | self.register_setting('sortOnMoreChildren', { 34 | 'type': 'boolean', 35 | 'default': false, 36 | 'title': 'Continue to sort children on "load more comments"' 37 | }); 38 | 39 | self.register_setting('displayNChildren', { 40 | 'type': 'boolean', 41 | 'default': true, 42 | 'title': 'Always display the number of children a comment has.' 43 | }); 44 | 45 | self.register_setting('displayNChildrenTop', { 46 | 'type': 'boolean', 47 | 'default': false, 48 | 'advanced': true, 49 | 'title': 'Display the number of children a comment has in the upper left. This may change the normal flow of the comments page slightly.' 50 | }); 51 | 52 | self.sorted = false; 53 | self.pending = []; 54 | 55 | self.init = function() { 56 | var neg_thresh_pref = self.setting('negHighlightThreshold'), 57 | highlightControversy = self.setting('highlightControversy'), 58 | expand = self.setting('expandOnLoad'), 59 | auto = self.setting('highlightAuto'), 60 | sortOnMoreChildren = self.setting('sortOnMoreChildren'), 61 | nChildren = self.setting('displayNChildren'), 62 | nChildrenTop = self.setting('displayNChildrenTop'), 63 | $body = $('body'), 64 | $buttons = $('
    '), 65 | $init_btn = $('').click(start), 66 | $sitetable; 67 | 68 | if(!TBUtils.isMod) return; 69 | 70 | if(!TBUtils.isCommentsPage) return; 71 | 72 | if($body.hasClass('listing-page')){ 73 | $sitetable = $('.content').children('.sitetable'); 74 | } else { 75 | $sitetable = $('.commentarea').children('.sitetable'); 76 | } 77 | 78 | $sitetable.before($buttons); 79 | 80 | if(auto){ 81 | start(); 82 | } else { 83 | $buttons.append($init_btn); 84 | } 85 | 86 | function start(){ 87 | 88 | $init_btn.remove(); 89 | 90 | $body.addClass('tb-trouble'); 91 | if(highlightControversy) $body.addClass('tb-controversy-hl'); 92 | if(nChildren) $body.addClass('tb-nchildren'); 93 | if(nChildrenTop) $body.addClass('tb-nchildrentop'); 94 | 95 | $buttons.append($('').click(sortChildren)) 96 | .append($('').click(collapseNonDrama)); 97 | 98 | if(sortOnMoreChildren){ 99 | $('.commentarea').on('click', '.morecomments', function(){ 100 | if(self.sorted) self.pending.push(sortMe.bind($(this).closest('.sitetable'))); 101 | }); 102 | } 103 | window.addEventListener("TBNewThings", function () { 104 | run(); 105 | }); 106 | 107 | run(); 108 | 109 | if (expand) $('.thing.tb-controversy, .thing.tb-ncontroversy').each(uncollapseThing); 110 | } 111 | 112 | function run() { 113 | var $things = $('.thing.comment').not('.tb-pc-proc'); 114 | 115 | highlightComments($things); 116 | 117 | while (self.pending.length) self.pending.pop()(); 118 | 119 | if (expand) $('.thing.tb-controversy, .thing.tb-ncontroversy').not('.tb-pc-proc').each(uncollapseThing); 120 | 121 | markProcessedThings(); 122 | } 123 | 124 | function highlightComments($things){ 125 | var controversial = new RegExp(/\bcontroversial\b/); 126 | 127 | $things.find('.numchildren').each(numChildren); 128 | 129 | $things.find('.score.unvoted').each(score); 130 | 131 | if(highlightControversy){ 132 | $things.filter(function(){ return controversial.test(this.className); }) 133 | .children('.entry').addClass('tb-controversy') 134 | .parents('.thing').addClass('tb-controversy'); 135 | } 136 | } 137 | 138 | function score(){ 139 | var $this = $(this), 140 | $thing = $this.closest('.thing'), 141 | neg_thresh = neg_thresh_pref; 142 | 143 | //lower the threashold by one for user's comments 144 | if(RegExp("\/"+TBUtils.logged+"\\b").test($thing.children('.entry').find('.author')[0].href)) --neg_thresh; 145 | 146 | //highlighting here to avoid another .each() iteration 147 | if( ($thing[0].dataset.score = $this.text().match(/^(-)?\d+/)[0]) <= neg_thresh ){ 148 | $thing.addClass('tb-neg tb-ncontroversy') 149 | .parents('.thing').addClass('tb-ncontroversy'); 150 | } 151 | } 152 | 153 | function numChildren(){ 154 | var $this = $(this); 155 | 156 | $this.closest('.thing')[0].dataset.nchildren = $this.text().match(/\d+/)[0]; 157 | } 158 | 159 | function sortChildren(){ 160 | 161 | self.sorted = true; 162 | 163 | sortMe.call($(this).closest('.sitetable, .commentarea, .content').children('.sitetable')); 164 | } 165 | 166 | function fixFlatNER($this){ 167 | var $NERs = $this.find('.linklisting'); 168 | if(!$NERs.length) return; 169 | 170 | $this.append($NERs.children('.thing')); 171 | $('.NERPageMarker, .clearleft + .clearleft').remove(); 172 | } 173 | 174 | function sortMe(){ 175 | var $this = $(this), 176 | $things; 177 | 178 | fixFlatNER($this); 179 | 180 | $things = $this.children('.thing').not('.morechildren') 181 | .sort(function(a, b){ 182 | return (b.dataset.nchildren - a.dataset.nchildren); 183 | }); 184 | 185 | $this.prepend($things) 186 | .prepend($this.children('.thing.tb-controversy')) 187 | .prepend($this.children('.thing.tb-ncontroversy')); 188 | 189 | $things.children('.child').children('.sitetable').each(sortMe); 190 | } 191 | 192 | function collapseThing(){ 193 | $(this).addClass('collapsed').children('.entry').find('.expand').text('[+]'); 194 | } 195 | 196 | function uncollapseThing(){ 197 | $(this).removeClass('collapsed').children('.entry').find('.expand').text('[–]'); 198 | } 199 | 200 | function markProcessedThings(){ 201 | $('.thing').not('.tb-pc-proc').addClass('tb-pc-proc'); 202 | } 203 | 204 | function collapseNonDrama(){ 205 | 206 | $('.thing.tb-controversy, .thing.tb-ncontroversy').each(uncollapseThing); 207 | 208 | $('.commentarea').add($('.thing.tb-controversy, .thing.tb-ncontroversy').children('.child')) 209 | .children('.sitetable').children('.thing').not('.tb-controversy, .tb-ncontroversy') 210 | .each(collapseThing);//collapsing only top-level-most comment children of drama 211 | } 212 | /* TODO 213 | 214 | Include below threshold comments when the score is hidden? 215 | 216 | */ 217 | }; 218 | 219 | TB.register_module(self); 220 | } 221 | 222 | (function() { 223 | window.addEventListener("TBModuleLoaded", function () { 224 | trouble(); //run 225 | }); 226 | })(); 227 | -------------------------------------------------------------------------------- /extension/data/styles/removalreasons.css: -------------------------------------------------------------------------------- 1 | .mod-toolbox .reason-popup { 2 | padding: 10px 15px !important; 3 | background-color: rgba(250, 250, 250, 0.84); 4 | border: 0; 5 | border-radius: 0; 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | right: 0; 10 | bottom: 0; 11 | overflow: auto; 12 | z-index: 2147483647 !important; 13 | font-size: 11px; 14 | } 15 | 16 | .reason-popup-header { 17 | right: 0; 18 | left: 0; 19 | display: block; 20 | background-color: #CEE3F8; 21 | text-align: center; 22 | font-weight: bold; 23 | padding: 4px !important; 24 | font-size: small; 25 | } 26 | 27 | .reason-popup-innercontent { 28 | overflow: auto; 29 | left: 0; 30 | right: 0; 31 | padding: 5px !important; 32 | } 33 | 34 | .reason-popup-footer { 35 | width: 100%; 36 | height: 30px; 37 | line-height: 30px; 38 | display: block; 39 | background-color: #CEE3F8; 40 | text-align: center; 41 | } 42 | 43 | .reason-popup input,.reason-popup select,.reason-popup textarea { 44 | font-size: x-small; 45 | border: 1px solid #ccc; 46 | background-color: #fafafa; 47 | } 48 | 49 | .reason-popup input[type="checkbox"],.reason-popup input[type="radio"] { 50 | /* CENTER DAT SHIT */ 51 | vertical-align: text-bottom; 52 | margin-right: 2px; 53 | margin-top: 0; 54 | } 55 | 56 | .reason-popup textarea { 57 | min-width: 400px; 58 | } 59 | 60 | .reason-popup #reason-table { 61 | border-left: solid 1px transparent; 62 | border-right: solid 1px transparent; 63 | } 64 | 65 | .reason-popup #reason-table.error-highlight { 66 | border: solid 1px red; 67 | } 68 | 69 | .reason-popup thead th { 70 | padding: 3px; 71 | white-space: nowrap; 72 | font-weight: bold; 73 | border-bottom: 1px dotted rgb(187, 187, 187); 74 | } 75 | 76 | .reason-popup tbody { 77 | border-bottom: 1px dotted #BBBBBB; 78 | } 79 | 80 | .reason-popup tbody tr { 81 | margin-top: 2px; 82 | vertical-align: top; 83 | } 84 | 85 | .reason-popup tbody td p { 86 | margin-top: 2px; 87 | } 88 | 89 | .reason-popup .reason-check { 90 | margin: 4px 6px 6px 6px !important; 91 | } 92 | 93 | .reason-popup .reason-num { 94 | width: 15px; 95 | height: 15px; 96 | margin-left: 2px; 97 | margin-bottom: 6px; 98 | display: inline-block; 99 | text-align: center; 100 | font-size: 10px; 101 | color: #D3D3D3; 102 | } 103 | 104 | .reason-popup .selectable-reason.reason-selected .reason-num { 105 | color: black; 106 | } 107 | 108 | .reason-popup select { 109 | max-width: 200px; 110 | } 111 | 112 | .reason-popup .styled-reason { 113 | margin-top: 4px; 114 | padding: 1px 3px 3px 3px; 115 | border: 0; 116 | } 117 | 118 | .reason-popup .selectable-reason:last-child .styled-reason { 119 | margin-bottom: 4px; 120 | } 121 | 122 | .mod-toolbox tr.removal-reason { 123 | border-bottom: solid 5px #F0F0F0; 124 | } 125 | 126 | .reason-popup tr.selectable-reason { 127 | border-bottom: solid 1px #D3D3D3; 128 | } 129 | 130 | .reason-popup tr.selectable-reason td:nth-child(1) { 131 | padding-top: 5px !important; 132 | } 133 | 134 | .reason-popup .selectable-reason.reason-selected .styled-reason { 135 | background: #B1DDAA; 136 | } 137 | 138 | .mod-toolbox .removal-reason-title { 139 | margin-top: 9px; 140 | margin-left: 6px; 141 | font-size: 14px; 142 | font-family: "Lucida Console", Monaco, monospace; 143 | } 144 | 145 | .mod-toolbox .reason-popup #buttons ul { 146 | list-style-type: none; 147 | margin: 5px; 148 | } 149 | 150 | .mod-toolbox .reason-popup #buttons ul li { 151 | margin-top: 5px; 152 | } 153 | 154 | .mod-toolbox .reason-popup #buttons ul li ul { 155 | margin-left: 15px; 156 | margin-bottom: 5px; 157 | } 158 | 159 | .reason-popup .reason-content { 160 | padding-left: 8px; 161 | } 162 | 163 | .reason-popup #header-reason, .reason-popup #footer-reason { 164 | border: 0; 165 | padding: 3px; 166 | } 167 | 168 | .reason-popup #buttons { 169 | margin-top: 2px; 170 | padding: 6px 0 6px 2px; 171 | vertical-align: middle; 172 | border: solid 1px transparent; 173 | } 174 | 175 | .reason-popup #buttons.error-highlight { 176 | border-color: red; 177 | } 178 | 179 | .reason-popup #log-reason { 180 | border-top: 1px dotted #BBBBBB; 181 | padding-top: 6px; 182 | } 183 | 184 | .reason-popup #log-reason-input { 185 | padding-left: 1px; 186 | min-width: 365px; 187 | } 188 | 189 | .reason-popup #log-reason-input.error-highlight { 190 | border-color: red; 191 | } 192 | 193 | .reason-popup #log-reason p { 194 | margin-bottom: 4px; 195 | } 196 | 197 | .reason-popup .reason-popup-content { 198 | position: relative; 199 | top: 40px; 200 | max-width: 1000px; 201 | padding: 0 !important; 202 | background-color: #FFF; 203 | box-shadow: 0 1px 3px 1px #D6D6D6; 204 | margin-left: auto !important; 205 | margin-right: auto !important; 206 | margin-bottom: 50px; 207 | font-size: 11px; 208 | } 209 | 210 | .reason-popup .reason-popup-content span p { 211 | padding: 3px !important; 212 | } 213 | 214 | .reason-popup strong { 215 | font-weight: bold; 216 | } 217 | 218 | .reason-popup em { 219 | font-style: oblique; 220 | } 221 | 222 | /* Nightmode */ 223 | 224 | .mod-toolbox.res-nightmode .reason-popup { 225 | background-color: rgba(250, 250, 250, 0.84); 226 | color: black; 227 | } 228 | 229 | .res-nightmode .reason-popup-header { 230 | background-color: #6e7a86; 231 | border-bottom: solid 1px #39526b; 232 | } 233 | 234 | .res-nightmode .reason-popup-footer { 235 | background-color: #6e7a86; 236 | border-top: solid 1px #39526b; 237 | } 238 | 239 | .res-nightmode .reason-popup input,.reason-popup select,.reason-popup textarea { 240 | border: 1px solid #ccc; 241 | background-color: #fafafa; 242 | } 243 | 244 | .res-nightmode .reason-popup #reason-table.error-highlight { 245 | border: solid 1px red; 246 | } 247 | 248 | .res-nightmode .reason-popup thead th { 249 | border-bottom: 1px dotted rgb(187, 187, 187); 250 | } 251 | 252 | .res-nightmode .reason-popup tbody { 253 | border-bottom: 1px dotted #BBBBBB; 254 | } 255 | 256 | .res-nightmode .reason-popup .reason-num { 257 | color: #D3D3D3; 258 | } 259 | 260 | .res-nightmode .reason-popup .selectable-reason.reason-selected .reason-num { 261 | color: black; 262 | } 263 | 264 | .res-nightmode .reason-popup .styled-reason { 265 | border: 0 !important; 266 | padding: 1px 3px 3px 3px !important; 267 | } 268 | 269 | .res-nightmode .reason-popup .selectable-reason.reason-selected .styled-reason { 270 | background: #B1DDAA; 271 | } 272 | 273 | .res-nightmode .reason-popup #header-reason, .reason-popup #footer-reason { 274 | border-color: #E9E9E9 !important; 275 | } 276 | 277 | .res-nightmode .reason-popup #buttons.error-highlight { 278 | border-color: red; 279 | } 280 | 281 | .res-nightmode .reason-popup #log-reason { 282 | border-top: 1px dotted #BBBBBB; 283 | padding-top: 6px; 284 | } 285 | 286 | .res-nightmode .reason-popup #log-reason-input.error-highlight { 287 | border-color: red; 288 | } 289 | 290 | .res-nightmode .reason-popup .reason-popup-content { 291 | background-color: #878787; 292 | border: solid 1px #808080; 293 | box-shadow: 0 1px 3px 1px rgba(83, 83, 83, 0.61); 294 | } 295 | 296 | .mod-toolbox.res-nightmode tr.removal-reason { 297 | border-bottom: solid 5px #7A7A7A; 298 | } 299 | 300 | /* 3.0 fixes */ 301 | 302 | .mod-toolbox #removal-reasons-table { 303 | width: 99%; 304 | } 305 | 306 | .mod-toolbox .removal-toggle { 307 | width: 40px; 308 | } 309 | 310 | .mod-toolbox .flair-text { 311 | width: 75px; 312 | } 313 | 314 | .mod-toolbox .flair-css { 315 | width: 50px; 316 | overflow: hidden; 317 | } 318 | 319 | .mod-toolbox td.flair-css, 320 | .mod-toolbox td.flair-text { 321 | padding-top: 8px; 322 | } 323 | 324 | .mod-toolbox .reason-popup .reason-check { 325 | margin: 4px 0 0 3px !important; 326 | display: inline; 327 | } 328 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/addon/searchcursor.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | var Pos = CodeMirror.Pos; 14 | 15 | function SearchCursor(doc, query, pos, caseFold) { 16 | this.atOccurrence = false; this.doc = doc; 17 | if (caseFold == null && typeof query == "string") caseFold = false; 18 | 19 | pos = pos ? doc.clipPos(pos) : Pos(0, 0); 20 | this.pos = {from: pos, to: pos}; 21 | 22 | // The matches method is filled in based on the type of query. 23 | // It takes a position and a direction, and returns an object 24 | // describing the next occurrence of the query, or null if no 25 | // more matches were found. 26 | if (typeof query != "string") { // Regexp match 27 | if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g"); 28 | this.matches = function(reverse, pos) { 29 | if (reverse) { 30 | query.lastIndex = 0; 31 | var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start; 32 | for (;;) { 33 | query.lastIndex = cutOff; 34 | var newMatch = query.exec(line); 35 | if (!newMatch) break; 36 | match = newMatch; 37 | start = match.index; 38 | cutOff = match.index + (match[0].length || 1); 39 | if (cutOff == line.length) break; 40 | } 41 | var matchLen = (match && match[0].length) || 0; 42 | if (!matchLen) { 43 | if (start == 0 && line.length == 0) {match = undefined;} 44 | else if (start != doc.getLine(pos.line).length) { 45 | matchLen++; 46 | } 47 | } 48 | } else { 49 | query.lastIndex = pos.ch; 50 | var line = doc.getLine(pos.line), match = query.exec(line); 51 | var matchLen = (match && match[0].length) || 0; 52 | var start = match && match.index; 53 | if (start + matchLen != line.length && !matchLen) matchLen = 1; 54 | } 55 | if (match && matchLen) 56 | return {from: Pos(pos.line, start), 57 | to: Pos(pos.line, start + matchLen), 58 | match: match}; 59 | }; 60 | } else { // String query 61 | var origQuery = query; 62 | if (caseFold) query = query.toLowerCase(); 63 | var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;}; 64 | var target = query.split("\n"); 65 | // Different methods for single-line and multi-line queries 66 | if (target.length == 1) { 67 | if (!query.length) { 68 | // Empty string would match anything and never progress, so 69 | // we define it to match nothing instead. 70 | this.matches = function() {}; 71 | } else { 72 | this.matches = function(reverse, pos) { 73 | if (reverse) { 74 | var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig); 75 | var match = line.lastIndexOf(query); 76 | if (match > -1) { 77 | match = adjustPos(orig, line, match); 78 | return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)}; 79 | } 80 | } else { 81 | var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig); 82 | var match = line.indexOf(query); 83 | if (match > -1) { 84 | match = adjustPos(orig, line, match) + pos.ch; 85 | return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)}; 86 | } 87 | } 88 | }; 89 | } 90 | } else { 91 | var origTarget = origQuery.split("\n"); 92 | this.matches = function(reverse, pos) { 93 | var last = target.length - 1; 94 | if (reverse) { 95 | if (pos.line - (target.length - 1) < doc.firstLine()) return; 96 | if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return; 97 | var to = Pos(pos.line, origTarget[last].length); 98 | for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln) 99 | if (target[i] != fold(doc.getLine(ln))) return; 100 | var line = doc.getLine(ln), cut = line.length - origTarget[0].length; 101 | if (fold(line.slice(cut)) != target[0]) return; 102 | return {from: Pos(ln, cut), to: to}; 103 | } else { 104 | if (pos.line + (target.length - 1) > doc.lastLine()) return; 105 | var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length; 106 | if (fold(line.slice(cut)) != target[0]) return; 107 | var from = Pos(pos.line, cut); 108 | for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln) 109 | if (target[i] != fold(doc.getLine(ln))) return; 110 | if (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return; 111 | return {from: from, to: Pos(ln, origTarget[last].length)}; 112 | } 113 | }; 114 | } 115 | } 116 | } 117 | 118 | SearchCursor.prototype = { 119 | findNext: function() {return this.find(false);}, 120 | findPrevious: function() {return this.find(true);}, 121 | 122 | find: function(reverse) { 123 | var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to); 124 | function savePosAndFail(line) { 125 | var pos = Pos(line, 0); 126 | self.pos = {from: pos, to: pos}; 127 | self.atOccurrence = false; 128 | return false; 129 | } 130 | 131 | for (;;) { 132 | if (this.pos = this.matches(reverse, pos)) { 133 | this.atOccurrence = true; 134 | return this.pos.match || true; 135 | } 136 | if (reverse) { 137 | if (!pos.line) return savePosAndFail(0); 138 | pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length); 139 | } 140 | else { 141 | var maxLine = this.doc.lineCount(); 142 | if (pos.line == maxLine - 1) return savePosAndFail(maxLine); 143 | pos = Pos(pos.line + 1, 0); 144 | } 145 | } 146 | }, 147 | 148 | from: function() {if (this.atOccurrence) return this.pos.from;}, 149 | to: function() {if (this.atOccurrence) return this.pos.to;}, 150 | 151 | replace: function(newText, origin) { 152 | if (!this.atOccurrence) return; 153 | var lines = CodeMirror.splitLines(newText); 154 | this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin); 155 | this.pos.to = Pos(this.pos.from.line + lines.length - 1, 156 | lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0)); 157 | } 158 | }; 159 | 160 | // Maps a position in a case-folded line back to a position in the original line 161 | // (compensating for codepoints increasing in number during folding) 162 | function adjustPos(orig, folded, pos) { 163 | if (orig.length == folded.length) return pos; 164 | for (var pos1 = Math.min(pos, orig.length);;) { 165 | var len1 = orig.slice(0, pos1).toLowerCase().length; 166 | if (len1 < pos) ++pos1; 167 | else if (len1 > pos) --pos1; 168 | else return pos1; 169 | } 170 | } 171 | 172 | CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) { 173 | return new SearchCursor(this.doc, query, pos, caseFold); 174 | }); 175 | CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) { 176 | return new SearchCursor(this, query, pos, caseFold); 177 | }); 178 | 179 | CodeMirror.defineExtension("selectMatches", function(query, caseFold) { 180 | var ranges = []; 181 | var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold); 182 | while (cur.findNext()) { 183 | if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break; 184 | ranges.push({anchor: cur.from(), head: cur.to()}); 185 | } 186 | if (ranges.length) 187 | this.setSelections(ranges, 0); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /extension/data/modules/banlist.js: -------------------------------------------------------------------------------- 1 | function banlist() { 2 | var self = new TB.Module('Ban List'); 3 | self.shortname = 'BanList'; 4 | 5 | self.settings['enabled']['default'] = true; 6 | 7 | self.register_setting('automatic', { 8 | 'type': 'boolean', 9 | 'default': false, 10 | 'title': 'Automatically pre-load the whole ban list for live filtering.' 11 | }); 12 | 13 | // extracts a url parameter value from a URL string 14 | // from http://stackoverflow.com/a/15780907/362042 15 | // TODO: move to tbutils 16 | self.getURLParameter = function getURLParameter(url, name) { 17 | return (new RegExp(name + '=' + '(.+?)(&|$)').exec(url) || [, null])[1]; 18 | }; 19 | 20 | self.init = function () { 21 | if (!TB.utils.isEditUserPage) return; 22 | 23 | var banlist_updating = false, 24 | banlist_last_update = 0, 25 | last_request = 0, 26 | time_to_update = 1000 * 60 * 5, // in milliseconds (last value is minutes) 27 | pages_back = 0, 28 | $num_bans = $(''); 29 | 30 | function _get_next_ban_page(after, pages_back) { 31 | 32 | // default parameter value handling 33 | after = typeof after !== 'undefined' ? after : ''; 34 | pages_back = typeof pages_back !== 'undefined' ? pages_back : 0; 35 | 36 | self.log("_get_next_ban_page(" + after + ")"); 37 | 38 | var parameters = {'limit': 1000, 'after': after}; 39 | 40 | after = null; 41 | last_request = Date.now(); 42 | 43 | $.ajax({ 44 | url: document.location.href, 45 | data: parameters, 46 | type: 'get', 47 | dataType: 'html', 48 | async: true, 49 | done: function (data) { 50 | self.log(" success!"); 51 | self.log(" " + pages_back + " pages back"); 52 | var response_page = $(data); 53 | // append to the list, using clever jQuery context parameter to create jQuery object to parse out the HTML response 54 | // var $new_banlist = $('.usertable', response_page); 55 | self.log($('.usertable table tbody tr', response_page).length); 56 | if ($('.usertable table tbody tr', response_page).length > 0) { 57 | $('.usertable table tbody tr', response_page).each(function () { 58 | // workaround for known bug in listings where "next" button is available on last page 59 | if (this.className == 'notfound') { 60 | return; 61 | } 62 | 63 | var t = $(this).find('.user a').text().toLowerCase(); // username 64 | if ($(this).find('input[name="note"]').length > 0) { 65 | t += ' ' + $(this).find('input[name="note"]').val().toLowerCase(); // ban note text, if available 66 | } 67 | $("").hide().text(t).appendTo(this); 68 | $(this).addClass('visible'); 69 | }); 70 | var value = $('input#user').val().toLowerCase(); 71 | filter_banlist($('.usertable', response_page), value, true); 72 | $('.usertable table tbody').append($('.usertable table tbody tr', response_page)); 73 | // update the results counter 74 | $num_bans.html($('.usertable tr:visible').length); 75 | } else { 76 | return; 77 | } 78 | 79 | after_url = $('.nextprev a[rel~="next"]', response_page).prop('href'); 80 | self.log(after_url); 81 | after = self.getURLParameter(after_url, 'after'); 82 | self.log(after); 83 | if (after) { 84 | // hit the API hard the first 10, to make it more responsive on small subs 85 | if (pages_back < 10) { 86 | pages_back++; 87 | _get_next_ban_page(after, pages_back); 88 | } else { 89 | sleep = last_request + 2000 - Date.now(); 90 | setTimeout(_get_next_ban_page, sleep, after, pages_back); 91 | } 92 | } else { 93 | self.log(" last page"); 94 | banlist_updating = false; 95 | banlist_last_update = Date.now(); 96 | TB.ui.longLoadSpinner(false); 97 | } 98 | }, 99 | fail: function (data) { 100 | self.log(" failed"); 101 | self.log(data.status); 102 | if (data.status == 504) { 103 | // "504, post some more" 104 | this.done(data); 105 | } else { 106 | // Did we get logged out during the process, or some other error? 107 | banlist_updating = false; 108 | TB.ui.longLoadSpinner(false); 109 | $num_bans.html("Something went wrong while fetching the banlist. You should reload this page."); 110 | } 111 | } 112 | }); 113 | 114 | } 115 | 116 | function filter_banlist(banlist, value, ignore_last) { 117 | self.log('filter(' + value + ')'); 118 | var last_value = typeof last_value !== 'undefined' ? last_value : ''; 119 | ignore_last = typeof ignore_last !== 'undefined' ? ignore_last : false; 120 | 121 | if (value == '') { 122 | self.log('empty'); 123 | // empty search? show all 124 | $('tr', banlist).show().addClass('visible'); 125 | } else if (!ignore_last && last_value && value.indexOf(last_value) > -1) { 126 | self.log('subset'); 127 | // is this query a subset of the last query? 128 | // filter *out* non-matching 129 | $("tr.visible .indexColumn:not(:contains('" + value + "'))", banlist).parent().hide().removeClass('visible'); 130 | } else { 131 | self.log('full search'); 132 | $('tr', banlist).hide().removeClass('visible'); 133 | // combine and use a single selector for increased performance 134 | // credit: http://kobikobi.wordpress.com/2008/09/15/using-jquery-to-filter-table-rows/ 135 | $("tr .indexColumn:contains('" + value + "')", banlist).parent().show().addClass('visible'); 136 | } 137 | $("tr", banlist).removeClass('even'); 138 | $("tr.visible:even", banlist).addClass('even'); 139 | 140 | // update last value 141 | last_value = value; 142 | } 143 | 144 | function liveFilter() { 145 | var $user = $('#user'); 146 | 147 | // counter for number of bans 148 | $num_bans.appendTo($user.parent()); 149 | 150 | $user.prop('placeholder', 'Begin typing to live filter the list.'); 151 | 152 | $('.usertable').addClass('filtered'); 153 | 154 | $(".usertable tr").each(function () { 155 | var t = $(this).text().toLowerCase(); //all row text 156 | $("").hide().text(t).appendTo(this); 157 | });//each tr 158 | 159 | 160 | function _filter(value) { 161 | if (!banlist_updating // don't trigger an update if we're still running 162 | && (banlist_last_update === 0 // catch the first run, before last_update has been set 163 | || (banlist_last_update + time_to_update) <= Date.now()) 164 | ) { 165 | banlist_updating = true; 166 | TB.ui.longLoadSpinner(true); 167 | 168 | self.log("Updating now"); 169 | // clean up 170 | $('.usertable table tbody').empty(); 171 | pages_back = 0; 172 | _get_next_ban_page(); 173 | } 174 | 175 | filter_banlist($('.usertable'), value); 176 | // update the results counter 177 | $num_bans.html($('.usertable tr:visible').length); 178 | } 179 | 180 | // text input trigger 181 | var $userInput = $('input#user'); 182 | $userInput.keyup(function () { 183 | if ($('.usertable tr').length > 1000) { 184 | return; 185 | } // don't live filter 186 | var value = $(this).val().toLowerCase(); 187 | _filter(value); 188 | }); 189 | 190 | $userInput.parent().submit(function (e) { 191 | _filter($('input#user').val().toLowerCase()); 192 | e.preventDefault(); 193 | }); 194 | 195 | // we want to populate the table immediately on load. 196 | $userInput.keyup(); 197 | } 198 | 199 | if (self.setting('automatic')) { 200 | liveFilter(); 201 | } else { 202 | var $tb_liveFilter = $(''); 203 | $tb_liveFilter.insertAfter($('input#user').next()); 204 | $tb_liveFilter.click(function () { 205 | liveFilter(); 206 | $(this).remove(); 207 | }); 208 | } 209 | }; 210 | 211 | TB.register_module(self); 212 | } 213 | 214 | (function() { 215 | window.addEventListener("TBModuleLoaded", function () { 216 | banlist(); 217 | }); 218 | })(); 219 | -------------------------------------------------------------------------------- /extension/data/styles/codemirror/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .mod-toolbox .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace; 6 | height: 600px; 7 | color: black; 8 | } 9 | 10 | .mod-toolbox .CodeMirror-fullscreen { 11 | z-index: 2147483646; 12 | bottom: 25px; 13 | 14 | } 15 | 16 | .tb-syntax-keyboard i { 17 | font-size: medium; 18 | } 19 | 20 | .mod-toolbox .tb-syntax-keyboard { 21 | position: absolute; 22 | width: 150px; 23 | background-color: rgba(206, 227, 248, 0.55); 24 | color: black; 25 | right: 0; 26 | top: 0; 27 | z-index: 99999999999; 28 | padding: 3px; 29 | transition: width 0.3s; 30 | } 31 | 32 | .mod-toolbox .tb-syntax-keyboard:hover { 33 | width: 500px; 34 | } 35 | 36 | .mod-toolbox .tb-syntax-keyboard ul { 37 | display: none; 38 | } 39 | 40 | 41 | 42 | .tb-syntax-keyboard:hover ul { 43 | display: block; 44 | } 45 | /* PADDING */ 46 | 47 | .CodeMirror-lines { 48 | padding: 4px 0; /* Vertical padding around content */ 49 | } 50 | .CodeMirror pre { 51 | padding: 0 4px; /* Horizontal padding of content */ 52 | } 53 | 54 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 55 | background-color: white; /* The little square between H and V scrollbars */ 56 | } 57 | 58 | /* GUTTER */ 59 | 60 | .CodeMirror-gutters { 61 | border-right: 1px solid #ddd; 62 | background-color: #f7f7f7; 63 | white-space: nowrap; 64 | } 65 | .CodeMirror-linenumbers {} 66 | .CodeMirror-linenumber { 67 | padding: 0 3px 0 5px; 68 | min-width: 20px; 69 | text-align: right; 70 | color: #999; 71 | white-space: nowrap; 72 | } 73 | 74 | .CodeMirror-guttermarker { color: black; } 75 | .CodeMirror-guttermarker-subtle { color: #999; } 76 | 77 | /* CURSOR */ 78 | 79 | .CodeMirror-cursor { 80 | border-left: 1px solid black; 81 | border-right: none; 82 | width: 0; 83 | } 84 | /* Shown when moving in bi-directional text */ 85 | .CodeMirror div.CodeMirror-secondarycursor { 86 | border-left: 1px solid silver; 87 | } 88 | .cm-fat-cursor .CodeMirror-cursor { 89 | width: auto; 90 | border: 0 !important; 91 | background: #7e7; 92 | } 93 | .cm-fat-cursor div.CodeMirror-cursors { 94 | z-index: 1; 95 | } 96 | 97 | .cm-animate-fat-cursor { 98 | width: auto; 99 | border: 0; 100 | -webkit-animation: blink 1.06s steps(1) infinite; 101 | -moz-animation: blink 1.06s steps(1) infinite; 102 | animation: blink 1.06s steps(1) infinite; 103 | background-color: #7e7; 104 | } 105 | @-moz-keyframes blink { 106 | 0% {} 107 | 50% { background-color: transparent; } 108 | 100% {} 109 | } 110 | @-webkit-keyframes blink { 111 | 0% {} 112 | 50% { background-color: transparent; } 113 | 100% {} 114 | } 115 | @keyframes blink { 116 | 0% {} 117 | 50% { background-color: transparent; } 118 | 100% {} 119 | } 120 | 121 | /* Can style cursor different in overwrite (non-insert) mode */ 122 | .CodeMirror-overwrite .CodeMirror-cursor {} 123 | 124 | .cm-tab { display: inline-block; text-decoration: inherit; } 125 | 126 | .CodeMirror-rulers { 127 | position: absolute; 128 | left: 0; right: 0; top: -50px; bottom: -20px; 129 | overflow: hidden; 130 | } 131 | .CodeMirror-ruler { 132 | border-left: 1px solid #ccc; 133 | top: 0; bottom: 0; 134 | position: absolute; 135 | } 136 | 137 | /* DEFAULT THEME */ 138 | 139 | .cm-s-default .cm-header {color: blue;} 140 | .cm-s-default .cm-quote {color: #090;} 141 | .cm-negative {color: #d44;} 142 | .cm-positive {color: #292;} 143 | .cm-header, .cm-strong {font-weight: bold;} 144 | .cm-em {font-style: italic;} 145 | .cm-link {text-decoration: underline;} 146 | .cm-strikethrough {text-decoration: line-through;} 147 | 148 | .cm-s-default .cm-keyword {color: #708;} 149 | .cm-s-default .cm-atom {color: #219;} 150 | .cm-s-default .cm-number {color: #164;} 151 | .cm-s-default .cm-def {color: #00f;} 152 | .cm-s-default .cm-variable, 153 | .cm-s-default .cm-punctuation, 154 | .cm-s-default .cm-property, 155 | .cm-s-default .cm-operator {} 156 | .cm-s-default .cm-variable-2 {color: #05a;} 157 | .cm-s-default .cm-variable-3 {color: #085;} 158 | .cm-s-default .cm-comment {color: #a50;} 159 | .cm-s-default .cm-string {color: #a11;} 160 | .cm-s-default .cm-string-2 {color: #f50;} 161 | .cm-s-default .cm-meta {color: #555;} 162 | .cm-s-default .cm-qualifier {color: #555;} 163 | .cm-s-default .cm-builtin {color: #30a;} 164 | .cm-s-default .cm-bracket {color: #997;} 165 | .cm-s-default .cm-tag {color: #170;} 166 | .cm-s-default .cm-attribute {color: #00c;} 167 | .cm-s-default .cm-hr {color: #999;} 168 | .cm-s-default .cm-link {color: #00c;} 169 | 170 | .cm-s-default .cm-error {color: #f00;} 171 | .cm-invalidchar {color: #f00;} 172 | 173 | .CodeMirror-composing { border-bottom: 2px solid; } 174 | 175 | /* Default styles for common addons */ 176 | 177 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} 178 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} 179 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 180 | .CodeMirror-activeline-background {background: #e8f2ff;} 181 | 182 | /* STOP */ 183 | 184 | /* The rest of this file contains styles related to the mechanics of 185 | the editor. You probably shouldn't touch them. */ 186 | 187 | .CodeMirror { 188 | position: relative; 189 | overflow: hidden; 190 | background: white; 191 | } 192 | 193 | .CodeMirror-scroll { 194 | overflow: scroll !important; /* Things will break if this is overridden */ 195 | /* 30px is the magic margin used to hide the element's real scrollbars */ 196 | /* See overflow: hidden in .CodeMirror */ 197 | margin-bottom: -30px; margin-right: -30px; 198 | padding-bottom: 30px; 199 | height: 100%; 200 | outline: none; /* Prevent dragging from highlighting the element */ 201 | position: relative; 202 | } 203 | .CodeMirror-sizer { 204 | position: relative; 205 | border-right: 30px solid transparent; 206 | } 207 | 208 | /* The fake, visible scrollbars. Used to force redraw during scrolling 209 | before actual scrolling happens, thus preventing shaking and 210 | flickering artifacts. */ 211 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 212 | position: absolute; 213 | z-index: 6; 214 | display: none; 215 | } 216 | .CodeMirror-vscrollbar { 217 | right: 0; top: 0; 218 | overflow-x: hidden; 219 | overflow-y: scroll; 220 | } 221 | .CodeMirror-hscrollbar { 222 | bottom: 0; left: 0; 223 | overflow-y: hidden; 224 | overflow-x: scroll; 225 | } 226 | .CodeMirror-scrollbar-filler { 227 | right: 0; bottom: 0; 228 | } 229 | .CodeMirror-gutter-filler { 230 | left: 0; bottom: 0; 231 | } 232 | 233 | .CodeMirror-gutters { 234 | position: absolute; left: 0; top: 0; 235 | min-height: 100%; 236 | z-index: 3; 237 | } 238 | .CodeMirror-gutter { 239 | white-space: normal; 240 | height: 100%; 241 | display: inline-block; 242 | vertical-align: top; 243 | margin-bottom: -30px; 244 | /* Hack to make IE7 behave */ 245 | *zoom:1; 246 | *display:inline; 247 | } 248 | .CodeMirror-gutter-wrapper { 249 | position: absolute; 250 | z-index: 4; 251 | background: none !important; 252 | border: none !important; 253 | } 254 | .CodeMirror-gutter-background { 255 | position: absolute; 256 | top: 0; bottom: 0; 257 | z-index: 4; 258 | } 259 | .CodeMirror-gutter-elt { 260 | position: absolute; 261 | cursor: default; 262 | z-index: 4; 263 | } 264 | .CodeMirror-gutter-wrapper { 265 | -webkit-user-select: none; 266 | -moz-user-select: none; 267 | user-select: none; 268 | } 269 | 270 | .CodeMirror-lines { 271 | cursor: text; 272 | min-height: 1px; /* prevents collapsing before first draw */ 273 | } 274 | .CodeMirror pre { 275 | /* Reset some styles that the rest of the page might have set */ 276 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 277 | border-width: 0; 278 | background: transparent; 279 | font-family: inherit; 280 | font-size: inherit; 281 | margin: 0; 282 | white-space: pre; 283 | word-wrap: normal; 284 | line-height: inherit; 285 | color: inherit; 286 | z-index: 2; 287 | position: relative; 288 | overflow: visible; 289 | -webkit-tap-highlight-color: transparent; 290 | -webkit-font-variant-ligatures: none; 291 | font-variant-ligatures: none; 292 | } 293 | .CodeMirror-wrap pre { 294 | word-wrap: break-word; 295 | white-space: pre-wrap; 296 | word-break: normal; 297 | } 298 | 299 | .CodeMirror-linebackground { 300 | position: absolute; 301 | left: 0; right: 0; top: 0; bottom: 0; 302 | z-index: 0; 303 | } 304 | 305 | .CodeMirror-linewidget { 306 | position: relative; 307 | z-index: 2; 308 | overflow: auto; 309 | } 310 | 311 | .CodeMirror-widget {} 312 | 313 | .CodeMirror-code { 314 | outline: none; 315 | } 316 | 317 | /* Force content-box sizing for the elements where we expect it */ 318 | .CodeMirror-scroll, 319 | .CodeMirror-sizer, 320 | .CodeMirror-gutter, 321 | .CodeMirror-gutters, 322 | .CodeMirror-linenumber { 323 | -moz-box-sizing: content-box; 324 | box-sizing: content-box; 325 | } 326 | 327 | .CodeMirror-measure { 328 | position: absolute; 329 | width: 100%; 330 | height: 0; 331 | overflow: hidden; 332 | visibility: hidden; 333 | } 334 | 335 | .CodeMirror-cursor { 336 | position: absolute; 337 | pointer-events: none; 338 | } 339 | .CodeMirror-measure pre { position: static; } 340 | 341 | div.CodeMirror-cursors { 342 | visibility: hidden; 343 | position: relative; 344 | z-index: 3; 345 | } 346 | div.CodeMirror-dragcursors { 347 | visibility: visible; 348 | } 349 | 350 | .CodeMirror-focused div.CodeMirror-cursors { 351 | visibility: visible; 352 | } 353 | 354 | .CodeMirror-selected { background: #d9d9d9; } 355 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 356 | .CodeMirror-crosshair { cursor: crosshair; } 357 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 358 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 359 | 360 | .cm-searching { 361 | background: #ffa; 362 | background: rgba(255, 255, 0, .4); 363 | } 364 | 365 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */ 366 | .CodeMirror span { *vertical-align: text-bottom; } 367 | 368 | /* Used to force a border model for a node */ 369 | .cm-force-border { padding-right: .1px; } 370 | 371 | @media print { 372 | /* Hide the cursor when printing */ 373 | .CodeMirror div.CodeMirror-cursors { 374 | visibility: hidden; 375 | } 376 | } 377 | 378 | /* See issue #2901 */ 379 | .cm-tab-wrap-hack:after { content: ''; } 380 | 381 | /* Help users use markselection to safely style text background */ 382 | span.CodeMirror-selectedtext { background: none; } 383 | -------------------------------------------------------------------------------- /extension/data/libs/codemirror/addon/search.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | // Define search commands. Depends on dialog.js or another 5 | // implementation of the openDialog method. 6 | 7 | // Replace works a little oddly -- it will do the replace on the next 8 | // Ctrl-G (or whatever is bound to findNext) press. You prevent a 9 | // replace by making sure the match is no longer selected when hitting 10 | // Ctrl-G. 11 | 12 | (function(mod) { 13 | if (typeof exports == "object" && typeof module == "object") // CommonJS 14 | mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog")); 15 | else if (typeof define == "function" && define.amd) // AMD 16 | define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod); 17 | else // Plain browser env 18 | mod(CodeMirror); 19 | })(function(CodeMirror) { 20 | "use strict"; 21 | 22 | function searchOverlay(query, caseInsensitive) { 23 | if (typeof query == "string") 24 | query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g"); 25 | else if (!query.global) 26 | query = new RegExp(query.source, query.ignoreCase ? "gi" : "g"); 27 | 28 | return {token: function(stream) { 29 | query.lastIndex = stream.pos; 30 | var match = query.exec(stream.string); 31 | if (match && match.index == stream.pos) { 32 | stream.pos += match[0].length || 1; 33 | return "searching"; 34 | } else if (match) { 35 | stream.pos = match.index; 36 | } else { 37 | stream.skipToEnd(); 38 | } 39 | }}; 40 | } 41 | 42 | function SearchState() { 43 | this.posFrom = this.posTo = this.lastQuery = this.query = null; 44 | this.overlay = null; 45 | } 46 | 47 | function getSearchState(cm) { 48 | return cm.state.search || (cm.state.search = new SearchState()); 49 | } 50 | 51 | function queryCaseInsensitive(query) { 52 | return typeof query == "string" && query == query.toLowerCase(); 53 | } 54 | 55 | function getSearchCursor(cm, query, pos) { 56 | // Heuristic: if the query string is all lowercase, do a case insensitive search. 57 | return cm.getSearchCursor(query, pos, queryCaseInsensitive(query)); 58 | } 59 | 60 | function persistentDialog(cm, text, deflt, onEnter, onKeyDown) { 61 | cm.openDialog(text, onEnter, { 62 | value: deflt, 63 | selectValueOnOpen: true, 64 | closeOnEnter: false, 65 | onClose: function() { clearSearch(cm); }, 66 | onKeyDown: onKeyDown 67 | }); 68 | } 69 | 70 | function dialog(cm, text, shortText, deflt, f) { 71 | if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true}); 72 | else f(prompt(shortText, deflt)); 73 | } 74 | 75 | function confirmDialog(cm, text, shortText, fs) { 76 | if (cm.openConfirm) cm.openConfirm(text, fs); 77 | else if (confirm(shortText)) fs[0](); 78 | } 79 | 80 | function parseString(string) { 81 | return string.replace(/\\(.)/g, function(_, ch) { 82 | if (ch == "n") return "\n" 83 | if (ch == "r") return "\r" 84 | return ch 85 | }) 86 | } 87 | 88 | function parseQuery(query) { 89 | var isRE = query.match(/^\/(.*)\/([a-z]*)$/); 90 | if (isRE) { 91 | try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); } 92 | catch(e) {} // Not a regular expression after all, do a string search 93 | } else { 94 | query = parseString(query) 95 | } 96 | if (typeof query == "string" ? query == "" : query.test("")) 97 | query = /x^/; 98 | return query; 99 | } 100 | 101 | var queryDialog = 102 | 'Search: (Use /re/ syntax for regexp search)'; 103 | 104 | function startSearch(cm, state, query) { 105 | state.queryText = query; 106 | state.query = parseQuery(query); 107 | cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query)); 108 | state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query)); 109 | cm.addOverlay(state.overlay); 110 | if (cm.showMatchesOnScrollbar) { 111 | if (state.annotate) { state.annotate.clear(); state.annotate = null; } 112 | state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query)); 113 | } 114 | } 115 | 116 | function doSearch(cm, rev, persistent, immediate) { 117 | var state = getSearchState(cm); 118 | if (state.query) return findNext(cm, rev); 119 | var q = cm.getSelection() || state.lastQuery; 120 | if (persistent && cm.openDialog) { 121 | var hiding = null 122 | var searchNext = function(query, event) { 123 | CodeMirror.e_stop(event); 124 | if (!query) return; 125 | if (query != state.queryText) { 126 | startSearch(cm, state, query); 127 | state.posFrom = state.posTo = cm.getCursor(); 128 | } 129 | if (hiding) hiding.style.opacity = 1 130 | findNext(cm, event.shiftKey, function(_, to) { 131 | var dialog 132 | if (to.line < 3 && document.querySelector && 133 | (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) && 134 | dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top) 135 | (hiding = dialog).style.opacity = .4 136 | }) 137 | }; 138 | persistentDialog(cm, queryDialog, q, searchNext, function(event, query) { 139 | var keyName = CodeMirror.keyName(event) 140 | var cmd = CodeMirror.keyMap[cm.getOption("keyMap")][keyName] 141 | if (!cmd) cmd = cm.getOption('extraKeys')[keyName] 142 | if (cmd == "findNext" || cmd == "findPrev" || 143 | cmd == "findPersistentNext" || cmd == "findPersistentPrev") { 144 | CodeMirror.e_stop(event); 145 | startSearch(cm, getSearchState(cm), query); 146 | cm.execCommand(cmd); 147 | } else if (cmd == "find" || cmd == "findPersistent") { 148 | CodeMirror.e_stop(event); 149 | searchNext(query, event); 150 | } 151 | }); 152 | if (immediate && q) { 153 | startSearch(cm, state, q); 154 | findNext(cm, rev); 155 | } 156 | } else { 157 | dialog(cm, queryDialog, "Search for:", q, function(query) { 158 | if (query && !state.query) cm.operation(function() { 159 | startSearch(cm, state, query); 160 | state.posFrom = state.posTo = cm.getCursor(); 161 | findNext(cm, rev); 162 | }); 163 | }); 164 | } 165 | } 166 | 167 | function findNext(cm, rev, callback) {cm.operation(function() { 168 | var state = getSearchState(cm); 169 | var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo); 170 | if (!cursor.find(rev)) { 171 | cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)); 172 | if (!cursor.find(rev)) return; 173 | } 174 | cm.setSelection(cursor.from(), cursor.to()); 175 | cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20); 176 | state.posFrom = cursor.from(); state.posTo = cursor.to(); 177 | if (callback) callback(cursor.from(), cursor.to()) 178 | });} 179 | 180 | function clearSearch(cm) {cm.operation(function() { 181 | var state = getSearchState(cm); 182 | state.lastQuery = state.query; 183 | if (!state.query) return; 184 | state.query = state.queryText = null; 185 | cm.removeOverlay(state.overlay); 186 | if (state.annotate) { state.annotate.clear(); state.annotate = null; } 187 | });} 188 | 189 | var replaceQueryDialog = 190 | ' (Use /re/ syntax for regexp search)'; 191 | var replacementQueryDialog = 'With: '; 192 | var doReplaceConfirm = "Replace? "; 193 | 194 | function replaceAll(cm, query, text) { 195 | cm.operation(function() { 196 | for (var cursor = getSearchCursor(cm, query); cursor.findNext();) { 197 | if (typeof query != "string") { 198 | var match = cm.getRange(cursor.from(), cursor.to()).match(query); 199 | cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];})); 200 | } else cursor.replace(text); 201 | } 202 | }); 203 | } 204 | 205 | function replace(cm, all) { 206 | if (cm.getOption("readOnly")) return; 207 | var query = cm.getSelection() || getSearchState(cm).lastQuery; 208 | var dialogText = all ? "Replace all:" : "Replace:" 209 | dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) { 210 | if (!query) return; 211 | query = parseQuery(query); 212 | dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) { 213 | text = parseString(text) 214 | if (all) { 215 | replaceAll(cm, query, text) 216 | } else { 217 | clearSearch(cm); 218 | var cursor = getSearchCursor(cm, query, cm.getCursor("from")); 219 | var advance = function() { 220 | var start = cursor.from(), match; 221 | if (!(match = cursor.findNext())) { 222 | cursor = getSearchCursor(cm, query); 223 | if (!(match = cursor.findNext()) || 224 | (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return; 225 | } 226 | cm.setSelection(cursor.from(), cursor.to()); 227 | cm.scrollIntoView({from: cursor.from(), to: cursor.to()}); 228 | confirmDialog(cm, doReplaceConfirm, "Replace?", 229 | [function() {doReplace(match);}, advance, 230 | function() {replaceAll(cm, query, text)}]); 231 | }; 232 | var doReplace = function(match) { 233 | cursor.replace(typeof query == "string" ? text : 234 | text.replace(/\$(\d)/g, function(_, i) {return match[i];})); 235 | advance(); 236 | }; 237 | advance(); 238 | } 239 | }); 240 | }); 241 | } 242 | 243 | CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);}; 244 | CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);}; 245 | CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);}; 246 | CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);}; 247 | CodeMirror.commands.findNext = doSearch; 248 | CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);}; 249 | CodeMirror.commands.clearSearch = clearSearch; 250 | CodeMirror.commands.replace = replace; 251 | CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);}; 252 | }); 253 | -------------------------------------------------------------------------------- /extension/data/modules/achievements.js: -------------------------------------------------------------------------------- 1 | function achievements() { 2 | var self = new TB.Module('Achievements'); 3 | self.shortname = 'Achievements'; 4 | 5 | // Default settings 6 | self.settings['enabled']['default'] = true; 7 | 8 | self.register_setting('save', { 9 | 'type': 'achievement_save', 10 | 'default': '' 11 | }); 12 | 13 | self.register_setting('lastSeen', { 14 | 'type': 'number', 15 | 'default': TBUtils.getTime(), 16 | 'hidden': true 17 | }); 18 | 19 | // Saves 20 | function Manager() { 21 | var saves = [], 22 | saveIndex = 0, 23 | 24 | achievements = []; 25 | 26 | this.init = function () { 27 | var save = self.setting('save'); 28 | if (save.length > 0) { 29 | saves = this.decodeSave(save); 30 | } 31 | }; 32 | 33 | this.register = function (title, description, achievement) { 34 | this.registerTarget(title, description, 1, achievement); 35 | }; 36 | 37 | this.registerTarget = function (title, description, target, achievement) { 38 | this.registerSeries([title], description, [target], achievement); 39 | }; 40 | 41 | this.registerSeries = function (titles, description, maxValues, achievement) { 42 | if (saveIndex >= saves.length) { 43 | saves.push(0); 44 | } 45 | 46 | var achievementsBlock = []; 47 | for (var i = 0; i < maxValues.length; i++) { 48 | var title = titles[i], 49 | maxValue = maxValues[i]; 50 | 51 | self.log('Registering Achievement'); 52 | if (TB.utils.devMode) self.log(' name=' + title); // spoilers 53 | self.log(' maxValue=' + maxValue); 54 | self.log(' saveIndex=' + saveIndex); 55 | 56 | achievementsBlock.push({ 57 | title: title, 58 | descr: description.format(maxValue), 59 | maxValue: maxValue, 60 | saveIndex: saveIndex 61 | }); 62 | } 63 | achievements.push(achievementsBlock); 64 | 65 | achievement(saveIndex); 66 | saveIndex++; 67 | }; 68 | 69 | this.unlock = function (saveIndex, value) { 70 | if (value === undefined) { 71 | value = 1; 72 | } 73 | self.log('Unlocking achievement block: index=' + saveIndex + ', value=' + value); 74 | 75 | var old = saves[saveIndex]; 76 | self.log(' Old value: ' + saves[saveIndex]); 77 | saves[saveIndex] += value; 78 | self.log(' New value: ' + saves[saveIndex]); 79 | 80 | var achievementsBlock = achievements[saveIndex]; 81 | for (var index = 0; index < achievementsBlock.length; index++) { 82 | self.log(' Checking achievement ' + index); 83 | var achievement = achievementsBlock[index]; 84 | self.log(' Comparing to max value: ' + achievement.maxValue); 85 | if (saves[saveIndex] >= achievement.maxValue && old < achievement.maxValue) { 86 | var title = achievement.title; 87 | 88 | // eh, close enough. 89 | // any better solution for links requires re-writing all the rewriting register functions 90 | // to support another prop. If someone want to do that, go for it. 91 | try { 92 | title = $(achievement.title).text() ? $(achievement.title).text() : achievement.title; 93 | } catch(e) {} 94 | 95 | self.log(' '+ title +' Unlocked!'); 96 | TBUtils.notification('Mod achievement unlocked!', title, window.location + '#?tbsettings=' + self.shortname); 97 | } 98 | } 99 | 100 | if (saves[saveIndex] > achievement.maxValue) { 101 | saves[saveIndex] = achievement.maxValue 102 | } 103 | this.save(); 104 | }; 105 | 106 | this.save = function () { 107 | var save = ''; 108 | saves.forEach(function (saveValue, saveIndex) { 109 | save += saveValue; 110 | if (saveIndex < saves.length - 1) { 111 | save += ';'; 112 | } 113 | }); 114 | save = btoa(save); 115 | self.setting('save', save); 116 | }; 117 | 118 | // Utilities 119 | 120 | this.decodeSave = function (save) { 121 | var vals = atob(self.setting('save')).split(';'); 122 | // Because '2' + 1 = 21 123 | if (vals && vals.length > 0) { 124 | for (var i = 0; i < vals.length; i++) { 125 | vals[i] = parseInt(vals[i]); 126 | } 127 | } 128 | return vals; 129 | }; 130 | 131 | this.getAchievementBlockCount = function () { 132 | return achievements.length; 133 | }; 134 | 135 | this.getAchievementCount = function (saveIndex) { 136 | return achievements[saveIndex].length; 137 | }; 138 | 139 | this.getAchievementTotal = function () { 140 | var total = 0; 141 | for (var saveIndex = 0; saveIndex < achievements.length; saveIndex++) { 142 | total += this.getAchievementCount(saveIndex); 143 | } 144 | return total; 145 | }; 146 | 147 | this.getUnlockedCount = function () { 148 | var count = 0; 149 | for (var saveIndex = 0; saveIndex < achievements.length; saveIndex++) { 150 | var achievementsBlock = achievements[saveIndex]; 151 | for (var index = 0; index < achievementsBlock.length; index++) { 152 | if (this.isUnlocked(saveIndex, index, saves)) { 153 | count++; 154 | } 155 | } 156 | } 157 | return count; 158 | }; 159 | 160 | this.getAchievement = function (saveIndex, index) { 161 | return achievements[saveIndex][index]; 162 | }; 163 | 164 | this.isUnlocked = function (saveIndex, index, saves) { 165 | var a = this.getAchievement(saveIndex, index); 166 | if (!(saves instanceof Array) || a.saveIndex >= saves.length) { 167 | return false; 168 | } 169 | 170 | return saves[a.saveIndex] >= a.maxValue; 171 | }; 172 | } 173 | 174 | // Always load the manager so achievements can still be viewed if the module is disabled 175 | self.manager = new Manager(); 176 | self.manager.init(); 177 | 178 | // Init module 179 | self.init = function () { 180 | var $body = $('body'); 181 | 182 | // Individual achievement stuff 183 | var lastSeen = self.setting('lastSeen'); 184 | 185 | // Achievement definitions 186 | self.log('Registering achievements'); 187 | 188 | 189 | // Random awesome 190 | self.manager.register('being awesome', "toolbox just feels like you're awesome today", function (saveIndex) { 191 | var awesome = 7, 192 | chanceOfBeingAwesome = TB.utils.getRandomNumber(10000); 193 | 194 | self.log('You rolled a: ' + chanceOfBeingAwesome); 195 | if (awesome == chanceOfBeingAwesome) { 196 | self.manager.unlock(saveIndex); 197 | } 198 | }); 199 | 200 | // Still Alive (TODO: can we make links work?) 201 | self.manager.register('not dead yet', 'Spent a week away from reddit', function (saveIndex) { 202 | // BUG: this one keeps firing on default no value for lastSeen. 203 | // I tried defaulting to now but it's still wonky. 204 | var now = TBUtils.getTime(), 205 | timeSince = now - lastSeen, 206 | daysSince = TBUtils.millisecondsToDays(timeSince); 207 | self.log('daysSince: ' + daysSince); 208 | 209 | if (daysSince >= 7) { 210 | //self.log("you've got an award!"); 211 | self.manager.unlock(saveIndex); 212 | } 213 | 214 | self.setting('lastSeen', now); 215 | }); 216 | 217 | //toolbox Loves You: Look at the about page 218 | self.manager.register('toolbox loves you', 'Looked at the about page. <3', function (saveIndex) { 219 | TB.utils.catchEvent(TB.utils.events.TB_ABOUT_PAGE, function () { 220 | self.manager.unlock(saveIndex); 221 | }); 222 | }); 223 | 224 | // Beta testers 225 | self.manager.register('bug hunter', 'Beta testing toolbox', function (saveIndex) { 226 | if (TB.utils.betaRelease) { 227 | self.manager.unlock(saveIndex, 1); 228 | } 229 | }); 230 | 231 | // Judas 232 | self.manager.register('Judas', "Why do you hate toolbox devs? :'( ", function (saveIndex) { 233 | $body.on('click', 'form.remove-button, a.pretty-button.negative, a.pretty-button.neutral', function () { 234 | var $this = $(this); 235 | var auth = TB.utils.getThingInfo($this).author; 236 | 237 | if (TB.utils.tbDevs.indexOf(auth) != -1) { 238 | self.manager.unlock(saveIndex, 1); 239 | } 240 | // TODO: wait for 'yes' click. 241 | //$body.on('click', '.yes', function(){ 242 | // self.log('yes clicked'); 243 | //}); 244 | }); 245 | }); 246 | 247 | // approving stuff 248 | self.manager.registerSeries(['too nice', 'way too nice', 'big softie', 'approvening master', 'the kinda mod reddit deserves'], 'Approved {0} things', [50, 200, 1000, 10000, 20000], function (saveIndex) { 249 | 250 | // If just the button is used. 251 | $body.on('click', '.pretty-button, .approve-button', function () { 252 | var $this = $(this); 253 | if ($this.hasClass('positive') || $this.hasClass('approve-button')) { 254 | self.manager.unlock(saveIndex, 1); 255 | } 256 | }); 257 | 258 | // If the API is used 259 | TB.utils.catchEvent(TB.utils.events.TB_APPROVE_THING, function () { 260 | self.manager.unlock(saveIndex, 1); 261 | }); 262 | }); 263 | 264 | // Mod mail 265 | self.manager.registerSeries(['hic sunt dracones', "just checkin' the mail", 'Mr. Postman', "You've got mail!"], 'Checked mod mail {0} times!', [1, 100, 1000, 10000], function (saveIndex) { 266 | if (TB.utils.isModmail) { 267 | self.manager.unlock(saveIndex, 1); 268 | } 269 | }); 270 | 271 | // Empty queue 272 | self.manager.registerSeries(['kitteh get!', 'puppy power!','Dr. Jan Itor', '/u/Kylde'], 'Cleared your queues {0} times!', [10, 50, 100, 700], function (saveIndex) { 273 | if (TBUtils.isModpage && $body.find('p#noresults').length > 0) { 274 | self.manager.unlock(saveIndex, 1); 275 | } 276 | }); 277 | 278 | // Found flying Snoo 279 | self.manager.register('Cadbury Bunny', 'Found flying Snoo.', function (saveIndex) { 280 | TB.utils.catchEvent(TB.utils.events.TB_FLY_SNOO, function () { 281 | self.manager.unlock(saveIndex); 282 | }); 283 | }); 284 | 285 | // Killed Snoo 286 | self.manager.register('you bastard!', 'Killed Snoo.', function (saveIndex) { 287 | TB.utils.catchEvent(TB.utils.events.TB_KILL_SNOO, function () { 288 | self.manager.unlock(saveIndex); 289 | }); 290 | }); 291 | 292 | // New modmail access 293 | self.manager.register('mannomail', 'No one knows, send hate mail to /u/thatastronautguy', function (saveIndex) { 294 | if (window.location.href.startsWith("https://mod.reddit.com/mail")) { 295 | self.manager.unlock(saveIndex); 296 | } 297 | }); 298 | }; 299 | 300 | TB.register_module(self); 301 | } 302 | 303 | (function() { 304 | window.addEventListener('TBModuleLoaded', function () { 305 | achievements(); 306 | }); 307 | })(); 308 | -------------------------------------------------------------------------------- /extension/data/modules/syntax.js: -------------------------------------------------------------------------------- 1 | function syntax() { 2 | 3 | 4 | var self = new TB.Module('Syntax Highlighter'); 5 | self.shortname = 'Syntax'; 6 | 7 | self.settings['enabled']['default'] = true; 8 | 9 | self.register_setting('enableWordWrap', { 10 | 'type': 'boolean', 11 | 'default': true, 12 | 'title': 'Enable word wrap in editor' 13 | }); 14 | self.register_setting('selectedTheme', { 15 | 'type': 'syntaxTheme', 16 | 'default': 'dracula', 17 | 'title': 'Syntax highlight theme selection' 18 | }); 19 | 20 | self.settings['enabled']['default'] = true; // on by default 21 | 22 | // we reference this from tbobject for settings generation 23 | self.themeSelect = '\ 24 | \ 72 | '; 73 | 74 | self.init = function () { 75 | var $body = $('body'), 76 | selectedTheme = this.setting('selectedTheme'), 77 | enableWordWrap = this.setting('enableWordWrap'), 78 | editor, session, textarea; 79 | 80 | // This makes sure codemirror behaves and uses spaces instead of tabs. 81 | function betterTab(cm) { 82 | if (cm.somethingSelected()) { 83 | cm.indentSelection("add"); 84 | } else { 85 | cm.replaceSelection(cm.getOption("indentWithTabs")? "\t": 86 | Array(cm.getOption("indentUnit") + 1).join(" "), "end", "+input"); 87 | } 88 | } 89 | 90 | var keyboardShortcutsHelper = `
    91 | Keyboard shortcuts 92 |
      93 |
    • F11: Fullscreen
    • 94 |
    • Esc: Close Fullscreen
    • 95 |
    • Ctrl-F / Cmd-F: Start searching
    • 96 |
    • Ctrl-Alt-F / Cmd-Alt-F: Persistent search (dialog doesn't autoclose)
    • 97 |
    • Ctrl-G / Cmd-G: Find next
    • 98 |
    • Shift-Ctrl-G / Shift-Cmd-G: Find previous
    • 99 |
    • Shift-Ctrl-F / Cmd-Option-F: Replace
    • 100 |
    • Shift-Ctrl-R / Shift-Cmd-Option-F: Replace all
    • 101 |
    • Alt-G: Jump to line
    • 102 |
    • Ctrl-Space / Cmd-Space: autocomplete
    • 103 |
    104 |
    `; 105 | // Editor for css. 106 | if (location.pathname.match(/\/about\/stylesheet\/?/)) { 107 | var stylesheetEditor; 108 | 109 | // Class added to apply some specific css. 110 | $body.addClass('mod-syntax'); 111 | // Theme selector, doesn't really belong here but gives people the opportunity to see how it looks with the css they want to edit. 112 | $('.sheets .col').before(this.themeSelect); 113 | 114 | $('#theme_selector').val(selectedTheme); 115 | 116 | // Here apply codeMirror to the text area, the each itteration allows us to use the javascript object as codemirror works with those. 117 | $('#stylesheet_contents').each(function(index, elem){ 118 | 119 | // Editor setup. 120 | stylesheetEditor = CodeMirror.fromTextArea(elem, { 121 | mode: 'text/css', 122 | autoCloseBrackets: true, 123 | lineNumbers: true, 124 | theme: selectedTheme, 125 | indentUnit: 4, 126 | extraKeys: { 127 | "Ctrl-Space": 'autocomplete', 128 | "Ctrl-Alt-F": "findPersistent", 129 | "F11": function(cm) { 130 | cm.setOption("fullScreen", !cm.getOption("fullScreen")); 131 | }, 132 | "Esc": function(cm) { 133 | if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false); 134 | }, 135 | "Tab": betterTab, 136 | "Shift-Tab": function (cm) { 137 | cm.indentSelection("subtract"); 138 | } 139 | }, 140 | lineWrapping: enableWordWrap 141 | }); 142 | 143 | $body.find('.CodeMirror.CodeMirror-wrap').prepend(keyboardShortcutsHelper); 144 | }); 145 | 146 | // In order to make save buttons work we need to hijack and replace them. 147 | var tbSyntaxButtonsHTML = '
    {{save}} - {{preview}}
    '; 148 | 149 | var tbSyntaxButtons = TB.utils.template(tbSyntaxButtonsHTML, { 150 | 'save': TB.ui.actionButton('save', 'tb-syntax-button-save'), 151 | 'preview': TB.ui.actionButton('preview', 'tb-syntax-button-preview') 152 | }); 153 | 154 | $body.find('.sheets .buttons').before(tbSyntaxButtons); 155 | 156 | // When the toolbox buttons are clicked we put back the content in the text area and click the now hidden original buttons. 157 | $body.delegate('.tb-syntax-button-save', 'click', function() { 158 | stylesheetEditor.save(); 159 | $('.sheets .buttons .btn[name="save"]').click(); 160 | }); 161 | 162 | $body.delegate('.tb-syntax-button-preview', 'click', function() { 163 | stylesheetEditor.save(); 164 | $('.sheets .buttons .btn[name="preview"]').click(); 165 | }); 166 | 167 | // Actually dealing with the theme dropdown is done here. 168 | $body.on('change keydown', '#theme_selector', function () { 169 | var thingy = $(this); 170 | setTimeout(function () { 171 | stylesheetEditor.setOption("theme", thingy.val()); 172 | }, 0); 173 | }); 174 | } 175 | 176 | // Here we deal with automod and toolbox pages containing json. 177 | if (location.pathname.match(/\/wiki\/(edit|create)\/(config\/)?automoderator(-schedule)?\/?$/) 178 | || location.pathname.match(/\/wiki\/edit\/toolbox\/?$/)) { 179 | var miscEditor; 180 | var $editform = $('#editform'); 181 | var defaultMode = 'default'; 182 | 183 | if (location.pathname.match(/\/wiki\/(edit|create)\/(config\/)?automoderator(-schedule)?\/?$/)) { 184 | defaultMode = "text/x-yaml"; 185 | } 186 | if (location.pathname.match(/\/wiki\/edit\/toolbox\/?$/)) { 187 | defaultMode = "application/json"; 188 | } 189 | // Class added to apply some specific css. 190 | $body.addClass('mod-syntax'); 191 | 192 | // We also need to remove some stuff RES likes to add. 193 | $body.find('.markdownEditor-wrapper, .RESBigEditorPop, .help-toggle').remove(); 194 | 195 | // Theme selector, doesn't really belong here but gives people the opportunity to see how it looks with the css they want to edit. 196 | $editform.prepend(this.themeSelect); 197 | 198 | $('#theme_selector').val(selectedTheme); 199 | 200 | // Here apply codeMirror to the text area, the each itteration allows us to use the javascript object as codemirror works with those. 201 | $('#wiki_page_content').each(function(index, elem){ 202 | 203 | // Editor setup. 204 | miscEditor = CodeMirror.fromTextArea(elem, { 205 | mode: defaultMode, 206 | autoCloseBrackets: true, 207 | lineNumbers: true, 208 | theme: selectedTheme, 209 | indentUnit: 4, 210 | extraKeys: { 211 | "Ctrl-Alt-F": "findPersistent", 212 | "F11": function(cm) { 213 | cm.setOption("fullScreen", !cm.getOption("fullScreen")); 214 | }, 215 | "Esc": function(cm) { 216 | if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false); 217 | }, 218 | "Tab": betterTab, 219 | "Shift-Tab": function (cm) { 220 | cm.indentSelection("subtract"); 221 | } 222 | }, 223 | lineWrapping: enableWordWrap 224 | }); 225 | 226 | $body.find('.CodeMirror.CodeMirror-wrap').prepend(keyboardShortcutsHelper); 227 | }); 228 | 229 | // In order to make save button work we need to hijack and replace it. 230 | $('#wiki_save_button').after(TB.ui.actionButton('save page', 'tb-syntax-button-save-wiki')); 231 | 232 | 233 | // When the toolbox buttons is clicked we put back the content in the text area and click the now hidden original button. 234 | $body.delegate('.tb-syntax-button-save-wiki', 'click', function() { 235 | miscEditor.save(); 236 | $('#wiki_save_button').click(); 237 | }); 238 | 239 | // Actually dealing with the theme dropdown is done here. 240 | $body.on('change keydown', '#theme_selector', function () { 241 | var thingy = $(this); 242 | setTimeout(function () { 243 | miscEditor.setOption("theme", thingy.val()); 244 | }, 0); 245 | }); 246 | 247 | } 248 | }; 249 | 250 | TB.register_module(self); 251 | } 252 | 253 | (function() { 254 | window.addEventListener("TBModuleLoaded", function () { 255 | syntax(); 256 | }); 257 | })(); 258 | --------------------------------------------------------------------------------