// ==UserScript== // @name Kbin Usability Pack // @namespace https://perry.dev // @license MIT // @version 0.2.0 // @description A collection of usability enhancements for Kbin // @author Daniel Pervan // @match https://kbin.social/* // @icon https://www.google.com/s2/favicons?sz=64&domain=kbin.social // @run-at document-end // @downloadURL none // ==/UserScript== (function () { 'use strict'; (() => { // src/Classes/User/User.js var User = class { username; avatar; constructor(username, avatar = null) { this.username = username; this.avatar = avatar; } }; var User_default = User; // style-helper:index.js function inject_style(text) { if (typeof document !== "undefined") { var style = document.createElement("style"); var node = document.createTextNode(text); style.appendChild(node); document.head.appendChild(style); } } // src/Classes/Article/Article.scss inject_style('body:not(.kup-show-article-preview) .show-article-preview{display:none!important}#content article.entry aside.meta.entry__meta{line-height:3.5em}#content article.entry aside.meta.entry__meta img{margin-right:.25em;margin-left:.25em}#content article.entry .magazine-inline{white-space:nowrap}#content article.entry.no-image{grid-template-areas:"vote title" "vote shortDesc" "vote meta" "vote footer" "body body" "preview preview"}#content article.entry:not(.no-image){grid-template-areas:"vote image title" "vote image shortDesc" "vote image meta" "vote image footer" "body body body" "preview preview preview"!important;grid-template-columns:min-content min-content auto}#content article.entry.selected{border:var(--kbin-meta-border)}#content article.entry>figure{margin:0 1rem 0 0;align-self:center}#content article.entry>figure img{max-width:180px;max-height:100%;width:auto;height:auto;object-fit:contain}#content article.entry aside.vote{place-content:center}#content article.entry button.show-preview,#content article.entry button.preview-button{border:1px solid var(--kbin-section-text-color);padding:.5em;background:var(--kbin-bg)}main#main.view-compact article.entry{grid-template-areas:"vote image title" "vote image meta" "vote image footer" "body body body" "preview preview preview"!important}main#main.view-compact article.entry.no-image{grid-template-areas:"vote title" "vote meta" "vote footer" "body body" "preview preview"!important}main#main.view-compact article.entry figure{margin-right:1rem}main#main.view-compact article.entry figure img{max-height:100px;max-width:120px}@media (max-width: 689.98px){main#main.view-compact article.entry{grid-template-areas:"image vote" "title vote" "meta vote" "footer vote" "body body" "preview preview"!important;grid-template-columns:auto min-content}main#main.view-compact article.entry figure img{object-fit:contain}main#main.view-compact article.entry.no-image{grid-template-areas:"title vote" "meta vote" "footer vote" "body body" "preview preview"!important;grid-template-columns:auto min-content}}@media (max-width: 991.98px){#content article.entry:not(.no-image){grid-template-areas:"vote image image" "vote title title" "vote shortDesc shortDesc" "vote meta meta" "vote footer footer" "body body body" "preview preview preview"!important}}@media (max-width: 689.98px){#content article.entry:not(.no-image){grid-template-areas:"vote image" "title title" "shortDesc shortDesc" "meta meta" "footer footer" "body body" "preview preview"!important;grid-template-columns:min-content auto}#content article.entry.no-image{grid-template-areas:"vote title" "vote shortDesc" "vote meta" "vote footer" "body body" "preview preview"!important;grid-template-columns:min-content auto}#content article.entry>figure{margin:0}#content article.entry aside.vote{place-self:center}}.preview-outer{grid-area:preview}.preview-outer .preview-inner{position:relative;display:none;border-top:var(--kbin-meta-border);padding:1em}.preview-outer .preview-inner.show{display:block}.preview-outer .preview-inner .loading{position:relative;height:3em;z-index:1;display:flex;justify-content:center;align-items:center;animation:showPreviewLoading .25s ease-in-out}.preview-outer .preview-inner .article-preview-content{position:relative}.preview-outer .preview-inner .article-preview-content.loaded{animation:articlePreviewFadeIn .5s ease-in-out}.preview-outer .preview-inner .media-preview-content{position:relative}.preview-outer .preview-inner .media-preview-content img{max-width:100%;max-height:100%;min-width:100px;object-fit:contain}.preview-outer .preview-inner .media-preview-content img.animateMinResize{animation:animateMinResize .25s ease-out;transform-origin:left top}@keyframes animateMinResize{0%{opacity:1;transform:scale(1)}33%{opacity:.8;transform:scale(.95)}66%{opacity:1;transform:scale(1.01)}to{opacity:1;transform:scale(1)}}.preview-outer .preview-inner .media-preview-content.oembed-embed{position:relative;overflow:hidden;max-width:100%;height:100vh;margin:0 auto}.preview-outer .preview-inner .media-preview-content.oembed-embed iframe{position:absolute;top:0;left:0;overflow:hidden;width:100%;height:100%;border:none}.preview-outer .preview-inner .media-preview-content.youtube-embed{position:relative;overflow:hidden;max-width:100%;max-height:100vh;margin:0 auto;aspect-ratio:16/9}.preview-outer .preview-inner .media-preview-content.youtube-embed iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:none}.preview-outer .preview-inner .media-preview-content.loaded{animation:articlePreviewFadeIn .5s ease-in-out}@keyframes showPreviewLoading{0%{opacity:0;transform:scale(.8)}50%{opacity:1;transform:scale(1.15)}80%{transform:scale(.9)}to{transform:scale(1)}}@keyframes articlePreviewFadeIn{0%{opacity:0;transform:translateY(-.5em)}to{opacity:1}}'); // src/Classes/Settings.js var Settings = class { constructor() { } get(key) { const settings = this.getAll(); if (settings[key] === void 0) { return null; } return settings[key]; } getAll() { const data = localStorage.getItem("kup-settings"); let settings = {}; if (data) { settings = JSON.parse(data); } if (settings.showUrlSubheader === void 0) { settings.showUrlSubheader = true; } if (settings.removeCommentAnchor === void 0) { settings.removeCommentAnchor = true; } if (settings.showArticlePreview === void 0) { settings.showArticlePreview = true; } return settings; } replace(settings) { localStorage.setItem("kup-settings", JSON.stringify(settings)); window.dispatchEvent(new CustomEvent("kup-settings-changed")); } save(key, value) { const settings = this.getAll(); settings[key] = value; this.replace(settings); } reset() { localStorage.removeItem("kup-settings"); } }; var Settings_default = Settings; // src/Classes/Article/Article.js var Article = class _Article { feedElement = null; subject = null; author = null; date = null; articleUrl = null; mediaUrl = null; magazine = null; linkUrl = null; hasMedia = false; articlePreviewOpen = false; mediaPreviewOpen = false; TYPES = { ARTICLE: "article", LINK: "link", IMAGE: "image" }; #content; #articleLoaded; articlePageElement; enableArticlePreview = true; constructor() { } static fromFeedElement(element) { let article = new _Article(); article.feedElement = element; article.subject = element.querySelector("header h2").innerText; article.author = new User_default(element.querySelector(".meta .user-inline").innerText, element.querySelector(".meta .user-inline img")?.src); article.date = new Date(element.querySelector(".meta.entry__meta time")?.innerText); article.articleUrl = element.querySelector("header h2 a")?.href; article.thumbUrl = element.querySelector("figure a img")?.src; article.mediaUrl = element.querySelector("button.show-preview")?.dataset?.previewUrlParam; article.magazine = element.querySelector(".meta.entry__meta .magazine-inline")?.innerText; article.shortDescription = element.querySelector(".content.short-desc")?.innerText?.trim(); const upvoteElement = element.querySelector("aside.vote .vote__up"); const downvoteElement = element.querySelector("aside.vote .vote__down"); article.upvotes = parseInt(upvoteElement?.querySelector("span")?.innerText) || 0; article.downvotes = parseInt(downvoteElement?.querySelector("span")?.innerText) || 0; if (article.feedElement.querySelector("li.meta-link")) { article.type = article.TYPES.ARTICLE; } else if (article.feedElement.querySelector("li>button.show-preview")) { article.type = article.TYPES.IMAGE; } else { article.type = article.TYPES.LINK; } return article; } upvote() { const element = this.feedElement ?? this.articlePageElement; if (!element) { return; } const voteElement = element.querySelector("aside.vote .vote__up button"); if (voteElement) { voteElement.click(); this.upvotes++; } } downvote() { const element = this.feedElement ?? this.articlePageElement; if (!element) { return; } const voteElement = element.querySelector("aside.vote .vote__down button"); if (voteElement) { voteElement.click(); this.upvotes--; } } boost() { const element = this.feedElement ?? this.articlePageElement; if (!element) { return; } const boostElement = element.querySelector("footer menu li form button"); if (boostElement && boostElement.dataset.action === "subject#favourite") { boostElement.click(); } } loadArticle() { if (this.#articleLoaded) { return Promise.resolve(this); } const fetchOptions = { method: "GET", headers: { "Accept": "text/html" } }; return fetch(this.articleUrl, fetchOptions).then((response) => { return response.text(); }).then((text) => { const parser = new DOMParser(); const htmlDocument = parser.parseFromString(text, "text/html"); const contentElement = htmlDocument.querySelector("article .entry__body .content"); this.#content = contentElement?.innerHTML ?? null; const linkElement = htmlDocument.querySelector("article header h1 a"); if (linkElement) { this.linkUrl = linkElement.href; } else { this.linkUrl = null; } this.#articleLoaded = true; return this; }); } static fromArticlePage(articleElement) { let article = new _Article(); article.subject = articleElement.querySelector("header h1")?.childNodes[0]?.innerText?.trim(); article.author = new User_default(articleElement.querySelector(".meta .user-inline").innerText, articleElement.querySelector(".meta .user-inline img")?.src); article.date = new Date(articleElement.querySelector(".meta.entry__meta time")?.innerText); article.linkUrl = articleElement.querySelector("header h1>a")?.href; article.thumbUrl = articleElement.querySelector("figure a img")?.src; article.mediaUrl = articleElement.querySelector("button.show-preview")?.dataset?.previewUrlParam; article.magazine = articleElement.querySelector(".meta.entry__meta .magazine-inline")?.innerText; article.#content = articleElement.querySelector(".entry__body .content")?.innerHTML ?? null; const upvoteElement = articleElement.querySelector("aside.vote .vote__up"); const downvoteElement = articleElement.querySelector("aside.vote .vote__down"); article.upvotes = parseInt(upvoteElement?.querySelector("span")?.innerText) || 0; article.downvotes = parseInt(downvoteElement?.querySelector("span")?.innerText) || 0; article.enableArticlePreview = false; article.#articleLoaded = true; article.articlePageElement = articleElement; if (articleElement.querySelector("li.meta-link")) { article.type = article.TYPES.ARTICLE; } else if (articleElement.querySelector("li>button.show-preview")) { article.type = article.TYPES.IMAGE; } else { article.type = article.TYPES.LINK; } return article; } getContent() { if (!this.shortDescription) { this.#content = null; return Promise.resolve(this.#content); } return this.loadArticle().then(() => { return this.#content; }); } hasContent() { return !!this.shortDescription || !!this.#content; } enrichFeedElement() { if (!this.feedElement) { return; } const settings = new Settings_default(); const footer = this.feedElement.querySelector("footer"); const footerMenu = footer.querySelector("menu"); const previewOuter = Object.assign(document.createElement("div"), { className: "preview-outer" }); this.feedElement.append(previewOuter); const thumbnail = this.feedElement.querySelector("figure a img"); if (thumbnail) { thumbnail.style.objectFit = null; } this.replaceMediaPreview(this.feedElement); if (this.hasContent()) { if (this.type === this.TYPES.ARTICLE) { footer.querySelector("li.meta-link i")?.remove(); } const previewButton = document.createElement("button"); previewButton.classList.add("show-article-preview", "preview-button"); previewButton.innerHTML = ''; previewButton.addEventListener("click", (event) => { event.preventDefault(); this.toggleArticlePreview(); }); let metaLinkElement = footer.querySelector("li.meta-link"); if (!metaLinkElement) { metaLinkElement = document.createElement("li"); metaLinkElement.classList.add("meta-link"); footerMenu.insertBefore(metaLinkElement, footerMenu.firstChild); } metaLinkElement.append(previewButton); let previewElement = this.feedElement.querySelector(".article-preview"); let previewContentElement; if (!previewElement) { previewElement = document.createElement("div"); previewElement.classList.add("article-preview", "preview-inner"); previewContentElement = document.createElement("div"); previewContentElement.classList.add("article-preview-content", "content"); previewElement.append(previewContentElement); previewOuter.append(previewElement); } } if (settings.get("removeCommentAnchor")) { const commentsLinkElements = footer.querySelectorAll("menu li > a.stretched-link"); commentsLinkElements.forEach((commentsLinkElement) => { if (commentsLinkElement && commentsLinkElement.href.endsWith("#comments")) { const url = new URL(commentsLinkElement.href); commentsLinkElement.href = url.pathname; } }); } } replaceMediaPreview(parent) { const footer = parent.querySelector("footer"); const mediaPreviewButton = footer.querySelector("button.show-preview"); const previewOuter = parent.querySelector(".preview-outer"); const footerMenu = footer.querySelector("menu"); if (mediaPreviewButton && mediaPreviewButton.dataset.action === "preview#show") { this.hasMedia = true; mediaPreviewButton.remove(); const newMediaPreviewButton = document.createElement("button"); newMediaPreviewButton.classList.add("show-media-preview", "preview-button"); newMediaPreviewButton.innerHTML = ''; newMediaPreviewButton.addEventListener("click", (event) => { event.preventDefault(); this.toggleMediaPreview(); }); const li = document.createElement("li"); li.append(newMediaPreviewButton); footer.querySelector("menu").insertBefore(li, footerMenu.firstChild); let previewElement = parent.querySelector(".media-preview"); if (!previewElement) { previewElement = document.createElement("div"); previewElement.classList.add("media-preview", "preview-inner"); const previewContentElement = document.createElement("div"); previewContentElement.classList.add("media-preview-content"); previewElement.append(previewContentElement); previewOuter.append(previewElement); } } } enrichArticlePage() { if (!this.articlePageElement) { return; } const previewOuter = Object.assign(document.createElement("div"), { className: "preview-outer" }); this.articlePageElement.append(previewOuter); this.replaceMediaPreview(this.articlePageElement); } showArticlePreview() { if (!this.feedElement || !this.type === this.TYPES.ARTICLE || !this.hasContent()) { return; } this.articlePreviewOpen = true; let previewElement = this.feedElement.querySelector(".article-preview"); let previewContentElement = previewElement.querySelector(".article-preview-content"); previewElement.classList.add("show"); previewContentElement.innerHTML = '
'; previewContentElement.classList.remove("loaded"); this.getContent().then((content) => { if (content) { previewContentElement.innerHTML = content ?? ""; } else { previewContentElement.innerHTML = '
No content
'; } previewContentElement.classList.add("loaded"); }).catch(() => { previewContentElement.innerHTML = '
Error loading content
'; previewContentElement.classList.add("loaded"); }); } hideArticlePreview() { if (!this.feedElement || !this.type === this.TYPES.ARTICLE) { return; } this.articlePreviewOpen = false; let previewElement = this.feedElement.querySelector(".article-preview"); if (previewElement) { previewElement.classList.remove("show"); } } toggleArticlePreview() { if (this.articlePreviewOpen) { this.hideArticlePreview(); } else { this.showArticlePreview(); } } showMediaPreview() { let element = this.feedElement ?? this.articlePageElement; if (!element || !this.mediaUrl) { return; } this.mediaPreviewOpen = true; let previewElement = element.querySelector(".media-preview"); let previewContentElement = previewElement.querySelector(".media-preview-content"); previewElement.classList.add("show"); previewContentElement.innerHTML = '
'; previewContentElement.classList.remove("loaded"); const mediaURLObject = new URL(this.mediaUrl); if (this.mediaUrl.match(/\.(jpeg|jpg|gif|png|svg)$/) !== null) { const imageElement = document.createElement("img"); imageElement.addEventListener("load", () => { previewContentElement.innerHTML = ""; previewContentElement.append(imageElement); previewContentElement.classList.add("loaded"); }); imageElement.src = this.mediaUrl; imageElement.addEventListener("error", () => { previewContentElement.innerHTML = '
Error loading image
'; previewContentElement.classList.add("loaded"); }); imageElement.addEventListener("dragstart", (event) => { event.preventDefault(); }); imageElement.addEventListener("mousedown", (event) => { this.handleMediaPreviewResizeDrag(event, imageElement, "start"); }); document.body.addEventListener("mouseup", (event) => { this.handleMediaPreviewResizeDrag(event, imageElement, "end"); }); element.addEventListener("mousemove", (event) => { this.handleMediaPreviewResizeDrag(event, imageElement, "move"); }); } else if (mediaURLObject.hostname === "www.youtube.com" || mediaURLObject.hostname === "youtube.com" || mediaURLObject.hostname === "youtu.be") { const videoElement = document.createElement("iframe"); let videoSrc = "https://www.youtube.com/embed/"; if (mediaURLObject.hostname === "youtu.be") { videoSrc += mediaURLObject.pathname.substring(1); } else { videoSrc += mediaURLObject.searchParams.get("v"); } videoElement.src = videoSrc; videoElement.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"; videoElement.allowFullscreen = true; previewContentElement.innerHTML = ""; previewContentElement.append(videoElement); previewContentElement.classList.add("youtube-embed"); previewContentElement.classList.add("loaded"); } else { let url = "/ajax/fetch_embed?url=" + encodeURIComponent(this.mediaUrl); fetch(url).then((response) => response.json().then((data) => { previewContentElement.innerHTML = ""; previewContentElement.insertAdjacentHTML("beforeend", data.html); previewContentElement.classList.add("loaded"); })).catch(() => { previewContentElement.innerHTML = '
Error loading media
'; previewContentElement.classList.add("loaded"); }); } } hideMediaPreview() { const element = this.feedElement ?? this.articlePageElement; if (!element) { return; } this.mediaPreviewOpen = false; let previewElement = element.querySelector(".media-preview"); if (previewElement) { previewElement.classList.remove("show"); previewElement.querySelector(".media-preview-content").innerHTML = ""; } } toggleMediaPreview() { if (this.mediaPreviewOpen) { this.hideMediaPreview(); } else { this.showMediaPreview(); } } select() { this.selected = true; this.feedElement.classList.add("selected"); } unselect() { this.selected = false; this.feedElement.classList.remove("selected"); } /** Resize media preview on drag */ #isDraggingMediaPreview = false; handleMediaPreviewResizeDrag(event, imageElement) { if (event.type === "mousedown") { this.#isDraggingMediaPreview = true; } else if (event.type === "mouseup") { this.#isDraggingMediaPreview = false; } else if (event.type === "mousemove" && this.#isDraggingMediaPreview) { let imageRect = imageElement.getBoundingClientRect(); imageElement.style.width = imageRect.width + event.movementX * 2 + "px"; if (imageRect.width <= 100 && !imageElement.classList.contains("animateMinResize")) { imageElement.classList.add("animateMinResize"); } else if (imageRect.width > 110) { imageElement.classList.remove("animateMinResize"); } } } togglePreviews() { if (!this.articlePreviewOpen && this.enableArticlePreview && this.hasContent()) { this.showArticlePreview(); } else if (!this.mediaPreviewOpen && this.hasMedia) { this.showMediaPreview(); } else { this.hideArticlePreview(); this.hideMediaPreview(); } } }; var Article_default = Article; // src/Classes/ArticlesHandler/ArticlesHandler.js var ArticlesHandler = class { #articles; currentArticle; constructor() { this.#articles = []; } init() { if (!document.querySelector("#content>div article.entry")) { return; } this.applySettings(); window.addEventListener("kup-settings-changed", () => { this.applySettings(); }); this.parseArticles(); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.addedNodes.length > 0) { mutation.addedNodes.forEach((node) => { if (node.classList.contains("entry")) { this.parseArticle(node); } }); } }); }); observer.observe(document.querySelector("#content>div"), { childList: true }); document.addEventListener("keydown", (e) => { this.handleKeydown(e); }); } parseArticle(element) { const article = Article_default.fromFeedElement(element); article.enrichFeedElement(); element.addEventListener("click", () => { this.selectArticle(article); }); this.#articles.push(article); } parseArticles() { document.querySelectorAll("#content>div>article.entry").forEach((el) => { this.parseArticle(el); }); } selectArticle(article) { if (this.currentArticle) { this.currentArticle.unselect(); } this.currentArticle = article; article.select(); } unselectCurrentArticle() { if (this.currentArticle) { this.currentArticle.unselect(); this.currentArticle = null; } } selectClosestArticle() { console.log("Selecting closest article"); if (!this.currentArticle) { this.#articles.some((article) => { if (article.feedElement?.getBoundingClientRect().top > 0 && article.feedElement?.getBoundingClientRect().top < window.innerHeight) { this.selectArticle(article); return true; } }); } } handleKeydown(event) { if (event.target.tagName !== "BODY") { return; } if (event.key === "Escape") { this.unselectCurrentArticle(); } else if (event.key === "ArrowUp") { if (!this.currentArticle) { return; } event.preventDefault(); const index = this.#articles.indexOf(this.currentArticle); if (index > 0) { this.selectArticle(this.#articles[index - 1]); } if (this.currentArticle.feedElement?.getBoundingClientRect().top < 0) { this.currentArticle.feedElement.scrollIntoView(); } } else if (event.key === "ArrowDown") { if (!this.currentArticle) { return; } event.preventDefault(); const index = this.#articles.indexOf(this.currentArticle); if (index < this.#articles.length - 1) { this.selectArticle(this.#articles[index + 1]); } if (this.currentArticle.feedElement?.getBoundingClientRect().top > window.innerHeight) { this.currentArticle.feedElement.scrollIntoView(false); } } else if (event.key === "Enter") { if (!this.currentArticle) { this.selectClosestArticle(); } else { event.preventDefault(); this.currentArticle.togglePreviews(); } } else if (event.key === "a") { if (!this.currentArticle) { return; } event.preventDefault(); this.currentArticle.upvote(); } else if (event.key === "z") { if (!this.currentArticle) { return; } event.preventDefault(); this.currentArticle.downvote(); } else if (event.key === "b") { if (!this.currentArticle) { return; } event.preventDefault(); this.currentArticle.boost(); } else if (event.key === "o") { if (!this.currentArticle) { return; } event.preventDefault(); window.location.href = this.currentArticle.articleUrl; } } applySettings() { const settings = new Settings_default(); if (settings.get("showArticlePreview")) { document.body.classList.add("kup-show-article-preview"); } else { document.body.classList.remove("kup-show-article-preview"); this.#articles.forEach((article) => { article.hideArticlePreview(); }); } } }; var ArticlesHandler_default = ArticlesHandler; // src/Classes/Navigator/Navigator.js var Navigator = class { constructor() { } init() { this.fixMobileLogoLink(); } fixMobileLogoLink() { const nav = document.querySelector("nav.head-nav"); const logo = document.querySelector("nav.head-nav .brand > a"); const hamburger = document.querySelector("nav.head-nav .brand > div"); const homeButton = document.querySelector("#sidebar .section.mobile-close a.btn"); const action = nav.dataset.action; nav.dataset.action = ""; hamburger.dataset.action = action; homeButton.href = logo.href; } }; var Navigator_default = Navigator; // src/Classes/ArticlePage/ArticlePage.scss inject_style("body.kup-show-url-subheader article.entry header .url-subheader{display:block}article.entry header .url-subheader{display:none;font-size:.6rem;font-weight:600;flex-basis:100%}article.entry header .url-subheader .url-subheader__path{font-weight:200}"); // src/Classes/ArticlePage/ArticlePage.js var ArticlePage = class { currentPage = 1; numberOfPages = 1; constructor() { } init() { const currentURL = new URL(window.location.href); if (!currentURL.pathname.match(/\/m\/.+?\/t\/.+?/)) { return; } this.url = currentURL; this.articleElement = document.querySelector("article.entry"); this.article = Article_default.fromArticlePage(this.articleElement); this.article.enrichArticlePage(); const paginationElement = document.querySelector("nav.pagination.section"); let currentPage = this.url.searchParams.get("p"); if (currentPage) { this.currentPage = parseInt(currentPage); } else { this.currentPage = 1; } let numberOfPages = paginationElement?.querySelectorAll(".pagination__item:not(.pagination__item--next-page):not(.pagination__item--previous-page)"); if (numberOfPages && numberOfPages[numberOfPages.length - 1]) { this.numberOfPages = parseInt(numberOfPages[numberOfPages.length - 1].textContent); } else { this.numberOfPages = this.currentPage; } if (paginationElement) { paginationElement.innerHTML = ""; const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { this.handleInfiniteScroll(); } }); }); observer.observe(paginationElement); } document.addEventListener("keydown", (e) => { this.handleKeydown(e); }); if (this.article.linkUrl) { const subheader = document.createElement("h3"); subheader.classList.add("url-subheader"); let url = new URL(this.article.linkUrl); let subheaderText = ' ' + url.hostname + '' + url.pathname + url.search + ""; subheader.innerHTML = `${subheaderText}`; this.articleElement.querySelector("header").appendChild(subheader); } this.applySettings(); window.addEventListener("kup-settings-changed", () => { this.applySettings(); }); } handleInfiniteScroll() { if (this.numberOfPages > this.currentPage) { this.currentPage++; this.loadPage(this.currentPage).then((html) => { const dom = new DOMParser().parseFromString(html, "text/html"); const comments = dom.querySelectorAll("#comments section.comments > .comment"); const paginationElement = document.querySelector("nav.pagination.section"); comments.forEach((comment) => { document.querySelector("#comments section.comments").insertBefore(comment, paginationElement); }); }); } } applySettings() { const settings = new Settings_default(); if (settings.get("showUrlSubheader") === true) { document.body.classList.add("kup-show-url-subheader"); } else { document.body.classList.remove("kup-show-url-subheader"); } } handleKeydown(event) { if (event.target.tagName !== "BODY") { return; } if (event.key === "Enter") { event.preventDefault(); this.article.togglePreviews(); } else if (event.key === "a") { event.preventDefault(); this.article.upvote(); } else if (event.key === "z") { event.preventDefault(); this.article.downvote(); } else if (event.key === "b") { event.preventDefault(); this.article.boost(); } else if (event.key === "o") { if (this.article.linkUrl) { event.preventDefault(); window.open(this.article.linkUrl, "_blank"); } } } loadPage(page) { let url = this.url.pathname + "?p=" + page; return fetch(url).then((response) => { return response.text(); }); } }; var ArticlePage_default = ArticlePage; // src/Classes/SettingsPanel/SettingsRow.js var SettingsRow = class _SettingsRow { element; name; description; value; type; options; static TYPES = { BOOLEAN: "boolean", STRING: "string", NUMBER: "number", ENUM: "enum", CUSTOM: "custom" }; constructor(name, type, value, options = {}) { this.name = name; this.type = type; this.value = value; const { description } = options || {}; this.description = description; } getElement() { if (this.element) { return this.element; } const element = Object.assign(document.createElement("div"), { className: "settings-row" }); this.element = element; const name = Object.assign(document.createElement("span"), { className: "name", innerText: this.name }); element.appendChild(name); if (this.description) { element.classList.add("has-description"); const description = Object.assign(document.createElement("span"), { className: "description", innerText: this.description }); element.appendChild(description); } return element; } static fromElement(element) { return new _SettingsRow(element.innerText, _SettingsRow.detectType(element), "", {}); } static detectType(element) { const valueElement = element.querySelector(":scope > div"); const booleanRegex = /Yes\s*\|\s*No/; const enumRegex = /(\w+)\s*\|\s*(\w+)/; if (valueElement.innerText.trim().match(booleanRegex)) { return _SettingsRow.TYPES.BOOLEAN; } else if (valueElement.innerText.trim().match(enumRegex)) { return _SettingsRow.TYPES.ENUM; } else { return _SettingsRow.TYPES.CUSTOM; } } }; var SettingsRow_default = SettingsRow; // src/Classes/SettingsPanel/SettingsRowCustom.js var SettingsRowCustom = class _SettingsRowCustom extends SettingsRow_default { name; description; value; valueElement; constructor(name, value) { super(name, SettingsRow_default.TYPES.CUSTOM, value); } getElement() { if (this.element) { return this.element; } const element = Object.assign(document.createElement("div"), { className: "settings-row" }); this.element = element; const name = Object.assign(document.createElement("span"), { className: "name", innerText: this.name }); element.appendChild(name); if (this.description) { element.classList.add("has-description"); const description = Object.assign(document.createElement("span"), { className: "description", innerText: this.description }); element.appendChild(description); } const valueContainer = Object.assign(document.createElement("div"), { className: "value-container" }); valueContainer.appendChild(this.valueElement); element.appendChild(valueContainer); return element; } static fromElement(element) { const settingsRow = new _SettingsRowCustom(); settingsRow.name = element.querySelector(":scope > span")?.innerText.trim(); settingsRow.name = settingsRow.name.endsWith(":") ? settingsRow.name.slice(0, -1) : settingsRow.name; settingsRow.description = element.querySelector(".description")?.innerText; settingsRow.value = element.querySelector("input")?.value; settingsRow.valueElement = element.querySelector(":scope > div"); return settingsRow; } }; var SettingsRowCustom_default = SettingsRowCustom; // src/Classes/SettingsPanel/SettingsSection.js var SettingsSection = class _SettingsSection { settingsRows = []; name; expanded = false; constructor(name) { this.name = name; this.settingsRows = []; } addSettingsRow(settingRow) { this.settingsRows.push(settingRow); } addSettingsRows(settingRows) { settingRows.forEach((row) => { this.addSettingsRow(row); }); } static fromHeaderElement(headerElement) { return new _SettingsSection(headerElement.innerText); } expand() { this.expanded = true; this.element.classList.add("expanded"); } collapse() { this.expanded = false; this.element.classList.remove("expanded"); } toggle() { this.expanded ? this.collapse() : this.expand(); } getElement() { if (this.element) { return this.element; } const element = Object.assign(document.createElement("div"), { className: "settings-section" }); this.element = element; const header = Object.assign(document.createElement("h3"), { className: "settings-section-header", innerHTML: `${this.name}` }); header.addEventListener("click", (e) => { if (e.shiftKey) { window.dispatchEvent(new CustomEvent("kup-settings-expand-all-sections", { detail: { expand: !this.expanded } })); } else { this.toggle(); } }); element.appendChild(header); const settingsRows = Object.assign(document.createElement("div"), { className: "settings-rows" }); element.appendChild(settingsRows); this.settingsRows.forEach((row) => { settingsRows.appendChild(row.getElement()); }); return element; } }; var SettingsSection_default = SettingsSection; // src/Classes/SettingsPanel/SettingsPanel.scss inject_style('#settings .settings-list{display:none!important;visibility:hidden}#settings .settings-panel-footer{font-size:.8em;font-weight:100}#settings .settings-panel-footer span{margin-left:.25em}.settings-panel .settings-section{margin-bottom:2em}.settings-panel .settings-section .settings-section-header{font-weight:700;margin-bottom:1em;cursor:pointer}.settings-panel .settings-section .settings-section-header:hover{color:var(--kbin-primary)}.settings-panel .settings-section .settings-section-header .icon{margin-right:.5em}.settings-panel .settings-section .settings-section-header .icon-chevron{transition:transform .25s ease-in-out;transform:rotate(-90deg);margin-left:.5em}.settings-panel .settings-section.expanded .icon-chevron{transform:rotate(0)}.settings-panel .settings-section.expanded .settings-row{display:grid}.settings-panel .settings-section .settings-row{display:none;grid-template-areas:"name value" "description value";grid-template-columns:auto;align-items:center;margin-bottom:1em;animation:showSettingsRow .25s ease-in-out}@keyframes showSettingsRow{0%{opacity:0;transform:translateY(-1em)}to{opacity:1;transform:translateY(0)}}.settings-panel .settings-section .settings-row .name{margin-right:1em;grid-area:name}.settings-panel .settings-section .settings-row .description{grid-area:description;font-size:.8em;color:var(--kbin-secondary-text-color)}.settings-panel .settings-section .settings-row .value-container{flex-grow:1;text-align:right;grid-area:value;margin-left:1em}.settings-panel .settings-section .settings-row .value-container .link-muted.active{color:var(--kbin-primary);font-weight:800!important}.settings-panel .settings-section .settings-row .value-container.enum{border:var(--kbin-button-primary-border);border-radius:.5em;display:grid;grid-template-columns:repeat(auto-fit,minmax(0,1fr));align-items:center;text-align:center;background-color:var(--kbin-button-secondary-bg);overflow:hidden;font-size:.8em}.settings-panel .settings-section .settings-row .value-container.enum .value{padding:.5em .25em;font-weight:100;color:var(--kbin-button-secondary-text-color)}.settings-panel .settings-section .settings-row .value-container.enum .value:not(:last-child){border-right:var(--kbin-button-primary-border)}.settings-panel .settings-section .settings-row .value-container.enum .value.selected{background:var(--kbin-button-primary-bg);color:var(--kbin-button-primary-text-color);font-weight:800!important}.settings-panel .settings-section .settings-row .value-container .switch{position:relative;display:inline-block;width:3em;height:1.5em;border-radius:.75em;overflow:hidden;border:var(--kbin-button-primary-border)}.settings-panel .settings-section .settings-row .value-container .switch input{width:0;height:0;visibility:hidden}.settings-panel .settings-section .settings-row .value-container .switch:hover .slider{background-color:var(--kbin-button-secondary-text-hover-color)}.settings-panel .settings-section .settings-row .value-container .switch:hover .slider:before{background-color:var(--kbin-button-primary-text-hover-color);border:.5em solid var(--kbin-button-primary-text-hover-color)}.settings-panel .settings-section .settings-row .value-container .switch:hover input:checked+.slider{background-color:var(--kbin-button-primary-hover-bg)}.settings-panel .settings-section .settings-row .value-container .switch:hover input:checked+.slider:before{background:var(--kbin-button-primary-hover-bg)}.settings-panel .settings-section .settings-row .value-container .slider{position:absolute;cursor:pointer;inset:0;background-color:var(--kbin-button-secondary-text-color);transition:.25s}.settings-panel .settings-section .settings-row .value-container .slider:before{position:absolute;content:"";height:100%;aspect-ratio:1;left:0;bottom:0;background-color:var(--kbin-button-primary-text-color);transition:.25s;border-radius:.75em;border:.5em solid var(--kbin-button-primary-text-color)}.settings-panel .settings-section .settings-row .value-container input:checked+.slider{background-color:var(--kbin-button-primary-bg)}.settings-panel .settings-section .settings-row .value-container input:checked+.slider:before{transform:translate(1.5em);background:var(--kbin-button-primary-bg)}#settings-notification-container{position:fixed;bottom:0;left:0;right:0;z-index:1000;padding:1em;display:none;flex-direction:column;align-items:center;pointer-events:none}#settings-notification-container.visible{display:flex}#settings-notification-container .notification{pointer-events:initial;margin-bottom:1em;padding:1em;border-radius:.25rem;background-color:var(--kbin-bg);box-shadow:var(--kbin-shadow);border:var(--kbin-section-border);display:flex;flex-direction:row;align-items:center;justify-content:space-between;animation:showNotification .25s ease-in-out}@keyframes showNotification{0%{opacity:0;transform:translateY(1em)}to{opacity:1;transform:translateY(0)}}#settings-notification-container .notification .message,#settings-notification-container .notification .message-icon{margin-right:1em}'); // src/Classes/SettingsPanel/SettingsRowBoolean.js var SettingsRowBoolean = class _SettingsRowBoolean extends SettingsRow_default { element; postAction; trueAction; falseAction; id; requireReload; constructor(name, defaultValue, options = {}) { super(name, SettingsRow_default.TYPES.BOOLEAN, defaultValue, options); const { id, requireReload, postAction } = options || {}; if (id) { this.id = id; const settings = new Settings_default(); const value = settings.get(id); if (value !== void 0) { this.value = value; } } if (requireReload) { this.requireReload = requireReload; } if (postAction) { this.postAction = postAction; } const action = (newValue) => { if (this.id) { const settings = new Settings_default(); settings.save(this.id, newValue); } if (this.postAction) { this.postAction(newValue); } if (this.requireReload) { console.log("reload required"); window.dispatchEvent(new CustomEvent("kup-settings-needs-reload")); } }; this.bindTrueAction(() => { action(true); }); this.bindFalseAction(() => { action(false); }); } bindPostAction(action) { this.postAction = action; } setValue(value) { this.value = !!value; console.log(this.value); if (this.value) { this.trueAction(); } else { this.falseAction(); } } bindTrueAction(action) { this.trueAction = action; } bindFalseAction(action) { this.falseAction = action; } getElement() { if (this.element) { return this.element; } const element = Object.assign(document.createElement("div"), { className: "settings-row" }); this.element = element; const name = Object.assign(document.createElement("span"), { className: "name", innerText: this.name }); element.appendChild(name); if (this.description) { element.classList.add("has-description"); const description = Object.assign(document.createElement("span"), { className: "description", innerText: this.description }); element.appendChild(description); } const valueContainer = Object.assign(document.createElement("div"), { className: "value-container" }); const label = Object.assign(document.createElement("label"), { className: "switch" }); const input = Object.assign(document.createElement("input"), { type: "checkbox", checked: this.value }); const slider = Object.assign(document.createElement("span"), { className: "slider" }); input.addEventListener("change", () => { this.setValue(!this.value); }); label.appendChild(input); label.appendChild(slider); valueContainer.appendChild(label); element.appendChild(valueContainer); return element; } static fromElement(element) { let name = element.querySelector(":scope > span")?.innerText.trim(); name = name.endsWith(":") ? name.slice(0, -1) : name; const settingsRow = new _SettingsRowBoolean(name, element.querySelector(":scope > div a.active").innerText.trim() === "Yes"); const valueElements = element.querySelectorAll(":scope > div a"); valueElements.forEach((valueElement) => { if (valueElement.innerText.trim() === "Yes") { settingsRow.bindTrueAction(() => { return fetch(valueElement.href).then(() => { window.dispatchEvent(new CustomEvent("kup-settings-needs-reload")); }); }); } else { settingsRow.bindFalseAction(() => { return fetch(valueElement.href).then(() => { window.dispatchEvent(new CustomEvent("kup-settings-needs-reload")); }); }); } }); return settingsRow; } }; var SettingsRowBoolean_default = SettingsRowBoolean; // src/Classes/SettingsPanel/SettingsRowEnum.js var SettingsRowEnum = class _SettingsRowEnum extends SettingsRow_default { selectedId; constructor(name, defaultValue, options = {}) { super(name, SettingsRow_default.TYPES.ENUM, "", options); const { id, values } = options || {}; if (id) { this.id = id; const settings = new Settings_default(); const value = settings.get(id); if (value !== void 0) { this.value = value; } } else { this.selectedId = defaultValue; } if (values) { this.values = values; } else { throw new Error("SettingsRowEnum requires values"); } } getElement() { if (this.element) { return this.element; } const element = Object.assign(document.createElement("div"), { className: "settings-row" }); this.element = element; const name = Object.assign(document.createElement("span"), { className: "name", innerText: this.name }); element.appendChild(name); if (this.description) { element.classList.add("has-description"); const description = Object.assign(document.createElement("span"), { className: "description", innerText: this.description }); element.appendChild(description); } const valueContainer = Object.assign(document.createElement("div"), { className: "value-container enum" }); this.values.forEach((value) => { const valueElement = Object.assign(document.createElement("a"), { className: "value", innerText: value.value, href: value.id }); console.log(this.selectedId, value.id); if (this.selectedId === value.id) { valueElement.classList.add("selected"); } valueElement.addEventListener("click", (e) => { e.preventDefault(); value.action(); element.querySelectorAll(".value").forEach((valueElement2) => { valueElement2.classList.remove("selected"); }); valueElement.classList.add("selected"); }); valueContainer.appendChild(valueElement); }); element.appendChild(valueContainer); return element; } static fromElement(element) { let name = element.querySelector(":scope > span")?.innerText.trim(); name = name.endsWith(":") ? name.slice(0, -1) : name; const valueElements = element.querySelectorAll(":scope > div a"); let values = []; let selectedId = null; valueElements.forEach((valueElement) => { let value = {}; value.value = valueElement.innerText.trim(); value.id = valueElement.href; value.action = () => { return fetch(valueElement.href).then(() => { window.dispatchEvent(new CustomEvent("kup-settings-needs-reload")); }); }; if (valueElement.classList.contains("active")) { selectedId = value.id; } values.push(value); }); return new _SettingsRowEnum(name, selectedId, { values }); } }; var SettingsRowEnum_default = SettingsRowEnum; // src/Classes/SettingsPanel/SettingsPanel.js function settingsRowFromElement(element) { let settingsRow; switch (SettingsRow_default.detectType(element)) { case SettingsRow_default.TYPES.BOOLEAN: settingsRow = SettingsRowBoolean_default.fromElement(element); break; case SettingsRow_default.TYPES.ENUM: settingsRow = SettingsRowEnum_default.fromElement(element); break; default: settingsRow = SettingsRowCustom_default.fromElement(element); break; } return settingsRow; } var SettingsPanel = class { sections = []; constructor() { } init() { this.settingsPanelContainerElement = document.getElementById("settings"); this.addSettingsNotificationElement(); this.enrichSettingsPanel().then(() => { this.populateKUPSettings(); }); window.addEventListener("kup-settings-needs-reload", () => { this.showNotification("Settings updated. Some changes require a reload to take effect."); }); window.addEventListener("kup-settings-expand-all-sections", (e) => { this.sections.forEach((section) => { e.detail.expand ? section.expand() : section.collapse(); }); }); } addSection(section) { this.sections.push(section); this.settingsPanelElement.appendChild(section.getElement()); } enrichSettingsPanel() { const settingsListElement = this.settingsPanelContainerElement.querySelector(".settings-list"); const settingsList = settingsListElement.querySelectorAll(":scope > *"); return new Promise((resolve) => { setTimeout(() => { let currentSection = null; let sections = []; settingsList.forEach((el) => { if (el.tagName === "STRONG") { if (currentSection) { sections.push(currentSection); currentSection = null; } currentSection = SettingsSection_default.fromHeaderElement(el); } else { if (!currentSection) { console.error("Found setting without section: ", el); currentSection = new SettingsSection_default("Other"); } const settingsRow = settingsRowFromElement(el); currentSection.addSettingsRow(settingsRow); } }); if (currentSection) { sections.push(currentSection); } const settingsPanel2 = document.createElement("div"); settingsPanel2.classList.add("settings-panel"); sections.forEach((section) => { settingsPanel2.appendChild(section.getElement()); }); this.settingsPanelElement = settingsPanel2; this.settingsPanelContainerElement.appendChild(settingsPanel2); settingsListElement.remove(); this.sections = sections; this.settingsPanelContainerElement.appendChild(Object.assign(document.createElement("div"), { className: "settings-panel-footer", innerHTML: '
Shift click to toggle all sections
' })); resolve(); }, 500); }); } addSettingsNotificationElement() { const settingsNotificationContainer = Object.assign(document.createElement("div"), { id: "settings-notification-container" }); const settingsNotificationElement = document.createElement("div"); settingsNotificationElement.classList.add("notification"); settingsNotificationElement.innerHTML = ` `; settingsNotificationElement.querySelector("button").addEventListener("click", () => { window.location.reload(); }); settingsNotificationContainer.appendChild(settingsNotificationElement); document.body.appendChild(settingsNotificationContainer); this.notificationElement = settingsNotificationContainer; } showNotification(message) { this.notificationElement.classList.add("visible"); this.notificationElement.querySelector(".message").innerText = message; } hideNotification() { this.notificationElement.classList.remove("visible"); } populateKUPSettings() { const section = new SettingsSection_default("Kbin Usability Pack"); section.addSettingsRows([ new SettingsRowBoolean_default("Show URL subheader", true, { id: "showUrlSubheader", description: "Show the link URL beneath the title" }), new SettingsRowBoolean_default("Remove comment anchor", true, { id: "removeCommentAnchor", description: "Make the comment links go to the top of the article instead of the comments section.", requireReload: true }), new SettingsRowBoolean_default("Show article preview button", true, { id: "showArticlePreview" }) ]); this.addSection(section); } }; var SettingsPanel_default = SettingsPanel; // src/index.js var articlesHandler = new ArticlesHandler_default(); var navigator = new Navigator_default(); var articlePage = new ArticlePage_default(); var settingsPanel = new SettingsPanel_default(); articlesHandler.init(); navigator.init(); articlePage.init(); settingsPanel.init(); })(); })();