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.

当前为 2022-01-19 提交的版本,查看 最新版本

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

QingJ © 2025

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