Reddit - Load 'Continue this thread' inline

Changes 'Continue this thread' links to insert the linked comments into the current page

当前为 2018-07-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Reddit - Load 'Continue this thread' inline
  3. // @description Changes 'Continue this thread' links to insert the linked comments into the current page
  4. // @author James Skinner <spiralx@gmail.com> (http://github.com/spiralx)
  5. // @namespace http://spiralx.org/
  6. // @version 1.9.5
  7. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiAAABYgAWToQQYAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuNWWFMmUAAAF3SURBVDhP1ZI/SAJhGMZN3WpKOAgsKGiIoKWg0K2LXIJWj6DAhpAgKMiW7irBqziHbhYE12sLGtWGKHBx0kHcQrBJXNIz9e35vvsQoj841g9+fHy8z/PyHZzrXzANNbgKF2ASHsOhOYGTUIb3cBSq4hyKa3Ey2GsMeMRvvzALd+AVvIPbcAb+yAq8hBehUOgxnU5TtVol27ap3W5TpVKhVCpFsiw/IHMusstwgOHxeDZM07T7/T5Rr0vUfacBvR7uHWIzwzBayK+zDm8KktFo9FXEifaXiHbnxAXEZKKtqcHSSCTygs6nBYuWZfEhR1eIzjbFBdzsYcma8xKQyWQInXmn6jCeSCQ6fDoEqqqyzxhzqgK/368Xi0UeKJfLpGka5fN5yuVyrEClUonPCoUCSZLE/oMvjGBwqut6q16vU6PRoGw2yxc0m02q1WoUj8fffD5fjGWdyvdMeL3ew2AweKsoynM4HH4KBAKW2+0+wExyIn8Hl+sDt5ENCrpr91QAAAAASUVORK5CYII=
  8. // @icon64 data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2029%2025%22%3E%3Cg%20transform=%22translate(1%201)%22%20stroke-width=%221.1%22%20fill-rule=%22evenodd%22%20fill=%22none%22%3E%3Ccircle%20stroke=%22#000%22%20cx=%222.7%22%20r=%222.7%22%20cy=%2210.7%22%20fill=%22#fff%22/%3E%3Ccircle%20stroke=%22#000%22%20cx=%2224.7%22%20r=%222.7%22%20cy=%2210.7%22%20fill=%22#fff%22/%3E%3Cpath%20stroke-linejoin=%22round%22%20stroke=%22#000%22%20stroke-linecap=%22round%22%20d=%22M21.23%201.35L15.83.08l-2%207.28%22/%3E%3Ccircle%20stroke=%22#000%22%20cx=%2223.13%22%20r=%222.13%22%20cy=%222.13%22%20fill=%22#fff%22/%3E%3Cellipse%20cy=%2214.99%22%20rx=%2212.24%22%20ry=%227.99%22%20stroke=%22#000%22%20cx=%2213.24%22%20fill=%22#fff%22/%3E%3Cg%20transform=%22translate(8%2012)%22%3E%3Ccircle%20stroke=%22#FF4500%22%20cx=%221%22%20r=%221.43%22%20cy=%221.43%22%20fill=%22#FF4500%22/%3E%3Ccircle%20stroke=%22#FF4500%22%20cx=%2210%22%20r=%221.43%22%20cy=%221.43%22%20fill=%22#FF4500%22/%3E%3Cpath%20stroke=%22#000%22%20d=%22M1.5%206.23C2.58%207.3%204.3%207.5%205.73%207.5m4.24-1.27C8.9%207.3%207.17%207.5%205.77%207.5%22/%3E%3C/g%3E%3C/g%3E%3C/svg%3E
  9. // @match *://*.reddit.com/r/*/comments/*
  10. // @grant none
  11. // @run-at document-end
  12. // @require https://unpkg.com/jquery@3/dist/jquery.min.js
  13. // @require https://greasyfork.org/scripts/7602-mutation-observer/code/mutation-observer.js
  14. // ==/UserScript==
  15.  
  16. /* jshint asi: true, es6: true, laxbreak: true */
  17. /* global jQuery, MutationSummary */
  18.  
  19. /*
  20. ==== 1.9.5 (2018.07.11) ====
  21. * Updated jQuery to v3 and source from unpkg.com
  22. * Add downloadURL to update from Gist
  23. ==== 1.9.4 (2018.02.11) ====
  24. * Added @icon field in metadata as SVG wasn't displaying on the installed userscript page
  25. ==== 1.9.3 (2017.12.03) ====
  26. * Changed base-64 encoded PNG icons to an SVG icon
  27. ==== 1.9.2 (2017.10.11) ====
  28. * Gets correct comment ID for links
  29. * Changed location in comment HTML to use as its root
  30. * Get children of first comment when it is already on the page
  31. ==== 1.9.1 (2017.10.11) ====
  32. * Fix broken $target selector
  33. ==== 1.9.0 ====
  34. * Catch failed loads, log them to the console and then restore original load link
  35. */
  36.  
  37. ; (function userScript($) {
  38. 'use strict'
  39.  
  40. const NORMAL = 'font-weight: normal; text-decoration: none; color: black'
  41. const ERROR = 'font-weight: bold; color: #f4f'
  42. const LINK = [ 'color: #05f; text-decoration: underline', NORMAL ]
  43. const BOLD = [ 'font-weight: bold', NORMAL ]
  44. const BLUE = [ 'color: #05f', NORMAL ]
  45. const RED = [ 'color: #e32636', NORMAL ]
  46.  
  47. const EXPAND_ICON = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADZklEQVR42tVXv08TURyvIdbYjg0DCQuaLjA4Y5rQ61lo0jASHIQFEnUQ/BGDkqJiAj00YkuPa0Fw4S8gYYDgxEYTdHaDgAMDDQUGBunz+7m79t6112srFPEln6Tpfd/3+3nfX+/7HI7/bQmCcDsQCNwnjIqBwEcAv/EfvtXFaCgUahQFYSwgCD/bu/tZa7/EWh4vseahZRUtj5bU//ANMpAN+XyNF2H4BpR1dIZPvINJ5n6/wxzTjDk+F2FaxyfG3OM7zDuQZNijEiEdf2VcFMVb5NYfbX2TzBnNaIZiOuIWiJkJOSczDHuhA7pqjfMdQQztNz1fMwzDyIyOhAXy33gy5JGmZ2vML3btQ2fVJxdog+d12jCeNyprcE3tMc/YpgkuaddMJmZ4wzOSVklU9ERPT4+Tkuh74eRxzvgsQQFy7OHwS/Ztfd2E4RcjrGHmVCOZ4LzBeQK6YaP86SlpEDeTcZkznmTsWirHJElixUuWZXY9ntXkZvV9M+ZwtD2YYLBhnfFUNshcp5Qx3J43ToYdKcIcEZi3IZDIanJJfR8fDj0xO+6FTyxLFMxQaoXTJ4qMzxO+EIEFGwJyVpNLMcMTCXMovAOKtRfQQNQ650+vmI07FonAog0BJavJzXEkZLMX3O921GZV0l7bu/ssTp+jmJ+R2wkLhK/A7/IEkoeaHOSxTzkjPbkSL8CWqSLQx9FK+dO7Puyp2T5FxoqxsrJSQgCVYCULHS5pz0SgtS8KAr08gVH0dp6A502arZPS8y7o8IyljTAQAdwdsGkkIN1ozcPLZgJv60SA8qD5yTKDzatD4J+HwDIJp3bV9ionEiWwIraxsWEpO0Q6XNFd+yRESViVYYN8St3tiBoMQSGkgMOyZXgz/kuTgzz2xY+0+6FSGV5uI9oubUR6K454B5XLasURy8vI3xk+VqefOl5G/mD42FduXtSu44l6X8eRKgaS1YoDCSqBR8WB5Olq5YEEKxgMtmB88rzatB/JqE8UQHVuP5JtqiMZdNcwlHbtFzxxrqF0tbahlO8NFK8t5IQzelDdWB7jx/KDfMy3qj65VU4gaZC5KFH3+Lb9wwR1TjIoNezB3ooxr2ahbKAMDeQudTG00uKnWRv9h2/60yziu4inWdnQUB8vfpyKfn9vzS+gq7D+AAlDQCI1XwNKAAAAAElFTkSuQmCC'
  48. // --------------------------------------------------------------------
  49.  
  50. const units = (v, s) => `${v}${s}`
  51. const pluralise = (w, n) => w + (n !== 1 ? 's' : '')
  52. const capitalise = s => typeof s === 'string' && s && s.split(/\s+/g).map(w => w[0].toUpperCase() + w.substr(1).toLowerCase()).join(' ')
  53. function* flatten (arr) {
  54. for (let x of arr) {
  55. if (Array.isArray(x)) {
  56. yield* (flatten(x))
  57. }
  58. else {
  59. yield x
  60. }
  61. }
  62. }
  63.  
  64. // --------------------------------------------------------------------
  65. $.fn.extend({
  66. spinner (options) {
  67. options = Object.assign({}, $.fn.spinner.defaults, options)
  68.  
  69. const $spinner = $('<div class="pulsar-horizontal"></div>')
  70. .css({
  71. padding: units(options.size * 0.25, 'px'),
  72. height: units(options.size, 'px')
  73. })
  74.  
  75. const total_duration = (options.steps + 1) * options.step_duration
  76.  
  77. for (let i = 0; i < options.steps; i++) {
  78. const delay = i * options.step_duration
  79.  
  80. $('<div></div>')
  81. .css({
  82. width: units(options.size, 'px'),
  83. height: units(options.size, 'px'),
  84. backgroundColor: options.colour,
  85. animationDuration: units(total_duration, 's'),
  86. animationDelay: units(delay, 's')
  87. })
  88. .appendTo($spinner)
  89. }
  90.  
  91. if (options.replace) {
  92. this.empty()
  93. }
  94.  
  95. return options.mode === 'prepend'
  96. ? this.prepend($spinner)
  97. : this.append($spinner)
  98. },
  99. log (name, ...extras) {
  100. const title = [ `%c${name || '$'}%c : %c${this.length}%c ${pluralise('item', this.length)}`, ...BOLD, ...BLUE ]
  101. if (this.length > 0 || extras.length > 0) {
  102. console.group.apply(console, title)
  103.  
  104. if (this.length > 0) {
  105. console.info(this)
  106. }
  107. extras.forEach(extra => {
  108. console.log(extra)
  109. })
  110.  
  111. console.groupEnd()
  112. }
  113. else {
  114. console.info.apply(console, title)
  115. }
  116. return this
  117. }
  118. })
  119.  
  120. $.fn.spinner.defaults = {
  121. replace: true,
  122. mode: 'append',
  123. steps: 3,
  124. size: 24,
  125. colour: '#28f',
  126. step_duration: 0.25
  127. }
  128.  
  129. // --------------------------------------------------------------------
  130.  
  131. async function getCommentPage (id) {
  132. const url = postUrl + id
  133.  
  134. const data = await $.get(url)
  135.  
  136. const $listing = $('.nestedlisting', data)
  137.  
  138. console.groupCollapsed(`getCommentPage(%c${id}%c, %c${url}%c)`, ...RED, ...LINK)
  139. // console.log(data)
  140. console.log($listing.get(0).outerHTML)
  141. console.groupEnd()
  142.  
  143. return $listing
  144. }
  145.  
  146. // --------------------------------------------------------------------
  147.  
  148. function addComments($target, $comments) {
  149. $target
  150. .empty()
  151. .append($comments)
  152. .find('.usertext.border .usertext-body')
  153. .css('animation', 'fadenewpost 4s ease-out 4s both')
  154. }
  155.  
  156. // --------------------------------------------------------------------
  157.  
  158. function loadComments ($span, $target, ids) {
  159. let insertChildren = false
  160.  
  161. if (!Array.isArray(ids)) {
  162. ids = [ ids ]
  163. insertChildren = true
  164. }
  165.  
  166. const urls = ids.map(id => postUrl + id)
  167.  
  168. /*
  169. console.group(`%cloadComments%c(${ids.length} ${ids.length > 1 ? 'ids' : 'id'}: ${ids.join(', ')})`, ...BOLD)
  170. console.info($span[0].outerHTML)
  171. console.log(`%c${urls.join('\n')}%c`, ...LINK)
  172. console.groupEnd()
  173. */
  174. const original = $span.parent().html()
  175.  
  176. $span.spinner({
  177. colour: '#28f',
  178. size: 24,
  179. step_duration: 0.25,
  180. replace: true
  181. })
  182.  
  183. const pageRequests = urls.map(url => {
  184. return $.get(url)
  185. .then(
  186. // data => $('.nestedlisting > .thing', data).next().andSelf().get(),
  187. data => $('.nestedlisting', data).get(),
  188. (xhr, textStatus, errorThrown) => {
  189. console.warn(`%c${capitalise(textStatus)}: ${xhr.status} ${xhr.statusText}%c %c${url}%c`, ERROR, NORMAL, LINK, NORMAL)
  190. }
  191. )
  192. })
  193.  
  194. $.when(...pageRequests)
  195. .then((...children) => {
  196. let $children = $([...flatten(children)])
  197. // $children.log('$children')
  198.  
  199. if (insertChildren) {
  200. $children = $children.find('> .thing > .child > .sitetable')
  201.  
  202. $children
  203. .find('> .entry > .usertext.border')
  204. .removeClass('border')
  205. }
  206.  
  207. $target
  208. .empty()
  209. .append($children)
  210. .find('.usertext.border .usertext-body')
  211. .css('animation', 'fadenewpost 4s ease-out 4s both')
  212. })
  213. .fail((xhr, textStatus, errorThrown) => {
  214. $span.parent().html(original)
  215. })
  216. }
  217.  
  218. // --------------------------------------------------------------------
  219.  
  220. function getCommentId (linkElem) {
  221. const m = linkElem.pathname.match(/\/([a-z0-9]+)\/?$/)
  222. if (!m) {
  223. throw new Error(`No comment ID parsed from link URL "${linkElem.href}"`)
  224. }
  225. return m[1]
  226. }
  227.  
  228. // --------------------------------------------------------------------
  229.  
  230. function processDeepThreadSpans (deepThreadSpans) {
  231. const $deepThreadSpans = $(deepThreadSpans)
  232. .filter(':not([data-comment-ids])')
  233.  
  234. // console.info(`processDeepThreadSpans: processing ${$deepThreadSpans.length}/${deepThreadSpans.length} deep thread spans`)
  235.  
  236. $deepThreadSpans.each(function() {
  237. const $span = $(this),
  238. $target = $span.closest('.child'),
  239. $a = $span.children('a'),
  240. cid = getCommentId($a[0])
  241.  
  242. // $span.log('$span')
  243. // $target.log('$target')
  244.  
  245. $span
  246. .attr('data-comment-ids', cid)
  247. .addClass('expand-inline')
  248.  
  249. async function load () {
  250. $span.spinner()
  251.  
  252. const $listing = await getCommentPage(cid)
  253. const $children = $listing.find('> .thing > .child > .sitetable')
  254.  
  255. addComments($target, $children)
  256. }
  257.  
  258. $a.one('click', event => {
  259. load()
  260.  
  261. return false
  262. })
  263. })
  264. }
  265.  
  266. // --------------------------------------------------------------------
  267. function processMoreCommentsSpans(moreCommentsSpans) {
  268. const $moreCommentsSpans = $(moreCommentsSpans)
  269. .filter(':not([data-comment-ids])')
  270. // console.info(`processMoreCommentsSpans: processing ${$moreCommentsSpans.length}/${moreCommentsSpans.length} more comment spans`)
  271. $moreCommentsSpans.each(function() {
  272. const $span = $(this),
  273. $target = $span.closest('.child'),
  274. $a = $span.children('a'),
  275. onclick = $a.attr('onclick'),
  276. cids = onclick.split(', ')[3].slice(1, -1).split(',')
  277. $span
  278. .attr('data-comment-ids', cids.join(','))
  279. .addClass('expand-inline')
  280.  
  281. async function load () {
  282. $span.spinner()
  283.  
  284. const $listings = $(await Promise.all(cids.map(getCommentPage)))
  285.  
  286. addComments($target, $listings)
  287. }
  288.  
  289. $a
  290. .removeAttr('onclick')
  291. .attr('data-onclick', onclick)
  292. .one('click', event => {
  293. // loadComments($span, $target, ...cids)
  294. load()
  295.  
  296. return false
  297. })
  298. })
  299. }
  300.  
  301. function processMoreCommentsSpans2 (moreCommentsSpans) {
  302. $(moreCommentsSpans).addClass('expand-inline')
  303. }
  304.  
  305. // --------------------------------------------------------------------
  306. const rootUrl = `https://${location.hostname}/`
  307. const postUrl = $('.thing.link > .entry a.comments').prop('href')
  308. // console.info(`%cSite:%c %c${rootUrl}%c\n%cPost:%c %c${postUrl}%c`, ...BOLD, ...LINK, ...BOLD, ...LINK)
  309.  
  310. // --------------------------------------------------------------------
  311.  
  312. const observer = new MutationSummary({
  313. callback(summaries) {
  314. const deepThreadSpans = summaries.shift().added,
  315. moreCommentsSpans = summaries.shift().added
  316.  
  317. // console.log(`Added ${deepThreadSpans.length} deep thread spans and ${moreCommentsSpans.length} more comment spans`)
  318.  
  319. processDeepThreadSpans(deepThreadSpans)
  320. processMoreCommentsSpans2(moreCommentsSpans)
  321. },
  322. rootNode: document.body,
  323. queries: [
  324. { element: 'span.deepthread' },
  325. { element: 'span.morecomments' }
  326. ]
  327. })
  328.  
  329. // To process spans in the HTML source
  330. processDeepThreadSpans($('span.deepthread'))
  331. processMoreCommentsSpans2($('span.morecomments'))
  332.  
  333. // --------------------------------------------------------------------
  334. $(document.body).append(`<style type="text/css">
  335. .expand-inline {
  336. display: block;
  337. padding: 0;
  338. }
  339. .expand-inline:after {
  340. display: none !important;
  341. }
  342. .expand-inline a {
  343. display: block;
  344. background: transparent url(${EXPAND_ICON}) no-repeat center left;
  345. padding-left: 40px;
  346. height: 40px;
  347. line-height: 40px;
  348. font-size: 1.4rem !important;
  349. font-weight: normal !important;
  350. vertical-align: middle;
  351. text-align: left;
  352. }
  353. .expand-inline a:hover {
  354. background-color: rgba(0, 105, 255, 0.05);
  355. text-decoration: none;
  356. }
  357. .pulsar-horizontal {
  358. display: inline-block;
  359. }
  360. .pulsar-horizontal > div {
  361. display: inline-block;
  362. border-radius: 100%;
  363. animation-name: pulsing;
  364. animation-timing-function: ease-in-out;
  365. animation-iteration-count: infinite;
  366. animation-fill-mode: both;
  367. }
  368. @keyframes pulsing {
  369. 0%, 100% {
  370. transform: scale(0);
  371. opacity: 0.5;
  372. }
  373. 50% {
  374. transform: scale(1);
  375. opacity: 1;
  376. }
  377. }
  378. @keyframes fadenewpost {
  379. 0% {
  380. background-color: #ffc;
  381. padding-left: 5px;
  382. }
  383. 100% {
  384. background-color: transparent;
  385. padding-left: 0;
  386. }
  387. }
  388. </style>`)
  389.  
  390. })(jQuery)
  391.  
  392. jQuery.noConflict(true)