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

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

QingJ © 2025

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