TF.TV Thread Title Hider

Hides thread titles you don't want to see immediately

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        TF.TV Thread Title Hider
// @namespace   sheybey
// @description Hides thread titles you don't want to see immediately
// @include     https://www.teamfortress.tv/*
// @version     1.3
// @grant       none
// ==/UserScript==

// I don't agree with some of jslint's whitespace rules
/*jslint browser, for, multivar, this, white*/
/*global NodeList*/
/*property
    add, addEventListener, appendChild, backgroundColor, border, call,
    classList, clear, color, contains, createElement, currentTarget, dataset,
    display, float, fontSize, fontStyle, forEach, getAttribute, getElementById,
    getItem, height, insertBefore, join, keys, length, lineHeight, marginTop,
    oldHref, oldTitle, overflow, padding, parentNode, parse, position,
    preventDefault, prototype, push, querySelector, querySelectorAll, remove,
    removeChild, removeEventListener, replace, setAttribute, setItem, slice,
    split, stopPropagation, stringify, style, test, textContent, toLowerCase,
    top, trim, value, width, zIndex
*/

(function () {
    "use strict";
    var hidden = localStorage.getItem("user-hidden"),
        def = [
            "esea",
            "etf2l",
            "ozf",
            "lan"
        ],
        // these elements are explained where they are first used
        navbar = document.querySelector("nav"),
        spoiler = document.createElement("a"),
        spoilerdiv = document.createElement("div"),
        spoilericon = document.createElement("i"),
        container = document.createElement("div"),
        add = document.createElement("span"),
        savetext = document.createElement("span"),
        firstrow = document.createElement("div"),
        shown = false,
        before, threads, articles, news;

    if (hidden === null) {
        hidden = def;
    } else {
        hidden = JSON.parse(hidden);
    }
    hidden = (function () {
        var o = {};
        hidden.forEach(function (keyword) {
            o[keyword] = new RegExp("\\b" +
                // this replace call escapes regex characters
                keyword.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") +
                "\\b", "i");
        });
        return o;
    }());

    // remove the title of a thread from a link, while preserving the effective target
    function sanitize(link, title) {
        link.classList.add("user-hidden");
        link.dataset.oldTitle = title; // <a title="..."> is not always set
        link.dataset.oldHref = link.getAttribute("href");
        // this slice and join removes the last component of the url, which is the slug, from href
        link.setAttribute("href", link.dataset.oldHref.split("/").slice(0, -1).join("/"));
        link.setAttribute("title", "Click to reveal");
    }
    // restore the information removed by sanitize()
    function restore(link) {
        link.classList.remove("user-hidden");
        link.setAttribute("title", link.dataset.oldTitle);
        link.setAttribute("href", link.dataset.oldHref);
    }

    // these functions find link and title elements given a click event and restore information as necessary
    function revealsidebar(event) {
        var link = event.currentTarget,
            span = link.querySelector(".module-item-title");

        event.preventDefault();
        event.stopPropagation();

        restore(link);
        link.removeEventListener("click", revealsidebar);

        span.textContent = link.dataset.oldTitle;
        span.style.fontStyle = "";
    }
    function revealthread(event) {
        var link = event.currentTarget;

        event.preventDefault();
        event.stopPropagation();

        restore(link);
        link.removeEventListener("click", revealthread);
        link.textContent = link.dataset.oldTitle;
        link.style.fontStyle = "";
    }
    function revealarticle(event) {
        var link = event.currentTarget,
            span = link.querySelector(".news-item-title");

        event.preventDefault();
        event.stopPropagation();

        restore(link);
        link.removeEventListener("click", revealarticle);

        span.textContent = link.dataset.oldTitle;
        span.style.fontStyle = "";
    }

    // basically Array.prototype.forEach, but safer than using it directly
    if (typeof NodeList.prototype.forEach !== "function") {
        NodeList.prototype.forEach = function (f) {
            var i;
            for (i = 0; i < this.length; i += 1) {
                f.call(this[i], this[i], i, this);
            }
        };
    }

    // look for links to sanitize
    // the sidebar exists on all pages
    document.getElementById("sidebar-left").querySelectorAll(".module").forEach(function (module) {
        module.querySelectorAll(".module-item").forEach(function (link) {
            var span = link.querySelector(".module-item-title"),
                title;
            if (span.classList.contains("mod-event")) {
                return; // event posts generally do not have spoilers
            }
            title = span.textContent.trim();
            // Object.keys is usually faster than querySelector[All], so it is on the inside of the loop
            Object.keys(hidden).forEach(function (keyword) {
                var newtitle = "[" + keyword + " thread]";
                if (link.classList.contains("user-hidden")) {
                    return;
                }
                if (hidden[keyword].test(title)) {
                    sanitize(link, title);
                    link.addEventListener("click", revealsidebar);

                    span.textContent = newtitle;
                    span.style.fontStyle = "italic";
                }
            });
        });
    });
    // this element only exists on /forum... and /threads
    threads = document.getElementById("thread-list");
    if (threads !== null) {
        threads.querySelectorAll(".thread-inner").forEach(function (thread) {
            // this element only exists on /threads
            var category = thread.querySelector(".description a"),
                link, title;
            if (category !== null && category.textContent.trim() === "Events") {
                return;
            }
            link = thread.querySelector("a.title");
            title = link.textContent.trim();
            Object.keys(hidden).forEach(function (keyword) {
                var newtitle = "[" + keyword + " thread]";
                if (link.classList.contains("user-hidden")) {
                    return;
                }
                if (hidden[keyword].test(title)) {
                    sanitize(link, title);
                    link.addEventListener("click", revealthread);
                    link.textContent = newtitle;
                    link.style.fontStyle = "italic";
                }
            });
        });
    }
    // this element only exists on the front page
    articles = document.getElementById("article-list");
    if (articles !== null) {
        articles.querySelectorAll("a").forEach(function (link) {
            var span = link.querySelector(".news-item-title"),
                title = span.textContent.trim();

            Object.keys(hidden).forEach(function (keyword) {
                var newtitle = "[" + keyword + " article]";
                if (link.classList.contains("user-hidden")) {
                    return;
                }
                if (hidden[keyword].test(title)) {
                    sanitize(link, title);
                    link.addEventListener("click", revealarticle);

                    span.textContent = newtitle;
                    span.style.fontStyle = "italic";
                }
            });
        });
    }
    // this element only exists on /news
    news = document.querySelector("#content > .list-table:not(#schedule-table)");
    if (news !== null) {
        news.querySelectorAll("a").forEach(function (link) {
            var title = link.textContent.trim();

            Object.keys(hidden).forEach(function (keyword) {
                var newtitle = "[" + keyword + " article]";
                if (link.classList.contains("user-hidden")) {
                    return;
                }
                if (hidden[keyword].test(title)) {
                    sanitize(link, title);
                    link.addEventListener("click", revealthread);
                    link.textContent = newtitle;
                    link.style.fontStyle = "italic";
                }
            });
        });
    }

    // this function creates an <input> and remove button for the config div
    function createrow(word) {
        var row = document.createElement("div"),
            input = document.createElement("input"),
            remove = document.createElement("span");

        if (word === undefined) {
            word = "";
        }

        // the <input>
        input.style.height = "30px";
        input.style.marginTop = "5px";
        input.style.fontSize = "11px";
        input.style.lineHeight = "30px";
        input.style.width = "100px";
        input.value = word;

        // remove button
        remove.style.float = "right";
        remove.classList.add("btn");
        remove.textContent = "Remove";
        remove.addEventListener("click", function () {
            row.parentNode.removeChild(row);
        });

        // containing row
        row.style.height = "40px";
        row.style.padding = "5px 0";
        row.style.clear = "both";
        row.appendChild(input);
        row.appendChild(remove);
        return row;
    }

    // the spoiler <a> in the navbar
    spoiler.classList.add("header-nav-item-wrapper");
    spoiler.classList.add("mod-last");
    spoiler.setAttribute("href", "#");
    spoiler.style.overflow = "visible";
    spoiler.addEventListener("click", function (event) {
        event.preventDefault(); // # causes "scroll jump" to top otherwise
        var newhidden = [];
        if (shown) {
            container.style.display = "none";
            container.querySelectorAll("input").forEach(function (input) {
                newhidden.push(input.value.toLowerCase());
            });

            localStorage.setItem("user-hidden", JSON.stringify(newhidden));

            spoilericon.classList.remove("fa-caret-up");
            spoilericon.classList.add("fa-caret-down");

            shown = false;
        } else {
            container.style.display = "";

            spoilericon.classList.remove("fa-caret-down");
            spoilericon.classList.add("fa-caret-up");

            shown = true;
        }
    });

    // each navbar button has a <div> child that contains its label
    spoilerdiv.classList.add("header-nav-item");
    spoilerdiv.classList.add("mod-solo");
    spoilerdiv.textContent = "Spoilers ";

    // font awesome icon that reflects whether the config <div> is shown or not
    spoilericon.classList.add("fa");
    spoilericon.classList.add("fa-caret-down");

    // this <div> is shown and hidden when the spoiler button is clicked
    container.style.backgroundColor = "#f5f5f5";
    container.style.position = "absolute";
    container.style.top = "40px";
    container.style.display = "none";
    container.style.zIndex = "99";
    container.style.width = "200px";
    container.style.padding = "5px";
    container.style.border = "1px solid #b4b4b4";
    // this is necessary to prevent clicks on the config <div> from propagating to the spoiler button
    container.addEventListener("click", function (event) {
        event.preventDefault();
        event.stopPropagation();
    });

    // this row contains the help text and add button
    firstrow.style.height = "40px";
    firstrow.style.padding = "5px 0";
    firstrow.style.clear = "both";

    // help text for config <div>
    savetext.style.fontSize = "11px";
    savetext.style.color = "#555555";
    savetext.style.lineHeight = "40px";
    savetext.textContent = "Close to save";

    // the add button
    add.classList.add("btn");
    add.textContent = "Add";
    add.style.float = "right";
    add.addEventListener("click", function () {
        container.appendChild(createrow());
    });

    firstrow.appendChild(savetext);
    firstrow.appendChild(add);

    container.appendChild(firstrow);
    Object.keys(hidden).forEach(function (keyword) {
        container.appendChild(createrow(keyword));
    });

    spoilerdiv.appendChild(spoilericon);

    spoiler.appendChild(spoilerdiv);
    spoiler.appendChild(container);

    // spoiler button is inserted before this element
    before = navbar.querySelector("#user-actions"); // logged in
    if (before === null) {
        before = navbar.querySelector("a:last-child"); // not logged in
    }
    navbar.insertBefore(spoiler, before);
}());