Derpibooru Comment Enhancements

Improvements to Derpibooru's comment section

Pada tanggal 22 Juli 2017. Lihat %(latest_version_link).

  1. // ==UserScript==
  2. // @name Derpibooru Comment Enhancements
  3. // @description Improvements to Derpibooru's comment section
  4. // @version 1.3.9
  5. // @author Marker
  6. // @namespace https://github.com/marktaiwan/
  7. // @homepageURL https://github.com/marktaiwan/Derpibooru-Link-Preview
  8. // @supportURL https://github.com/marktaiwan/Derpibooru-Link-Preview/issues
  9. // @include https://derpibooru.org/*
  10. // @include https://trixiebooru.org/*
  11. // @include https://www.derpibooru.org/*
  12. // @include https://www.trixiebooru.org/*
  13. // @include /^https?://(www\.)?(derpibooru|trixiebooru)\.org(/.*|)$/
  14. // @grant none
  15. // @noframes
  16. // @require https://openuserjs.org/src/libs/soufianesakhi/node-creation-observer.js
  17. // ==/UserScript==
  18.  
  19. (function() {
  20. 'use strict';
  21.  
  22. const HOVER_ATTRIBUTE = 'comment-preview-active';
  23. const FONT_AWESOME = 'font: normal normal normal 14px/1 FontAwesome; text-rendering: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;';
  24. var fetchCache = {};
  25. var backlinksCache = {};
  26. var textareaSelectors = [
  27. '#comment_body',
  28. '#post_body',
  29. '#description-form #description',
  30. '#image_description',
  31. '#topic_posts_attributes_0_body',
  32. '#message_body'
  33. ];
  34. var formattingSyntax = {
  35. bold: {
  36. displayText: 'B',
  37. altText: 'bold',
  38. styleCSS: 'font-weight: bold;',
  39. options: {
  40. prefix: '*',
  41. suffix: '*'
  42. },
  43. edit: wrapSelection,
  44. shortcutKey: 'b'
  45. },
  46. italics: {
  47. displayText: 'i',
  48. altText: 'italics',
  49. styleCSS: 'font-style: italic;',
  50. options: {
  51. prefix: '_',
  52. suffix: '_'
  53. },
  54. edit: wrapSelection,
  55. shortcutKey: 'i'
  56. },
  57. under: {
  58. displayText: 'U',
  59. altText: 'underline',
  60. styleCSS: 'text-decoration: underline;',
  61. options: {
  62. prefix: '+',
  63. suffix: '+'
  64. },
  65. edit: wrapSelection,
  66. shortcutKey: 'u'
  67. },
  68. spoiler: {
  69. displayText: 'spoiler',
  70. altText: 'mark as spoiler',
  71. styleCSS: '',
  72. options: {
  73. prefix: '[spoiler]',
  74. suffix: '[/spoiler]'
  75. },
  76. edit: wrapSelection,
  77. shortcutKey: 's'
  78. },
  79. code: {
  80. displayText: 'code',
  81. altText: 'code formatting',
  82. styleCSS: 'font-family: "Courier New", Courier, monospace;',
  83. options: {
  84. prefix: '@',
  85. suffix: '@'
  86. },
  87. edit: wrapSelection,
  88. shortcutKey: 'e'
  89. },
  90. strike: {
  91. displayText: 'strike',
  92. altText: 'strikethrough',
  93. styleCSS: 'text-decoration: line-through;',
  94. options: {
  95. prefix: '-',
  96. suffix: '-'
  97. },
  98. edit: wrapSelection
  99. },
  100. superscript: {
  101. displayText: '<sup>sup</sup>',
  102. altText: 'superscript',
  103. options: {
  104. prefix: '^',
  105. suffix: '^'
  106. },
  107. edit: wrapSelection
  108. },
  109. subscript: {
  110. displayText: '<sub>sub</sub>',
  111. altText: 'subscript',
  112. options: {
  113. prefix: '~',
  114. suffix: '~'
  115. },
  116. edit: wrapSelection
  117. },
  118. quote: {
  119. displayText: '',
  120. altText: 'insert blockquote',
  121. styleCSS: FONT_AWESOME,
  122. glyph: '\\f10e',
  123. options: {
  124. prefix: '[bq]',
  125. suffix: '[/bq]'
  126. },
  127. edit: wrapSelection
  128. },
  129. link: {
  130. displayText: '',
  131. altText: 'insert hyperlink',
  132. styleCSS: FONT_AWESOME,
  133. glyph: '\\f0c1',
  134. options: {
  135. prefix: '"',
  136. suffix: '":',
  137. insertLink: true
  138. },
  139. edit: wrapSelection,
  140. shortcutKey: 'l'
  141. },
  142. image: {
  143. displayText: '',
  144. altText: 'insert image',
  145. styleCSS: FONT_AWESOME,
  146. glyph: '\\f03e',
  147. options: {
  148. prefix: '!',
  149. suffix: '!',
  150. insertImage: true
  151. },
  152. edit: wrapSelection,
  153. shortcutKey: 'k'
  154. },
  155. no_parse: {
  156. displayText: 'no parse',
  157. altText: 'Text you want the parser to ignore',
  158. options: {
  159. prefix: '[==',
  160. suffix: '==]'
  161. },
  162. edit: wrapSelection
  163. }
  164. };
  165.  
  166. function displayHover(comment, sourceLink) {
  167. const PADDING = 5; // in pixels
  168.  
  169. comment = comment.cloneNode(true);
  170. comment.id = 'hover_preview';
  171. comment.style.position = 'absolute';
  172. comment.style.maxWidth = '980px';
  173. comment.style.minWidth = '490px';
  174. comment.style.boxShadow = '0px 0px 12px 0px rgba(0, 0, 0, 0.4)';
  175.  
  176. // Make spoiler visible
  177. var i;
  178. var list = comment.querySelectorAll('span.spoiler, span.imgspoiler, span.imgspoiler img');
  179. if (list !== null) {
  180. for (i = 0; i < list.length; i++) {
  181.  
  182. if (list[i].matches('span')) {
  183. list[i].style.color = '#333';
  184. list[i].style.backgroundColor = (list[i].matches('span.imgspoiler')) ? '' : '#f7d4d4';
  185. } else {
  186. list[i].style.visibility = 'visible';
  187. }
  188.  
  189. }
  190. }
  191.  
  192. // highlight reply link
  193. var ele = sourceLink;
  194. while (ele.parentElement !== null && !ele.matches('article')) ele = ele.parentElement;
  195. var sourceCommentID = ele.id.slice(8);
  196.  
  197. list = comment.querySelectorAll('a[href$="#comment_' + sourceCommentID + '"]');
  198. if (list !== null) {
  199. for (i = 0; i < list.length; i++) list[i].style.textDecoration = 'underline dashed';
  200. }
  201.  
  202. // relative time
  203. window.booru.timeAgo(comment.querySelectorAll('time'));
  204.  
  205. var container = document.getElementById('image_comments') || document.getElementById('content');
  206. if (container) container.appendChild(comment);
  207.  
  208. // calculate link position
  209. var linkRect = sourceLink.getBoundingClientRect();
  210. var linkTop = linkRect.top + viewportPosition().top;
  211. var linkLeft = linkRect.left + viewportPosition().left;
  212.  
  213. var commentRect = comment.getBoundingClientRect();
  214. var commentTop;
  215. var commentLeft;
  216.  
  217.  
  218. if (sourceLink.parentElement.classList.contains('comment_backlinks')) {
  219. // When there is room, place the preview below the link,
  220. // otherwise place it above the link
  221. if (document.documentElement.clientHeight - linkRect.bottom > commentRect.height + PADDING) {
  222.  
  223. commentTop = linkTop + linkRect.height + PADDING;
  224. commentLeft = (commentRect.width + linkLeft < document.documentElement.clientWidth) ? (
  225. linkLeft
  226. ) : (
  227. document.documentElement.clientWidth - (commentRect.width + PADDING)
  228. );
  229.  
  230. } else {
  231.  
  232. commentTop = linkTop - commentRect.height - PADDING;
  233. commentLeft = (commentRect.width + linkLeft < document.documentElement.clientWidth) ? (
  234. linkLeft
  235. ) : (
  236. document.documentElement.clientWidth - (commentRect.width + PADDING)
  237. );
  238.  
  239. }
  240. } else {
  241. // When there is room, place the preview above the link
  242. // otherwise place it to the right and aligns it to the top of viewport
  243. if (linkRect.top > commentRect.height + PADDING) {
  244.  
  245. commentTop = linkTop - commentRect.height - PADDING;
  246. commentLeft = (commentRect.width + linkLeft < document.documentElement.clientWidth) ? (
  247. linkLeft
  248. ) : (
  249. document.documentElement.clientWidth - (commentRect.width + PADDING)
  250. );
  251.  
  252. } else {
  253.  
  254. commentTop = viewportPosition().top + PADDING;
  255. commentLeft = linkLeft + linkRect.width + PADDING;
  256.  
  257. }
  258. }
  259.  
  260. comment.style.top = commentTop + 'px';
  261. comment.style.left = commentLeft + 'px';
  262. }
  263.  
  264. function linkEnter(sourceLink, targetCommentID) {
  265.  
  266. sourceLink.setAttribute(HOVER_ATTRIBUTE, 1);
  267.  
  268. var targetComment = document.getElementById('comment_' + targetCommentID);
  269.  
  270. if (targetComment !== null) {
  271.  
  272. if (!elementInViewport(targetComment)) {
  273. displayHover(targetComment, sourceLink);
  274. }
  275.  
  276. // Highlight linked post
  277. targetComment.children[0].style.backgroundColor = 'rgba(230,230,30,0.3)';
  278. if (targetComment.querySelector('.comment_backlinks') !== null) targetComment.children[1].style.backgroundColor = 'rgba(230,230,30,0.3)';
  279.  
  280. } else {
  281.  
  282. // External post, display from cached response if possible
  283. if (fetchCache[targetCommentID] !== undefined) {
  284.  
  285. displayHover(fetchCache[targetCommentID], sourceLink);
  286.  
  287. } else {
  288.  
  289. fetch(window.location.origin + '/comment/' + targetCommentID + '.html')
  290. .then((response) => response.text())
  291. .then((text) => {
  292. if (fetchCache[targetCommentID] === undefined && sourceLink.getAttribute(HOVER_ATTRIBUTE) !== '0') {
  293. var d = document.createElement('div');
  294. d.innerHTML = text;
  295. fetchCache[targetCommentID] = d.firstChild;
  296. displayHover(d.firstChild, sourceLink);
  297. }
  298. });
  299.  
  300. }
  301.  
  302. }
  303. }
  304.  
  305. function linkLeave(sourceLink, targetCommentID) {
  306.  
  307. sourceLink.setAttribute(HOVER_ATTRIBUTE, 0);
  308. var targetComment = document.getElementById('comment_' + targetCommentID);
  309. var preview = document.getElementById('hover_preview');
  310.  
  311. if (targetComment !== null) {
  312. targetComment.children[0].style.backgroundColor = ''; //remove comment highlight
  313. if (targetComment.querySelector('.comment_backlinks') !== null) targetComment.children[1].style.backgroundColor = '';
  314. }
  315. if (preview !== null) preview.parentElement.removeChild(preview);
  316.  
  317. }
  318.  
  319. // Chrome/Firefox compatibility hack for getting viewport position
  320. function viewportPosition() {
  321. return {
  322. top: (document.documentElement.scrollTop || document.body.scrollTop),
  323. left: (document.documentElement.scrollLeft || document.body.scrollLeft)
  324. };
  325. }
  326.  
  327. function elementInViewport(el) {
  328. var rect = el.getBoundingClientRect();
  329.  
  330. return (
  331. rect.top + (rect.height - 50) >= 0 &&
  332. rect.bottom - (rect.height - 50) <= document.documentElement.clientHeight
  333. );
  334. }
  335.  
  336. function createBacklinksContainer(commentBody) {
  337. var ele = commentBody.querySelector('div.comment_backlinks');
  338.  
  339. if (ele === null) {
  340.  
  341. ele = document.createElement('div');
  342. ele.className = 'block__content comment_backlinks';
  343. ele.style.fontSize = '12px';
  344. ele.style.borderTop = window.getComputedStyle(commentBody.firstChild)['border-top'];
  345.  
  346. commentBody.insertBefore(ele, commentBody.querySelector('.communication__options'));
  347.  
  348. }
  349. return ele;
  350. }
  351.  
  352. function insertBacklink(backlink, commentID) {
  353.  
  354. // add to cache
  355. if (backlinksCache[commentID] === undefined) backlinksCache[commentID] = [];
  356. if (backlinksCache[commentID].findIndex((ele) => (ele.hash == backlink.hash)) == -1) {
  357. backlinksCache[commentID].push(backlink);
  358. }
  359.  
  360. var commentBody = document.getElementById('comment_' + commentID);
  361. if (commentBody !== null) {
  362. var linksContainer = createBacklinksContainer(commentBody);
  363.  
  364. // insertion sort the links so they are ordered by id
  365. if (linksContainer.children.length > 0) {
  366.  
  367. var iLinkID = parseInt(backlink.hash.slice(9), 10);
  368. var iTempID;
  369. var i;
  370.  
  371. for (i = 0; i < linksContainer.children.length; i++) {
  372. iTempID = parseInt(linksContainer.children[i].hash.slice(9), 10);
  373.  
  374. if (iLinkID == iTempID) { // prevent links to the same comment from being added multiple times
  375. return;
  376. }
  377. if (iLinkID < iTempID) {
  378. linksContainer.insertBefore(backlink, linksContainer.children[i]);
  379. return;
  380. }
  381. }
  382.  
  383. }
  384. linksContainer.appendChild(backlink);
  385. return;
  386. }
  387.  
  388. }
  389.  
  390. function getQueryVariable(key, HTMLAnchorElement) {
  391. var i;
  392. var array = HTMLAnchorElement.search.substring(1).split('&');
  393.  
  394. for (i = 0; i < array.length; i++) {
  395. if (key == array[i].split('=')[0]) {
  396. return array[i].split('=')[1];
  397. }
  398. }
  399. }
  400.  
  401. function insertButton(displayText) {
  402.  
  403. var commentsBlock = document.querySelector('.js-editable-comments');
  404.  
  405. var ele = document.createElement('div');
  406. ele.className = 'block__header';
  407. ele.id = 'comment_loading_button';
  408. ele.style.textAlign = 'center';
  409.  
  410. ele.appendChild(document.createElement('a'));
  411. ele.firstChild.style.padding = '0px';
  412. ele.firstChild.style.width = '100%';
  413. ele.firstChild.innerText = displayText;
  414.  
  415. commentsBlock.insertBefore(ele, commentsBlock.lastElementChild);
  416.  
  417. return ele;
  418. }
  419.  
  420. function loadComments(e, imageId, nextPage, lastPage) {
  421.  
  422. e.target.parentElement.remove();
  423. var btn = insertButton('Loading...');
  424.  
  425. var fetchURL = window.location.origin + '/images/' + imageId + '/comments?id=' + imageId + '&page=' + nextPage;
  426.  
  427. fetch(fetchURL, {credentials: 'same-origin'}) // cookie needed for correct pagination
  428. .then((response) => response.text())
  429. .then((text) => {
  430.  
  431. // response text => documentFragment
  432. var ele = document.createElement('div');
  433. var range = document.createRange();
  434.  
  435. ele.innerHTML = text;
  436. range.selectNodeContents(ele.firstChild);
  437.  
  438. var fragment = range.extractContents();
  439. var commentsBlock = document.getElementById('image_comments');
  440.  
  441. // update pagination blocks
  442. commentsBlock.replaceChild(fragment.firstChild, commentsBlock.firstElementChild);
  443. commentsBlock.replaceChild(fragment.lastChild, commentsBlock.lastElementChild);
  444.  
  445. // page marker
  446. ele.innerHTML = '';
  447. ele.className = 'block block__header';
  448. ele.style.textAlign = 'center';
  449. ele.innerText = 'Page ' + nextPage;
  450.  
  451. // relative time
  452. window.booru.timeAgo(fragment.querySelectorAll('time'));
  453.  
  454. fragment.insertBefore(ele, fragment.firstElementChild);
  455. commentsBlock.insertBefore(fragment, commentsBlock.lastElementChild);
  456.  
  457.  
  458. // configure button to load the next batch of comments
  459. btn.remove();
  460. if (nextPage < lastPage) {
  461. btn = insertButton('Load more comments');
  462.  
  463. btn.addEventListener('click', (e) => {
  464. loadComments(e, imageId, nextPage + 1, lastPage);
  465. });
  466. }
  467.  
  468. });
  469. }
  470.  
  471. function wrapSelection(box, options) {
  472. if (box === null) {
  473. return;
  474. }
  475. var hyperlink;
  476. var prefix = options.prefix;
  477. var suffix = options.suffix;
  478.  
  479. // record scroll top to restore it later.
  480. var scrollTop = box.scrollTop;
  481. var selectionStart = box.selectionStart;
  482. var selectionEnd = box.selectionEnd;
  483. var text = box.value;
  484. var beforeSelection = text.substring(0, selectionStart);
  485. var selectedText = text.substring(selectionStart, selectionEnd);
  486. var afterSelection = text.substring(selectionEnd);
  487.  
  488. var emptySelection = (selectedText === '');
  489.  
  490. var trailingSpace = '';
  491. var cursor = selectedText.length - 1;
  492.  
  493. // deselect trailing space and carriage return
  494. while (cursor > 0 && (selectedText[cursor] === ' ' || selectedText[cursor] === '\n')) {
  495. trailingSpace = selectedText[cursor] + trailingSpace;
  496. cursor--;
  497. }
  498. selectedText = selectedText.substring(0, cursor + 1);
  499.  
  500. if (options.insertLink) {
  501. hyperlink = window.prompt('Link:');
  502. if (hyperlink === null || hyperlink === '') return;
  503. // change on-site link to use relative url
  504. if (hyperlink.startsWith(window.origin)) {
  505. hyperlink = hyperlink.substring(window.origin.length);
  506. }
  507. suffix += hyperlink;
  508. }
  509.  
  510. if (options.insertImage) {
  511. hyperlink = window.prompt('Link to image:');
  512. if (hyperlink === null || hyperlink === '') return;
  513. // change on-site image to embed
  514. var resultsArray = hyperlink.match(/https?:\/\/(?:www\.)?(?:(?:derpibooru\.org|trixiebooru\.org)\/(?:images\/)?(\d{1,})(?:\?|\?.{1,}|\/|\.html)?|derpicdn\.net\/img\/(?:view\/)?\d{1,}\/\d{1,}\/\d{1,}\/(\d+))/i);
  515. if (resultsArray !== null) {
  516. var imageId = resultsArray[1] || resultsArray[2];
  517. if (imageId === undefined) {
  518. console.error('Derpibooru Comment Preview: Unable to extract image ID from link: ' + hyperlink);
  519. return;
  520. }
  521. prefix = '>>';
  522. selectedText = imageId;
  523. suffix = 'p';
  524. } else {
  525. selectedText = hyperlink;
  526. }
  527. emptySelection = false;
  528. }
  529.  
  530. box.value = beforeSelection + prefix + selectedText + suffix + trailingSpace + afterSelection;
  531. if (emptySelection) {
  532. box.selectionStart = beforeSelection.length + prefix.length;
  533. } else {
  534. box.selectionStart = beforeSelection.length + prefix.length + selectedText.length + suffix.length;
  535. }
  536. box.selectionEnd = box.selectionStart;
  537. box.scrollTop = scrollTop;
  538. }
  539.  
  540. function initCSS(formats) {
  541. var ele;
  542. var stringBuilder = [];
  543. var styleElement = document.createElement('style');
  544. styleElement.id = 'dce-css';
  545. styleElement.type = 'text/css';
  546. stringBuilder.push('/* Generated by Derpibooru Comment Preview */');
  547. stringBuilder.push('.dce-toolbar {margin-bottom: 6px;}');
  548. stringBuilder.push('.dce-toolbar .button {height: 28px; min-width: 28px; text-align: center; vertical-align: middle; margin: 2px;}');
  549. for (ele in formats) {
  550. if (formats[ele].styleCSS !== undefined) {
  551. stringBuilder.push(`.dce-toolbar .button[formatting="${ele}"] {${formats[ele].styleCSS}}`);
  552. }
  553. if (formats[ele].glyph !== undefined) {
  554. stringBuilder.push(`.dce-toolbar .button[formatting="${ele}"]:before {content: "${formats[ele].glyph}"; font-size: 18px; vertical-align: middle;}`);
  555. }
  556. }
  557. styleElement.innerHTML = stringBuilder.join('\n');
  558. document.head.appendChild(styleElement);
  559. }
  560.  
  561. function initToolbar(formats, textarea) {
  562. if (textarea.getAttribute('dce-toolbar') !== null) {
  563. return;
  564. }
  565. var commentBox = textarea;
  566. var toolbar = document.createElement('div');
  567. var ele;
  568.  
  569. // HTML
  570. for (ele in formats) {
  571. if (formats[ele].displayText !== null) {
  572. var btn = document.createElement('div');
  573. var name = formats[ele].displayText;
  574. var altText = formats[ele].altText;
  575. var key = formats[ele].shortcutKey;
  576.  
  577. btn.className = 'button';
  578. btn.innerHTML = name;
  579. if (altText !== undefined) {
  580. if (key !== undefined) {
  581. altText += ` (ctrl+${key})`;
  582. }
  583. btn.title = altText;
  584. }
  585. btn.setAttribute('formatting', ele);
  586.  
  587. toolbar.appendChild(btn);
  588. }
  589. }
  590. toolbar.classList.add('dce-toolbar');
  591. commentBox.parentElement.insertBefore(toolbar, commentBox);
  592.  
  593. // Event listeners
  594. toolbar.addEventListener('click', function (e) {
  595. if (e.target.matches('.button') || e.target.parentElement.matches('.button')) {
  596. var ele;
  597. var btn = (e.target.matches('.button')) ? e.target : e.target.parentElement;
  598.  
  599. for (ele in formats) {
  600. if (btn.getAttribute('formatting') == ele) {
  601. formats[ele].edit(commentBox, formats[ele].options);
  602. }
  603. }
  604. commentBox.focus();
  605. }
  606. });
  607. commentBox.addEventListener('keydown', function (e) {
  608. if (e.ctrlKey && !e.shiftKey) {
  609. var ele;
  610. var ch = e.key.toLowerCase();
  611. var box = e.target;
  612.  
  613. for (ele in formats) {
  614. if (ch == formats[ele].shortcutKey) {
  615. formats[ele].edit(box, formats[ele].options);
  616. e.preventDefault();
  617. }
  618. }
  619. }
  620. });
  621.  
  622. textarea.setAttribute('dce-toolbar', 1);
  623. }
  624.  
  625. NodeCreationObserver.onCreation('article[id^="comment_"]', function (sourceCommentBody) {
  626.  
  627. var links = sourceCommentBody.querySelectorAll('.communication__body__text a[href*="#comment_"]');
  628. var sourceCommentID = sourceCommentBody.id.slice(8);
  629.  
  630. var ele = sourceCommentBody.querySelector('.communication__body__sender-name');
  631. var sourceAuthor = (ele.firstElementChild !== null && ele.firstElementChild.matches('a')) ? ele.firstElementChild.innerText : ele.innerHTML;
  632.  
  633. links.forEach((link) => {
  634.  
  635. var targetCommentID = link.hash.slice(9); // Example: link.hash == "#comment_5430424"
  636. var backlink;
  637.  
  638. // add backlink if the comment is not part of a quote
  639. // and not fetched
  640. if (!link.matches('blockquote a') && !sourceCommentBody.matches('.fetched-comment')) {
  641.  
  642. backlink = document.createElement('a');
  643.  
  644. backlink.style.marginRight = '5px';
  645. backlink.href = '#comment_' + sourceCommentID;
  646. backlink.textContent = '►';
  647. backlink.innerHTML += sourceAuthor;
  648.  
  649. backlink.addEventListener('mouseenter', () => {
  650. linkEnter(backlink, sourceCommentID);
  651. });
  652. backlink.addEventListener('mouseleave', () => {
  653. linkLeave(backlink, sourceCommentID);
  654. });
  655. backlink.addEventListener('click', () => {
  656. // force pageload instead of trying to navigate to a nonexistent anchor on the current page
  657. if (document.getElementById('comment_' + sourceCommentID) === null) window.location.reload();
  658. });
  659.  
  660. insertBacklink(backlink, targetCommentID);
  661. }
  662.  
  663.  
  664. // ignore quotes
  665. // this is terrible
  666. if (link.nextElementSibling &&
  667. link.nextElementSibling.nextElementSibling &&
  668. link.nextElementSibling.nextElementSibling.matches('blockquote')) return;
  669.  
  670. link.addEventListener('mouseenter', () => {
  671. linkEnter(link, targetCommentID);
  672. });
  673. link.addEventListener('mouseleave', () => {
  674. linkLeave(link, targetCommentID);
  675. });
  676.  
  677. });
  678.  
  679. // If other pages had replied to this comment
  680. if (backlinksCache[sourceCommentID] !== undefined) {
  681. backlinksCache[sourceCommentID].forEach((backlink) => {
  682. insertBacklink(backlink, sourceCommentID);
  683. });
  684. }
  685.  
  686. });
  687.  
  688. // Comment loading
  689. NodeCreationObserver.onCreation('#image_comments nav>a.js-next', function (btnNextPage) {
  690.  
  691. if (document.getElementById('comment_loading_button') !== null) return;
  692.  
  693. var btnLastPage = btnNextPage.nextElementSibling;
  694.  
  695. var imageId = getQueryVariable('id', btnNextPage);
  696. var nextPage = parseInt(getQueryVariable('page', btnNextPage), 10);
  697. var lastPage = parseInt(getQueryVariable('page', btnLastPage), 10);
  698.  
  699. var btn = insertButton('Load more comments');
  700.  
  701. btn.addEventListener('click', (e) => {
  702. loadComments(e, imageId, nextPage, lastPage);
  703. });
  704.  
  705. });
  706.  
  707. NodeCreationObserver.onCreation(textareaSelectors.join(','), function (textarea) {
  708. initToolbar(formattingSyntax, textarea);
  709. });
  710.  
  711. initCSS(formattingSyntax);
  712. })();