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-10-17 提交的版本,查看 最新版本

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

QingJ © 2025

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