Format a webpage on Stack Enchange websites such as stackoverflow.com so that web clippers can save a pretty webpage containing only the content you need.
// ==UserScript== // @name Stack Exchange Formatter // @namespace https://greasyfork.org/en/users/211578 // @version 1.0.2 // @description Format a webpage on Stack Enchange websites such as stackoverflow.com so that web clippers can save a pretty webpage containing only the content you need. // @author twchen // @include https://stackoverflow.com/questions/* // @include https://*.stackexchange.com/questions/* // @include https://superuser.com/questions/* // @include https://serverfault.com/questions/* // @include https://askubuntu.com/questions/* // @run-at document-idle // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM.getValue // @grant GM.setValue // ==/UserScript== "use strict"; if (typeof GM_addStyle == "undefined") { this.GM_addStyle = (css) => { const style = document.createElement("style"); style.textContent = css; document.documentElement.appendChild(style); return style; }; } if (typeof GM == "undefined") { this.GM = {}; [ ["getValue", GM_getValue], ["setValue", GM_setValue], ].forEach(([newFunc, oldFunc]) => { GM[newFunc] = (...args) => { return new Promise((resolve, reject) => { try { resolve(oldFunc(...args)); } catch (error) { reject(error); } }); }; }); } function createElement(type, props, ...children) { const element = document.createElement(type); Object.entries(props || {}).forEach(([name, value]) => { if (name.startsWith("on")) { const eventName = name.slice(2); element.addEventListener(eventName, value); } else if (name == "style" && typeof value !== "string") { Object.assign(element.style, value); } else { element.setAttribute(name, value); } }); children .map((child) => typeof child === "string" ? document.createTextNode(child) : child ) .forEach((child) => element.appendChild(child)); return element; } const body = document.body; const formatted = createElement("div", { id: "formatted" }); body.appendChild(formatted); const posts = document.querySelectorAll(".question, .answer"); let postCheckboxes = []; let commentCheckboxes = []; function addCheckboxes() { posts.forEach((post, i) => { post.querySelectorAll(".post-layout--right").forEach((layout, j) => { const isCommentLayout = layout.querySelector(".comments") !== null; if (isCommentLayout && layout.querySelectorAll("li.comment").length === 0) return; layout.style.position = "relative"; const checkboxId = `post${i}-layout${j}`; const checkbox = createElement("input", { type: "checkbox", id: checkboxId, }); if (isCommentLayout) commentCheckboxes.push(checkbox); else postCheckboxes.push(checkbox); const container = createElement( "div", { class: "ft-checkbox-container" }, createElement("label", { for: checkboxId }, "Keep"), createElement("br"), checkbox ); layout.appendChild(container); }); }); } function addLinks() { const question = document.querySelector(".question"); const answers = document.querySelectorAll(".answer"); answers.forEach((answer) => { const menu = answer.querySelector(".post-menu"); const saveAnsLink = createElement( "a", { href: "#", style: "margin-right: 0.5rem", onclick: async (event) => { event.preventDefault(); unselectAllCheckboxes(); await keepPost(answer); save(); }, }, "save this answer" ); const saveQALink = createElement( "a", { href: "#", onclick: async (event) => { event.preventDefault(); unselectAllCheckboxes(); await keepPost(question); await keepPost(answer); save(); }, }, "save this Q&A" ); menu.append(saveAnsLink, saveQALink); }); const advancedSaveLink = createElement( "div", { style: "padding-left: 1rem" }, createElement( "a", { href: "#", class: "ws-nowrap s-btn s-btn__primary", onclick: (event) => { event.preventDefault(); startChoosing(); }, }, "Advanced Save" ) ); const header = document.querySelector("#question-header"); header.append(advancedSaveLink); } function startChoosing() { document.querySelectorAll(".ft-checkbox-container").forEach((container) => { container.style.display = "block"; }); let dialog = document.getElementById("ft-dialog"); if (dialog) { dialog.style.display = "block"; } else { createDialog(); } } async function createDialog() { const dialog = createElement( "div", { id: "ft-dialog" }, createElement("label", { for: "selectAllPosts" }, "Select All Posts"), createElement("input", { type: "checkbox", id: "selectAllPosts", onchange: (event) => { for (let checkbox of postCheckboxes) { checkbox.checked = event.target.checked; } }, }), createElement("br"), createElement("label", { for: "selectAllComments" }, "Select All Comments"), createElement("input", { type: "checkbox", id: "selectAllComments", onchange: (event) => { for (let checkbox of commentCheckboxes) { checkbox.checked = event.target.checked; } }, }), createElement("br"), createElement( "label", { for: "selectCommentsByDefault" }, "Select Comments by Default" ), createElement("input", { type: "checkbox", id: "selectCommentsByDefault", onchange: (event) => { GM.setValue("selectCommentsByDefault", event.target.checked); }, }), createElement("br"), createElement( "button", { onclick: (event) => { document .querySelectorAll(".ft-checkbox-container") .forEach((container) => { container.style.display = "none"; }); dialog.style.display = "none"; }, }, "Cancel" ), createElement("button", { onclick: (event) => save() }, "Save") ); const selectComments = await GM.getValue("selectCommentsByDefault"); dialog.querySelector("#selectCommentsByDefault").checked = selectComments; for (let checkbox of commentCheckboxes) { checkbox.checked = selectComments; } body.appendChild(dialog); } function save() { const children = []; const questionLink = document.querySelector( "#question-header .question-hyperlink" ); const hr = createElement("hr", { style: "height: 0px" }); let title = undefined; if (questionLink) { title = createElement( "div", { class: "post-layout--right" }, questionLink.cloneNode(true) ); children.push(title); } posts.forEach((post, i) => { const layouts = []; post.querySelectorAll(".post-layout--right").forEach((layout, j) => { const checkboxId = `post${i}-layout${j}`; const checkbox = document.getElementById(checkboxId); if (checkbox && checkbox.checked) { layouts.push(layout.cloneNode(true)); } }); if (layouts.length > 0) { children.push(...layouts); children.push(hr.cloneNode(true)); } }); if (children.length === 0 || (children.length === 1 && title)) { alert("Select at least one post!"); return; } children.pop(); hideAllChildren(body); removeAllChildren(formatted); formatted.append(...children); formatted.style.display = "block"; window.history.pushState("formatted", ""); } function unselectAllCheckboxes() { for (let checkboxes of [postCheckboxes, commentCheckboxes]) { for (let checkbox of checkboxes) { checkbox.checked = false; } } } async function keepPost(post) { const layouts = post.querySelectorAll(".post-layout--right"); const selectComments = await GM.getValue("selectCommentsByDefault"); for (let layout of layouts) { console.log(layout); const checkbox = layout.querySelector( '.ft-checkbox-container input[type="checkbox"]' ); if (checkbox === null) { console.log(1); continue; } if (layout.querySelector(".comments") && selectComments === false) { console.log(2); checkbox.checked = false; } else { checkbox.checked = true; } } } function removeAllChildren(el) { while (el.firstChild) { el.removeChild(el.firstChild); } } function showAllChildren(el) { [...el.children].forEach((child) => { child.style.display = child.old_display === undefined ? "" : child.old_display; }); } function hideAllChildren(el) { [...el.children].forEach((child) => { child.old_display = child.style.display; child.style.display = "none"; }); } GM_addStyle(` .ft-checkbox-container { position: absolute; top: 0; right: -1rem; text-align: center; display: none; } #ft-dialog { background-color: white; position: fixed; top: 50%; right: 2rem; transform: translateY(-50%); z-index: 100; text-align: center; padding: 0.8rem; border: 1px solid black; border-radius: 5px; } #ft-dialog label { width: 10rem; display: inline-block; text-align: left; } #ft-dialog button { width: 5rem; margin: 0 0.5rem; } #formatted { background-color: #f6f6f6; position: absolute; top: 0; left: 0; width: 100%; } #formatted .question-hyperlink { color: black; font-size: 2rem; } #formatted .ft-checkbox-container { display: none !important; } #formatted .post-layout--right { background-color: white; padding: 2rem; margin: 0 2rem; box-shadow: 0 1px 3px #808080b5; } #formatted .post-menu, #formatted .post-signature, #formatted *[id^="comments-link-"] { display: none; } `); // handle backward/forward events window.addEventListener("popstate", function (event) { if (event.state === "formatted") { hideAllChildren(body); formatted.style.display = "block"; } else { showAllChildren(body); formatted.style.display = "none"; } }); addCheckboxes(); addLinks();