YouTube: Expand All Video Comments

Adds a "Expand all" button to video comments which expands every comment and replies - no more clicking "Read more".

  1. // ==UserScript==
  2. // @name YouTube: Expand All Video Comments
  3. // @namespace org.sidneys.userscripts
  4. // @homepage https://gist.githubusercontent.com/sidneys/6756166a781bd76b97eeeda9fb0bc0c1/raw/
  5. // @version 4.7.8
  6. // @description Adds a "Expand all" button to video comments which expands every comment and replies - no more clicking "Read more".
  7. // @author sidneys
  8. // @icon https://www.youtube.com/favicon.ico
  9. // @noframes
  10. // @match http*://www.youtube.com/*
  11. // @require https://greasyfork.org/scripts/38888-greasemonkey-color-log/code/Greasemonkey%20%7C%20Color%20Log.js
  12. // @require https://greasyfork.org/scripts/374849-library-onelementready-es7/code/Library%20%7C%20onElementReady%20ES7.js
  13. // @run-at document-end
  14. // @grant GM_addStyle
  15. // ==/UserScript==
  16.  
  17.  
  18. /* global Debug, onElementReady */
  19.  
  20. /**
  21. * ESLint
  22. * @global
  23. */
  24. Debug = false
  25.  
  26.  
  27. /**
  28. * Applicable URL paths
  29. * @default
  30. * @constant
  31. */
  32. const urlPathList = [
  33. '/watch'
  34. ]
  35.  
  36.  
  37. /**
  38. * Inject Stylesheet
  39. */
  40. let injectStylesheet = () => {
  41. console.debug('injectStylesheet')
  42.  
  43. GM_addStyle(`
  44. /* =======================================
  45. ELEMENTS
  46. ======================================= */
  47.  
  48. /* Button: Expand all Comments
  49. --------------------------------------- */
  50.  
  51. .expand-all-comments-button
  52. {
  53. padding: 0;
  54. align-self: start;
  55. margin-left: var(--ytd-margin-4x, 8px);
  56. margin-top: 0;
  57. }
  58.  
  59.  
  60. .expand-all-comments-button #checkboxLabel
  61. {
  62. margin-left: var(--ytd-margin-2x, 8px);
  63. padding-left: 0;
  64. display: inline-flex;
  65. }
  66.  
  67. .busy .expand-all-comments-button #checkboxContainer,
  68. .busy .expand-all-comments-button #checkboxLabel
  69. {
  70. animation: var(--animation-busy-on);
  71. }
  72.  
  73. /* Button: Expand all Comments
  74. Spinner
  75. --------------------------------------- */
  76.  
  77. .expand-all-comments-button #checkboxLabel::after
  78. {
  79. background-image: url("https://i.imgur.com/z4V4Os8.png");
  80. content: "";
  81. background-size: 100%;
  82. background-repeat: no-repeat;
  83. margin-left: var(--ytd-margin-2x, 8px);
  84. height: var(--ytd-margin-4x, 16px);
  85. width: var(--ytd-margin-4x, 16px);
  86. }
  87.  
  88. .expand-all-comments-button #checkboxLabel,
  89. .expand-all-comments-button #checkboxLabel::after
  90. {
  91. transition: filter 1000ms ease-in-out;
  92. }
  93.  
  94. :not(.busy) .expand-all-comments-button #checkboxLabel::after
  95. {
  96.  
  97. filter: opacity(0);
  98. }
  99.  
  100. .busy .expand-all-comments-button #checkboxLabel::after
  101. {
  102.  
  103. filter: opacity(1);
  104. }
  105.  
  106.  
  107. /* =======================================
  108. ANIMATIONS
  109. ======================================= */
  110.  
  111. :root
  112. {
  113. --animation-busy-on: 'busy-on' 500ms ease-in-out 1000ms 1 normal forwards running;
  114. }
  115.  
  116. @keyframes busy-on {
  117. from {
  118. pointer-events: none;
  119. cursor: default;
  120. }
  121. to {
  122. filter: saturate(0.1);
  123. color: hsla(0deg, 0%, 100%, 0.5);
  124. }
  125. }
  126. `)
  127. }
  128.  
  129. /**
  130. * Set global busy mode
  131. * @param {Boolean} isBusy - Yes/No
  132. * @param {String=} selector - Contextual element selector
  133. */
  134. let setBusy = (isBusy, selector = 'ytd-comments') => {
  135. // console.debug('setBusy', 'isBusy:', isBusy)
  136.  
  137. let element = document.querySelector(selector)
  138.  
  139. if (isBusy === true) {
  140. element.classList.add('busy')
  141. return
  142. } else {
  143. element.classList.remove('busy')
  144. }
  145. }
  146.  
  147. /**
  148. * Get Button element
  149. * @returns {Boolean} - On/Off
  150. */
  151. let getButtonElement = () => document.querySelector('.expand-all-comments-button')
  152.  
  153. /**
  154. * Get Toggle state
  155. * @returns {Boolean} - On/Off
  156. */
  157. let getToggleState = () => Boolean(getButtonElement() && getButtonElement().checked)
  158.  
  159.  
  160. /**
  161. * Expand all comments
  162. */
  163. let expandAllComments = () => {
  164. console.debug('expandAllComments')
  165.  
  166. // Look for "View X replies" buttons in comment section
  167. onElementReady('ytd-comment-replies-renderer #more-replies.ytd-comment-replies-renderer', false, (buttonElement) => {
  168. // Abort if toggle disabled
  169. if (!getToggleState()) { return }
  170.  
  171. /** @listens buttonElement:Event#click */
  172. // buttonElement.addEventListener('click', () => setBusy(false), { once: true, passive: true })
  173.  
  174. // Busy = yes
  175. // setBusy(true)
  176.  
  177. // Click button
  178. buttonElement.click()
  179. })
  180.  
  181. // Look for "Read More" buttons in comment section
  182. onElementReady('ytd-comments tp-yt-paper-button.ytd-expander#more:not([hidden])', false, (buttonElement) => {
  183. // Abort if toggle disabled
  184. if (!getToggleState()) { return }
  185.  
  186. /** @listens buttonElement:Event#click */
  187. // buttonElement.addEventListener('click', () => setBusy(false), { once: true, passive: true })
  188.  
  189. // Busy = yes
  190. // setBusy(true)
  191.  
  192. // Click button
  193. buttonElement.click()
  194. })
  195. }
  196.  
  197.  
  198. /**
  199. * Check if the toggle is enabled, if yes, start expanding
  200. */
  201. let tryExpandAllComments = () => {
  202. console.debug('tryExpandAllComments')
  203.  
  204. const toggleState = getToggleState()
  205.  
  206. console.debug('toggle state:', toggleState)
  207.  
  208. // Abort if toggle disabled
  209. if (!toggleState) { return }
  210.  
  211. expandAllComments()
  212. }
  213.  
  214.  
  215. /**
  216. * Render button: 'Expand all Comments'
  217. * @param {Element} element - Container element
  218. */
  219. let renderButton = (element) => {
  220. console.debug('renderButton')
  221.  
  222. const buttonElement = document.createElement('tp-yt-paper-checkbox')
  223. buttonElement.className = 'expand-all-comments-button'
  224. buttonElement.innerHTML = `
  225. <div id="icon-label" class="yt-dropdown-menu">
  226. Expand all Comments
  227. </div>
  228. `
  229.  
  230. // Add button
  231. element.appendChild(buttonElement)
  232.  
  233. // Handle button toggle
  234. buttonElement.onchange = tryExpandAllComments
  235.  
  236. // Status
  237. console.debug('rendered button')
  238. }
  239.  
  240.  
  241. /**
  242. * Init
  243. */
  244. let init = () => {
  245. console.info('init')
  246.  
  247. // Verify URL path
  248. if (!urlPathList.some(urlPath => window.location.pathname.startsWith(urlPath))) { return }
  249.  
  250. // Add Stylesheet
  251. injectStylesheet()
  252.  
  253. // Wait for menu container
  254. onElementReady('ytd-comments ytd-comments-header-renderer > #title', false, (element) => {
  255. console.debug('onElementReady', 'ytd-comments ytd-comments-header-renderer > #title')
  256.  
  257. // Render button
  258. renderButton(element)
  259. })
  260.  
  261. // // Wait for variable section container
  262. // onElementReady('ytd-item-section-renderer#sections.style-scope.ytd-comments > #contents', false, (element) => {
  263. // console.debug('onElementReady', 'element:', '#contents')
  264. //
  265. // /**
  266. // * YouTube: Detect "Load More" stuff
  267. // * @listens ytd-item-section-rendere:Event#yt-load-next-continuation
  268. // */
  269. // element.parentElement.addEventListener('yt-load-next-continuation', (event) => {
  270. // console.debug('ytd-item-section-renderer#yt-load-next-continuation')
  271. //
  272. // const currentTarget = event.currentTarget
  273. // const shownItems = currentTarget && currentTarget.__data && currentTarget.__data.shownItems || []
  274. // const shownItemsCount = shownItems.length
  275. //
  276. // // DEBUG
  277. // // console.debug('currentTarget.__data:')
  278. // // console.dir(currentTarget.__data)
  279. //
  280. // // Probe whether this is still the initial item batch, if yes, skip
  281. // if (shownItemsCount === 0) { return }
  282. //
  283. // tryExpandAllComments()
  284. // })
  285. // })
  286. }
  287.  
  288.  
  289. /**
  290. * YouTube: Detect in-page navigation
  291. * @listens window:Event#yt-navigate-finish
  292. */
  293. window.addEventListener('yt-navigate-finish', () => {
  294. console.debug('window#yt-navigate-finish')
  295.  
  296. init()
  297. })