// ==UserScript==
// @name Replace stat names with icons
// @match https://archiveofourown.org/*
// @grant none
// @author genusslicht
// @description Replaces stat titles and user navigation with icons from https://boxicons.com
// @license MIT
// @namespace ao3-boxicons
// @version 1.0.4
// @icon https://archiveofourown.org/favicon.ico
// @supportURL https://gist.github.com/genusslicht/2ba4be62a30f936e7cc9d8f2c33409f5
// ==/UserScript==
// AO3 css selectors
const WordsTotal = "dl.statistics dd.words";
const WordsWork = "dl.stats dd.words";
const WordsSeries = ".series.meta.group dl.stats>dd:nth-of-type(1)";
const ChaptersWork = "dl.stats dd.chapters";
const CollectionsWork = "dl.stats dd.collections";
const CommentsWork = "dl.stats dd.comments";
const KudosTotal = "dl.statistics dd.kudos";
const KudosWork = "dl.stats dd.kudos";
const BookmarksTotal = "dl.statistics dd.bookmarks";
const BookmarksWork = "dl.stats dd.bookmarks";
const BookmarksSeries = ".series.meta.group dl.stats>dd:nth-of-type(4)";
const BookmarksCollection = "li.collection dl.stats dd a[href$=bookmarks]";
const HitsTotal = "dl.statistics dd.hits";
const HitsWork = "dl.stats dd.hits";
const SubscribersTotal = "dl.statistics dd[class=subscriptions]";
const SubscribersWork = "dl.stats dd.subscriptions";
const FandomsCollection = "li.collection dl.stats dd a[href$=fandoms]";
const AuthorSubscribers = "dl.statistics dd.user.subscriptions";
const CommentThreads = "dl.statistics dd.comment.thread";
const WorksCollection = "li.collection dl.stats dd a[href$=works]";
const WorksSeries = ".series.meta.group dl.stats>dd:nth-of-type(2)";
const SeriesComplete = ".series.meta.group dl.stats>dd:nth-of-type(3)";
const Kudos2HitsWork = "dl.stats dd.kudos-hits-ratio";
const ReadingTimeWork = "dl.stats dd.reading-time";
const DatePublishedWork = "dl.work dl.stats dd.published";
const DateStatusTitle = "dl.work dl.stats dt.status";
const DateStatusWork = "dl.work dl.stats dd.status";
const AccountUserNav = "#header a.dropdown-toggle[href*='/users/']";
const PostUserNav = "#header a.dropdown-toggle[href*='/works/new']";
const LogoutUserNav = "#header a[href*='/users/logout']";
/**
* Initialises boxicons.com css and adds a small css to add some space between icon and stats count.
*/
function initBoxicons() {
// load boxicon style
const boxicons = document.createElement("link");
boxicons.setAttribute("href", "https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css");
boxicons.setAttribute("rel", "stylesheet");
document.head.appendChild(boxicons);
// css that adds margin for icons
const boxiconsCSS = document.createElement("style");
boxiconsCSS.setAttribute("type", "text/css");
boxiconsCSS.innerHTML = `
i.bx {
margin-right: .3em;
}`;
document.head.appendChild(boxiconsCSS);
}
/**
* Creates a new element with the icon class added to the classList.
*
* @param {String} iconClass Name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
* @param {Object} options
* @param {Object} options.tooltip adds a tooltip to the element
* @param {Object} options.solid Indicates if the icon should be of the "solid" variant.
* Will be ignored if iconClass has "bx(s)" prefix.
* @returns <i> Element with the neccessary classes for a boxicons icon.
*/
function getNewIconElement(iconClass, options = {}) {
const i = document.createElement("i");
i.classList.add("bx");
if (options?.addTooltip && options?.tooltip) i.setAttribute("title", options.tooltip);
if (/^bxs?-/i.test(iconClass)) {
i.classList.add(iconClass);
} else {
i.classList.add(options?.solid ? "bxs-" + iconClass : "bx-" + iconClass);
}
return i;
}
/**
* Prepends the given boxicons class to the given element.
* Note: If the element is an <i> tag, nothing will happen, as we assume that the <i> is already an icon.
*
* @param {HTMLElement} element parent element that the icon class should be prepended to.
* @param {String} iconClass name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
* @param {Object} options
* @param {Object} options.tooltip adds a tooltip to the element
* @param {Object} options.solid Indicates if the icon should be of the "solid" variant.
* Will be ignored if iconClass has "bx(s)" prefix.
*/
function setIcon(element, iconClass, options = {}) {
if (element.tagName !== "I") element.prepend(getNewIconElement(iconClass, options));
if (options?.tooltip) element.setAttribute("title", options.tooltip);
}
/**
* Iterates through all elements that apply to the given querySelector and adds an element with the given icon class to it.
*
* @param {String} querySelector CSS selector for the elements to find and iconify.
* @param {String} iconClass name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
* @param {Object} options
* @param {Object} options.tooltip adds a tooltip to the element
* @param {Object} options.solid Indicates if the icon should be of the "solid" variant.
* Will be ignored if iconClass has "bx(s)" prefix.
*/
function findElementsAndSetIcon(querySelector, iconClass, options = {}) {
const els = document.querySelectorAll(querySelector);
els.forEach((el) => (el.firstChild.nodeType === Node.ELEMENT_NODE ? setIcon(el.firstChild, iconClass, options) : setIcon(el, iconClass, options)));
}
/**
* Adds an CSS that will hide the stats titles and prepends an icon to all stats.
*/
function iconifyStats() {
// css to hide stats titles
const statsCSS = document.createElement("style");
statsCSS.setAttribute("type", "text/css");
statsCSS.innerHTML = `
dl.stats dt {
display: none !important;
}`;
document.head.appendChild(statsCSS);
findElementsAndSetIcon(`${WordsTotal}, ${WordsWork}, ${WordsSeries}`, "pen", { tooltip: "Word Count", solid: true });
findElementsAndSetIcon(ChaptersWork, "food-menu", { tooltip: "Chapters" });
findElementsAndSetIcon(CollectionsWork, "collection", { tooltip: "Collections", solid: true });
findElementsAndSetIcon(CommentsWork, "chat", { tooltip: "Comments", solid: true });
findElementsAndSetIcon(`${KudosTotal}, ${KudosWork}`, "heart", { tooltip: "Kudos", solid: true });
findElementsAndSetIcon(`${BookmarksTotal}, ${BookmarksWork}, ${BookmarksCollection}, ${BookmarksSeries}`, "bookmarks", { tooltip: "Bookmarks", solid: true });
findElementsAndSetIcon(`${HitsTotal}, ${HitsWork}`, "show-alt", { tooltip: "Hits" });
findElementsAndSetIcon(`${SubscribersTotal}, ${SubscribersWork}`, "bell", { tooltip: "Subscriptions", solid: true });
findElementsAndSetIcon(AuthorSubscribers, "bell-ring", { tooltip: "User Subscriptions", solid: true });
findElementsAndSetIcon(CommentThreads, "conversation", { tooltip: "Comment Threads", solid: true });
findElementsAndSetIcon(FandomsCollection, "crown", { tooltip: "Fandoms", solid: true });
findElementsAndSetIcon(`${WorksCollection}, ${WorksSeries}`, "library", { tooltip: "Work Count" });
findElementsAndSetIcon(SeriesComplete, "flag-checkered", { tooltip: "Series Complete", solid: true });
// AO3E elements
findElementsAndSetIcon(Kudos2HitsWork, "hot", { tooltip: "Kudos to Hits", solid: true });
findElementsAndSetIcon(ReadingTimeWork, "hourglass", { tooltip: "Time to Read", solid: true });
// calendar icons at works page
findElementsAndSetIcon(DatePublishedWork, "calendar-plus", { tooltip: "Published" });
const workStatus = document.querySelector(DateStatusTitle);
if (workStatus && workStatus.innerHTML.startsWith("Updated")) {
setIcon(document.querySelector(DateStatusWork), "calendar-edit", { tooltip: "Updated" });
} else if (workStatus && workStatus.innerHTML.startsWith("Completed")) {
setIcon(document.querySelector(DateStatusWork), "calendar-check", { tooltip: "Completed" });
}
}
/**
* Replaces the "Hi, {user}!", "Post" and "Log out" text at the top of the page with icons.
*/
function iconifyUserNav() {
// add css for user navigation icons
const userNavCss = document.createElement("style");
userNavCss.setAttribute("type", "text/css");
userNavCss.innerHTML = `
${LogoutUserNav},
${AccountUserNav},
${PostUserNav} {
/* font size needs to be higher to make icons the right size */
font-size: 1.25rem;
/* left and right padding for a slightly bigger hover hitbox */
padding: 0 .3rem;
}
${LogoutUserNav} i.bx {
/* overwrite the right margin for logout icon */
margin-right: 0;
/* add left margin instead to add more space to user actions */
margin-left: .3em;
}`;
document.head.appendChild(userNavCss);
// replace text with icons
document.querySelector(AccountUserNav).replaceChildren(getNewIconElement("user-circle", { tooltip: "User Area", addTooltip: true, solid: true }));
document.querySelector(PostUserNav).replaceChildren(getNewIconElement("book-add", { tooltip: "New Work", addTooltip: true, solid: true }));
document.querySelector(LogoutUserNav).replaceChildren(getNewIconElement("log-out", { tooltip: "Logout", addTooltip: true }));
}
(function () {
initBoxicons();
iconifyStats();
iconifyUserNav();
})();