Sleazy Fork is available in English.

Derpibooru Comment Enhancements

Improvements to Derpibooru's comment section

目前为 2017-11-23 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Derpibooru Comment Enhancements
  3. // @description Improvements to Derpibooru's comment section
  4. // @version 1.3.12
  5. // @author Marker
  6. // @license MIT
  7. // @namespace https://github.com/marktaiwan/
  8. // @homepageURL https://github.com/marktaiwan/Derpibooru-Link-Preview
  9. // @supportURL https://github.com/marktaiwan/Derpibooru-Link-Preview/issues
  10. // @include https://derpibooru.org/*
  11. // @include https://trixiebooru.org/*
  12. // @include https://www.derpibooru.org/*
  13. // @include https://www.trixiebooru.org/*
  14. // @include /^https?://(www\.)?(derpibooru|trixiebooru)\.org(/.*|)$/
  15. // @grant none
  16. // @noframes
  17. // @require https://openuserjs.org/src/libs/soufianesakhi/node-creation-observer.js
  18. // ==/UserScript==
  19.  
  20. (function() {
  21. 'use strict';
  22.  
  23. const HOVER_ATTRIBUTE = 'comment-preview-active';
  24. var fetchCache = {};
  25. var backlinksCache = {};
  26.  
  27. function timeAgo(ele) {
  28. // Firefox 57/Greasemonkey 4 compatibility
  29. var booru = window.booru || window.wrappedJSObject.booru;
  30. booru.timeAgo(ele);
  31. }
  32.  
  33. function displayHover(comment, sourceLink) {
  34. const PADDING = 5; // in pixels
  35.  
  36. comment = comment.cloneNode(true);
  37. comment.id = 'hover_preview';
  38. comment.style.position = 'absolute';
  39. comment.style.maxWidth = '980px';
  40. comment.style.minWidth = '490px';
  41. comment.style.boxShadow = '0px 0px 12px 0px rgba(0, 0, 0, 0.4)';
  42.  
  43. // Make spoiler visible
  44. var i;
  45. var list = comment.querySelectorAll('span.spoiler, span.imgspoiler, span.imgspoiler img');
  46. if (list !== null) {
  47. for (i = 0; i < list.length; i++) {
  48.  
  49. if (list[i].matches('span')) {
  50. list[i].style.color = '#333';
  51. list[i].style.backgroundColor = (list[i].matches('span.imgspoiler')) ? '' : '#f7d4d4';
  52. } else {
  53. list[i].style.visibility = 'visible';
  54. }
  55.  
  56. }
  57. }
  58.  
  59. // highlight reply link
  60. var ele = sourceLink;
  61. while (ele.parentElement !== null && !ele.matches('article')) ele = ele.parentElement;
  62. var sourceCommentID = ele.id.slice(8);
  63.  
  64. list = comment.querySelectorAll('a[href$="#comment_' + sourceCommentID + '"]');
  65. if (list !== null) {
  66. for (i = 0; i < list.length; i++) list[i].style.textDecoration = 'underline dashed';
  67. }
  68.  
  69. // relative time
  70. timeAgo(comment.querySelectorAll('time'));
  71.  
  72. var container = document.getElementById('image_comments') || document.getElementById('content');
  73. if (container) container.appendChild(comment);
  74.  
  75. // calculate link position
  76. var linkRect = sourceLink.getBoundingClientRect();
  77. var linkTop = linkRect.top + viewportPosition().top;
  78. var linkLeft = linkRect.left + viewportPosition().left;
  79.  
  80. var commentRect = comment.getBoundingClientRect();
  81. var commentTop;
  82. var commentLeft;
  83.  
  84.  
  85. if (sourceLink.parentElement.classList.contains('comment_backlinks')) {
  86. // When there is room, place the preview below the link,
  87. // otherwise place it above the link
  88. if (document.documentElement.clientHeight - linkRect.bottom > commentRect.height + PADDING) {
  89.  
  90. commentTop = linkTop + linkRect.height + PADDING;
  91. commentLeft = (commentRect.width + linkLeft < document.documentElement.clientWidth) ? (
  92. linkLeft
  93. ) : (
  94. document.documentElement.clientWidth - (commentRect.width + PADDING)
  95. );
  96.  
  97. } else {
  98.  
  99. commentTop = linkTop - commentRect.height - PADDING;
  100. commentLeft = (commentRect.width + linkLeft < document.documentElement.clientWidth) ? (
  101. linkLeft
  102. ) : (
  103. document.documentElement.clientWidth - (commentRect.width + PADDING)
  104. );
  105.  
  106. }
  107. } else {
  108. // When there is room, place the preview above the link
  109. // otherwise place it to the right and aligns it to the top of viewport
  110. if (linkRect.top > commentRect.height + PADDING) {
  111.  
  112. commentTop = linkTop - commentRect.height - PADDING;
  113. commentLeft = (commentRect.width + linkLeft < document.documentElement.clientWidth) ? (
  114. linkLeft
  115. ) : (
  116. document.documentElement.clientWidth - (commentRect.width + PADDING)
  117. );
  118.  
  119. } else {
  120.  
  121. commentTop = viewportPosition().top + PADDING;
  122. commentLeft = linkLeft + linkRect.width + PADDING;
  123.  
  124. }
  125. }
  126.  
  127. comment.style.top = commentTop + 'px';
  128. comment.style.left = commentLeft + 'px';
  129. }
  130.  
  131. function linkEnter(sourceLink, targetCommentID) {
  132. sourceLink.setAttribute(HOVER_ATTRIBUTE, 1);
  133.  
  134. var targetComment = document.getElementById('comment_' + targetCommentID);
  135.  
  136. if (targetComment !== null) {
  137.  
  138. if (!elementInViewport(targetComment)) {
  139. displayHover(targetComment, sourceLink);
  140. }
  141.  
  142. // Highlight linked post
  143. targetComment.children[0].style.backgroundColor = 'rgba(230,230,30,0.3)';
  144. if (targetComment.querySelector('.comment_backlinks') !== null) targetComment.children[1].style.backgroundColor = 'rgba(230,230,30,0.3)';
  145.  
  146. } else {
  147.  
  148. // External post, display from cached response if possible
  149. if (fetchCache[targetCommentID] !== undefined) {
  150. displayHover(fetchCache[targetCommentID], sourceLink);
  151. } else {
  152. fetch(window.location.origin + '/comment/' + targetCommentID + '.html')
  153. .then((response) => response.text())
  154. .then((text) => {
  155. if (fetchCache[targetCommentID] === undefined && sourceLink.getAttribute(HOVER_ATTRIBUTE) !== '0') {
  156. var d = document.createElement('div');
  157. d.innerHTML = text;
  158. fetchCache[targetCommentID] = d.firstChild;
  159. displayHover(d.firstChild, sourceLink);
  160. }
  161. });
  162. }
  163.  
  164. }
  165. }
  166.  
  167. function linkLeave(sourceLink, targetCommentID) {
  168. sourceLink.setAttribute(HOVER_ATTRIBUTE, 0);
  169. var targetComment = document.getElementById('comment_' + targetCommentID);
  170. var preview = document.getElementById('hover_preview');
  171.  
  172. if (targetComment !== null) {
  173. targetComment.children[0].style.backgroundColor = ''; //remove comment highlight
  174. if (targetComment.querySelector('.comment_backlinks') !== null) targetComment.children[1].style.backgroundColor = '';
  175. }
  176. if (preview !== null) preview.parentElement.removeChild(preview);
  177. }
  178.  
  179. // Chrome/Firefox compatibility hack for getting viewport position
  180. function viewportPosition() {
  181. return {
  182. top: (document.documentElement.scrollTop || document.body.scrollTop),
  183. left: (document.documentElement.scrollLeft || document.body.scrollLeft)
  184. };
  185. }
  186.  
  187. function elementInViewport(el) {
  188. var rect = el.getBoundingClientRect();
  189.  
  190. return (
  191. rect.top + (rect.height - 50) >= 0 &&
  192. rect.bottom - (rect.height - 50) <= document.documentElement.clientHeight
  193. );
  194. }
  195.  
  196. function createBacklinksContainer(commentBody) {
  197. var ele = commentBody.querySelector('div.comment_backlinks');
  198.  
  199. if (ele === null) {
  200.  
  201. ele = document.createElement('div');
  202. ele.className = 'block__content comment_backlinks';
  203. ele.style.fontSize = '12px';
  204. // Firefox 57 Workaround: getComputedStyle(commentBody.firstChild)['border-top'] returns an empty string
  205. ele.style.borderTopStyle = window.getComputedStyle(commentBody.firstChild)['border-top-style'];
  206. ele.style.borderTopWidth = window.getComputedStyle(commentBody.firstChild)['border-top-width'];
  207. ele.style.borderTopColor = window.getComputedStyle(commentBody.firstChild)['border-top-color'];
  208.  
  209. commentBody.insertBefore(ele, commentBody.querySelector('.communication__options'));
  210. }
  211. return ele;
  212. }
  213.  
  214. function insertBacklink(backlink, commentID) {
  215.  
  216. // add to cache
  217. if (backlinksCache[commentID] === undefined) backlinksCache[commentID] = [];
  218. if (backlinksCache[commentID].findIndex((ele) => (ele.hash == backlink.hash)) == -1) {
  219. backlinksCache[commentID].push(backlink);
  220. }
  221.  
  222. var commentBody = document.getElementById('comment_' + commentID);
  223. if (commentBody !== null) {
  224. var linksContainer = createBacklinksContainer(commentBody);
  225.  
  226. // insertion sort the links so they are ordered by id
  227. if (linksContainer.children.length > 0) {
  228. var iLinkID = parseInt(backlink.hash.slice(9), 10);
  229. var iTempID;
  230. var i;
  231.  
  232. for (i = 0; i < linksContainer.children.length; i++) {
  233. iTempID = parseInt(linksContainer.children[i].hash.slice(9), 10);
  234.  
  235. if (iLinkID == iTempID) { // prevent links to the same comment from being added multiple times
  236. return;
  237. }
  238. if (iLinkID < iTempID) {
  239. linksContainer.insertBefore(backlink, linksContainer.children[i]);
  240. return;
  241. }
  242. }
  243. }
  244. linksContainer.appendChild(backlink);
  245. return;
  246. }
  247.  
  248. }
  249.  
  250. function getQueryVariable(key, HTMLAnchorElement) {
  251. var i;
  252. var array = HTMLAnchorElement.search.substring(1).split('&');
  253.  
  254. for (i = 0; i < array.length; i++) {
  255. if (key == array[i].split('=')[0]) {
  256. return array[i].split('=')[1];
  257. }
  258. }
  259. }
  260.  
  261. function insertButton(displayText) {
  262.  
  263. var commentsBlock = document.querySelector('.js-editable-comments');
  264.  
  265. var ele = document.createElement('div');
  266. ele.className = 'block__header';
  267. ele.id = 'comment_loading_button';
  268. ele.style.textAlign = 'center';
  269.  
  270. ele.appendChild(document.createElement('a'));
  271. ele.firstChild.style.padding = '0px';
  272. ele.firstChild.style.width = '100%';
  273. ele.firstChild.innerText = displayText;
  274.  
  275. commentsBlock.insertBefore(ele, commentsBlock.lastElementChild);
  276.  
  277. return ele;
  278. }
  279.  
  280. function loadComments(e, imageId, nextPage, lastPage) {
  281. e.target.parentElement.remove();
  282.  
  283. var btn = insertButton('Loading...');
  284. var fetchURL = window.location.origin + '/images/' + imageId + '/comments?id=' + imageId + '&page=' + nextPage;
  285.  
  286. fetch(fetchURL, {credentials: 'same-origin'}) // cookie needed for correct pagination
  287. .then((response) => response.text())
  288. .then((text) => {
  289. // response text => documentFragment
  290. var ele = document.createElement('div');
  291. var range = document.createRange();
  292.  
  293. ele.innerHTML = text;
  294. range.selectNodeContents(ele.firstChild);
  295.  
  296. var fragment = range.extractContents();
  297. var commentsBlock = document.getElementById('image_comments');
  298.  
  299. // update pagination blocks
  300. commentsBlock.replaceChild(fragment.firstChild, commentsBlock.firstElementChild);
  301. commentsBlock.replaceChild(fragment.lastChild, commentsBlock.lastElementChild);
  302.  
  303. // page marker
  304. ele.innerHTML = '';
  305. ele.className = 'block block__header';
  306. ele.style.textAlign = 'center';
  307. ele.innerText = 'Page ' + nextPage;
  308.  
  309. // relative time
  310. timeAgo(fragment.querySelectorAll('time'));
  311.  
  312. fragment.insertBefore(ele, fragment.firstElementChild);
  313. commentsBlock.insertBefore(fragment, commentsBlock.lastElementChild);
  314.  
  315. // configure button to load the next batch of comments
  316. btn.remove();
  317. if (nextPage < lastPage) {
  318. btn = insertButton('Load more comments');
  319. btn.addEventListener('click', (e) => {
  320. loadComments(e, imageId, nextPage + 1, lastPage);
  321. });
  322. }
  323.  
  324. });
  325. }
  326.  
  327. NodeCreationObserver.onCreation('article[id^="comment_"]', function (sourceCommentBody) {
  328. var links = sourceCommentBody.querySelectorAll('.communication__body__text a[href*="#comment_"]');
  329. var sourceCommentID = sourceCommentBody.id.slice(8);
  330. var ele = sourceCommentBody.querySelector('.communication__body__sender-name');
  331. var sourceAuthor = (ele.firstElementChild !== null && ele.firstElementChild.matches('a')) ? ele.firstElementChild.innerText : ele.innerHTML;
  332.  
  333. links.forEach((link) => {
  334. var targetCommentID = link.hash.slice(9); // Example: link.hash == "#comment_5430424"
  335. var backlink;
  336.  
  337. // add backlink if the comment is not part of a quote
  338. // and not fetched
  339. if (!link.matches('blockquote a') && !sourceCommentBody.matches('.fetched-comment')) {
  340. backlink = document.createElement('a');
  341.  
  342. backlink.style.marginRight = '5px';
  343. backlink.href = '#comment_' + sourceCommentID;
  344. backlink.textContent = '►';
  345. backlink.innerHTML += sourceAuthor;
  346.  
  347. backlink.addEventListener('mouseenter', () => {
  348. linkEnter(backlink, sourceCommentID);
  349. });
  350. backlink.addEventListener('mouseleave', () => {
  351. linkLeave(backlink, sourceCommentID);
  352. });
  353. backlink.addEventListener('click', () => {
  354. // force pageload instead of trying to navigate to a nonexistent anchor on the current page
  355. if (document.getElementById('comment_' + sourceCommentID) === null) window.location.reload();
  356. });
  357.  
  358. insertBacklink(backlink, targetCommentID);
  359. }
  360.  
  361. // ignore quotes
  362. // this is terrible
  363. if (link.nextElementSibling &&
  364. link.nextElementSibling.nextElementSibling &&
  365. link.nextElementSibling.nextElementSibling.matches('blockquote')) return;
  366.  
  367. link.addEventListener('mouseenter', () => {
  368. linkEnter(link, targetCommentID);
  369. });
  370. link.addEventListener('mouseleave', () => {
  371. linkLeave(link, targetCommentID);
  372. });
  373.  
  374. });
  375.  
  376. // If other pages had replied to this comment
  377. if (backlinksCache[sourceCommentID] !== undefined) {
  378. backlinksCache[sourceCommentID].forEach((backlink) => {
  379. insertBacklink(backlink, sourceCommentID);
  380. });
  381. }
  382.  
  383. });
  384.  
  385. // Comment loading
  386. NodeCreationObserver.onCreation('#image_comments nav>a.js-next', function (btnNextPage) {
  387. if (document.getElementById('comment_loading_button') !== null) return;
  388.  
  389. var btnLastPage = btnNextPage.nextElementSibling;
  390. var imageId = getQueryVariable('id', btnNextPage);
  391. var nextPage = parseInt(getQueryVariable('page', btnNextPage), 10);
  392. var lastPage = parseInt(getQueryVariable('page', btnLastPage), 10);
  393. var btn = insertButton('Load more comments');
  394.  
  395. btn.addEventListener('click', (e) => {
  396. loadComments(e, imageId, nextPage, lastPage);
  397. });
  398. });
  399. })();