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

QingJ © 2025

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