NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Better /tg/ Guides // @namespace https://faulty.equipment // @version 0.2.2 // @description Make /tg/station guides better with extra features // @author Hamcha // @collaborator D // @license ISC // @copyright 2020, Hamcha (https://openuserjs.org/users/Hamcha), D // @match https://tgstation13.org/wiki/Guide_to_* // @grant GM_addStyle // ==/UserScript== (function () { "use strict"; const VERSION = GM_info.script.version; const pxSMOOTH_SCROLL_MAX_DISTANCE = (() => { if ( window.matchMedia( "@media (prefers-reduced-motion, prefers-reduced-motion: reduce)" ).matches ) { return -Infinity; } if ( ["Firefox", "Iceweasel"].some((e) => window.navigator.userAgent.includes(e) ) ) { return Infinity; } return 3000; })(); // Tell user that better chemistry is loading const postbody = document.getElementById("mw-content-text"); const statusMessage = document.createElement("div"); statusMessage.innerHTML = ` <table style="background-color: rgb(255, 248, 219); margin-bottom:10px;" width="95%" align="center"> <tbody><tr><td align="center"> <b>Hang on...</b> Better guides is loading. </td></tr></tbody> </table>`; postbody.insertBefore(statusMessage, postbody.firstChild); GM_addStyle(` .bgus_hidden { display: none !important; } .bgus_nobreak { white-space: nowrap; } `); // TODO Refactor this mess function searchBox(el, search_candidate) { // Fuzzy search box GM_addStyle( ` #bgus_fz_searchbox { position: fixed; top: 20px; left: 30%; right: 30%; background: rgba(10,10,10,0.8); display: flex; flex-direction: column; z-index: 9999; color: #fff; } #bgus_fz_searchbox input { font-size: 14pt; padding: 5pt 8pt; border: 1px solid #555; margin: 5px; margin-bottom: 0; background-color: #111; color: #fff; } #bgus_fz_searchbox ul { list-style: none; margin: 5px; } #bgus_fz_searchbox li { padding: 5px; cursor: pointer; } #bgus_fz_searchbox li:hover { background-color: rgba(100, 100, 100, 0.5); } #bgus_fz_searchbox li.selected { border-left: 3px solid white; } ` ); const resultList = document.createElement("ul"); const searchBox = document.createElement("div"); let selected_result = null; let results = []; const jumpTo = function (id) { el[id].scrollIntoView({ block: "center", inline: "nearest", behavior: Math.abs(el[id].getBoundingClientRect().top) < pxSMOOTH_SCROLL_MAX_DISTANCE ? "smooth" : "auto", }); document .querySelectorAll("table.wikitable .bgus_fz_selected") .forEach((el) => el.classList.remove("bgus_fz_selected")); el[id].parentElement.classList.add("bgus_fz_selected"); }; const setSelectedResult = function (i) { selected_result = i; resultList .querySelectorAll(".selected") .forEach((el) => el.classList.remove("selected")); resultList.children[i].classList.add("selected"); jumpTo(results[i].id); }; const search = (str) => { if (!str) { return; } const regex = new RegExp( "^(.*?)([" + str .split("") .map((c) => (c.includes(["\\", "]", "^"]) ? "\\" + c : c)) .join("])(.*?)([") + "])(.*?)$", "i" ); const arr = search_candidate .map((o) => { o.matches = (o.str.match(regex) || []) .slice(1) .reduce((list, group, i, or) => { // Initialize first placeholder (always empty) and first matching "sections" if (i < 2) { list.push([group]); } // If group is second match in a row join to previous section else if (or[i - 1] === "") { list[list.length - 1].push(group); } // If group is a match create a new section else if (group !== "") { list.push([group]); } return list; }, []) .map((str) => str.join("")); return o; }) // Strike non-matching rows .filter((o) => o.matches.length > 0) .sort((oA, oB) => { const iA = oA.id, a = oA.matches; const iB = oB.id, b = oB.matches; // Exact match if (a.length === 1 && b.length !== 1) return -1; if (a.length !== 1 && b.length === 1) return 1; // Most complete groups (alphanumeric) const clean = (el) => !/[^a-zA-Z0-9]*$/.test(el); const cLen = a.filter(clean).length - b.filter(clean).length; if (cLen !== 0) return cLen; // Least distant first gropus for (let i = 0; i < Math.min(a.length, b.length) - 1; i += 2) { const gLen = a[i].length - b[i].length; if (gLen !== 0) return gLen; } // Most complete groups (raw) const len = a.length - b.length; if (len !== 0) return len; // Make the search stable since ECMAScript doesn't mandate it return iA - iB; }); results = arr; window.requestAnimationFrame(() => { resultList.innerHTML = ""; arr.forEach(({ matches, id }) => { const li = document.createElement("li"); li.innerHTML = matches .map((c, i) => (i % 2 ? "<strong>" + c + "</strong>" : c)) .join(""); li.addEventListener("click", () => { jumpTo(id); searchBox.classList.add("bgus_hidden"); }); resultList.appendChild(li); }); if (results.length > 0) { setSelectedResult(0); } }); }; // Create fuzzy search box const sel = document.createElement("input"); searchBox.id = "bgus_fz_searchbox"; searchBox.classList.add("bgus_hidden"); searchBox.appendChild(sel); searchBox.appendChild(resultList); document.body.appendChild(searchBox); // Bind events let oldValue = ""; sel.onkeyup = function (ev) { switch (event.keyCode) { case 27: // Escape - Hide bar searchBox.classList.add("bgus_hidden"); return; case 13: // Enter - Jump to first result and hide bar if (results.length > 0) { jumpTo(results[selected_result].id); } searchBox.classList.add("bgus_hidden"); return; case 40: // Down arrow - Select next result if (selected_result < results.length - 1) { setSelectedResult(selected_result + 1); } return; case 38: // Up arrow - Select previous result if (selected_result > 0) { setSelectedResult(selected_result - 1); } return; default: if (this.value != oldValue) { search(this.value); oldValue = this.value; } } }; document.body.addEventListener("keyup", function (ev) { if (ev.keyCode === 83) { sel.focus(); } }); document.body.addEventListener("keydown", function (ev) { if (ev.shiftKey) { switch (ev.keyCode) { // SHIFT+S = Fuzzy search case 83: { searchBox.classList.remove("bgus_hidden"); sel.value = ""; return; } } } }); } function betterChemistry() { // Chem styles GM_addStyle( ` .bgus_twistie:after { color: red; display: inline-block; font-weight: bold; margin-left: .2em; content: '⯆'; } .bgus_collapsed > .bgus_twistie:after{ content: '⯈'; } span.bgus_nested_element:not(.bgus_collapsed) + div.tooltiptext { z-index: unset; visibility: inherit; opacity: 1; position: relative; width: auto; border-left-width: 3px; background: transparent; margin-left: 5px; margin-top: 5px; font-size: 8pt; padding: 5px 8px; line-height: 10pt; } table.wikitable > tbody > tr > td:nth-child(2) { min-width: 30%; padding: 10px; } .bgus_fz_selected { border: 3px solid yellow; } body.bgus_cbox input[type="checkbox"] + span[data-src]:before { display: inline-block; width: 1.5em; content: '[_]'; } body.bgus_cbox input[type="checkbox"]:checked + span[data-src]:before { content: '[X]'; } body.bgus_cbox input[type="checkbox"]:checked + span[data-src] { text-decoration: line-through; } body.bgus_cbox input[type="checkbox"] + span[data-src] { cursor: pointer; } body.bgus_cbox input[type="checkbox"] + span[data-src]:before, body.bgus_cbox input[type="checkbox"] + span[data-src] { color: orange; font-weight: bold; } body.bgus_cbox input[type="checkbox"]:checked + span[data-src]:before, body.bgus_cbox input[type="checkbox"]:checked + span[data-src] { color: green; } ` ); // Fix inconsistencies with <p> on random parts // Ideally I'd like a <p> or something on every part, wrapping it completely, but for now let's just kill 'em document .querySelectorAll( "table.wikitable > tbody > tr:not(:first-child) > td:nth-child(2)" ) .forEach((td) => { const tmp = td.cloneNode(); // The cast to Array is necessary because, while childNodes's NodeList technically has a forEach method, it's a live list and operations mess with its lenght in the middle of the loop. // Nodes can only have one parent so append removes them from the original NodeList and shifts the following one back into the wrong index. Array.from(td.childNodes).forEach((el) => { if (el.tagName === "P") { tmp.append(...el.childNodes); } else { tmp.append(el); } }); td.parentNode.replaceChild(tmp, td); }); // Enrich "x part" with checkboxes and parts Array.from(document.querySelectorAll("td")) .filter((el) => el.innerText.indexOf(" part") >= 0) .forEach((el) => { el.innerHTML = el.innerHTML.replace( /((\d+)\s+(?:parts?|units?))(.*?(?:<\/a>|\n|$))/gi, (match, ...m) => `<label class="bgus_part ${ m[2].includes("</a>") ? "bgus_part_tooltip" : "" }" data-amount="${ m[1] }"><input type="checkbox" class='bgus_checkbox bgus_hidden'/> <span class="bgus_part_label" data-src="${ m[0] }">${m[0]}</span></label>${m[2].replace( /(<a .+?<\/a>)/gi, '<span class="bgus_nobreak bgus_nested_element">$1<span class="bgus_twistie"></span></span>' )}` ); }); // Add event to autofill child checkboxes document .querySelectorAll(".bgus_part_tooltip > .bgus_checkbox") .forEach((box) => { const tooltip = box.parentElement.nextElementSibling; box.addEventListener("click", function () { tooltip .querySelectorAll(".bgus_checkbox") .forEach((el) => (el.checked = this.checked)); }); }); // Add event to collapse subsections document.querySelectorAll(".bgus_nested_element").forEach((twistie) => { twistie.addEventListener("click", function (evt) { twistie.classList.toggle("bgus_collapsed"); }); }); // Wrap every recipe with extra metadata document.querySelectorAll(".bgus_part").forEach((el) => { if ("parts" in el.parentElement.dataset) { el.parentElement.dataset.parts = parseInt(el.parentElement.dataset.parts) + parseInt(el.dataset.amount); } else { el.parentElement.dataset.parts = el.dataset.amount; } }); const setPartSize = function (labels, ml) { labels.forEach((el) => { const part = el.parentElement.dataset.amount; const total = el.parentElement.parentElement.dataset.parts; const amt = Math.ceil(ml * (part / total)); el.innerHTML = `${amt} ml`; // Lookup tooltips let next = el.parentElement.nextElementSibling; while (next) { if (next.classList.contains("tooltip")) { let sublabels = []; next.querySelector(".tooltiptext").childNodes.forEach((ch) => { if (ch.classList && ch.classList.contains("bgus_part")) { sublabels.push(ch.querySelector(".bgus_part_label")); } }); setPartSize(sublabels, amt); } if (next.classList.contains("bgus_part")) { // Done searching break; } next = next.nextElementSibling; } }); }; // Init fuzzy search with elements const el = Array.from( document.querySelectorAll( "table.wikitable > tbody > tr:not(:first-child) > th" ) ); const name = el.map((elem) => { let name = ""; elem.childNodes.forEach((t) => { if (t instanceof Text) { name += t.textContent; } }); return name.trim(); }); searchBox( el, name.map((e, i) => ({ id: i, str: e })) ); document.body.addEventListener("keydown", function (ev) { if (ev.shiftKey) { switch (ev.keyCode) { // SHIFT+C = Toggle checkboxes case 67: { document.body.classList.toggle("bgus_cbox"); document .querySelectorAll(".bgus_checkbox:checked") .forEach((el) => { el.checked = false; }); return; } // SHIFT+B = Set whole size (beaker?) for parts/units case 66: { let size = parseInt(prompt("Write target ml (0 to reset)", "90")); if (isNaN(size) || size <= 0) { // Reset to parts/unit document .querySelectorAll(".bgus_part_label") .forEach((el) => (el.innerHTML = el.dataset.src)); return; } setPartSize( document.querySelectorAll("td > .bgus_part > .bgus_part_label"), size ); return; } } } }); // Everything is loaded, show welcome message statusMessage.innerHTML = ` <table style="background-color: rgb(255, 248, 219); margin-bottom:10px;" width="95%" align="center"> <tbody><tr><td style="padding:5px 10px;"> <h3>Better Guides (Chemistry) <small>${VERSION}</small></h3> <p>These commands are available:</p> <ul> <li><b>SHIFT + S</b> Fuzzy search recipes (navigate with up/down arrow keys, hide with RETURN/ESC)</li> <li><b>SHIFT + C</b> Toggle checkboxes</li> <li><b>SHIFT + B</b> Convert parts/units to ml (given target/beaker size), beware: inaccurate</li> </ul> </td></tr></tbody> </table> `; } function betterGeneric() { // Everything is loaded, show welcome message statusMessage.innerHTML = ` <table style="background-color: rgb(255, 248, 219); margin-bottom:10px;" width="95%" align="center"> <tbody><tr><td style="padding:5px 10px;"> <h3>Better Guides (generic) <small>${VERSION}</small></h3> <p>These commands are available:</p> <ul> <li><b>SHIFT + S</b> Fuzzy search (navigate with up/down arrow keys, hide with RETURN/ESC)</li> </ul> </td></tr></tbody> </table> `; const el = Array.from(document.querySelectorAll(".mw-headline")); const name = el.map((elem) => elem.innerText.trim()); // Init fuzzy search with headlines searchBox( el, name.map((e, i) => ({ id: i, str: e })) ); } window.requestAnimationFrame(() => { switch (location.pathname) { case "/wiki/Guide_to_chemistry": betterChemistry(); break; default: betterGeneric(); break; } }); })();