Greasy Fork镜像 还支持 简体中文。

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-15 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Stack Exchange comment template context menu
  3. // @namespace http://ostermiller.org/
  4. // @version 1.01
  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"
  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.parents('.js-comment-flag-option').length) return "flag-comment"
  251. if (node.parents('.js-flagged-post').length){
  252. if (/decline/.exec(node.attr('placeholder'))) return "decline-flag"
  253. else return "helpful-flag"
  254. }
  255.  
  256. if (node.parents('.site-specific-pane').length) prefix = "close-"
  257. else if (node.parents('.mod-attention-subform').length) prefix = "flag-"
  258. else if (node.is('.edit-comment,#edit-comment')) prefix = "edit-"
  259. else if(node.is('.js-comment-text-input')) prefix = ""
  260. else return null
  261.  
  262. if (node.parents('#question,.question').length) return prefix + "question"
  263. if (node.parents('#answers,.answer').length) return prefix + "answer"
  264.  
  265. // Fallback for single post edit page
  266. if (node.parents('.post-form').find('h2:last').text()=='Question') return prefix + "question"
  267. if (node.parents('.post-form').find('h2:last').text()=='Answer') return prefix + "answer"
  268.  
  269. return null
  270. }
  271.  
  272. // Mostly moderator or non-moderator (user.)
  273. // Not-logged in and low rep users are not able to comment much
  274. // and are unlikely to use this tool, no need to identify them
  275. // and give them special behavior.
  276. // Maybe add a class for staff in the future?
  277. var userclass
  278. function getUserClass(){
  279. if (!userclass){
  280. if ($('.js-mod-inbox-button').length) userclass="moderator"
  281. else if ($('.my-profile').length) userclass="user"
  282. else userclass="anonymous"
  283. }
  284. return userclass
  285. }
  286.  
  287. // The Stack Exchange site this is run on (just the subdoman, eg "stackoverflow")
  288. var site
  289. function getSite(){
  290. if(!site) site=validateSite(location.hostname)
  291. return site
  292. }
  293.  
  294. // Which tags are on the question currently being viewed
  295. var tags
  296. function getTags(){
  297. if(!tags) tags=$.map($('.post-taglist .post-tag'),function(tag){return $(tag).text()})
  298. return tags
  299. }
  300.  
  301. // The id of the question currently being viewed
  302. var questionid
  303. function getQuestionId(){
  304. if (!questionid) questionid=$('.question').attr('data-questionid')
  305. var l = $('.answer-hyperlink')
  306. if (!questionid && l.length) questionid=l.attr('href').replace(/^\/questions\/([0-9]+).*/,"$1")
  307. if (!questionid) questionid="-"
  308. return questionid
  309. }
  310.  
  311. // The human readable name of the current Stack Exchange site
  312. var sitename
  313. function getSiteName(){
  314. if (!sitename){
  315. sitename = seOpts.site.name || ""
  316. sitename = sitename.replace(/ ?Stack Exchange/, "")
  317. }
  318. return sitename
  319. }
  320.  
  321. // The Stack Exchange user id for the person using this tool
  322. var myUserId
  323. function getMyUserId() {
  324. if (!myUserId) myUserId = seOpts.user.userId || ""
  325. return myUserId
  326. }
  327.  
  328. // The full host name of the Stack Exchange site
  329. var siteurl
  330. function getSiteUrl(){
  331. if (!siteurl) siteurl = location.hostname
  332. return siteurl
  333. }
  334.  
  335. // Store the comment text field that was clicked on
  336. // so that it can be filled with the comment template
  337. var commentTextField
  338.  
  339. // Insert the comment template into the text field
  340. // called when a template is clicked in the dialog box
  341. // so "this" refers to the clicked item
  342. function insertComment(){
  343. // The comment to insert is stored in a div
  344. // near the item that was clicked
  345. var cmt = $(this).parent().children('.ctcm-body').text()
  346.  
  347. // Put in the comment
  348. commentTextField.val(cmt).focus()
  349.  
  350. // highlight place for additional input,
  351. // if specified in the template
  352. var typeHere="[type here]"
  353. var typeHereInd = cmt.indexOf(typeHere)
  354. if (typeHereInd >= 0) commentTextField[0].setSelectionRange(typeHereInd, typeHereInd + typeHere.length)
  355.  
  356. closeMenu()
  357. }
  358.  
  359. // User clicked on the expand icon in the dialog
  360. // to show the full text of a comment
  361. function expandFullComment(){
  362. $(this).parent().children('.ctcm-body').show()
  363. $(this).hide()
  364. }
  365.  
  366. // Apply comment tag filters
  367. // For a given comment, say whether it
  368. // should be shown given the current context
  369. function commentMatches(comment, type, user, site, tags){
  370. if (comment.types && !comment.types[type]) return false
  371. if (comment.users && !comment.users[user]) return false
  372. if (comment.sites && !comment.sites[site]) return false
  373. if (comment.tags){
  374. var hasTag = false
  375. for(var i=0; tags && i<tags.length; i++){
  376. if (comment.tags[tags[i]]) hasTag=true
  377. }
  378. if(!hasTag) return false
  379. }
  380. return true
  381. }
  382.  
  383. // User clicked "Save" when editing the list of comment templates
  384. function doneEditing(){
  385. comments = parseComments($(this).prev('textarea').val())
  386. storeComments()
  387. closeMenu()
  388. }
  389.  
  390. // Show the edit comment dialog
  391. function editComments(){
  392. // Pointless to edit comments that will just get overwritten
  393. // If there is a URL, only allow the URL to be edited
  394. if(GM_getValue(storageKeys.url)) return urlConf()
  395. ctcmi.html(
  396. "<pre># Comment title\n"+
  397. "Comment body\n"+
  398. "types: "+typeMapInput.replace(/,/g, ", ")+"\n"+
  399. "users: "+userMapInput.replace(/,/g, ", ")+"\n"+
  400. "sites: stackoverflow, physics, meta.stackoverflow, physics.meta, etc\n"+
  401. "tags: javascript, python, etc</pre>"+
  402. "<p>Limiting by types, users, sites, and tags is optional.</p>"
  403. )
  404. ctcmi.append($('<textarea>').val(exportComments()))
  405. ctcmi.append($('<button>Save</Button>').click(doneEditing))
  406. ctcmi.append($('<button>Cancel</Button>').click(closeMenu))
  407. ctcmi.append($('<button>From URL...</Button>').click(urlConf))
  408. return false
  409. }
  410.  
  411. function getAuthorNode(postNode){
  412. return postNode.find('.post-signature .user-details[itemprop="author"]')
  413. }
  414.  
  415. var opNode
  416. function getOpNode(){
  417. if (!opNode) opNode = getAuthorNode($('#question,.question'))
  418. return opNode
  419. }
  420.  
  421. function getUserNodeId(node){
  422. if (!node) return "-"
  423. var link = node.find('a')
  424. if (!link.length) return "-"
  425. var href = link.attr('href')
  426. if (!href) return "-"
  427. return href.replace(/[^0-9]+/g, "")
  428. }
  429.  
  430. var opId
  431. function getOpId(){
  432. if (!opId) opId = getUserNodeId(getOpNode())
  433. return opId
  434. }
  435.  
  436. function getUserNodeName(node){
  437. if (!node) return "-"
  438. var link = node.find('a')
  439. if (!link.length) return "-"
  440. // Remove spaces from user names so that they can be used in @name references
  441. return link.text().replace(/ /,"")
  442. }
  443.  
  444. var opName
  445. function getOpName(){
  446. if (!opName) opName = getUserNodeName(getOpNode())
  447. return opName
  448. }
  449.  
  450. function getUserNodeRep(node){
  451. if (!node) return "-"
  452. var r = node.find('.reputation-score')
  453. if (!r.length) return "-"
  454. return r.text()
  455. }
  456.  
  457. var opRep
  458. function getOpRep(){
  459. if (!opRep) opRep = getUserNodeRep(getOpNode())
  460. return opRep
  461. }
  462.  
  463. function getPostNode(){
  464. return commentTextField.parents('#question,.question,.answer')
  465. }
  466.  
  467. function getPostAuthorNode(){
  468. return getAuthorNode(getPostNode())
  469. }
  470.  
  471. function getAuthorId(){
  472. return getUserNodeId(getPostAuthorNode())
  473. }
  474.  
  475. function getAuthorName(){
  476. return getUserNodeName(getPostAuthorNode())
  477. }
  478.  
  479. function getAuthorRep(){
  480. return getUserNodeRep(getPostAuthorNode())
  481. }
  482.  
  483. function getPostId(){
  484. var postNode = getPostNode();
  485. if (!postNode.length) return "-"
  486. if (postNode.attr('data-questionid')) return postNode.attr('data-questionid')
  487. if (postNode.attr('data-answerid')) return postNode.attr('data-answerid')
  488. return "-"
  489. }
  490.  
  491. // Map of variables to functions that return their replacements
  492. var varMap = {
  493. 'SITENAME': getSiteName,
  494. 'SITEURL': getSiteUrl,
  495. 'MYUSERID': getMyUserId,
  496. 'QUESTIONID': getQuestionId,
  497. 'OPID': getOpId,
  498. 'OPNAME': getOpName,
  499. 'OPREP': getOpRep,
  500. 'POSTID': getPostId,
  501. 'AUTHORID': getAuthorId,
  502. 'AUTHORNAME': getAuthorName,
  503. 'AUTHORREP': getAuthorRep
  504. }
  505.  
  506. // Build regex to find variables from keys of map
  507. var varRegex = new RegExp('\\$('+Object.keys(varMap).join('|')+')\\$?', 'g')
  508. function fillVariables(s){
  509. // Perform the variable replacement
  510. return s.replace(varRegex, function (m) {
  511. // Remove $ before looking up in map
  512. return varMap[m.replace(/\$/g,"")]()
  513. });
  514. }
  515.  
  516. // Show the URL configuration dialog
  517. function urlConf(){
  518. var url = GM_getValue(storageKeys.url)
  519. ctcmi.html(
  520. "<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>"
  521. )
  522. if (url) ctcmi.append("<p>Remove the URL to be able to edit the comments in your browser.</p>")
  523. else ctcmi.append("<p>Using a URL will <b>overwrite</b> any edits to the comments you have made.</p>")
  524. ctcmi.append($('<input type=text placeholder=https://raw.githubusercontent.com/user/repo/123/stack-exchange-comments.txt>').val(url))
  525. ctcmi.append($('<button>Save</Button>').click(doneUrlConf))
  526. ctcmi.append($('<button>Cancel</Button>').click(closeMenu))
  527. return false
  528. }
  529.  
  530. // User clicked "Save" in URL configuration dialog
  531. function doneUrlConf(){
  532. GM_setValue(storageKeys.url, ($(this).prev('input').val()))
  533. // Force a load by removing the timestamp of the last load
  534. GM_deleteValue(storageKeys.lastUpdate)
  535. loadStorageUrlComments()
  536. closeMenu()
  537. }
  538.  
  539. // Look up the URL from local storage, fetch the URL
  540. // and parse the comment templates from it
  541. // unless it has already been done recently
  542. function loadStorageUrlComments(){
  543. var url = GM_getValue(storageKeys.url)
  544. if (!url) return
  545. var lu = GM_getValue(storageKeys.lastUpdate);
  546. if (lu && lu > Date.now() - 8600000) return
  547. loadComments(url)
  548. }
  549.  
  550. // Hook into clicks for the entire page that should show a context menu
  551. // Only handle the clicks on comment input areas (don't prevent
  552. // the context menu from appearing in other places.)
  553. $(document).contextmenu(function(e){
  554. var target = $(e.target)
  555. if (target.is('.comments-link')){
  556. // The "Add a comment" link
  557. var parent = target.parents('.answer,#question,.question')
  558. // Show the comment text area
  559. target.trigger('click')
  560. // Bring up the context menu for it
  561. showMenu(parent.find('textarea'))
  562. e.preventDefault()
  563. return false
  564. } else if (target.is('.js-flag-post-link')){
  565. // the "Flag" link for a question or answer
  566. // Click it to show pop up
  567. target.trigger('click')
  568. // Wait for the popup
  569. setTimeout(function(){
  570. $('input[value="PostOther"]').trigger('click')
  571. },100)
  572. setTimeout(function(){
  573. showMenu($('input[value="PostOther"]').parents('label').find('textarea'))
  574. },200)
  575. e.preventDefault()
  576. return false
  577. } else if (target.parents('.js-comment-flag').length){
  578. // The flag icon next to a comment
  579. target.trigger('click')
  580. setTimeout(function(){
  581. // Click "Something else"
  582. $('#comment-flag-type-CommentOther').prop('checked',true).parents('.js-comment-flag-option').find('.js-required-comment').removeClass('d-none')
  583. },100)
  584. setTimeout(function(){
  585. showMenu($('#comment-flag-type-CommentOther').parents('.js-comment-flag-option').find('textarea'))
  586. },200)
  587. e.preventDefault()
  588. return false
  589. } else if (target.is('.js-close-question-link')){
  590. // The "Close" link for a question
  591. target.trigger('click')
  592. setTimeout(function(){
  593. $('#closeReasonId-SiteSpecific').trigger('click')
  594. },100)
  595. setTimeout(function(){
  596. $('#siteSpecificCloseReasonId-other').trigger('click')
  597. },200)
  598. setTimeout(function(){
  599. showMenu($('#siteSpecificCloseReasonId-other').parents('.js-popup-radio-action').find('textarea'))
  600. },300)
  601. e.preventDefault()
  602. return false
  603. } else if (target.is('textarea,input[type="text"]') && (!target.val() || target.val() == target[0].defaultValue)){
  604. // A text field that is blank or hasn't been modified
  605. var type = getType(target)
  606. //console.log("Type: " + type)
  607. if (type){
  608. // A text field for entering a comment
  609. showMenu(target)
  610. e.preventDefault()
  611. return false
  612. }
  613. }
  614. })
  615.  
  616. function showMenu(target){
  617. commentTextField=target
  618. console.log(fillVariables("Post id: $POSTID, Author id: $AUTHORID, Author name: $AUTHORNAME, Author rep: $AUTHORREP"))
  619. var type = getType(target)
  620. var user = getUserClass()
  621. var site = getSite()
  622. var tags = getTags()
  623. ctcmi.html("")
  624. for (var i=0; i<comments.length; i++){
  625. if(commentMatches(comments[i], type, user, site, tags)){
  626. ctcmi.append(
  627. $('<div class=ctcm-comment>').append(
  628. $('<span class=ctcm-expand>\u25bc</span>').click(expandFullComment)
  629. ).append(
  630. $('<h4 class=ctcm-title>').text(comments[i].title).click(insertComment)
  631. ).append(
  632. $('<div class=ctcm-body>').text(fillVariables(comments[i].comment)).click(insertComment)
  633. )
  634. )
  635. }
  636. }
  637. ctcmi.append($('<button>Edit</Button>').click(editComments))
  638. ctcmi.append($('<button>Cancel</Button>').click(closeMenu))
  639. if (target.parents('.popup,#modal-base').length) target.after(ctcmo)
  640. else $(document.body).append(ctcmo)
  641. ctcmo.show()
  642. }
  643.  
  644. function closeMenu(){
  645. ctcmo.hide()
  646. ctcmo.remove()
  647. }
  648.  
  649. // Hook into clicks anywhere in the document
  650. // and listen for ones that related to our dialog
  651. $(document).click(function(e){
  652. if(ctcmo.is(':visible')){
  653. // dialog is open
  654. if($(e.target).parents('#ctcm-back').length == 0) {
  655. // click wasn't on the dialog itself
  656. closeMenu()
  657. }
  658. // Clicks when the dialog are open belong to us,
  659. // prevent other things from happening
  660. e.preventDefault()
  661. return false
  662. }
  663. })
  664. })();

QingJ © 2025

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