您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Improvements to Derpibooru's comment section
当前为
// ==UserScript== // @name Derpibooru Comment Enhancements // @description Improvements to Derpibooru's comment section // @version 1.3.9 // @author Marker // @namespace https://github.com/marktaiwan/ // @homepageURL https://github.com/marktaiwan/Derpibooru-Link-Preview // @supportURL https://github.com/marktaiwan/Derpibooru-Link-Preview/issues // @include https://derpibooru.org/* // @include https://trixiebooru.org/* // @include https://www.derpibooru.org/* // @include https://www.trixiebooru.org/* // @include /^https?://(www\.)?(derpibooru|trixiebooru)\.org(/.*|)$/ // @grant none // @noframes // @require https://openuserjs.org/src/libs/soufianesakhi/node-creation-observer.js // ==/UserScript== (function() { 'use strict'; const HOVER_ATTRIBUTE = 'comment-preview-active'; const FONT_AWESOME = 'font: normal normal normal 14px/1 FontAwesome; text-rendering: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;'; var fetchCache = {}; var backlinksCache = {}; var textareaSelectors = [ '#comment_body', '#post_body', '#description-form #description', '#image_description', '#topic_posts_attributes_0_body', '#message_body' ]; var formattingSyntax = { bold: { displayText: 'B', altText: 'bold', styleCSS: 'font-weight: bold;', options: { prefix: '*', suffix: '*' }, edit: wrapSelection, shortcutKey: 'b' }, italics: { displayText: 'i', altText: 'italics', styleCSS: 'font-style: italic;', options: { prefix: '_', suffix: '_' }, edit: wrapSelection, shortcutKey: 'i' }, under: { displayText: 'U', altText: 'underline', styleCSS: 'text-decoration: underline;', options: { prefix: '+', suffix: '+' }, edit: wrapSelection, shortcutKey: 'u' }, spoiler: { displayText: 'spoiler', altText: 'mark as spoiler', styleCSS: '', options: { prefix: '[spoiler]', suffix: '[/spoiler]' }, edit: wrapSelection, shortcutKey: 's' }, code: { displayText: 'code', altText: 'code formatting', styleCSS: 'font-family: "Courier New", Courier, monospace;', options: { prefix: '@', suffix: '@' }, edit: wrapSelection, shortcutKey: 'e' }, strike: { displayText: 'strike', altText: 'strikethrough', styleCSS: 'text-decoration: line-through;', options: { prefix: '-', suffix: '-' }, edit: wrapSelection }, superscript: { displayText: '<sup>sup</sup>', altText: 'superscript', options: { prefix: '^', suffix: '^' }, edit: wrapSelection }, subscript: { displayText: '<sub>sub</sub>', altText: 'subscript', options: { prefix: '~', suffix: '~' }, edit: wrapSelection }, quote: { displayText: '', altText: 'insert blockquote', styleCSS: FONT_AWESOME, glyph: '\\f10e', options: { prefix: '[bq]', suffix: '[/bq]' }, edit: wrapSelection }, link: { displayText: '', altText: 'insert hyperlink', styleCSS: FONT_AWESOME, glyph: '\\f0c1', options: { prefix: '"', suffix: '":', insertLink: true }, edit: wrapSelection, shortcutKey: 'l' }, image: { displayText: '', altText: 'insert image', styleCSS: FONT_AWESOME, glyph: '\\f03e', options: { prefix: '!', suffix: '!', insertImage: true }, edit: wrapSelection, shortcutKey: 'k' }, no_parse: { displayText: 'no parse', altText: 'Text you want the parser to ignore', options: { prefix: '[==', suffix: '==]' }, edit: wrapSelection } }; function displayHover(comment, sourceLink) { const PADDING = 5; // in pixels comment = comment.cloneNode(true); comment.id = 'hover_preview'; comment.style.position = 'absolute'; comment.style.maxWidth = '980px'; comment.style.minWidth = '490px'; comment.style.boxShadow = '0px 0px 12px 0px rgba(0, 0, 0, 0.4)'; // Make spoiler visible var i; var list = comment.querySelectorAll('span.spoiler, span.imgspoiler, span.imgspoiler img'); if (list !== null) { for (i = 0; i < list.length; i++) { if (list[i].matches('span')) { list[i].style.color = '#333'; list[i].style.backgroundColor = (list[i].matches('span.imgspoiler')) ? '' : '#f7d4d4'; } else { list[i].style.visibility = 'visible'; } } } // highlight reply link var ele = sourceLink; while (ele.parentElement !== null && !ele.matches('article')) ele = ele.parentElement; var sourceCommentID = ele.id.slice(8); list = comment.querySelectorAll('a[href$="#comment_' + sourceCommentID + '"]'); if (list !== null) { for (i = 0; i < list.length; i++) list[i].style.textDecoration = 'underline dashed'; } // relative time window.booru.timeAgo(comment.querySelectorAll('time')); var container = document.getElementById('image_comments') || document.getElementById('content'); if (container) container.appendChild(comment); // calculate link position var linkRect = sourceLink.getBoundingClientRect(); var linkTop = linkRect.top + viewportPosition().top; var linkLeft = linkRect.left + viewportPosition().left; var commentRect = comment.getBoundingClientRect(); var commentTop; var commentLeft; if (sourceLink.parentElement.classList.contains('comment_backlinks')) { // When there is room, place the preview below the link, // otherwise place it above the link if (document.documentElement.clientHeight - linkRect.bottom > commentRect.height + PADDING) { commentTop = linkTop + linkRect.height + PADDING; commentLeft = (commentRect.width + linkLeft < document.documentElement.clientWidth) ? ( linkLeft ) : ( document.documentElement.clientWidth - (commentRect.width + PADDING) ); } else { commentTop = linkTop - commentRect.height - PADDING; commentLeft = (commentRect.width + linkLeft < document.documentElement.clientWidth) ? ( linkLeft ) : ( document.documentElement.clientWidth - (commentRect.width + PADDING) ); } } else { // When there is room, place the preview above the link // otherwise place it to the right and aligns it to the top of viewport if (linkRect.top > commentRect.height + PADDING) { commentTop = linkTop - commentRect.height - PADDING; commentLeft = (commentRect.width + linkLeft < document.documentElement.clientWidth) ? ( linkLeft ) : ( document.documentElement.clientWidth - (commentRect.width + PADDING) ); } else { commentTop = viewportPosition().top + PADDING; commentLeft = linkLeft + linkRect.width + PADDING; } } comment.style.top = commentTop + 'px'; comment.style.left = commentLeft + 'px'; } function linkEnter(sourceLink, targetCommentID) { sourceLink.setAttribute(HOVER_ATTRIBUTE, 1); var targetComment = document.getElementById('comment_' + targetCommentID); if (targetComment !== null) { if (!elementInViewport(targetComment)) { displayHover(targetComment, sourceLink); } // Highlight linked post targetComment.children[0].style.backgroundColor = 'rgba(230,230,30,0.3)'; if (targetComment.querySelector('.comment_backlinks') !== null) targetComment.children[1].style.backgroundColor = 'rgba(230,230,30,0.3)'; } else { // External post, display from cached response if possible if (fetchCache[targetCommentID] !== undefined) { displayHover(fetchCache[targetCommentID], sourceLink); } else { fetch(window.location.origin + '/comment/' + targetCommentID + '.html') .then((response) => response.text()) .then((text) => { if (fetchCache[targetCommentID] === undefined && sourceLink.getAttribute(HOVER_ATTRIBUTE) !== '0') { var d = document.createElement('div'); d.innerHTML = text; fetchCache[targetCommentID] = d.firstChild; displayHover(d.firstChild, sourceLink); } }); } } } function linkLeave(sourceLink, targetCommentID) { sourceLink.setAttribute(HOVER_ATTRIBUTE, 0); var targetComment = document.getElementById('comment_' + targetCommentID); var preview = document.getElementById('hover_preview'); if (targetComment !== null) { targetComment.children[0].style.backgroundColor = ''; //remove comment highlight if (targetComment.querySelector('.comment_backlinks') !== null) targetComment.children[1].style.backgroundColor = ''; } if (preview !== null) preview.parentElement.removeChild(preview); } // Chrome/Firefox compatibility hack for getting viewport position function viewportPosition() { return { top: (document.documentElement.scrollTop || document.body.scrollTop), left: (document.documentElement.scrollLeft || document.body.scrollLeft) }; } function elementInViewport(el) { var rect = el.getBoundingClientRect(); return ( rect.top + (rect.height - 50) >= 0 && rect.bottom - (rect.height - 50) <= document.documentElement.clientHeight ); } function createBacklinksContainer(commentBody) { var ele = commentBody.querySelector('div.comment_backlinks'); if (ele === null) { ele = document.createElement('div'); ele.className = 'block__content comment_backlinks'; ele.style.fontSize = '12px'; ele.style.borderTop = window.getComputedStyle(commentBody.firstChild)['border-top']; commentBody.insertBefore(ele, commentBody.querySelector('.communication__options')); } return ele; } function insertBacklink(backlink, commentID) { // add to cache if (backlinksCache[commentID] === undefined) backlinksCache[commentID] = []; if (backlinksCache[commentID].findIndex((ele) => (ele.hash == backlink.hash)) == -1) { backlinksCache[commentID].push(backlink); } var commentBody = document.getElementById('comment_' + commentID); if (commentBody !== null) { var linksContainer = createBacklinksContainer(commentBody); // insertion sort the links so they are ordered by id if (linksContainer.children.length > 0) { var iLinkID = parseInt(backlink.hash.slice(9), 10); var iTempID; var i; for (i = 0; i < linksContainer.children.length; i++) { iTempID = parseInt(linksContainer.children[i].hash.slice(9), 10); if (iLinkID == iTempID) { // prevent links to the same comment from being added multiple times return; } if (iLinkID < iTempID) { linksContainer.insertBefore(backlink, linksContainer.children[i]); return; } } } linksContainer.appendChild(backlink); return; } } function getQueryVariable(key, HTMLAnchorElement) { var i; var array = HTMLAnchorElement.search.substring(1).split('&'); for (i = 0; i < array.length; i++) { if (key == array[i].split('=')[0]) { return array[i].split('=')[1]; } } } function insertButton(displayText) { var commentsBlock = document.querySelector('.js-editable-comments'); var ele = document.createElement('div'); ele.className = 'block__header'; ele.id = 'comment_loading_button'; ele.style.textAlign = 'center'; ele.appendChild(document.createElement('a')); ele.firstChild.style.padding = '0px'; ele.firstChild.style.width = '100%'; ele.firstChild.innerText = displayText; commentsBlock.insertBefore(ele, commentsBlock.lastElementChild); return ele; } function loadComments(e, imageId, nextPage, lastPage) { e.target.parentElement.remove(); var btn = insertButton('Loading...'); var fetchURL = window.location.origin + '/images/' + imageId + '/comments?id=' + imageId + '&page=' + nextPage; fetch(fetchURL, {credentials: 'same-origin'}) // cookie needed for correct pagination .then((response) => response.text()) .then((text) => { // response text => documentFragment var ele = document.createElement('div'); var range = document.createRange(); ele.innerHTML = text; range.selectNodeContents(ele.firstChild); var fragment = range.extractContents(); var commentsBlock = document.getElementById('image_comments'); // update pagination blocks commentsBlock.replaceChild(fragment.firstChild, commentsBlock.firstElementChild); commentsBlock.replaceChild(fragment.lastChild, commentsBlock.lastElementChild); // page marker ele.innerHTML = ''; ele.className = 'block block__header'; ele.style.textAlign = 'center'; ele.innerText = 'Page ' + nextPage; // relative time window.booru.timeAgo(fragment.querySelectorAll('time')); fragment.insertBefore(ele, fragment.firstElementChild); commentsBlock.insertBefore(fragment, commentsBlock.lastElementChild); // configure button to load the next batch of comments btn.remove(); if (nextPage < lastPage) { btn = insertButton('Load more comments'); btn.addEventListener('click', (e) => { loadComments(e, imageId, nextPage + 1, lastPage); }); } }); } function wrapSelection(box, options) { if (box === null) { return; } var hyperlink; var prefix = options.prefix; var suffix = options.suffix; // record scroll top to restore it later. var scrollTop = box.scrollTop; var selectionStart = box.selectionStart; var selectionEnd = box.selectionEnd; var text = box.value; var beforeSelection = text.substring(0, selectionStart); var selectedText = text.substring(selectionStart, selectionEnd); var afterSelection = text.substring(selectionEnd); var emptySelection = (selectedText === ''); var trailingSpace = ''; var cursor = selectedText.length - 1; // deselect trailing space and carriage return while (cursor > 0 && (selectedText[cursor] === ' ' || selectedText[cursor] === '\n')) { trailingSpace = selectedText[cursor] + trailingSpace; cursor--; } selectedText = selectedText.substring(0, cursor + 1); if (options.insertLink) { hyperlink = window.prompt('Link:'); if (hyperlink === null || hyperlink === '') return; // change on-site link to use relative url if (hyperlink.startsWith(window.origin)) { hyperlink = hyperlink.substring(window.origin.length); } suffix += hyperlink; } if (options.insertImage) { hyperlink = window.prompt('Link to image:'); if (hyperlink === null || hyperlink === '') return; // change on-site image to embed 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); if (resultsArray !== null) { var imageId = resultsArray[1] || resultsArray[2]; if (imageId === undefined) { console.error('Derpibooru Comment Preview: Unable to extract image ID from link: ' + hyperlink); return; } prefix = '>>'; selectedText = imageId; suffix = 'p'; } else { selectedText = hyperlink; } emptySelection = false; } box.value = beforeSelection + prefix + selectedText + suffix + trailingSpace + afterSelection; if (emptySelection) { box.selectionStart = beforeSelection.length + prefix.length; } else { box.selectionStart = beforeSelection.length + prefix.length + selectedText.length + suffix.length; } box.selectionEnd = box.selectionStart; box.scrollTop = scrollTop; } function initCSS(formats) { var ele; var stringBuilder = []; var styleElement = document.createElement('style'); styleElement.id = 'dce-css'; styleElement.type = 'text/css'; stringBuilder.push('/* Generated by Derpibooru Comment Preview */'); stringBuilder.push('.dce-toolbar {margin-bottom: 6px;}'); stringBuilder.push('.dce-toolbar .button {height: 28px; min-width: 28px; text-align: center; vertical-align: middle; margin: 2px;}'); for (ele in formats) { if (formats[ele].styleCSS !== undefined) { stringBuilder.push(`.dce-toolbar .button[formatting="${ele}"] {${formats[ele].styleCSS}}`); } if (formats[ele].glyph !== undefined) { stringBuilder.push(`.dce-toolbar .button[formatting="${ele}"]:before {content: "${formats[ele].glyph}"; font-size: 18px; vertical-align: middle;}`); } } styleElement.innerHTML = stringBuilder.join('\n'); document.head.appendChild(styleElement); } function initToolbar(formats, textarea) { if (textarea.getAttribute('dce-toolbar') !== null) { return; } var commentBox = textarea; var toolbar = document.createElement('div'); var ele; // HTML for (ele in formats) { if (formats[ele].displayText !== null) { var btn = document.createElement('div'); var name = formats[ele].displayText; var altText = formats[ele].altText; var key = formats[ele].shortcutKey; btn.className = 'button'; btn.innerHTML = name; if (altText !== undefined) { if (key !== undefined) { altText += ` (ctrl+${key})`; } btn.title = altText; } btn.setAttribute('formatting', ele); toolbar.appendChild(btn); } } toolbar.classList.add('dce-toolbar'); commentBox.parentElement.insertBefore(toolbar, commentBox); // Event listeners toolbar.addEventListener('click', function (e) { if (e.target.matches('.button') || e.target.parentElement.matches('.button')) { var ele; var btn = (e.target.matches('.button')) ? e.target : e.target.parentElement; for (ele in formats) { if (btn.getAttribute('formatting') == ele) { formats[ele].edit(commentBox, formats[ele].options); } } commentBox.focus(); } }); commentBox.addEventListener('keydown', function (e) { if (e.ctrlKey && !e.shiftKey) { var ele; var ch = e.key.toLowerCase(); var box = e.target; for (ele in formats) { if (ch == formats[ele].shortcutKey) { formats[ele].edit(box, formats[ele].options); e.preventDefault(); } } } }); textarea.setAttribute('dce-toolbar', 1); } NodeCreationObserver.onCreation('article[id^="comment_"]', function (sourceCommentBody) { var links = sourceCommentBody.querySelectorAll('.communication__body__text a[href*="#comment_"]'); var sourceCommentID = sourceCommentBody.id.slice(8); var ele = sourceCommentBody.querySelector('.communication__body__sender-name'); var sourceAuthor = (ele.firstElementChild !== null && ele.firstElementChild.matches('a')) ? ele.firstElementChild.innerText : ele.innerHTML; links.forEach((link) => { var targetCommentID = link.hash.slice(9); // Example: link.hash == "#comment_5430424" var backlink; // add backlink if the comment is not part of a quote // and not fetched if (!link.matches('blockquote a') && !sourceCommentBody.matches('.fetched-comment')) { backlink = document.createElement('a'); backlink.style.marginRight = '5px'; backlink.href = '#comment_' + sourceCommentID; backlink.textContent = '►'; backlink.innerHTML += sourceAuthor; backlink.addEventListener('mouseenter', () => { linkEnter(backlink, sourceCommentID); }); backlink.addEventListener('mouseleave', () => { linkLeave(backlink, sourceCommentID); }); backlink.addEventListener('click', () => { // force pageload instead of trying to navigate to a nonexistent anchor on the current page if (document.getElementById('comment_' + sourceCommentID) === null) window.location.reload(); }); insertBacklink(backlink, targetCommentID); } // ignore quotes // this is terrible if (link.nextElementSibling && link.nextElementSibling.nextElementSibling && link.nextElementSibling.nextElementSibling.matches('blockquote')) return; link.addEventListener('mouseenter', () => { linkEnter(link, targetCommentID); }); link.addEventListener('mouseleave', () => { linkLeave(link, targetCommentID); }); }); // If other pages had replied to this comment if (backlinksCache[sourceCommentID] !== undefined) { backlinksCache[sourceCommentID].forEach((backlink) => { insertBacklink(backlink, sourceCommentID); }); } }); // Comment loading NodeCreationObserver.onCreation('#image_comments nav>a.js-next', function (btnNextPage) { if (document.getElementById('comment_loading_button') !== null) return; var btnLastPage = btnNextPage.nextElementSibling; var imageId = getQueryVariable('id', btnNextPage); var nextPage = parseInt(getQueryVariable('page', btnNextPage), 10); var lastPage = parseInt(getQueryVariable('page', btnLastPage), 10); var btn = insertButton('Load more comments'); btn.addEventListener('click', (e) => { loadComments(e, imageId, nextPage, lastPage); }); }); NodeCreationObserver.onCreation(textareaSelectors.join(','), function (textarea) { initToolbar(formattingSyntax, textarea); }); initCSS(formattingSyntax); })();