pixiv ツリー型コメント

Deletes stamps on comment sections. In addition, changes the comment section’s default linear mode to threaded mode on comment sections in group pages.

// ==UserScript==
// @name        pixiv ツリー型コメント
// @name:ja     pixiv スタンプを削除
// @name:en     pixiv Stamp Eraser
// @description Deletes stamps on comment sections. In addition, changes the comment section’s default linear mode to threaded mode on comment sections in group pages.
// @description:ja コメント欄のスタンプを削除します。また、グループ機能において、返信コメントを返信先の下に移動します。
// @namespace   https://greasyfork.org/users/137
// @version     5.1.0
// @match       https://www.pixiv.net/*
// @require     https://greasyfork.org/scripts/17896/code/start-script.js?version=112958
// @license     MPL-2.0
// @contributionURL https://www.amazon.co.jp/registry/wishlist/E7PJ5C3K7AM2
// @compatible  Edge
// @compatible  Firefox 推奨 (Recommended)
// @compatible  Opera
// @compatible  Chrome
// @grant       dummy
// @noframes
// @run-at      document-start
// @icon        https://www.pixiv.net/favicon.ico
// @author      100の人
// @homepageURL https://greasyfork.org/scripts/5291
// ==/UserScript==

'use strict';

class StampEraser
{
	constructor()
	{
		const root = document.getElementById('root');
		if (!root) {
			return;
		}

		new MutationObserver(mutations => {
			for (const mutation of mutations) {
				const commentListParentSelector = 'main h2 ~ div:nth-of-type(2)';
				if (!mutation.target.matches(`${commentListParentSelector},
					${commentListParentSelector} > ul, ${commentListParentSelector} > ul *`)) {
					continue;
				}

				for (const element of mutation.addedNodes) {
					switch (element.localName) {
						case 'ul':
							this.hideStampComments(element);
							break;
						case 'li':
							this.hideIfStamp(element);
							break;
					}
				}
			}
		}).observe(root, {childList: true, subtree: true});
	}

	/**
	 * コメントリストからスタンプを削除します。
	 * @access private
	 * @param {HTMLLIElement} comment
	 */
	hideStampComments(commentList)
	{
		for (const comment of Array.from(commentList.children)) {
			this.hideIfStamp(comment);
		}
	}

	/**
	 * スタンプコメント、または絵文字単体のコメントであれば削除します。
	 * @access private
	 * @param {HTMLLIElement} comment
	 */
	hideIfStamp(comment)
	{
		let ul, emoji;
		if (!comment.hidden
			&& (!(ul = comment.getElementsByTagName('ul')[0]) || ul.hidden)
			&& (comment.querySelector('div[style*="/stamp/"]')
				|| (emoji = comment.querySelector('img[src*="/emoji/"]'))
					&& !emoji.previousSibling && !emoji.nextSibling)) {
			// 返信が存在しない、かつスタンプコメントか絵文字単体のコメントであれば
			if (comment.matches('ul ul li')) {
				// 返信コメントであれば
				const list = comment.parentElement;
				const parentComment = list.closest('li');
				if (comment.parentElement.querySelectorAll('li:not([hidden])').length === 1) {
					// 他に返信が存在しなければ
					list.hidden = true;
					this.hideIfStamp(parentComment);
				} else {
					comment.hidden = true;
				}
				return;
			}

			comment.hidden = true;
		}
	}
}

function main()
{
	document.head.insertAdjacentHTML('beforeend', `<style>
		/*====================================
			グループのコメント欄
		*/
		/*------------------------------------
			区切り線
		*/
		#page-group #timeline li.post div.comment div.post,
		#page-group #timeline li.post div.comment div.post ~ div.post {
			border-top: dashed 1px #DEE0E8;
			border-bottom: none;
		}
		#page-group #timeline li.post div.comment div.post::before {
			content: "";
			position: absolute;
			top: 0;
			left: 0;
			right: 0;
			border-top: dashed 1px #FFFFFF;
		}
		#page-group #timeline li.post div.comment {
			border-bottom: dashed 1px #DEE0E8;
		}
		/*------------------------------------
			最初のコメント
		*/
		#page-group #timeline li.post div.comment > div.post:first-of-type,
		#page-group #timeline li.post div.comment > .tree:first-of-type > div.post:first-of-type,
		#page-group #timeline li.post div.comment > div.post:first-child::before,
		#page-group #timeline li.post div.comment > .tree:first-child > div.post:first-of-type::before {
			/* 一番上のコメント */
			/* 「以前のコメントを見る」ボタンがある時は、白色の破線は消さない */
			border-top: none;
		}
		/*------------------------------------
			返信コメント
		*/
		#page-group #timeline li.post div.comment .tree > :nth-of-type(n+2) {
			margin-left: 2em;
		}
		#page-group #timeline li.post div.comment .tree .postbody {
			width: initial;
		}
	</style>`);

	/**
	 * コメントリスト要素。
	 * @type {HTMLDivElement}
	 */
	const commentList = document.getElementById('timeline');

	/**
	 * {@link MutationObserver#observe}の第2引数に指定するオブジェクト。
	 * @type {MutationObserverInit}
	 */
	const observerOptions = {
		childList: true,
		subtree: true,
	};

	moveAllReplyComments();

	// コメントの増減を監視する
	new MutationObserver(function (mutations, observer) {
		const firstMutationRecord = mutations[0];
		const firstAddedNode = firstMutationRecord.addedNodes[0];
		if (firstAddedNode) {
			// コメントが増えていれば
			// 監視を一旦停止して返信コメントを移動する
			observer.disconnect();
			moveAllReplyComments();
			observer.observe(commentList, observerOptions);
		}
	}).observe(commentList, observerOptions);

	/**
	 * すべての返信コメントを返信先コメントの下に移動します。
	 */
	function moveAllReplyComments()
	{
		const repliedUserNames = document.querySelectorAll('.body > p > a');
		for (let i = repliedUserNames.length - 1; i >= 0; i--) {
			/**
			 * 返信先コメント投稿者名。
			 * @type {HTMLSpanElement}
			 */
			const repliedUserName = repliedUserNames[i];

			/**
			 * 返信先コメント。
			 * @type {?HTMLDivElement}
			 */
			const repliedComment = document.getElementById(repliedUserName.hash.replace('#', ''));

			if (repliedComment) {
				// 返信先コメントが存在すれば
				moveReplyComment(repliedComment, repliedUserName.closest('.post'));
				// 返信先コメント投稿者名を削除
				repliedUserName.parentElement.remove();
			}
		}
	}

	/**
	 * 返信コメントを返信先コメントの下に移動します。
	 * @param {HTMLDivElement} repliedComment - 返信先コメント。
	 * @param {HTMLDivElement} replyComment - 返信コメント。
	 */
	function moveReplyComment(repliedComment, replyComment)
	{
		if (!replyComment.previousElementSibling) {
			const parent = replyComment.parentNode;
			if (parent && parent.classList.contains('tree')) {
				// 返信コメントにラッパー要素が存在すれば
				replyComment = parent;
			}
		}

		/**
		 * 返信先コメントと返信コメントを格納するラッパー要素。
		 * @type {HTMLDivElement}
		 */
		let tree = null;
		if (!repliedComment.previousElementSibling) {
			const parent = repliedComment.parentNode;
			if (parent.classList.contains('tree')) {
				// ラッパー要素がすでに存在すれば
				tree = parent;
			}
		}

		if (!tree) {
			// ラッパー要素が存在しなければ
			// ラッパー要素を作成
			tree = document.createElement('div');
			tree.classList.add('tree');
			// 返信先コメントをラッパー要素に置換
			repliedComment.replaceWith(tree);
			// 返信先コメントをラッパーに追加
			tree.append(repliedComment);
		}

		// 返信コメント移動
		tree.append(replyComment);
	}
}

if (location.pathname.startsWith('/group/')) {
	startScript(
		main,
		parent => parent.id === 'wrapper',
		target => target.id === 'template-drawr-paint-ui',
		() => document.getElementById('template-drawr-paint-ui')
	);
} else {
	document.addEventListener('DOMContentLoaded', function () {
		new StampEraser();
	}, { passive: true, once: true });
}