Reusable library that initialized the boxicons css and serves functions to turn stats and menus into icons
لا ينبغي أن لا يتم تثبيت هذا السكريت مباشرة. هو مكتبة لسكبتات لتشمل مع التوجيه الفوقية // @require https://update.greasyfork.org/scripts/497064/1489249/AO3Boxicons.js
// ==UserScript==
// @exclude *
// @author Yours Truly
// @version 1.2.1
// ==UserLibrary==
// @name AO3Boxicons
// @description Reusable library that initialized the boxicons css and serves functions to turn stats and menus into icons
// @license MIT
// ==/UserScript==
// ==/UserLibrary==
/**
*
* @param {Object} settings
* @param {String} settings.boxiconsVersion Used version of https://boxicons.com/
* @param {Boolean} settings.iconifyStats Flag that indicates if the AO3 work stat names should be turned into icons
* @param {Object} settings.statsSettings Individual settings for stat icons
* that typically consist of { iconClass: string, solid: boolean, tooltip: string }
* @param {Object} settings.statsSettings.wordCountOptions
* @param {Object} settings.statsSettings.chaptersOptions
* @param {Object} settings.statsSettings.collectionsOptions
* @param {Object} settings.statsSettings.commentsOptions
* @param {Object} settings.statsSettings.kudosOptions
* @param {Object} settings.statsSettings.bookmarksOptions
* @param {Object} settings.statsSettings.hitsOptions
* @param {Object} settings.statsSettings.workSubsOptions
* @param {Object} settings.statsSettings.authorSubsOptions
* @param {Object} settings.statsSettings.commentThreadsOptions
* @param {Object} settings.statsSettings.challengesOptions
* @param {Object} settings.statsSettings.fandomsOptions
* @param {Object} settings.statsSettings.requestOptions
* @param {Object} settings.statsSettings.workCountOptions
* @param {Object} settings.statsSettings.seriesCompleteOptions
* @param {Object} settings.statsSettings.kudos2HitsOptions
* @param {Object} settings.statsSettings.timeToReadOptions
* @param {Object} settings.statsSettings.dateWorkPublishedOptions
* @param {Object} settings.statsSettings.dateWorkUpdateOptions
* @param {Object} settings.statsSettings.dateWorkCompleteOptions
* @param {Object} settings.iconifyUserNav Flag that indicates if the AO3 user navigation should be turned into icons
* @param {Object} settings.userNavSettings Individual settings for user nav icons
* that typically consist of { iconClass: string, solid: boolean, tooltip: string, addTooltip: boolean }
* @param {Object} settings.accountOptions
* @param {Object} settings.postNewOptions
* @param {Object} settings.logoutOptions
*
*/
function IconifyAO3(customSettings = {}) {
/**
* Merges the second object into the first
* If a value is in `a` but not in `b`, the value stays like it is.
* If a value is in `b` but not in `a`, it gets copied over.
* If a value is in both `a` and `b`, the value of `b` takes preference.
*
* @param {Object} a original settings
* @param {Object} b user settings overwrite
* @param {*} c used for temp storage, don't worry about it
*/
function mergeSettings(a, b, c) {
for (c in b) b.hasOwnProperty(c) && ((typeof a[c])[0] == "o" ? m(a[c], b[c]) : (a[c] = b[c]));
}
// set global settings and overwrite with incoming settings
const settings = {
boxiconsVersion: "2.1.4",
};
mergeSettings(settings, customSettings);
/**
* 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@${settings?.boxiconsVersion}/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 {Object} options
* @param {String} options.iconClass Name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
* @param {String} options.tooltip Adds an optional tooltip to the element.
* Only if `addTooltip = true`.
* @param {Boolean} options.addTooltip Indicates if a tooltip should be added to the element.
* `tooltip` needs to be present in `options`.
* @param {Boolean} 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(options = {}) {
const i = document.createElement("i");
i.classList.add("bx");
if (options?.addTooltip && options?.tooltip) i.setAttribute("title", options.tooltip);
if (/^bxs?-/i.test(options.iconClass)) {
// check if the icon class has the bx(s) prefix and simply set it, ignoring any settings for solid
i.classList.add(options.iconClass);
} else {
// else, add the fittings prefix
i.classList.add(options?.solid ? "bxs-" + options.iconClass : "bx-" + options.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 {Object} options
* @param {String} options.iconClass Name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
* @param {String} options.tooltip Adds a tooltip to the element
* @param {Boolean} options.solid Indicates if the icon should be of the "solid" variant.
* Will be ignored if iconClass has "bx(s)" prefix.
*/
function setIcon(element, options = {}) {
if (element.tagName !== "I") element.prepend(getNewIconElement(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 {Object} options
* @param {String} options.iconClass Name of the boxicons class to use. (The "bx(s)" prefix can be omitted)
* @param {String} options.tooltip Adds a tooltip to the element
* @param {Boolean} options.solid Indicates if the icon should be of the "solid" variant.
* Will be ignored if iconClass has "bx(s)" prefix.
*/
function findElementsAndSetIcon(querySelector, options = {}) {
const els = document.querySelectorAll(querySelector);
els.forEach((el) => (el.firstChild.nodeType === Node.ELEMENT_NODE ? setIcon(el.firstChild, options) : setIcon(el, options)));
}
/**
* Adds an CSS that will hide the stats titles and prepends an icon to all stats.
*/
function iconifyStats() {
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 ChallengesCollection = "li.collection dl.stats dd a[href$=collections]";
const FandomsCollection = "li.collection dl.stats dd a[href$=fandoms]";
const RequestsCollection = "li.collection dl.stats dd a[href$=requests]";
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 localSettings = {
wordCountOptions: { tooltip: "Word Count", iconClass: "pen", solid: true },
chaptersOptions: { tooltip: "Chapters", iconClass: "food-menu" },
collectionsOptions: { tooltip: "Collections", iconClass: "collection", solid: true },
commentsOptions: { tooltip: "Comments", iconClass: "chat", solid: true },
kudosOptions: { tooltip: "Kudos", iconClass: "heart", solid: true },
bookmarksOptions: { tooltip: "Bookmarks", iconClass: "bookmarks", solid: true },
hitsOptions: { tooltip: "Hits", iconClass: "show-alt" },
workSubsOptions: { tooltip: "Subscriptions", iconClass: "bell", solid: true },
authorSubsOptions: { tooltip: "User Subscriptions", iconClass: "bell-ring", solid: true },
commentThreadsOptions: { tooltip: "Comment Threads", iconClass: "conversation", solid: true },
challengesOptions: { tooltip: "Challenges/Subcollections", iconClass: "collection", solid: false },
fandomsOptions: { tooltip: "Fandoms", iconClass: "crown", solid: true },
requestsOptions: { tooltip: "Prompts", iconClass: "invader", solid: true },
workCountOptions: { tooltip: "Work Count", iconClass: "library" },
seriesCompleteOptions: { tooltip: "Series Complete", iconClass: "flag-checkered", solid: true },
kudos2HitsOptions: { tooltip: "Kudos to Hits", iconClass: "hot", solid: true },
timeToReadOptions: { tooltip: "Time to Read", iconClass: "hourglass", solid: true },
dateWorkPublishedOptions: { tooltip: "Published", iconClass: "calendar-plus" },
dateWorkUpdateOptions: { tooltip: "Updated", iconClass: "calendar-edit" },
dateWorkCompleteOptions: { tooltip: "Completed", iconClass: "calendar-check" },
};
// merge incoming settings into local settings (overwrite)
mergeSettings(localSettings, settings?.statsSettings);
// 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}`, localSettings.wordCountOptions);
findElementsAndSetIcon(ChaptersWork, localSettings.chaptersOptions);
findElementsAndSetIcon(CollectionsWork, localSettings.collectionsOptions);
findElementsAndSetIcon(CommentsWork, localSettings.commentsOptions);
findElementsAndSetIcon(`${KudosTotal}, ${KudosWork}`, localSettings.kudosOptions);
findElementsAndSetIcon(`${BookmarksTotal}, ${BookmarksWork}, ${BookmarksCollection}, ${BookmarksSeries}`, localSettings.bookmarksOptions);
findElementsAndSetIcon(`${HitsTotal}, ${HitsWork}`, localSettings.hitsOptions);
findElementsAndSetIcon(`${SubscribersTotal}, ${SubscribersWork}`, localSettings.workSubsOptions);
findElementsAndSetIcon(AuthorSubscribers, localSettings.authorSubsOptions);
findElementsAndSetIcon(CommentThreads, localSettings.commentThreadsOptions);
findElementsAndSetIcon(ChallengesCollection, localSettings.challengesOptions);
findElementsAndSetIcon(FandomsCollection, localSettings.fandomsOptions);
findElementsAndSetIcon(RequestsCollection, localSettings.requestsOptions);
findElementsAndSetIcon(`${WorksCollection}, ${WorksSeries}`, localSettings.workCountOptions);
findElementsAndSetIcon(SeriesComplete, localSettings.seriesCompleteOptions);
// AO3E elements
findElementsAndSetIcon(Kudos2HitsWork, localSettings.kudos2HitsOptions);
findElementsAndSetIcon(ReadingTimeWork, localSettings.timeToReadOptions);
// calendar icons at works page
findElementsAndSetIcon(DatePublishedWork, localSettings.dateWorkPublishedOptions);
const workStatus = document.querySelector(DateStatusTitle);
if (workStatus && workStatus.innerHTML.startsWith("Updated")) {
setIcon(document.querySelector(DateStatusWork), localSettings.dateWorkUpdateOptions);
} else if (workStatus && workStatus.innerHTML.startsWith("Completed")) {
setIcon(document.querySelector(DateStatusWork), localSettings.dateWorkCompleteOptions);
}
}
/**
* Replaces the "Hi, {user}!", "Post" and "Log out" text at the top of the page with icons.
*/
function iconifyUserNav() {
const localSettings = {
accountOptions: { tooltip: "User Area", addTooltip: true, iconClass: "user-circle", solid: true },
postNewOptions: { tooltip: "New Work", addTooltip: true, iconClass: "book-add", solid: true },
logoutOptions: { tooltip: "Logout", addTooltip: true, iconClass: "log-out" },
};
// merge incoming settings into local settings (overwrite)
mergeSettings(localSettings, settings?.userNavSettings);
const AccountUserNav = "#header a.dropdown-toggle[href*='/users/']";
const PostUserNav = "#header a.dropdown-toggle[href*='/works/new']";
const LogoutUserNav = "#header a[href*='/users/logout']";
// 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(localSettings.accountOptions));
document.querySelector(PostUserNav).replaceChildren(getNewIconElement(localSettings.postNewOptions));
document.querySelector(LogoutUserNav).replaceChildren(getNewIconElement(localSettings.logoutOptions));
}
initBoxicons();
if (settings?.iconifyStats) iconifyStats();
if (settings?.iconifyUserNav) iconifyUserNav();
}