ニコニコ大百科掲示板 あぼーん拡張

ニコニコ大百科掲示板に NGID ボタンを追加します

// ==UserScript==
// @name        ニコニコ大百科掲示板 あぼーん拡張
// @description ニコニコ大百科掲示板に NGID ボタンを追加します
// @namespace   https://gitlab.com/sigsign
// @version     0.2.3
// @author      Sigsign
// @license     MIT or Apache-2.0
// @include     https://dic.nicovideo.jp/a/*
// @include     https://dic.nicovideo.jp/b/a/*
// @include     https://dic.nicovideo.jp/t/a/*
// @include     https://dic.nicovideo.jp/t/b/a/*
// @include     /https:\/\/dic\.nicovideo\.jp(\/t)?(\/b)?\/[alviu]/
// @include     https://dic.nicovideo.jp/m/n/res/*
// @run-at      document-start
// @noframes
// @grant       none
// ==/UserScript==
(function () {
'use strict';

const getDesktopComments = (body) => {
    return Array.from(body.querySelectorAll('.st-bbs-contents > dl > dt.st-bbs_reshead'));
};
const getDesktopCommentInfo = (comment) => {
    const info = comment.querySelector('.st-bbs_resInfo');
    if (!info) {
        return null;
    }
    const text = info.lastChild;
    if (!text || text.nodeType !== text.TEXT_NODE || !text.textContent) {
        return null;
    }
    return text.textContent.replace(/\s+/g, ' ').trim();
};

const getMobileComments = (body) => {
    return Array.from(body.querySelectorAll('ul.sw-Article_List > li'));
};
const getMobileCommentInfo = (comment) => {
    const info = comment.querySelector('.at-List_Date');
    return info ? info.textContent : null;
};

const getComments = (body) => {
    const fn = body.querySelector('ul.sw-Article_List')
        ? getMobileComments
        : getDesktopComments;
    return fn(body);
};
const getCommentInfo = (comment) => {
    const fn = comment.tagName.toLowerCase() === 'li'
        ? getMobileCommentInfo
        : getDesktopCommentInfo;
    return fn(comment);
};
const getCommentID = (comment) => {
    const info = getCommentInfo(comment);
    if (!info) {
        return null;
    }
    const re = / ID: ([^\s]+)$/;
    const match = re.exec(info);
    return match ? match[1] : null;
};

const addButton = (elm, list) => {
    const exists = elm.querySelector(".us-NGButton");
    if (exists) {
        return exists;
    }
    const button = document.createElement('span');
    button.classList.add("us-NGButton");
    const id = getCommentID(elm);
    if (!id) {
        return button;
    }
    button.addEventListener('click', () => {
        list.contains(id).then((exists) => {
            exists ? list.remove(id) : list.add(id);
        });
    });
    button.dispose = list.on((value) => {
        value.includes(id)
            ? elm.classList.add("us-NGID")
            : elm.classList.remove("us-NGID", "us-NGView");
    });
    list.contains(id).then((exists) => {
        if (exists) {
            requestAnimationFrame(() => {
                elm.classList.add("us-NGID");
            });
        }
    });
    setupTemporaryDisplay(elm);
    requestAnimationFrame(() => {
        elm.tagName.toLowerCase() === 'li'
            ? elm.insertBefore(button, elm.querySelector('.at-List_Text'))
            : elm.appendChild(button);
    });
    return button;
};
const addDesktopReloadButton = (body) => {
    const link = body.querySelector('.st-bbs-contents .st-pg_contents :last-child');
    if (!link || !link.classList.contains('current')) {
        return null;
    }
    const pager = body.querySelector('.st-bbs-contents > div:nth-last-child(3)');
    if (!pager) {
        return null;
    }
    const parent = pager.parentElement;
    if (!parent) {
        return null;
    }
    const button = document.createElement('div');
    button.classList.add("us-Reload");
    parent.insertBefore(button, pager);
    return button;
};
const addMobileReloadButton = (body) => {
    if (body.querySelector('.sw-Paging .sw-Paging_Next-nextbtn a')) {
        return null;
    }
    const board = body.querySelector('ul.sw-Article_List');
    if (!board) {
        return null;
    }
    const button = document.createElement('li');
    button.classList.add("us-Reload");
    board.appendChild(button);
    return button;
};
const setupTemporaryDisplay = (elm) => {
    const selector = elm.tagName.toLowerCase() === 'li' ? '.at-List_Name' : '.st-bbs_name';
    const name = elm.querySelector(selector);
    if (!name) {
        return;
    }
    name.addEventListener('click', () => {
        if (!elm.classList.contains("us-NGID")) {
            return;
        }
        elm.classList.contains("us-NGView")
            ? elm.classList.remove("us-NGView")
            : elm.classList.add("us-NGView");
    });
};

const normalizePathname = (pathname) => {
    const re = /^(\/t)?(\/b)?(\/[alcviu]\/[^/]+)/;
    const matches = re.exec(pathname);
    if (!matches) {
        return '';
    }
    return matches[3];
};

const lazyLoadContents = (body, options) => {
    const loadContents = (content) => {
        if (content.dataset.src) {
            content.setAttribute('src', content.dataset.src);
        }
    };
    const observer = new IntersectionObserver((entries) => {
        for (const entry of entries) {
            if (entry.isIntersecting) {
                loadContents(entry.target);
            }
        }
    }, options);
    const contents = body.querySelectorAll('.lazy-contents');
    for (const content of contents) {
        if ('loading' in content) {
            content.loading = 'lazy';
            loadContents(content);
        }
        else {
            observer.observe(content);
        }
    }
};
const lazyLoadIframes = (body, options) => {
    const loadIframe = (iframe) => {
        if (iframe.contentWindow && iframe.dataset.src) {
            iframe.contentWindow.location.replace(iframe.dataset.src);
        }
    };
    const observer = new IntersectionObserver((entries) => {
        for (const entry of entries) {
            if (entry.isIntersecting) {
                loadIframe(entry.target);
            }
        }
    }, options);
    const iframes = body.querySelectorAll('.lazy-contents-iframe');
    for (const iframe of iframes) {
        observer.observe(iframe);
    }
};

const getBoard = (body) => {
    const selector = body.querySelector('.sw-Article_List')
        ? '.sw-Article_List'
        : '.st-bbs-contents';
    return body.querySelector(selector);
};
const getPagers = (body) => {
    if (!body.querySelector('.sw-Article_List')) {
        return [];
    }
    return Array.from(body.querySelectorAll('.sw-Paging'));
};
const getPagerLinks = (body) => {
    const selector = body.querySelector('.sw-Article_List')
        ? '.sw-Paging a'
        : '.st-bbs-contents .st-pg_contents a';
    return Array.from(body.querySelectorAll(selector));
};

const addReloadButton = (body, fn) => {
    const button = body.querySelector('.sw-Article_List')
        ? addMobileReloadButton(body)
        : addDesktopReloadButton(body);
    if (!button) {
        return;
    }
    button.textContent = '🔄更新';
    button.style.textAlign = 'center';
    button.addEventListener('click', (ev) => {
        ev.preventDefault();
        const board = getBoard(document.body);
        if (board) {
            board.style.filter = 'opacity(0.5)';
            board.style.transition = 'filter 0.1s ease-in';
        }
        const offset = window.scrollY;
        replaceBoard(location.href, true, 200, fn)
            .then(() => {
            document.addEventListener('pjax:complete', () => {
                window.scrollTo(0, offset);
            }, { once: true });
        })
            .catch((err) => {
            console.error(err);
            location.assign(location.href);
        });
    });
};
const ajaxBoard = (fn) => {
    if (location.origin !== 'https://dic.nicovideo.jp') {
        return;
    }
    if (!location.pathname.startsWith('/t/b/') &&
        !location.pathname.startsWith('/b/')) {
        return;
    }
    setupPagerLinks(document.body, fn);
    addReloadButton(document.body, fn);
    window.addEventListener('popstate', () => {
        const board = getBoard(document.body);
        if (board) {
            board.style.filter = 'opacity(0.5)';
            board.style.transition = 'filter 0.1s ease-in';
        }
        replaceBoard(location.href, false, 200, fn).catch((err) => {
            console.error(err);
            location.assign(location.href);
        });
    });
};
const fetchDocument = async (input, init) => {
    const response = await fetch(input, init);
    if (!response.ok) {
        throw new Error(`Request failed: ${response.status} - ${input}`);
    }
    const parser = new DOMParser();
    return parser.parseFromString(await response.text(), 'text/html');
};
const replaceBoard = async (url, fresh, minTime, fn) => {
    const startAt = Date.now();
    const option = fresh
        ? { cache: 'no-cache' }
        : { cache: 'default' };
    const doc = await fetchDocument(url, option);
    setupPagerLinks(doc.body, fn);
    addReloadButton(doc.body, fn);
    lazyLoadContents(doc.body);
    lazyLoadIframes(doc.body);
    if (fn) {
        fn(doc.body);
    }
    const oldBoard = getBoard(document.body);
    const newBoard = getBoard(doc.body);
    const oldPagers = getPagers(document.body);
    const newPagers = getPagers(doc.body);
    const time = Date.now() - startAt;
    setTimeout(() => {
        replaceElement(oldBoard, newBoard);
        oldPagers.forEach((pager, i) => {
            replaceElement(pager, newPagers[i]);
        });
        document.dispatchEvent(new Event('pjax:complete'));
    }, minTime > time ? minTime - time : 0);
};
const replaceElement = (oldElement, newElement) => {
    if (oldElement) {
        const parent = oldElement.parentElement;
        if (parent && newElement) {
            parent.replaceChild(newElement, oldElement);
        }
    }
};
const scrollTop = (body, pathname) => {
    if (pathname.startsWith('/t/')) {
        const pagers = getPagers(body);
        if (pagers[0]) {
            pagers[0].scrollIntoView(true);
        }
    }
    else {
        const board = getBoard(body);
        if (board) {
            board.scrollIntoView(true);
        }
    }
};
const setupPagerLinks = (body, fn) => {
    for (const link of getPagerLinks(body)) {
        link.addEventListener('click', (ev) => {
            ev.preventDefault();
            const board = getBoard(document.body);
            if (board) {
                board.style.filter = 'opacity(0.5)';
                board.style.transition = 'filter 0.1s ease-in 0.2s';
            }
            const url = ev.target.href;
            replaceBoard(url, false, 0, fn)
                .then(() => {
                document.addEventListener('pjax:complete', () => {
                    scrollTop(document.body, location.pathname);
                }, { once: true });
                history.pushState({}, '', url);
            })
                .catch((err) => {
                console.error(err);
                location.assign(url);
            });
        });
    }
};

const delayFirstContentfulPaint = () => {
    if (document.readyState !== 'loading') {
        return;
    }
    document.documentElement.style.visibility = 'hidden';
    document.addEventListener('DOMContentLoaded', () => {
        requestAnimationFrame(() => {
            document.documentElement.style.visibility = '';
        });
    });
};

const decodeToArray = (str) => {
    return str
        .split('\t')
        .filter((str) => {
        return str !== '';
    })
        .map((str) => {
        return str.replace(/\\t/g, '\t');
    });
};
const encodeFromArray = (arr) => {
    return arr
        .map((str) => {
        return str.replace(/\t/g, '\\t');
    })
        .join('\t');
};
class ListStore {
    constructor(prefix, uniqueKey) {
        this.prefix = prefix;
        this.uniqueKey = uniqueKey;
        this.key = `${prefix}${uniqueKey}`;
    }
    async add(value) {
        const array = await this.get();
        if (!array.includes(value)) {
            await this.set([...array, value]);
        }
    }
    async contains(value) {
        const array = await this.get();
        return array.includes(value);
    }
    createEvent(key, value) {
        return new CustomEvent('liststore:changed', {
            detail: {
                key: key,
                value: value,
            },
        });
    }
    emit(key, value) {
        if (typeof document !== 'undefined') {
            const event = this.createEvent(key, value);
            document.dispatchEvent(event);
        }
    }
    async get() {
        try {
            return decodeToArray(localStorage.getItem(this.key) || '');
        }
        catch (e) {
            throw new Error('localStorage.getItem() is failed');
        }
    }
    on(listener) {
        const storageListener = (ev) => {
            if (ev.key && ev.key === this.key) {
                listener(decodeToArray(ev.newValue || ''));
            }
        };
        const changedListener = (ev) => {
            const { key, value } = ev.detail;
            if (key === this.key) {
                listener(value);
            }
        };
        const disposer = () => {
            window.removeEventListener('storage', storageListener);
            document.removeEventListener('liststore:changed', changedListener);
        };
        window.addEventListener('storage', storageListener, false);
        document.addEventListener('liststore:changed', changedListener, false);
        return disposer;
    }
    async remove(value) {
        const array = await this.get();
        await this.set(array.filter((str) => {
            return str !== value;
        }));
    }
    async set(array) {
        try {
            if (array.length === 0) {
                localStorage.removeItem(this.key);
            }
            else {
                localStorage.setItem(this.key, encodeFromArray(array));
            }
        }
        catch (e) {
            throw new Error('localStorage.setItem() is failed');
        }
        this.emit(this.key, array);
    }
}

var css_248z = ":-webkit-any(.st-bbs-contents,.sw-Article_List) .us-NGButton:before{content:\"[NG]\"}:is(.st-bbs-contents,.sw-Article_List) .us-NGButton:before{content:\"[NG]\"}:-webkit-any(.st-bbs-contents,.sw-Article_List) .us-NGID .us-NGButton:before{content:\"[解除]\"}:is(.st-bbs-contents,.sw-Article_List) .us-NGID .us-NGButton:before{content:\"[解除]\"}:-webkit-any(.st-bbs-contents,.sw-Article_List) .us-NGButton:hover{text-decoration:underline}:is(.st-bbs-contents,.sw-Article_List) .us-NGButton:hover{text-decoration:underline}.st-bbs-contents .us-NGButton{color:#9b9b9b;font-size:12px;margin-left:8px}.sw-Article_List .at-List_Date{float:left;padding-right:4px}.sw-Article_List .us-NGButton{color:#999;font-size:10px;vertical-align:text-top}.sw-Article_List .us-NGButton:after{clear:both;content:\"\";display:block}.sw-Article_List .at-List_Text{width:100%}.st-bbs-contents .us-NGID:not(.us-NGView) .st-bbs_name{height:14px;visibility:hidden;width:4em}.st-bbs-contents .us-NGID:not(.us-NGView) .st-bbs_name:before{content:\"あぼーん\";visibility:visible}.st-bbs-contents .us-NGID:not(.us-NGView) .st-bbs_resInfo .trip{display:none}.sw-Article_List .us-NGID:not(.us-NGView) .at-List_Name{height:18px;visibility:hidden}.sw-Article_List .us-NGID:not(.us-NGView) .at-List_Name .at-List_Num{visibility:visible}.sw-Article_List .us-NGID:not(.us-NGView) .at-List_Name .at-List_Num:after{color:#222;content:\"あぼーん\";font-size:12px;font-weight:400;margin-left:4px;visibility:visible}.st-bbs-contents .us-NGID:not(.us-NGView)+.st-bbs_resbody{height:16px;visibility:hidden}.st-bbs-contents .us-NGID:not(.us-NGView)+.st-bbs_resbody:before{border-bottom:1px solid #e6e6e6;content:\"あぼーん\";display:block;padding-bottom:16px;visibility:visible}.sw-Article_List .us-NGID:not(.us-NGView) .at-List_Text{height:18px;visibility:hidden}.sw-Article_List .us-NGID:not(.us-NGView) .at-List_Text:before{content:\"あぼーん\";display:block;visibility:visible}.sw-Article_List .us-NGID:not(.us-NGView) :-webkit-any(.at-List_Illust,.at-List_Piko,.at-List_Piko-title,.at-Piko_Btn){display:none}.sw-Article_List .us-NGID:not(.us-NGView) :is(.at-List_Illust,.at-List_Piko,.at-List_Piko-title,.at-Piko_Btn){display:none}";
const asyncInjectCSS = (css) => {
    const style = document.createElement('style');
    style.setAttribute('type', 'text/css');
    style.appendChild(document.createTextNode(css));
    if (document.readyState !== 'loading') {
        document.head.appendChild(style);
    } else {
        document.addEventListener('DOMContentLoaded', () => {
            document.head.appendChild(style);
        });
    }
};
asyncInjectCSS(css_248z);

const exactRun = (fn) => {
    if (document.readyState !== 'loading') {
        fn();
    }
    document.addEventListener('DOMContentLoaded', fn);
};
const getLatestPosts = () => {
    const selector = '.st-pg_link-returnArticle a';
    return Array.from(document.querySelectorAll('.st-bbs-contents'))
        .filter((elm) => {
        return elm.querySelector(selector);
    })
        .map((elm) => {
        const link = elm.querySelector(selector);
        return { url: link.href, container: elm };
    });
};
const init = (url, elm) => {
    const u = new URL(url);
    if (u.origin !== 'https://dic.nicovideo.jp') {
        return;
    }
    const key = normalizePathname(u.pathname);
    const ngList = new ListStore('__BC__', key);
    getComments(elm).forEach((comment) => {
        addButton(comment, ngList);
    });
    return ngList;
};
if (location.pathname.startsWith('/b/') ||
    location.pathname.startsWith('/t/b/') ||
    location.pathname.startsWith('/m/n/res/')) {
    delayFirstContentfulPaint();
}
if (!location.pathname.startsWith('/m/n/res/')) {
    exactRun(() => {
        const list = init(location.href, document.documentElement);
        if (list) {
            ajaxBoard((elm) => {
                const buttons = document.querySelectorAll('.us-NGButton');
                buttons.forEach((button) => {
                    if (button.dispose) {
                        button.dispose();
                    }
                });
                getComments(elm).forEach((comment) => {
                    addButton(comment, list);
                });
            });
        }
    });
}
else {
    exactRun(() => {
        getLatestPosts().forEach((thread) => {
            init(thread.url, thread.container);
        });
    });
}
if (typeof completion === 'function') {
    completion();
}

}());