- // ==UserScript==
- // @name Stack Exchange comment template context menu
- // @namespace http://ostermiller.org/
- // @version 1.15
- // @description Adds a context menu (right click, long press, command click, etc) to comment boxes on Stack Exchange with customizable pre-written responses.
- // @match https://*.stackexchange.com/questions/*
- // @match https://*.stackexchange.com/review/*
- // @match https://*.stackexchange.com/admin/*
- // @match https://*.stackoverflow.com/*questions/*
- // @match https://*.stackoverflow.com/review/*
- // @match https://*.stackoverflow.com/admin/*
- // @match https://*.askubuntu.com/questions/*
- // @match https://*.askubuntu.com/review/*
- // @match https://*.askubuntu.com/admin/*
- // @match https://*.superuser.com/questions/*
- // @match https://*.superuser.com/review/*
- // @match https://*.superuser.com/admin/*
- // @match https://*.serverfault.com/questions/*
- // @match https://*.serverfault.com/review/*
- // @match https://*.serverfault.com/admin/*
- // @match https://*.mathoverflow.net/questions/*
- // @match https://*.mathoverflow.net/review/*
- // @match https://*.mathoverflow.net/admin/*
- // @match https://*.stackapps.com/questions/*
- // @match https://*.stackapps.com/review/*
- // @match https://*.stackapps.com/admin/*
- // @connect raw.githubusercontent.com
- // @connect *
- // @grant GM_addStyle
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_deleteValue
- // @grant GM_xmlhttpRequest
- // ==/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(/ +/g,"-")
- }
-
- // 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<s.length; i++){
- // original
- m[s[i]] = s[i]
- // plural
- m[s[i]+"s"] = s[i]
- // with spaces
- m[s[i].replace(/-/g," ")] = s[i]
- // plural with spaces
- m[s[i].replace(/-/g," ")+"s"] = s[i]
- // abbreviation
- m[s[i].replace(/([a-z])[a-z]+(-|$)/g,"$1")] = s[i]
- }
- return m
- }
-
- var userMapInput = "moderator,user"
- var userMap = makeFilterMap(userMapInput)
- function validateUser(s){
- return userMap[s.toLowerCase().trim()]
- }
-
- var typeMapInput = "question,answer,edit-question,edit-answer,close-question,flag-comment,flag-question,flag-answer,decline-flag,helpful-flag,reject-edit"
- var typeMap = makeFilterMap(typeMapInput)
- typeMap.c = 'close-question'
- typeMap.close = 'close-question'
-
- function loadComments(urls){
- loadCommentsRecursive([], urls.split(/[\r\n ]+/))
- }
-
- function loadCommentsRecursive(aComments, aUrls){
- if (!aUrls.length) {
- if (aComments.length){
- comments = aComments
- storeComments()
- if(GM_getValue(storageKeys.url)){
- GM_setValue(storageKeys.lastUpdate, Date.now())
- }
- }
- return
- }
- var url = aUrls.pop()
- if (!url){
- loadCommentsRecursive(aComments, aUrls)
- return
- }
- console.log("Loading comments from " + url)
- GM_xmlhttpRequest({
- method: "GET",
- url: url,
- onload: function(r){
- var lComments = parseComments(r.responseText)
- if (!lComments || !lComments.length){
- alert("No comment templates loaded from " + url)
- } else {
- aComments = aComments.concat(lComments)
- }
- loadCommentsRecursive(aComments, aUrls)
- },
- onerror: function(){
- alert("Could not load comment templates from " + url)
- loadCommentsRecursive(aComments, aUrls)
- }
- })
- }
-
- function validateType(s){
- return typeMap[s.toLowerCase().trim()]
- }
-
- // Map of functions that clean up the filter-tags on comment templates
- var tagValidators = {
- tags: validateTag,
- sites: validateSite,
- users: validateUser,
- types: validateType
- }
-
- var attributeValidators = {
- socvr: trim
- }
-
- function trim(s){
- return s.trim()
- }
-
- // Given a filter tag name and an array of filter tag values,
- // clean up and canonicalize each of them
- // Put them into a hash set (map each to true) for performant lookups
- function validateAllTagValues(tag, arr){
- var ret = {}
- for (var i=0; i<arr.length; i++){
- // look up the validation function for the filter tag type and call it
- var v = tagValidators[tag](arr[i])
- // Put it in the hash set
- if (v) ret[v]=1
- }
- if (Object.keys(ret).length) return ret
- return null
- }
-
- function validateValues(tag, value){
- if (tag in tagValidators) return validateAllTagValues(tag, value.split(/,/))
- if (tag in attributeValidators) return attributeValidators[tag](value)
- return null
- }
-
- // List of keys used for storage, centralized for multiple usages
- var storageKeys = {
- comments: "ctcm-comments",
- url: "ctcm-url",
- lastUpdate: "ctcm-last-update"
- }
-
- // On-load, parse comment templates from local storage
- var comments = parseComments(GM_getValue(storageKeys.comments))
- // The download comment templates from URL if configured
- if(GM_getValue(storageKeys.url)){
- loadStorageUrlComments()
- } else if (!comments || !comments.length){
- // If there are NO comments, fetch the defaults
- loadComments("https://raw.githubusercontent.com/stephenostermiller/stack-exchange-comment-templates/master/default-templates.txt")
- }
-
- function hasCommentWarn(){
- return checkCommentLengths().length > 0
- }
-
- function commentWarnHtml(){
- var problems = checkCommentLengths()
- if (!problems.length) return $('<span>')
- var s = $("<ul>")
- for (var i=0; i<problems.length; i++){
- s.append($('<li>').text("⚠️ " + problems[i]))
- }
- return $('<div>').append($('<h3>').text("Problems")).append(s)
- }
-
- function checkCommentLengths(){
- var problems = []
- for (var i=0; i<comments.length; i++){
- var c = comments[i]
- var length = c.comment.length;
- if (length > 600){
- problems.push("Comment template is too long (" + length + "/600): " + c.title)
- } else if (length > 500 && (!c.types || c.types['flag-question'] || c.types['flag-answer'])){
- problems.push("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'])){
- problems.push("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'])){
- problems.push("Comment template is too long for flag handling (" + length + "/200): " + c.title)
- } else if (length > 200 && (!c.types || c.types['flag-comment'])){
- problems.push("Comment template is too long for flagging comments (" + length + "/200): " + c.title)
- }
- }
- return problems
- }
-
- // 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<o.length; i++){
- var c = {
- title: o[i].name,
- comment: o[i].description
- }
- var m = /^(?:\[([A-Z,]+)\])\s*(.*)$/.exec(c.title);
- if (m){
- c.title=m[2]
- c.types=validateValues("types",m[1])
- }
- if (c && c.title && c.comment) cs.push(c)
- }
- }
- eval(s)
- return cs
- }
-
- function parseComments(s){
- if (!s) return []
- if (s.startsWith("callback(")) return parseJsonpComments(s)
- var lines = s.split(/\n|\r|\r\n/)
- var c, m, cs = []
- for (var i=0; i<lines.length; i++){
- var line = lines[i].trim()
- if (!line){
- // Blank line indicates end of comment
- if (c && c.title && c.comment) cs.push(c)
- c=null
- } else {
- // Comment template title
- // Starts with #
- // May contain type filter tag abbreviations (for compat with SE-AutoReviewComments)
- // eg # Comment title
- // eg ### [Q,A] Comment title
- m = /^#+\s*(?:\[([A-Z,]+)\])?\s*(.*)$/.exec(line);
- if (m){
- // Stash previous comment if it wasn't already ended by a new line
- if (c && c.title && c.comment) cs.push(c)
- // Start a new comment with title
- c={title:m[2]}
- // Handle type filter tags if they exist
- if (m[1]) c.types=validateValues("types",m[1])
- } else if (c) {
- // Already started parsing a comment, look for filter tags and comment body
- m = /^(sites|types|users|tags|socvr)\:\s*(.*)$/.exec(line);
- if (m){
- // Add filter tags
- c[m[1]]=validateValues(m[1],m[2])
- } else {
- // Comment body (join multiple lines with spaces)
- if (c.comment) c.comment=c.comment+" "+line
- else c.comment=line
- }
- } else {
- // No comment started, didn't find a comment title
- console.log("Could not parse line from comment templates: " + line)
- }
- }
- }
- // Stash the last comment if it isn't followed by a new line
- if (c && c.title && c.comment) cs.push(c)
- return cs
- }
-
- function sort(arr){
- if (!(arr instanceof Array)) arr = Object.keys(arr)
- arr.sort()
- return arr
- }
-
- function exportComments(){
- var s ="";
- for (var i=0; i<comments.length; i++){
- var c = comments[i]
- s += "# " + c.title + "\n"
- s += c.comment + "\n"
- if (c.types) s += "types: " + sort(c.types).join(", ") + "\n"
- if (c.sites) s += "sites: " + sort(c.sites).join(", ") + "\n"
- if (c.users) s += "users: " + sort(c.users).join(", ") + "\n"
- if (c.tags) s += "tags: " + sort(c.tags).join(", ") + "\n"
- if (c.socvr) s += "socvr: " + c.socvr + "\n"
- s += "\n"
- }
- return s;
- }
-
- // inner lightbox content area
- var ctcmi = $('<div id=ctcm-menu>')
- // outer translucent lightbox background that covers the whole page
- var ctcmo = $('<div id=ctcm-back>').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:var(--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:var(--black-050);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}")
- GM_addStyle("#ctcm-menu button.right{float:right}")
- GM_addStyle("#ctcm-menu h3{margin:.5em auto;font-size: 150%;}")
-
- // 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 subdomain, 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(){
- var id = $('.question').attr('data-questionid')
- if (!id){
- var l = $('.answer-hyperlink')
- if (l.length) id=l.attr('href').replace(/^\/questions\/([0-9]+).*/,"$1")
- }
- if (!id) id="-"
- return id
- }
-
- // The human readable name of the current Stack Exchange site
- function getSiteName(){
- return $('meta[property="og:site_name"]').attr('content').replace(/ ?Stack Exchange/, "")
- }
-
- // The Stack Exchange user id for the person using this tool
- function getMyUserId() {
- return $('a.s-topbar--item.s-user-card').attr('href').replace(/^\/users\/([0-9]+)\/.*/,"$1")
- }
-
- // The Stack Exchange user name for the person using this tool
- function getMyName() {
- var n=$('header .s-avatar[title]').attr('title')
- if (!n) return "-"
- return n.replace(/ /g,"")
- }
-
-
- // The full host name of the Stack Exchange site
- function getSiteUrl(){
- return location.hostname
- }
-
- // 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($(`<div style="border:5px solid blue;padding:.7em;margin:.5em 0"><a target=_blank href=//chat.stackoverflow.com/rooms/41570/so-close-vote-reviewers>SOCVR: </a><div>[tag:cv-pls] ${socvr} [${title}](${url})</div></div>`))
- }
- 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<tags.length; i++){
- if (comment.tags[tags[i]]) hasTag=true
- }
- if(!hasTag) return false
- }
- return true
- }
-
- // User clicked "Save" when editing the list of comment templates
- function doneEditing(){
- comments = parseComments($(this).prev('textarea').val())
- storeComments()
- closeMenu()
- }
-
- // Show the edit comment dialog
- function editComments(){
- // Pointless to edit comments that will just get overwritten
- // If there is a URL, only allow the URL to be edited
- if(GM_getValue(storageKeys.url)) return urlConf()
- ctcmi.html(
- "<pre># 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</pre>"+
- "<p>types, users, sites, tags, and socvr are optional.</p>"
- )
- .append($('<textarea>').val(exportComments()))
- .append($('<button>Save</button>').click(doneEditing))
- .append($('<button>Cancel</button>').click(closeMenu))
- .append($('<button>From URL...</button>').click(urlConf))
- return false
- }
-
- // Show info
- function showInfo(){
- ctcmi.html(
- "<div><h2><a target=_blank href=//github.com/stephenostermiller/stack-exchange-comment-templates>Stack Exchange Comment Templates Context Menu</a></h2></div>"
- )
- .append(commentWarnHtml())
- .append(htmlVars())
- .append($('<button>Cancel</button>').click(closeMenu))
- return false
- }
- function getAuthorNode(postNode){
- return postNode.find('.post-signature .user-details[itemprop="author"]')
- }
-
- function getOpNode(){
- return getAuthorNode($('#question,.question'))
- }
-
- function getUserNodeId(node){
- if (!node) return "-"
- var link = node.find('a')
- if (!link.length) return "-"
- var href = link.attr('href')
- if (!href) return "-"
- return href.replace(/[^0-9]+/g, "")
- }
-
- function getOpId(){
- return getUserNodeId(getOpNode())
- }
-
- function getUserNodeName(node){
- if (!node) return "-"
- var link = node.find('a')
- if (!link.length) return "-"
- // Remove spaces from user names so that they can be used in @name references
- return link.text().replace(/ /g,"")
- }
-
- function getOpName(){
- return getUserNodeName(getOpNode())
- }
-
- function getUserNodeRep(node){
- if (!node) return "-"
- var r = node.find('.reputation-score')
- if (!r.length) return "-"
- return r.text()
- }
-
- function getOpRep(){
- return getUserNodeRep(getOpNode())
- }
-
- function getPostNode(){
- return commentTextField.parents('#question,.question,.answer')
- }
-
- function getPostAuthorNode(){
- return getAuthorNode(getPostNode())
- }
-
- function getAuthorId(){
- return getUserNodeId(getPostAuthorNode())
- }
-
- function getAuthorName(){
- return getUserNodeName(getPostAuthorNode())
- }
-
- function getAuthorRep(){
- return getUserNodeRep(getPostAuthorNode())
- }
-
- function getPostId(){
- var postNode = getPostNode();
- if (!postNode.length) return "-"
- if (postNode.attr('data-questionid')) return postNode.attr('data-questionid')
- if (postNode.attr('data-answerid')) return postNode.attr('data-answerid')
- return "-"
- }
-
- // Map of variables to functions that return their replacements
- var varMap = {
- 'SITENAME': getSiteName,
- 'SITEURL': getSiteUrl,
- 'MYUSERID': getMyUserId,
- 'MYNAME': getMyName,
- 'QUESTIONID': getQuestionId,
- 'OPID': getOpId,
- 'OPNAME': getOpName,
- 'OPREP': getOpRep,
- 'POSTID': getPostId,
- 'AUTHORID': getAuthorId,
- 'AUTHORNAME': getAuthorName,
- 'AUTHORREP': getAuthorRep
- }
-
- // Cache variables so they don't have to be looked up for every single question
- var varCache={}
-
- function getCachedVar(key){
- if (!varCache[key]) varCache[key] = varMap[key]()
- return varCache[key]
- }
-
- function hasVarWarn(){
- var varnames = Object.keys(varMap)
- for (var i=0; i<varnames.length; i++){
- if (getCachedVar(varnames[i]).match(/^-?$/)) return true
- }
- return false
- }
-
- function htmlVars(){
- var n = $("<ul>")
- var varnames = Object.keys(varMap)
- for (var i=0; i<varnames.length; i++){
- var li=$("<li>")
- var val = getCachedVar(varnames[i])
- if (val.match(/^-?$/)) li.append($("<span>").text("⚠️ "))
- li.append($("<b>").text(varnames[i])).append($("<span>").text(": ")).append($("<span>").text(val))
- n.append(li)
- }
- return $('<div>').append($('<h3>').text("Variables")).append(n)
- }
-
- // Build regex to find variables from keys of map
- var varRegex = new RegExp('\\$('+Object.keys(varMap).join('|')+')\\$?', 'g')
- function fillVariables(s){
- // Perform the variable replacement
- return s.replace(varRegex, function (m) {
- // Remove $ from variable name
- return getCachedVar(m.replace(/\$/g,""))
- });
- }
-
- // Show the URL configuration dialog
- function urlConf(){
- var url = GM_getValue(storageKeys.url)
- ctcmi.html(
- "<p>Comments will be loaded from these URLs when saved and once a day afterwards. Multiple URLs can be specified, each on its own line. Github raw URLs have been whitelisted. Other URLs will ask for your permission.</p>"
- )
- if (url) ctcmi.append("<p>Remove all the URLs to be able to edit the comments in your browser.</p>")
- else ctcmi.append("<p>Using a URL will <b>overwrite</b> any edits to the comments you have made.</p>")
- ctcmi.append($('<textarea placeholder=https://raw.githubusercontent.com/user/repo/123/stack-exchange-comments.txt>').val(url))
- ctcmi.append($('<button>Save</button>').click(doneUrlConf))
- ctcmi.append($('<button>Cancel</button>').click(closeMenu))
- return false
- }
-
- // User clicked "Save" in URL configuration dialog
- function doneUrlConf(){
- GM_setValue(storageKeys.url, $(this).prev('textarea').val())
- // Force a load by removing the timestamp of the last load
- GM_deleteValue(storageKeys.lastUpdate)
- loadStorageUrlComments()
- closeMenu()
- }
-
- // Look up the URL from local storage, fetch the URL
- // and parse the comment templates from it
- // unless it has already been done recently
- function loadStorageUrlComments(){
- var url = GM_getValue(storageKeys.url)
- if (!url) return
- var lu = GM_getValue(storageKeys.lastUpdate);
- if (lu && lu > Date.now() - 8600000) return
- loadComments(url)
- }
-
- // Hook into clicks for the entire page that should show a context menu
- // Only handle the clicks on comment input areas (don't prevent
- // the context menu from appearing in other places.)
- $(document).contextmenu(function(e){
- var target = $(e.target)
- if (target.is('.comments-link')){
- // The "Add a comment" link
- var parent = target.parents('.answer,#question,.question')
- // Show the comment text area
- target.trigger('click')
- // Bring up the context menu for it
- showMenu(parent.find('textarea'))
- e.preventDefault()
- return false
- } else if (target.closest('#review-action-Reject,label[for="review-action-Reject"]').length){
- // Suggested edit review queue - reject
- target.trigger('click')
- $('button.js-review-submit').trigger('click')
- setTimeout(function(){
- // Click "causes harm"
- $('#rejection-reason-0').trigger('click')
- },100)
- setTimeout(function(){
- showMenu($('#rejection-reason-0').parents('.flex--item').find('textarea'))
- },200)
- e.preventDefault()
- return false
- } else if (target.closest('#review-action-Unsalvageable,label[for="review-action-Unsalvageable"]').length){
- // Triage review queue - unsalvageable
- target.trigger('click')
- $('button.js-review-submit').trigger('click')
- showMenuInFlagDialog()
- e.preventDefault()
- return false
- } else if (target.is('.js-flag-post-link')){
- // the "Flag" link for a question or answer
- // Click it to show pop up
- target.trigger('click')
- showMenuInFlagDialog()
- e.preventDefault()
- return false
- } else if (target.closest('.js-comment-flag').length){
- // The flag icon next to a comment
- target.trigger('click')
- setTimeout(function(){
- // Click "Something else"
- $('#comment-flag-type-CommentOther').prop('checked',true).parents('.js-comment-flag-option').find('.js-required-comment').removeClass('d-none')
- },100)
- setTimeout(function(){
- showMenu($('#comment-flag-type-CommentOther').parents('.js-comment-flag-option').find('textarea'))
- },200)
- e.preventDefault()
- return false
- } else if (target.closest('#review-action-Close,label[for="review-action-Close"],#review-action-NeedsAuthorEdit,label[for="review-action-NeedsAuthorEdit"]').length){
- // Close votes review queue - close action
- // or Triage review queue - needs author edit action
- target.trigger('click')
- $('button.js-review-submit').trigger('click')
- showMenuInCloseDialog()
- e.preventDefault()
- return false
- } else if (target.is('.js-close-question-link')){
- // The "Close" link for a question
- target.trigger('click')
- showMenuInCloseDialog()
- e.preventDefault()
- return false
- } else if (target.is('textarea,input[type="text"]') && (!target.val() || target.val() == target[0].defaultValue)){
- // A text field that is blank or hasn't been modified
- var type = getType(target)
- if (type){
- // A text field for entering a comment
- showMenu(target)
- e.preventDefault()
- return false
- }
- }
- })
-
- function showMenuInFlagDialog(){
- // Wait for the popup
- setTimeout(function(){
- $('input[value="PostOther"]').trigger('click')
- },100)
- setTimeout(function(){
- showMenu($('input[value="PostOther"]').parents('label').find('textarea'))
- },200)
- }
-
- function showMenuInCloseDialog(){
- setTimeout(function(){
- $('#closeReasonId-SiteSpecific').trigger('click')
- },100)
- setTimeout(function(){
- $('#siteSpecificCloseReasonId-other').trigger('click')
- },200)
- setTimeout(function(){
- showMenu($('#siteSpecificCloseReasonId-other').parents('.js-popup-radio-action').find('textarea'))
- },300)
- }
-
- function filterComments(e){
- if (e.key === "Enter") {
- // Pressing enter in the comment filter
- // should insert the first visible comment
- insertComment.call($('.ctcm-title:visible').first())
- e.preventDefault()
- return false
- }
- if (e.key == "Escape"){
- closeMenu()
- e.preventDefault()
- return false
- }
- // Show comments that contain the filter (case-insensitive)
- var f = $(this).val().toLowerCase()
- $('.ctcm-comment').each(function(){
- var c = $(this).text().toLowerCase()
- $(this).toggle(c.includes(f))
- })
- }
-
- function showMenu(target){
- varCache={} // Clear the variable cache
- commentTextField=target
- var type = getType(target)
- var user = getUserClass()
- var site = getSite()
- var tags = getTags()
- ctcmi.html("")
- var filter=$('<input type=text placeholder="filter... (type then press enter to insert the first comment)">').keyup(filterComments).change(filterComments)
- ctcmi.append(filter)
- for (var i=0; i<comments.length; i++){
- if(commentMatches(comments[i], type, user, site, tags)){
- ctcmi.append(
- $('<div class=ctcm-comment>').append(
- $('<span class=ctcm-expand>\u25bc</span>').click(expandFullComment)
- ).append(
- $('<h4 class=ctcm-title>').text(comments[i].title).click(insertComment)
- ).append(
- $('<div class=ctcm-body>').text(fillVariables(comments[i].comment)).click(insertComment).attr('data-socvr',comments[i].socvr||"")
- )
- )
- }
- }
- var info = (hasVarWarn()||hasCommentWarn())?"⚠️":"ⓘ"
- ctcmi.append($('<button>Edit</button>').click(editComments))
- ctcmi.append($('<button>Cancel</button>').click(closeMenu))
- ctcmi.append($('<button class=right>').text(info).click(showInfo))
- target.parents('.popup,#modal-base,body').first().append(ctcmo)
- ctcmo.show()
- filter.focus()
- }
-
- function closeMenu(){
- ctcmo.hide()
- ctcmo.remove()
- }
-
- // Hook into clicks anywhere in the document
- // and listen for ones that related to our dialog
- $(document).click(function(e){
- // dialog is open
- if(ctcmo.is(':visible')){
- // Allow clicks on links in the dialog to have default behavior
- if($(e.target).is('a')) return true
- // click wasn't on the dialog itself
- if(!$(e.target).parents('#ctcm-back').length) closeMenu()
- // Clicks when the dialog are open belong to us,
- // prevent other things from happening
- e.preventDefault()
- return false
- }
- })
- })();