// ==UserScript== // @name Stack Exchange comment template context menu // @namespace http://ostermiller.org/ // @version 1.09 // @description Adds a context menu (right click, long press, command click, etc) to comment boxes on Stack Exchange with customizable pre-written responses. // @match *://stackexchange.com/* // @match *://stackoverflow.com/* // @match *://askubuntu.com/* // @match *://superuser.com/* // @match *://serverfault.com/* // @match *://answers.onstartups.com/* // @match *://*.stackexchange.com/* // @match *://*.stackoverflow.com/* // @match *://*.askubuntu.com/* // @match *://*.superuser.com/* // @match *://*.serverfault.com/* // @match *://*.answers.onstartups.com/* // @exclude *://chat.stackoverflow.com/* // @exclude *://chat.stackexchange.com/* // @exclude *://chat.*.stackexchange.com/* // @exclude *://api.*.stackexchange.com/* // @exclude *://data.stackexchange.com/* // @connect raw.githubusercontent.com // @connect * // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @downloadURL none // ==/UserScript== (function() { 'use strict' // Access to JavaScript variables from the Stack Exchange site var $ = unsafeWindow.jQuery // eg. physics.stackexchange.com -> physics function validateSite(s){ var m = /^((?:meta\.)?[a-z0-9]+(?:\.meta)?)\.?[a-z0-9\.]*$/.exec(s.toLowerCase().trim().replace(/^(https?:\/\/)?(www\.)?/,"")) if (!m) return null return m[1] } function validateTag(s){ return s.toLowerCase().trim().replace(/ +/,"-") } // eg hello-world, hello-worlds, hello world, hello worlds, and hw all map to hello-world function makeFilterMap(s){ var m = {} s=s.split(/,/) for (var i=0; i 600){ console.log("Comment template is too long (" + length + "/600): " + c.title) } else if (length > 500 && (!c.types || c.types['flag-question'] || c.types['flag-answer'])){ console.log("Comment template is too long for flagging posts (" + length + "/500): " + c.title) } else if (length > 300 && (!c.types || c.types['edit-question'] || c.types['edit-answer'])){ console.log("Comment template is too long for an edit (" + length + "/300): " + c.title) } else if (length > 200 && (!c.types || c.types['decline-flag'] || c.types['helpful-flag'])){ console.log("Comment template is too long for flag handling (" + length + "/200): " + c.title) } else if (length > 200 && (!c.types || c.types['flag-comment'])){ console.log("Comment template is too long for flagging comments (" + length + "/200): " + c.title) } } } // Serialize the comment templates into local storage function storeComments(){ if (!comments || !comments.length) GM_deleteValue(storageKeys.comments) else GM_setValue(storageKeys.comments, exportComments()) } function parseJsonpComments(s){ var cs = [] var callback = function(o){ for (var i=0; i') // outer translucent lightbox background that covers the whole page var ctcmo = $('
').append(ctcmi) GM_addStyle("#ctcm-back{z-index:999998;display:none;position:fixed;left:0;top:0;width:100vw;height:100vh;background:rgba(0,0,0,.5)}") GM_addStyle("#ctcm-menu{z-index:999999;min-width:320px;position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);background:white;border:5px solid var(--theme-header-foreground-color);padding:1em;max-width:100vw;max-height:100vh;overflow:auto}") GM_addStyle(".ctcm-body{display:none;background:#EEE;padding:.3em;cursor: pointer;") GM_addStyle(".ctcm-expand{float:right;cursor: pointer;}") GM_addStyle(".ctcm-title{margin-top:.3em;cursor: pointer;}") GM_addStyle("#ctcm-menu textarea{width:90vw;min-width:300px;max-width:1000px;height:60vh;resize:both;display:block}") GM_addStyle("#ctcm-menu input[type='text']{width:90vw;min-width:300px;max-width:1000px;display:block}") GM_addStyle("#ctcm-menu button{margin-top:1em;margin-right:.5em}") // Node input: text field where content can be written. // Used for filter tags to know which comment templates to show in which contexts. // Also used for knowing which clicks should show the context menu, // if a type isn't returned by this method, no menu will show up function getType(node){ var prefix = ""; // Most of these rules use properties of the node or the node's parents // to deduce their context if (node.is('.js-rejection-reason-custom')) return "reject-edit" if (node.parents('.js-comment-flag-option').length) return "flag-comment" if (node.parents('.js-flagged-post').length){ if (/decline/.exec(node.attr('placeholder'))) return "decline-flag" else return "helpful-flag" } if (node.parents('.site-specific-pane').length) prefix = "close-" else if (node.parents('.mod-attention-subform').length) prefix = "flag-" else if (node.is('.edit-comment,#edit-comment')) prefix = "edit-" else if(node.is('.js-comment-text-input')) prefix = "" else return null if (node.parents('#question,.question').length) return prefix + "question" if (node.parents('#answers,.answer').length) return prefix + "answer" // Fallback for single post edit page if (node.parents('.post-form').find('h2:last').text()=='Question') return prefix + "question" if (node.parents('.post-form').find('h2:last').text()=='Answer') return prefix + "answer" return null } // Mostly moderator or non-moderator (user.) // Not-logged in and low rep users are not able to comment much // and are unlikely to use this tool, no need to identify them // and give them special behavior. // Maybe add a class for staff in the future? var userclass function getUserClass(){ if (!userclass){ if ($('.js-mod-inbox-button').length) userclass="moderator" else if ($('.s-topbar--item.s-user-card').length) userclass="user" else userclass="anonymous" } return userclass } // The Stack Exchange site this is run on (just the subdoman, eg "stackoverflow") var site function getSite(){ if(!site) site=validateSite(location.hostname) return site } // Which tags are on the question currently being viewed var tags function getTags(){ if(!tags) tags=$.map($('.post-taglist .post-tag'),function(tag){return $(tag).text()}) return tags } // The id of the question currently being viewed function getQuestionId(){ if (!varCache.questionid) varCache.questionid=$('.question').attr('data-questionid') var l = $('.answer-hyperlink') if (!varCache.questionid && l.length) varCache.questionid=l.attr('href').replace(/^\/questions\/([0-9]+).*/,"$1") if (!varCache.questionid) varCache.questionid="-" return varCache.questionid } // The human readable name of the current Stack Exchange site function getSiteName(){ if (!varCache.sitename) varCache.sitename = $('meta[property="og:site_name"]').attr('content').replace(/ ?Stack Exchange/, "") return varCache.sitename } // The Stack Exchange user id for the person using this tool function getMyUserId() { if (!varCache.myUserId) varCache.myUserId = $('a.s-topbar--item.s-user-card').attr('href').replace(/^\/users\/([0-9]+)\/.*/,"$1") return varCache.myUserId } // The full host name of the Stack Exchange site function getSiteUrl(){ if (!varCache.siteurl) varCache.siteurl = location.hostname return varCache.siteurl } // Store the comment text field that was clicked on // so that it can be filled with the comment template var commentTextField // Insert the comment template into the text field // called when a template is clicked in the dialog box // so "this" refers to the clicked item function insertComment(){ // The comment to insert is stored in a div // near the item that was clicked var body = $(this).parent().children('.ctcm-body') var socvr = body.attr('data-socvr') if (socvr){ var url = "//" + getSiteUrl() + "/questions/" + getQuestionId() var title = $('h1').first().text() title = new Option(title).innerHTML $('#content').prepend($(`
SOCVR:
[tag:cv-pls] ${socvr} [${title}](${url})
`)) } var cmt = body.text() // Put in the comment commentTextField.val(cmt).focus() // highlight place for additional input, // if specified in the template var typeHere="[type here]" var typeHereInd = cmt.indexOf(typeHere) if (typeHereInd >= 0) commentTextField[0].setSelectionRange(typeHereInd, typeHereInd + typeHere.length) closeMenu() } // User clicked on the expand icon in the dialog // to show the full text of a comment function expandFullComment(){ $(this).parent().children('.ctcm-body').show() $(this).hide() } // Apply comment tag filters // For a given comment, say whether it // should be shown given the current context function commentMatches(comment, type, user, site, tags){ if (comment.types && !comment.types[type]) return false if (comment.users && !comment.users[user]) return false if (comment.sites && !comment.sites[site]) return false if (comment.tags){ var hasTag = false for(var i=0; tags && i# Comment title\n"+ "Comment body\n"+ "types: "+typeMapInput.replace(/,/g, ", ")+"\n"+ "users: "+userMapInput.replace(/,/g, ", ")+"\n"+ "sites: stackoverflow, physics, meta.stackoverflow, physics.meta, etc\n"+ "tags: javascript, python, etc\n"+ "socvr: Message for Stack Overflow close vote reviews chat"+ "

types, users, sites, tags, and socvr are optional.

" ) ctcmi.append($('