YouTube - チャット欄にワードNG機能を追加

YouTubeのチャット欄にワードNG機能を追加します

目前为 2019-09-03 提交的版本。查看 最新版本

// ==UserScript==
// @name         YouTube - チャット欄にワードNG機能を追加
// @namespace    https://twitter.com/4chouyou
// @version      0.0.2
// @description  YouTubeのチャット欄にワードNG機能を追加します
// @author       mufuuuu
// @match        https://www.youtube.com/live_chat*
// @grant        none
// ==/UserScript==
/* jshint esversion: 6 */

(function() {
    var YouTubeChatWordNG = {};
    (function (YouTubeChatWordNG) {
        class Main {
            load() {
                let style = document.createElement('style');
                style.type = 'text/css';
                style.innerHTML = '.NG #message, .NG #author-name, .NG #chat-badges { display: none; } .NG yt-live-chat-author-chip.yt-live-chat-text-message-renderer { margin-right: 0px !important; } .NG #author-photo { visibility: collapse; } .NG #content::after { content: "[NGコメント]"; opacity: .25; } #ngMenu { display: inline-block; position: relative; width: 40px; height: 40px; text-align: center; line-height: 40px; color: var(--yt-spec-icon-inactive); cursor: pointer; } #ngMenu:hover, #ngMenu[active="true"] { color: var(--yt-spec-icon-active-other); } #ngMenu::after { content: ""; display: block; position: absolute; width: 0px; height: 0px; left: 20px; top: 20px; border-radius: 50%; background-color: var(--yt-spec-icon-active-other); opacity: .25; transition-duration: .2s; } #ngMenu[active="true"]::after { width: 40px; height: 40px; left: 0px; top: 0px; } #ngPopupContainer { position: fixed; width: 220px; top: 44px; right: 14px; padding: 12px; background-color: var(--yt-spec-brand-background-solid); color: var(--paper-listbox-color, #212121); box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); border-radius: 4px; } #addNGWordContainer { display: flex; } #ngWordInput { flex: 1; } #listContainer { padding: 0; margin: 0; max-height: 300px; overflow: auto; } #listContainer[empty="false"] { margin-top: 12px; } .listItem { display: flex; font-size: 1.2em; line-height: 18px; } .listItem:nth-child(even) { background-color: var(--yt-spec-general-background-a); } .title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .removeButton { width: 16px; cursor: pointer } .removeButton:hover { text-decoration: underline; }';
                document.head.append(style);
                YouTubeChatWordNG.wordNG.load();
                YouTubeChatWordNG.popup.load();
            }
        }
        class WordNG {
            constructor() {
                this.ngWords = [];
            }
            load() {
                let item = JSON.parse(localStorage.getItem('ngWord'));
                if(item) {
                    this.ngWords = item;
                }else {
                    this.ngWords = [];
                }

                let chatItems = Array.from(document.querySelectorAll('#chat #items > .yt-live-chat-item-list-renderer'));
                this.applyNG(chatItems);

                let observer = new MutationObserver((mutations) => {
                    let chatItems;
                    if(mutations.length == 1) {
                        chatItems = Array.from(mutations[0].addedNodes);
                    }else {
                        chatItems = Array.from(document.querySelectorAll('#chat #items > .yt-live-chat-item-list-renderer'));
                    }
                    this.applyNG(chatItems);
                });
                let chat = document.querySelector('#chat #items');
                observer.observe(chat, {childList: true});
            }
            save() {
                localStorage.setItem('ngWord', JSON.stringify(this.ngWords));
            }
            addNGWord(ngWord) {
                this.ngWords.push(ngWord);
                this.save();
            }
            removeNGWord(ngWord) {
                if (this.ngWords.indexOf(ngWord) >= 0){
                    this.ngWords.splice(this.ngWords.indexOf(ngWord), 1);
                }
                this.save();
            }
            applyNG(chatItems) {
                chatItems.forEach(node => {
                    if(node.tagName == 'YT-LIVE-CHAT-TEXT-MESSAGE-RENDERER' || node.tagName == 'YT-LIVE-CHAT-PAID-MESSAGE-RENDERER') {
                        let content = node.querySelector('#content');
                        let message = node.querySelector('#message');
                        content.removeAttribute('ngword');
                        node.classList.remove('NG');
                        let text = message.innerHTML.replace(/<[^<]*alt=\"([^"]+)\"[^>]*>/g, '$1');
                        for(let i = 0; i < this.ngWords.length; i++) {
                            let ngWord = this.ngWords[i];
                            if(ngWord.substring(0, 1) == '/' && ngWord.substring(ngWord.length - 1) == '/') {
                                ngWord = ngWord.substring(1, ngWord.length - 1);
                            }else {
                                ngWord = ngWord.replace(/[-.*+^|[\]()?${}\\]/g, '\\$&');
                            }
                            let regex = new RegExp(ngWord);
                            if(regex.test(text)) {
                                /*console.log('match: ' + this.ngWords[i] + ', text: ' + text);*/
                                content.setAttribute('ngword', this.ngWords[i]);
                                node.classList.add('NG');
                                break;
                            }
                        }
                    }
                });
            }
        }
        class Popup {
            constructor() {
                this.isReverse = true;
            }
            load() {
                let menu = document.querySelector('#overflow');
                let ngMenu = document.createElement('a');
                ngMenu.id = 'ngMenu';
                ngMenu.innerText = 'NG';
                menu.parentNode.insertBefore(ngMenu, menu);
                ngMenu.addEventListener('click', () => {
                    this.toggle();
                });

                let ngPopupContainer = document.createElement('div');
                ngPopupContainer.id = 'ngPopupContainer';
                ngPopupContainer.className = 'hidden';
                let addNGWordContainer = document.createElement('div');
                addNGWordContainer.id = 'addNGWordContainer';
                let inputText = document.createElement('input');
                inputText.id = 'ngWordInput';
                inputText.type = 'text';
                inputText.setAttribute('autocomplete', 'off');
                addNGWordContainer.appendChild(inputText);
                let inputButton = document.createElement('input');
                inputButton.type = 'button';
                inputButton.value = 'add';
                addNGWordContainer.appendChild(inputButton);
                let removeNGWordContainer = document.createElement('div');
                removeNGWordContainer.id = 'removeNGWordContainer';
                let listContainer = document.createElement('ul');
                listContainer.id = 'listContainer';
                listContainer.setAttribute('empty', 'true');
                removeNGWordContainer.appendChild(listContainer);
                ngPopupContainer.appendChild(addNGWordContainer);
                ngPopupContainer.appendChild(removeNGWordContainer);
                document.body.appendChild(ngPopupContainer);
                YouTubeChatWordNG.wordNG.ngWords.forEach(ngWord => {
                    this.addListItem(ngWord);
                });
                inputButton.addEventListener('click', () => {
                    this.addNGWord();
                });
                inputText.addEventListener('keypress', (e) => {
                    if(e.keyCode == '13') {
                        this.addNGWord();
                    }
                });

                document.body.addEventListener('click', (e) => {
                    let ngPopupContainer = document.querySelector('#ngPopupContainer');
                    let ngMenu = document.querySelector('#ngMenu');
                    let target = e.target;
                    if(!ngPopupContainer.classList.contains('hidden') && target != ngMenu && target.className != 'removeButton') {
                        while (target && (target != document.body)) {
                            if (target == ngPopupContainer) return;
                            target = target.parentNode;
                        }
                        this.toggle();
                    }
                });
            }
            addNGWord() {
                let inputText = document.querySelector('#ngWordInput');
                let ngWord = inputText.value;
                if(ngWord !== '' && YouTubeChatWordNG.wordNG.ngWords.indexOf(ngWord) == -1) {
                    YouTubeChatWordNG.wordNG.addNGWord(ngWord);
                    this.addListItem(ngWord);
                    inputText.value = '';
                    let chatItems = Array.from(document.querySelectorAll('#chat #items > .yt-live-chat-item-list-renderer'));
                    YouTubeChatWordNG.wordNG.applyNG(chatItems);
                }
            }
            toggle() {
                let ngPopupContainer = document.querySelector('#ngPopupContainer');
                let ngMenu = document.querySelector('#ngMenu');
                if(ngPopupContainer.classList.contains('hidden')) {
                    ngPopupContainer.classList.remove('hidden');
                    let inputText = document.querySelector('#ngWordInput');
                    inputText.focus();
                    ngMenu.setAttribute('active', 'true');
                }else {
                    ngPopupContainer.classList.add('hidden');
                    ngMenu.removeAttribute('active');
                }
            }
            addListItem(ngWord) {
                let parent = document.querySelector('#listContainer');
                let listItem = document.createElement('li');
                listItem.setAttribute('ngword', ngWord);
                listItem.className = 'listItem';
                let title = document.createElement('span');
                title.className = 'title';
                title.textContent = ngWord;
                let removeButton = document.createElement('a');
                removeButton.setAttribute('ngword', ngWord);
                removeButton.className = 'removeButton';
                removeButton.textContent = '[x]';
                listItem.appendChild(title);
                listItem.appendChild(removeButton);
                if(this.isReverse) {
                    parent.insertBefore(listItem, parent.firstChild);
                }else {
                    parent.appendChild(listItem);
                }

                removeButton.addEventListener('click', (e) => {
                    YouTubeChatWordNG.wordNG.removeNGWord(ngWord);
                    this.removeListItems(e.target.getAttribute('ngword'));
                    let chatItems = Array.from(document.querySelectorAll('#chat #items > .yt-live-chat-item-list-renderer'));
                    YouTubeChatWordNG.wordNG.applyNG(chatItems);
                });
                parent.setAttribute('empty', 'false');
            }
            removeListItems(ngWord) {
                let listItems = Array.from(document.querySelectorAll('.listItem'));
                listItems.forEach(item => {
                    if(ngWord == item.getAttribute('ngword')) {
                        item.remove();
                    }
                });
                if(listItems.length == 1) {
                    let parent = document.querySelector('#listContainer');
                    parent.setAttribute('empty', 'true');
                }
            }
        }
        YouTubeChatWordNG.main = new Main();
        YouTubeChatWordNG.wordNG = new WordNG();
        YouTubeChatWordNG.popup = new Popup();
    })(YouTubeChatWordNG);

    window.addEventListener('load', () => {
        YouTubeChatWordNG.main.load();
    });
})();