Stack Exchange comment template context menu

Adds a context menu (right click, long press, command click, etc) to comment boxes on Stack Exchange with customizable pre-written responses.

当前为 2021-11-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Stack Exchange comment template context menu
  3. // @namespace http://ostermiller.org/
  4. // @version 1.04
  5. // @description Adds a context menu (right click, long press, command click, etc) to comment boxes on Stack Exchange with customizable pre-written responses.
  6. // @include /https?\:\/\/([a-z\.]*\.)?(stackexchange|askubuntu|superuser|serverfault|stackoverflow|answers\.onstartups)\.com\/.*/
  7. // @exclude *://chat.stackoverflow.com/*
  8. // @exclude *://chat.stackexchange.com/*
  9. // @exclude *://chat.*.stackexchange.com/*
  10. // @exclude *://api.*.stackexchange.com/*
  11. // @exclude *://data.stackexchange.com/*
  12. // @connect raw.githubusercontent.com
  13. // @connect *
  14. // @grant GM_addStyle
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @grant GM_deleteValue
  18. // @grant GM_xmlhttpRequest
  19. // ==/UserScript==
  20. (function() {
  21. 'use strict'
  22.  
  23. // Access to JavaScript variables from the Stack Exchange site
  24. var $ = unsafeWindow.jQuery
  25. var seOpts = unsafeWindow.StackExchange.options || {site:"",user:{}}
  26.  
  27. // eg. physics.stackexchange.com -> physics
  28. function validateSite(s){
  29. var m = /^((?:meta\.)?[a-z0-9]+(?:\.meta)?)\.?[a-z0-9\.]*$/.exec(s.toLowerCase().trim().replace(/^(https?:\/\/)?(www\.)?/,""))
  30. if (!m) return null
  31. return m[1]
  32. }
  33.  
  34. function validateTag(s){
  35. return s.toLowerCase().trim().replace(/ +/,"-")
  36. }
  37.  
  38. // eg hello-world, hello-worlds, hello world, hello worlds, and hw all map to hello-world
  39. function makeFilterMap(s){
  40. var m = {}
  41. s=s.split(/,/)
  42. for (var i=0; i<s.length; i++){
  43. // original
  44. m[s[i]] = s[i]
  45. // plural
  46. m[s[i]+"s"] = s[i]
  47. // with spaces
  48. m[s[i].replace(/-/g," ")] = s[i]
  49. // plural with spaces
  50. m[s[i].replace(/-/g," ")+"s"] = s[i]
  51. // abbreviation
  52. m[s[i].replace(/([a-z])[a-z]+(-|$)/g,"$1")] = s[i]
  53. }
  54. return m
  55. }
  56.  
  57. var userMapInput = "moderator,user"
  58. var userMap = makeFilterMap(userMapInput)
  59. function validateUser(s){
  60. return userMap[s.toLowerCase().trim()]
  61. }
  62.  
  63. var typeMapInput = "question,answer,edit-question,edit-answer,close-question,flag-comment,flag-question,flag-answer,decline-flag,helpful-flag,reject-edit"
  64. var typeMap = makeFilterMap(typeMapInput)
  65. typeMap.c = 'close-question'
  66. typeMap.close = 'close-question'
  67.  
  68. function loadComments(urls){
  69. loadCommentsRecursive([], urls.split(/[\r\n ]+/))
  70. }
  71.  
  72. function loadCommentsRecursive(aComments, aUrls){
  73. if (!aUrls.length) {
  74. if (aComments.length){
  75. comments = aComments
  76. storeComments()
  77. if(GM_getValue(storageKeys.url)){
  78. GM_setValue(storageKeys.lastUpdate, Date.now())
  79. }
  80. }
  81. return
  82. }
  83. var url = aUrls.pop()
  84. if (!url){
  85. loadCommentsRecursive(aComments, aUrls)
  86. return
  87. }
  88. console.log("Loading comments from " + url)
  89. GM_xmlhttpRequest({
  90. method: "GET",
  91. url: url,
  92. onload: function(r){
  93. var lComments = parseComments(r.responseText)
  94. if (!lComments || !lComments.length){
  95. alert("No comment templates loaded from " + url)
  96. } else {
  97. aComments = aComments.concat(lComments)
  98. }
  99. loadCommentsRecursive(aComments, aUrls)
  100. },
  101. onerror: function(){
  102. alert("Could not load comment templates from " + url)
  103. loadCommentsRecursive(aComments, aUrls)
  104. }
  105. })
  106. }
  107.  
  108. function validateType(s){
  109. return typeMap[s.toLowerCase().trim()]
  110. }
  111.  
  112. // Map of functions that clean up the filter-tags on comment templates
  113. var tagValidators = {
  114. tags: validateTag,
  115. sites: validateSite,
  116. users: validateUser,
  117. types: validateType
  118. }
  119.  
  120. // Given a filter tag name and an array of filter tag values,
  121. // clean up and canonicalize each of them
  122. // Put them into a hash set (map each to true) for performant lookups
  123. function validateAllTagValues(tag, arr){
  124. var ret = {}
  125. for (var i=0; i<arr.length; i++){
  126. // look up the validation function for the filter tag type and call it
  127. var v = tagValidators[tag](arr[i])
  128. // Put it in the hash set
  129. if (v) ret[v]=1
  130. }
  131. if (Object.keys(ret).length) return ret
  132. return null
  133. }
  134.  
  135. // List of keys used for storage, centralized for multiple usages
  136. var storageKeys = {
  137. comments: "ctcm-comments",
  138. url: "ctcm-url",
  139. lastUpdate: "ctcm-last-update"
  140. }
  141.  
  142. // On-load, parse comment templates from local storage
  143. var comments = parseComments(GM_getValue(storageKeys.comments))
  144. // The download comment templates from URL if configured
  145. if(GM_getValue(storageKeys.url)){
  146. loadStorageUrlComments()
  147. } else if (!comments || !comments.length){
  148. // If there are NO comments, fetch the defaults
  149. loadComments("https://ostermiller.deadsea.ostermiller.org/stack-exchange-comments.txt")
  150. }
  151.  
  152. checkCommentLengths()
  153. function checkCommentLengths(){
  154. for (var i=0; i<comments.length; i++){
  155. var c = comments[i]
  156. var length = c.comment.length;
  157. if (length > 600){
  158. console.log("Comment template is too long (" + length + "/600): " + c.title)
  159. } else if (length > 500 && (c.types['flag-question'] || c.types['flag-answer'])){
  160. console.log("Comment template is too long for flagging posts (" + length + "/500): " + c.title)
  161. } else if (length > 300 && (c.types['edit-question'] || c.types['edit-answer'])){
  162. console.log("Comment template is too long for an edit (" + length + "/300): " + c.title)
  163. } else if (length > 200 && (c.types['decline-flag'] || c.types['helpful-flag'])){
  164. console.log("Comment template is too long for flag handling (" + length + "/200): " + c.title)
  165. } else if (length > 200 && c.types['flag-comment']){
  166. console.log("Comment template is too long for flagging comments (" + length + "/200): " + c.title)
  167. }
  168. }
  169. }
  170.  
  171. // Serialize the comment templates into local storage
  172. function storeComments(){
  173. if (!comments || !comments.length) GM_deleteValue(storageKeys.comments)
  174. else GM_setValue(storageKeys.comments, exportComments())
  175. }
  176.  
  177. function parseJsonpComments(s){
  178. var cs = []
  179. var callback = function(o){
  180. for (var i=0; i<o.length; i++){
  181. var c = {
  182. title: o[i].name,
  183. comment: o[i].description
  184. }
  185. var m = /^(?:\[([A-Z,]+)\])\s*(.*)$/.exec(c.title);
  186. if (m){
  187. c.title=m[2]
  188. c.types=validateAllTagValues("types",m[1].split(/,/))
  189. }
  190. if (c && c.title && c.comment) cs.push(c)
  191. }
  192. }
  193. eval(s)
  194. return cs
  195. }
  196.  
  197. function parseComments(s){
  198. if (!s) return []
  199. if (s.startsWith("callback(")) return parseJsonpComments(s)
  200. var lines = s.split(/\n|\r|\r\n/)
  201. var c, m, cs = []
  202. for (var i=0; i<lines.length; i++){
  203. var line = lines[i].trim()
  204. if (!line){
  205. // Blank line indicates end of comment
  206. if (c && c.title && c.comment) cs.push(c)
  207. c=null
  208. } else {
  209. // Comment template title
  210. // Starts with #
  211. // May contain type filter tag abbreviations (for compat with SE-AutoReviewComments)
  212. // eg # Comment title
  213. // eg ### [Q,A] Commment title
  214. m = /^#+\s*(?:\[([A-Z,]+)\])?\s*(.*)$/.exec(line);
  215. if (m){
  216. // Stash previous comment if it wasn't already ended by a new line
  217. if (c && c.title && c.comment) cs.push(c)
  218. // Start a new comment with title
  219. c={title:m[2]}
  220. // Handle type filter tags if they exist
  221. if (m[1]) c.types=validateAllTagValues("types",m[1].split(/,/))
  222. } else if (c) {
  223. // Already started parsing a comment, look for filter tags and comment body
  224. m = /^(sites|types|users|tags)\:\s*(.*)$/.exec(line);
  225. if (m){
  226. // Add filter tags
  227. c[m[1]]=validateAllTagValues(m[1], m[2].split(/,/))
  228. } else {
  229. // Comment body (join multiple lines with spaces)
  230. if (c.comment) c.comment=c.comment+" "+line
  231. else c.comment=line
  232. }
  233. } else {
  234. // No comment started, didn't find a comment title
  235. console.log("Could not parse line from comment templates: " + line)
  236. }
  237. }
  238. }
  239. // Stash the last comment if it isn't followed by a new line
  240. if (c && c.title && c.comment) cs.push(c)
  241. return cs
  242. }
  243.  
  244. function sort(arr){
  245. if (!(arr instanceof Array)) arr = Object.keys(arr)
  246. arr.sort()
  247. return arr
  248. }
  249.  
  250. function exportComments(){
  251. var s ="";
  252. for (var i=0; i<comments.length; i++){
  253. var c = comments[i]
  254. s += "# " + c.title + "\n"
  255. s += c.comment + "\n"
  256. if (c.types) s += "types: " + sort(c.types).join(", ") + "\n"
  257. if (c.sites) s += "sites: " + sort(c.sites).join(", ") + "\n"
  258. if (c.users) s += "users: " + sort(c.users).join(", ") + "\n"
  259. if (c.tags) s += "tags: " + sort(c.tags).join(", ") + "\n"
  260. s += "\n"
  261. }
  262. return s;
  263. }
  264.  
  265. // inner lightbox content area
  266. var ctcmi = $('<div id=ctcm-menu>')
  267. // outer translucent lightbox background that covers the whole page
  268. var ctcmo = $('<div id=ctcm-back>').append(ctcmi)
  269. 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)}")
  270. 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}")
  271. GM_addStyle(".ctcm-body{display:none;background:#EEE;padding:.3em;cursor: pointer;")
  272. GM_addStyle(".ctcm-expand{float:right;cursor: pointer;}")
  273. GM_addStyle(".ctcm-title{margin-top:.3em;cursor: pointer;}")
  274. GM_addStyle("#ctcm-menu textarea{width:90vw;min-width:300px;max-width:1000px;height:60vh;resize:both;display:block}")
  275. GM_addStyle("#ctcm-menu input[type='text']{width:90vw;min-width:300px;max-width:1000px;display:block}")
  276. GM_addStyle("#ctcm-menu button{margin-top:1em;margin-right:.5em}")
  277.  
  278. // Node input: text field where content can be written.
  279. // Used for filter tags to know which comment templates to show in which contexts.
  280. // Also used for knowing which clicks should show the context menu,
  281. // if a type isn't returned by this method, no menu will show up
  282. function getType(node){
  283. var prefix = "";
  284.  
  285. // Most of these rules use properties of the node or the node's parents
  286. // to deduce their context
  287.  
  288. if (node.is('.js-rejection-reason-custom')) return "reject-edit"
  289. if (node.parents('.js-comment-flag-option').length) return "flag-comment"
  290. if (node.parents('.js-flagged-post').length){
  291. if (/decline/.exec(node.attr('placeholder'))) return "decline-flag"
  292. else return "helpful-flag"
  293. }
  294.  
  295. if (node.parents('.site-specific-pane').length) prefix = "close-"
  296. else if (node.parents('.mod-attention-subform').length) prefix = "flag-"
  297. else if (node.is('.edit-comment,#edit-comment')) prefix = "edit-"
  298. else if(node.is('.js-comment-text-input')) prefix = ""
  299. else return null
  300.  
  301. if (node.parents('#question,.question').length) return prefix + "question"
  302. if (node.parents('#answers,.answer').length) return prefix + "answer"
  303.  
  304. // Fallback for single post edit page
  305. if (node.parents('.post-form').find('h2:last').text()=='Question') return prefix + "question"
  306. if (node.parents('.post-form').find('h2:last').text()=='Answer') return prefix + "answer"
  307.  
  308. return null
  309. }
  310.  
  311. // Mostly moderator or non-moderator (user.)
  312. // Not-logged in and low rep users are not able to comment much
  313. // and are unlikely to use this tool, no need to identify them
  314. // and give them special behavior.
  315. // Maybe add a class for staff in the future?
  316. var userclass
  317. function getUserClass(){
  318. if (!userclass){
  319. if ($('.js-mod-inbox-button').length) userclass="moderator"
  320. else if ($('.my-profile').length) userclass="user"
  321. else userclass="anonymous"
  322. }
  323. return userclass
  324. }
  325.  
  326. // The Stack Exchange site this is run on (just the subdoman, eg "stackoverflow")
  327. var site
  328. function getSite(){
  329. if(!site) site=validateSite(location.hostname)
  330. return site
  331. }
  332.  
  333. // Which tags are on the question currently being viewed
  334. var tags
  335. function getTags(){
  336. if(!tags) tags=$.map($('.post-taglist .post-tag'),function(tag){return $(tag).text()})
  337. return tags
  338. }
  339.  
  340. // The id of the question currently being viewed
  341. function getQuestionId(){
  342. if (!varCache.questionid) varCache.questionid=$('.question').attr('data-questionid')
  343. var l = $('.answer-hyperlink')
  344. if (!varCache.questionid && l.length) varCache.questionid=l.attr('href').replace(/^\/questions\/([0-9]+).*/,"$1")
  345. if (!varCache.questionid) varCache.questionid="-"
  346. return varCache.questionid
  347. }
  348.  
  349. // The human readable name of the current Stack Exchange site
  350. function getSiteName(){
  351. if (!varCache.sitename){
  352. varCache.sitename = seOpts.site.name || ""
  353. varCache.sitename = varCache.sitename.replace(/ ?Stack Exchange/, "")
  354. }
  355. return varCache.sitename
  356. }
  357.  
  358. // The Stack Exchange user id for the person using this tool
  359. function getMyUserId() {
  360. if (!varCache.myUserId) varCache.myUserId = seOpts.user.userId || ""
  361. return varCache.myUserId
  362. }
  363.  
  364. // The full host name of the Stack Exchange site
  365. function getSiteUrl(){
  366. if (!varCache.siteurl) varCache.siteurl = location.hostname
  367. return varCache.siteurl
  368. }
  369.  
  370. // Store the comment text field that was clicked on
  371. // so that it can be filled with the comment template
  372. var commentTextField
  373.  
  374. // Insert the comment template into the text field
  375. // called when a template is clicked in the dialog box
  376. // so "this" refers to the clicked item
  377. function insertComment(){
  378. // The comment to insert is stored in a div
  379. // near the item that was clicked
  380. var cmt = $(this).parent().children('.ctcm-body').text()
  381.  
  382. // Put in the comment
  383. commentTextField.val(cmt).focus()
  384.  
  385. // highlight place for additional input,
  386. // if specified in the template
  387. var typeHere="[type here]"
  388. var typeHereInd = cmt.indexOf(typeHere)
  389. if (typeHereInd >= 0) commentTextField[0].setSelectionRange(typeHereInd, typeHereInd + typeHere.length)
  390.  
  391. closeMenu()
  392. }
  393.  
  394. // User clicked on the expand icon in the dialog
  395. // to show the full text of a comment
  396. function expandFullComment(){
  397. $(this).parent().children('.ctcm-body').show()
  398. $(this).hide()
  399. }
  400.  
  401. // Apply comment tag filters
  402. // For a given comment, say whether it
  403. // should be shown given the current context
  404. function commentMatches(comment, type, user, site, tags){
  405. if (comment.types && !comment.types[type]) return false
  406. if (comment.users && !comment.users[user]) return false
  407. if (comment.sites && !comment.sites[site]) return false
  408. if (comment.tags){
  409. var hasTag = false
  410. for(var i=0; tags && i<tags.length; i++){
  411. if (comment.tags[tags[i]]) hasTag=true
  412. }
  413. if(!hasTag) return false
  414. }
  415. return true
  416. }
  417.  
  418. // User clicked "Save" when editing the list of comment templates
  419. function doneEditing(){
  420. comments = parseComments($(this).prev('textarea').val())
  421. storeComments()
  422. closeMenu()
  423. }
  424.  
  425. // Show the edit comment dialog
  426. function editComments(){
  427. // Pointless to edit comments that will just get overwritten
  428. // If there is a URL, only allow the URL to be edited
  429. if(GM_getValue(storageKeys.url)) return urlConf()
  430. ctcmi.html(
  431. "<pre># Comment title\n"+
  432. "Comment body\n"+
  433. "types: "+typeMapInput.replace(/,/g, ", ")+"\n"+
  434. "users: "+userMapInput.replace(/,/g, ", ")+"\n"+
  435. "sites: stackoverflow, physics, meta.stackoverflow, physics.meta, etc\n"+
  436. "tags: javascript, python, etc</pre>"+
  437. "<p>Limiting by types, users, sites, and tags is optional.</p>"
  438. )
  439. ctcmi.append($('<textarea>').val(exportComments()))
  440. ctcmi.append($('<button>Save</Button>').click(doneEditing))
  441. ctcmi.append($('<button>Cancel</Button>').click(closeMenu))
  442. ctcmi.append($('<button>From URL...</Button>').click(urlConf))
  443. return false
  444. }
  445.  
  446. function getAuthorNode(postNode){
  447. return postNode.find('.post-signature .user-details[itemprop="author"]')
  448. }
  449.  
  450. function getOpNode(){
  451. if (!varCache.opNode) varCache.opNode = getAuthorNode($('#question,.question'))
  452. return varCache.opNode
  453. }
  454.  
  455. function getUserNodeId(node){
  456. if (!node) return "-"
  457. var link = node.find('a')
  458. if (!link.length) return "-"
  459. var href = link.attr('href')
  460. if (!href) return "-"
  461. return href.replace(/[^0-9]+/g, "")
  462. }
  463.  
  464. function getOpId(){
  465. if (!varCache.opId) varCache.opId = getUserNodeId(getOpNode())
  466. return varCache.opId
  467. }
  468.  
  469. function getUserNodeName(node){
  470. if (!node) return "-"
  471. var link = node.find('a')
  472. if (!link.length) return "-"
  473. // Remove spaces from user names so that they can be used in @name references
  474. return link.text().replace(/ /,"")
  475. }
  476.  
  477. function getOpName(){
  478. if (!varCache.opName) varCache.opName = getUserNodeName(getOpNode())
  479. return varCache.opName
  480. }
  481.  
  482. function getUserNodeRep(node){
  483. if (!node) return "-"
  484. var r = node.find('.reputation-score')
  485. if (!r.length) return "-"
  486. return r.text()
  487. }
  488.  
  489. function getOpRep(){
  490. if (!varCache.opRep) varCache.opRep = getUserNodeRep(getOpNode())
  491. return varCache.opRep
  492. }
  493.  
  494. function getPostNode(){
  495. if (!varCache.postNode) varCache.postNode = commentTextField.parents('#question,.question,.answer')
  496. return varCache.postNode
  497. }
  498.  
  499. function getPostAuthorNode(){
  500. if (!varCache.postAuthorNode) varCache.postAuthorNode = getAuthorNode(getPostNode())
  501. return varCache.postAuthorNode
  502. }
  503.  
  504. function getAuthorId(){
  505. if (!varCache.authorId) varCache.authorId = getUserNodeId(getPostAuthorNode())
  506. return varCache.authorId
  507. }
  508.  
  509. function getAuthorName(){
  510. if (!varCache.authorName) varCache.authorName = getUserNodeName(getPostAuthorNode())
  511. return varCache.authorName
  512. }
  513.  
  514. function getAuthorRep(){
  515. if (!varCache.authorRep) varCache.authorRep = getUserNodeRep(getPostAuthorNode())
  516. return varCache.authorRep
  517. }
  518.  
  519. function getPostId(){
  520. var postNode = getPostNode();
  521. if (!postNode.length) return "-"
  522. if (postNode.attr('data-questionid')) return postNode.attr('data-questionid')
  523. if (postNode.attr('data-answerid')) return postNode.attr('data-answerid')
  524. return "-"
  525. }
  526.  
  527. // Map of variables to functions that return their replacements
  528. var varMap = {
  529. 'SITENAME': getSiteName,
  530. 'SITEURL': getSiteUrl,
  531. 'MYUSERID': getMyUserId,
  532. 'QUESTIONID': getQuestionId,
  533. 'OPID': getOpId,
  534. 'OPNAME': getOpName,
  535. 'OPREP': getOpRep,
  536. 'POSTID': getPostId,
  537. 'AUTHORID': getAuthorId,
  538. 'AUTHORNAME': getAuthorName,
  539. 'AUTHORREP': getAuthorRep
  540. }
  541.  
  542. function logVars(){
  543. var varnames = Object.keys(varMap)
  544. for (var i=0; i<varnames.length; i++){
  545. console.log(varnames[i] + ": " + varMap[varnames[i]]())
  546. }
  547. }
  548.  
  549. // Build regex to find variables from keys of map
  550. var varRegex = new RegExp('\\$('+Object.keys(varMap).join('|')+')\\$?', 'g')
  551. function fillVariables(s){
  552. // Perform the variable replacement
  553. return s.replace(varRegex, function (m) {
  554. // Remove $ before looking up in map
  555. return varMap[m.replace(/\$/g,"")]()
  556. });
  557. }
  558.  
  559. // Show the URL configuration dialog
  560. function urlConf(){
  561. var url = GM_getValue(storageKeys.url)
  562. ctcmi.html(
  563. "<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>"
  564. )
  565. if (url) ctcmi.append("<p>Remove all the URLs to be able to edit the comments in your browser.</p>")
  566. else ctcmi.append("<p>Using a URL will <b>overwrite</b> any edits to the comments you have made.</p>")
  567. ctcmi.append($('<textarea placeholder=https://raw.githubusercontent.com/user/repo/123/stack-exchange-comments.txt>').val(url))
  568. ctcmi.append($('<button>Save</Button>').click(doneUrlConf))
  569. ctcmi.append($('<button>Cancel</Button>').click(closeMenu))
  570. return false
  571. }
  572.  
  573. // User clicked "Save" in URL configuration dialog
  574. function doneUrlConf(){
  575. GM_setValue(storageKeys.url, $(this).prev('textarea').val())
  576. // Force a load by removing the timestamp of the last load
  577. GM_deleteValue(storageKeys.lastUpdate)
  578. loadStorageUrlComments()
  579. closeMenu()
  580. }
  581.  
  582. // Look up the URL from local storage, fetch the URL
  583. // and parse the comment templates from it
  584. // unless it has already been done recently
  585. function loadStorageUrlComments(){
  586. var url = GM_getValue(storageKeys.url)
  587. if (!url) return
  588. var lu = GM_getValue(storageKeys.lastUpdate);
  589. if (lu && lu > Date.now() - 8600000) return
  590. loadComments(url)
  591. }
  592.  
  593. // Hook into clicks for the entire page that should show a context menu
  594. // Only handle the clicks on comment input areas (don't prevent
  595. // the context menu from appearing in other places.)
  596. $(document).contextmenu(function(e){
  597. var target = $(e.target)
  598. if (target.is('.comments-link')){
  599. // The "Add a comment" link
  600. var parent = target.parents('.answer,#question,.question')
  601. // Show the comment text area
  602. target.trigger('click')
  603. // Bring up the context menu for it
  604. showMenu(parent.find('textarea'))
  605. e.preventDefault()
  606. return false
  607. } else if (target.closest('#review-action-Reject,label[for="review-action-Reject"]').length){
  608. // Suggested edit review queue - reject
  609. target.trigger('click')
  610. $('button.js-review-submit').trigger('click')
  611. setTimeout(function(){
  612. // Click "causes harm"
  613. $('#rejection-reason-0').trigger('click')
  614. },100)
  615. setTimeout(function(){
  616. showMenu($('#rejection-reason-0').parents('.flex--item').find('textarea'))
  617. },200)
  618. e.preventDefault()
  619. return false
  620. } else if (target.closest('#review-action-Unsalvageable,label[for="review-action-Unsalvageable"]').length){
  621. // Triage review queue - unsalvageable
  622. target.trigger('click')
  623. $('button.js-review-submit').trigger('click')
  624. showMenuInFlagDialog()
  625. e.preventDefault()
  626. return false
  627. } else if (target.is('.js-flag-post-link')){
  628. // the "Flag" link for a question or answer
  629. // Click it to show pop up
  630. target.trigger('click')
  631. showMenuInFlagDialog()
  632. e.preventDefault()
  633. return false
  634. } else if (target.closest('.js-comment-flag').length){
  635. // The flag icon next to a comment
  636. target.trigger('click')
  637. setTimeout(function(){
  638. // Click "Something else"
  639. $('#comment-flag-type-CommentOther').prop('checked',true).parents('.js-comment-flag-option').find('.js-required-comment').removeClass('d-none')
  640. },100)
  641. setTimeout(function(){
  642. showMenu($('#comment-flag-type-CommentOther').parents('.js-comment-flag-option').find('textarea'))
  643. },200)
  644. e.preventDefault()
  645. return false
  646. } else if (target.closest('#review-action-Close,label[for="review-action-Close"],#review-action-NeedsAuthorEdit,label[for="review-action-NeedsAuthorEdit"]').length){
  647. // Close votes review queue - close action
  648. // or Triage review queue - needs author edit action
  649. target.trigger('click')
  650. $('button.js-review-submit').trigger('click')
  651. showMenuInCloseDialog()
  652. e.preventDefault()
  653. return false
  654. } else if (target.is('.js-close-question-link')){
  655. // The "Close" link for a question
  656. target.trigger('click')
  657. showMenuInCloseDialog()
  658. e.preventDefault()
  659. return false
  660. } else if (target.is('textarea,input[type="text"]') && (!target.val() || target.val() == target[0].defaultValue)){
  661. // A text field that is blank or hasn't been modified
  662. var type = getType(target)
  663. //console.log("Type: " + type)
  664. if (type){
  665. // A text field for entering a comment
  666. showMenu(target)
  667. e.preventDefault()
  668. return false
  669. }
  670. }
  671. })
  672.  
  673. function showMenuInFlagDialog(){
  674. // Wait for the popup
  675. setTimeout(function(){
  676. $('input[value="PostOther"]').trigger('click')
  677. },100)
  678. setTimeout(function(){
  679. showMenu($('input[value="PostOther"]').parents('label').find('textarea'))
  680. },200)
  681. }
  682.  
  683. function showMenuInCloseDialog(){
  684. setTimeout(function(){
  685. $('#closeReasonId-SiteSpecific').trigger('click')
  686. },100)
  687. setTimeout(function(){
  688. $('#siteSpecificCloseReasonId-other').trigger('click')
  689. },200)
  690. setTimeout(function(){
  691. showMenu($('#siteSpecificCloseReasonId-other').parents('.js-popup-radio-action').find('textarea'))
  692. },300)
  693. }
  694.  
  695. var varCache={} // Cache variables so they don't have to be looked up for every single question
  696. function showMenu(target){
  697. varCache={} // Clear the variable cache
  698. commentTextField=target
  699. var type = getType(target)
  700. var user = getUserClass()
  701. var site = getSite()
  702. var tags = getTags()
  703. ctcmi.html("")
  704. //logVars()
  705. for (var i=0; i<comments.length; i++){
  706. if(commentMatches(comments[i], type, user, site, tags)){
  707. ctcmi.append(
  708. $('<div class=ctcm-comment>').append(
  709. $('<span class=ctcm-expand>\u25bc</span>').click(expandFullComment)
  710. ).append(
  711. $('<h4 class=ctcm-title>').text(comments[i].title).click(insertComment)
  712. ).append(
  713. $('<div class=ctcm-body>').text(fillVariables(comments[i].comment)).click(insertComment)
  714. )
  715. )
  716. }
  717. }
  718. ctcmi.append($('<button>Edit</Button>').click(editComments))
  719. ctcmi.append($('<button>Cancel</Button>').click(closeMenu))
  720. if (target.parents('.popup,#modal-base').length) target.after(ctcmo)
  721. else $(document.body).append(ctcmo)
  722. ctcmo.show()
  723. }
  724.  
  725. function closeMenu(){
  726. ctcmo.hide()
  727. ctcmo.remove()
  728. }
  729.  
  730. // Hook into clicks anywhere in the document
  731. // and listen for ones that related to our dialog
  732. $(document).click(function(e){
  733. if(ctcmo.is(':visible')){
  734. // dialog is open
  735. if($(e.target).parents('#ctcm-back').length == 0) {
  736. // click wasn't on the dialog itself
  737. closeMenu()
  738. }
  739. // Clicks when the dialog are open belong to us,
  740. // prevent other things from happening
  741. e.preventDefault()
  742. return false
  743. }
  744. })
  745. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址