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. if (socvr){
  394. var url = "//" + getSiteUrl() + "/questions/" + getQuestionId()
  395. var title = $('h1').first().text()
  396. title = new Option(title).innerHTML
  397. $('#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>`))
  398. }
  399. var cmt = body.text()
  400.  
  401. // Put in the comment
  402. commentTextField.val(cmt).focus()
  403.  
  404. // highlight place for additional input,
  405. // if specified in the template
  406. var typeHere="[type here]"
  407. var typeHereInd = cmt.indexOf(typeHere)
  408. if (typeHereInd >= 0) commentTextField[0].setSelectionRange(typeHereInd, typeHereInd + typeHere.length)
  409.  
  410. closeMenu()
  411. }
  412.  
  413. // User clicked on the expand icon in the dialog
  414. // to show the full text of a comment
  415. function expandFullComment(){
  416. $(this).parent().children('.ctcm-body').show()
  417. $(this).hide()
  418. }
  419.  
  420. // Apply comment tag filters
  421. // For a given comment, say whether it
  422. // should be shown given the current context
  423. function commentMatches(comment, type, user, site, tags){
  424. if (comment.types && !comment.types[type]) return false
  425. if (comment.users && !comment.users[user]) return false
  426. if (comment.sites && !comment.sites[site]) return false
  427. if (comment.tags){
  428. var hasTag = false
  429. for(var i=0; tags && i<tags.length; i++){
  430. if (comment.tags[tags[i]]) hasTag=true
  431. }
  432. if(!hasTag) return false
  433. }
  434. return true
  435. }
  436.  
  437. // User clicked "Save" when editing the list of comment templates
  438. function doneEditing(){
  439. comments = parseComments($(this).prev('textarea').val())
  440. storeComments()
  441. closeMenu()
  442. }
  443.  
  444. // Show the edit comment dialog
  445. function editComments(){
  446. // Pointless to edit comments that will just get overwritten
  447. // If there is a URL, only allow the URL to be edited
  448. if(GM_getValue(storageKeys.url)) return urlConf()
  449. ctcmi.html(
  450. "<pre># Comment title\n"+
  451. "Comment body\n"+
  452. "types: "+typeMapInput.replace(/,/g, ", ")+"\n"+
  453. "users: "+userMapInput.replace(/,/g, ", ")+"\n"+
  454. "sites: stackoverflow, physics, meta.stackoverflow, physics.meta, etc\n"+
  455. "tags: javascript, python, etc\n"+
  456. "socvr: Message for Stack Overflow close vote reviews chat</pre>"+
  457. "<p>types, users, sites, tags, and socvr are optional.</p>"
  458. )
  459. ctcmi.append($('<textarea>').val(exportComments()))
  460. ctcmi.append($('<button>Save</Button>').click(doneEditing))
  461. ctcmi.append($('<button>Cancel</Button>').click(closeMenu))
  462. ctcmi.append($('<button>From URL...</Button>').click(urlConf))
  463. return false
  464. }
  465.  
  466. function getAuthorNode(postNode){
  467. return postNode.find('.post-signature .user-details[itemprop="author"]')
  468. }
  469.  
  470. function getOpNode(){
  471. if (!varCache.opNode) varCache.opNode = getAuthorNode($('#question,.question'))
  472. return varCache.opNode
  473. }
  474.  
  475. function getUserNodeId(node){
  476. if (!node) return "-"
  477. var link = node.find('a')
  478. if (!link.length) return "-"
  479. var href = link.attr('href')
  480. if (!href) return "-"
  481. return href.replace(/[^0-9]+/g, "")
  482. }
  483.  
  484. function getOpId(){
  485. if (!varCache.opId) varCache.opId = getUserNodeId(getOpNode())
  486. return varCache.opId
  487. }
  488.  
  489. function getUserNodeName(node){
  490. if (!node) return "-"
  491. var link = node.find('a')
  492. if (!link.length) return "-"
  493. // Remove spaces from user names so that they can be used in @name references
  494. return link.text().replace(/ /,"")
  495. }
  496.  
  497. function getOpName(){
  498. if (!varCache.opName) varCache.opName = getUserNodeName(getOpNode())
  499. return varCache.opName
  500. }
  501.  
  502. function getUserNodeRep(node){
  503. if (!node) return "-"
  504. var r = node.find('.reputation-score')
  505. if (!r.length) return "-"
  506. return r.text()
  507. }
  508.  
  509. function getOpRep(){
  510. if (!varCache.opRep) varCache.opRep = getUserNodeRep(getOpNode())
  511. return varCache.opRep
  512. }
  513.  
  514. function getPostNode(){
  515. if (!varCache.postNode) varCache.postNode = commentTextField.parents('#question,.question,.answer')
  516. return varCache.postNode
  517. }
  518.  
  519. function getPostAuthorNode(){
  520. if (!varCache.postAuthorNode) varCache.postAuthorNode = getAuthorNode(getPostNode())
  521. return varCache.postAuthorNode
  522. }
  523.  
  524. function getAuthorId(){
  525. if (!varCache.authorId) varCache.authorId = getUserNodeId(getPostAuthorNode())
  526. return varCache.authorId
  527. }
  528.  
  529. function getAuthorName(){
  530. if (!varCache.authorName) varCache.authorName = getUserNodeName(getPostAuthorNode())
  531. return varCache.authorName
  532. }
  533.  
  534. function getAuthorRep(){
  535. if (!varCache.authorRep) varCache.authorRep = getUserNodeRep(getPostAuthorNode())
  536. return varCache.authorRep
  537. }
  538.  
  539. function getPostId(){
  540. var postNode = getPostNode();
  541. if (!postNode.length) return "-"
  542. if (postNode.attr('data-questionid')) return postNode.attr('data-questionid')
  543. if (postNode.attr('data-answerid')) return postNode.attr('data-answerid')
  544. return "-"
  545. }
  546.  
  547. // Map of variables to functions that return their replacements
  548. var varMap = {
  549. 'SITENAME': getSiteName,
  550. 'SITEURL': getSiteUrl,
  551. 'MYUSERID': getMyUserId,
  552. 'QUESTIONID': getQuestionId,
  553. 'OPID': getOpId,
  554. 'OPNAME': getOpName,
  555. 'OPREP': getOpRep,
  556. 'POSTID': getPostId,
  557. 'AUTHORID': getAuthorId,
  558. 'AUTHORNAME': getAuthorName,
  559. 'AUTHORREP': getAuthorRep
  560. }
  561.  
  562. function logVars(){
  563. var varnames = Object.keys(varMap)
  564. for (var i=0; i<varnames.length; i++){
  565. console.log(varnames[i] + ": " + varMap[varnames[i]]())
  566. }
  567. }
  568.  
  569. // Build regex to find variables from keys of map
  570. var varRegex = new RegExp('\\$('+Object.keys(varMap).join('|')+')\\$?', 'g')
  571. function fillVariables(s){
  572. // Perform the variable replacement
  573. return s.replace(varRegex, function (m) {
  574. // Remove $ before looking up in map
  575. return varMap[m.replace(/\$/g,"")]()
  576. });
  577. }
  578.  
  579. // Show the URL configuration dialog
  580. function urlConf(){
  581. var url = GM_getValue(storageKeys.url)
  582. ctcmi.html(
  583. "<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>"
  584. )
  585. if (url) ctcmi.append("<p>Remove all the URLs to be able to edit the comments in your browser.</p>")
  586. else ctcmi.append("<p>Using a URL will <b>overwrite</b> any edits to the comments you have made.</p>")
  587. ctcmi.append($('<textarea placeholder=https://raw.githubusercontent.com/user/repo/123/stack-exchange-comments.txt>').val(url))
  588. ctcmi.append($('<button>Save</Button>').click(doneUrlConf))
  589. ctcmi.append($('<button>Cancel</Button>').click(closeMenu))
  590. return false
  591. }
  592.  
  593. // User clicked "Save" in URL configuration dialog
  594. function doneUrlConf(){
  595. GM_setValue(storageKeys.url, $(this).prev('textarea').val())
  596. // Force a load by removing the timestamp of the last load
  597. GM_deleteValue(storageKeys.lastUpdate)
  598. loadStorageUrlComments()
  599. closeMenu()
  600. }
  601.  
  602. // Look up the URL from local storage, fetch the URL
  603. // and parse the comment templates from it
  604. // unless it has already been done recently
  605. function loadStorageUrlComments(){
  606. var url = GM_getValue(storageKeys.url)
  607. if (!url) return
  608. var lu = GM_getValue(storageKeys.lastUpdate);
  609. if (lu && lu > Date.now() - 8600000) return
  610. loadComments(url)
  611. }
  612.  
  613. // Hook into clicks for the entire page that should show a context menu
  614. // Only handle the clicks on comment input areas (don't prevent
  615. // the context menu from appearing in other places.)
  616. $(document).contextmenu(function(e){
  617. var target = $(e.target)
  618. if (target.is('.comments-link')){
  619. // The "Add a comment" link
  620. var parent = target.parents('.answer,#question,.question')
  621. // Show the comment text area
  622. target.trigger('click')
  623. // Bring up the context menu for it
  624. showMenu(parent.find('textarea'))
  625. e.preventDefault()
  626. return false
  627. } else if (target.closest('#review-action-Reject,label[for="review-action-Reject"]').length){
  628. // Suggested edit review queue - reject
  629. target.trigger('click')
  630. $('button.js-review-submit').trigger('click')
  631. setTimeout(function(){
  632. // Click "causes harm"
  633. $('#rejection-reason-0').trigger('click')
  634. },100)
  635. setTimeout(function(){
  636. showMenu($('#rejection-reason-0').parents('.flex--item').find('textarea'))
  637. },200)
  638. e.preventDefault()
  639. return false
  640. } else if (target.closest('#review-action-Unsalvageable,label[for="review-action-Unsalvageable"]').length){
  641. // Triage review queue - unsalvageable
  642. target.trigger('click')
  643. $('button.js-review-submit').trigger('click')
  644. showMenuInFlagDialog()
  645. e.preventDefault()
  646. return false
  647. } else if (target.is('.js-flag-post-link')){
  648. // the "Flag" link for a question or answer
  649. // Click it to show pop up
  650. target.trigger('click')
  651. showMenuInFlagDialog()
  652. e.preventDefault()
  653. return false
  654. } else if (target.closest('.js-comment-flag').length){
  655. // The flag icon next to a comment
  656. target.trigger('click')
  657. setTimeout(function(){
  658. // Click "Something else"
  659. $('#comment-flag-type-CommentOther').prop('checked',true).parents('.js-comment-flag-option').find('.js-required-comment').removeClass('d-none')
  660. },100)
  661. setTimeout(function(){
  662. showMenu($('#comment-flag-type-CommentOther').parents('.js-comment-flag-option').find('textarea'))
  663. },200)
  664. e.preventDefault()
  665. return false
  666. } else if (target.closest('#review-action-Close,label[for="review-action-Close"],#review-action-NeedsAuthorEdit,label[for="review-action-NeedsAuthorEdit"]').length){
  667. // Close votes review queue - close action
  668. // or Triage review queue - needs author edit action
  669. target.trigger('click')
  670. $('button.js-review-submit').trigger('click')
  671. showMenuInCloseDialog()
  672. e.preventDefault()
  673. return false
  674. } else if (target.is('.js-close-question-link')){
  675. // The "Close" link for a question
  676. target.trigger('click')
  677. showMenuInCloseDialog()
  678. e.preventDefault()
  679. return false
  680. } else if (target.is('textarea,input[type="text"]') && (!target.val() || target.val() == target[0].defaultValue)){
  681. // A text field that is blank or hasn't been modified
  682. var type = getType(target)
  683. //console.log("Type: " + type)
  684. if (type){
  685. // A text field for entering a comment
  686. showMenu(target)
  687. e.preventDefault()
  688. return false
  689. }
  690. }
  691. })
  692.  
  693. function showMenuInFlagDialog(){
  694. // Wait for the popup
  695. setTimeout(function(){
  696. $('input[value="PostOther"]').trigger('click')
  697. },100)
  698. setTimeout(function(){
  699. showMenu($('input[value="PostOther"]').parents('label').find('textarea'))
  700. },200)
  701. }
  702.  
  703. function showMenuInCloseDialog(){
  704. setTimeout(function(){
  705. $('#closeReasonId-SiteSpecific').trigger('click')
  706. },100)
  707. setTimeout(function(){
  708. $('#siteSpecificCloseReasonId-other').trigger('click')
  709. },200)
  710. setTimeout(function(){
  711. showMenu($('#siteSpecificCloseReasonId-other').parents('.js-popup-radio-action').find('textarea'))
  712. },300)
  713. }
  714.  
  715. var varCache={} // Cache variables so they don't have to be looked up for every single question
  716. function showMenu(target){
  717. varCache={} // Clear the variable cache
  718. commentTextField=target
  719. var type = getType(target)
  720. var user = getUserClass()
  721. var site = getSite()
  722. var tags = getTags()
  723. ctcmi.html("")
  724. //logVars()
  725. for (var i=0; i<comments.length; i++){
  726. if(commentMatches(comments[i], type, user, site, tags)){
  727. ctcmi.append(
  728. $('<div class=ctcm-comment>').append(
  729. $('<span class=ctcm-expand>\u25bc</span>').click(expandFullComment)
  730. ).append(
  731. $('<h4 class=ctcm-title>').text(comments[i].title).click(insertComment)
  732. ).append(
  733. $('<div class=ctcm-body>').text(fillVariables(comments[i].comment)).click(insertComment).attr('data-socvr',comments[i].socvr||"")
  734. )
  735. )
  736. }
  737. }
  738. ctcmi.append($('<button>Edit</Button>').click(editComments))
  739. ctcmi.append($('<button>Cancel</Button>').click(closeMenu))
  740. target.parents('.popup,#modal-base,body').first().append(ctcmo)
  741. ctcmo.show()
  742. }
  743.  
  744. function closeMenu(){
  745. ctcmo.hide()
  746. ctcmo.remove()
  747. }
  748.  
  749. // Hook into clicks anywhere in the document
  750. // and listen for ones that related to our dialog
  751. $(document).click(function(e){
  752. if(ctcmo.is(':visible')){
  753. // dialog is open
  754. if($(e.target).parents('#ctcm-back').length == 0) {
  755. // click wasn't on the dialog itself
  756. closeMenu()
  757. }
  758. // Clicks when the dialog are open belong to us,
  759. // prevent other things from happening
  760. e.preventDefault()
  761. return false
  762. }
  763. })
  764. })();

QingJ © 2025

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