diff --git a/blaseball-userscript-options.user.js b/blaseball-userscript-options.user.js index 922fd52..bd2fa58 100644 --- a/blaseball-userscript-options.user.js +++ b/blaseball-userscript-options.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Blaseball Userscript Options // @namespace https://glaceon.social/@monorail -// @version 0.4 +// @version 0.5 // @description Add a userscript options menu to the account settings page on the blaseball website. // @author monorail // @match https://blaseball.com/* @@ -11,7 +11,7 @@ // @updateURL https://git.hollymcfarland.com/monorail/blaseball-userscript-options/raw/branch/main/blaseball-userscript-options.user.js // ==/UserScript== -document._BLASEBALL_USERSCRIPT_OPTIONS_VERSION = "0.4"; +document._BLASEBALL_USERSCRIPT_OPTIONS_VERSION = "0.5"; (function() { "use strict"; @@ -25,6 +25,12 @@ document._BLASEBALL_USERSCRIPT_OPTIONS_VERSION = "0.4"; } const namespaceObj = JSON.parse((storage.getItem(namespace)) ?? "{}"); + + if (!("_hidden" in namespaceObj)) { + // Include an internal option called "_hidden" for every script that determines if the options + // should be shown or not + namespaceObj["_hidden"] = {value: true, type: "checkbox"}; + } if (!(option in namespaceObj)) { namespaceObj[option] = {value: defaultValue, type: type}; } @@ -52,22 +58,95 @@ document._BLASEBALL_USERSCRIPT_OPTIONS_VERSION = "0.4"; optionsHeader.innerText = "Userscript Options"; for (const namespace of Object.keys(namespaces)) { + const namespaceObj = JSON.parse(storage.getItem(namespace) ?? "{}"); + + const hidden = namespaceObj["_hidden"].value; + const title = document.createElement("h3"); - title.innerText = namespace; - settingsNode.appendChild(title); + title.innerText = `${namespace} ${hidden ? "▸" : "▾"}`; + const optionsDiv = document.createElement("div"); + optionsDiv.classList.add("_script_options"); + if (hidden) { + optionsDiv.classList.add("_hidden"); + } - const namespaceObj = JSON.parse(storage.getItem(namespace) ?? "{}"); + // This inner div is used so that we know how tall the outer div + // should be when expanding, it has no other purpose + const staticDiv = document.createElement("div"); + + title.addEventListener('click', function() { + const hidden = !document._BLASEBALL_USERSCRIPT_OPTIONS_GET(namespace, "_hidden"); + document._BLASEBALL_USERSCRIPT_OPTIONS_SET(namespace, "_hidden", hidden); + this.innerText = `${namespace} ${hidden ? "▸" : "▾"}`; + + // This next part is a bit cursed, and it took me a long time to figure out. + // Each script keeps its options inside of two divs, like this: + + //

Script Name

+ //
+ //
+ // + // + //
+ //
+ + // The reason for this is that I want to animate "outer" smoothly between a + // height of 0 and `auto`. But you can't transition to or from `auto`, only + // numeric values. "inner" exists only to be the same size as "outer" would be + // if its height was set to `auto`. Using Javascript, "outer" is given an + // inline height of `${inner.clientHeight}px`. The "_hidden" class sets height + // to 0, and overrides the inline style with `!important`. So by adding and + // removing that class, the animation moves between a kind of "fake auto" an 0. + + // In principle, this works, but there's one problem: At the time "outer" and + // "inner" are created, "inner" doesn't yet have any height because it hasn't + // been rendered. Instead, we have to set the inline style on "outer" at some + // other time: Right now, inside the click handler. This *almost* works. + + // Because the inline style is set and the class is added in such a short time, + // the browser doesn't actually handle the CSS in between those events. It + // tries to optimize by only waking up the CSS engine after both things are + // done. But that's a problem, because the CSS engine doesn't see "this + // element has an inline height of [whatever] that's being overwritten by a + // class with 0 height". It only sees that it currently has a height of 0, and + // last time it looked, it had a height of `auto`. It can't animate between + // those, so in the case of the first click being to close the div, the + // animation doesn't play. + + // That's why the following code has `setTimeout(() => {...}, 0)`. Naively, + // this seems unecessary. But by telling the browser to run that function "in + // the future" (even though it will actually be right away), it doesn't try to + // wait for a more optimal time to process CSS. It immediately handles the + // inline style, which has no visible effect, but it now knows that element + // has a numeric height and can be animated when it later processes the added + // class. + + const optionsDiv = this.nextSibling; + optionsDiv.style.height = `${optionsDiv.children[0].clientHeight}px`; + + setTimeout(() => { + if (hidden) { + optionsDiv.classList.add("_hidden"); + } else { + optionsDiv.classList.remove("_hidden"); + } + }, 0); + }) + + settingsNode.appendChild(title); + settingsNode.appendChild(optionsDiv); + optionsDiv.appendChild(staticDiv); for (const option of namespaces[namespace]) { const isCheckbox = namespaceObj[option].type === "checkbox"; const newDiv = document.createElement("div"); - settingsNode.appendChild(newDiv); + newDiv.classList.add("_script_option"); + staticDiv.appendChild(newDiv); const label = document.createElement("label"); label.for = `${namespace} - ${option}`; label.innerText = option; - label.style.float = "left"; newDiv.appendChild(label); const input = document.createElement("input"); @@ -78,8 +157,6 @@ document._BLASEBALL_USERSCRIPT_OPTIONS_VERSION = "0.4"; input.value = namespaceObj[option].value; } input.name = `${namespace} - ${option}`; - input.style.float = "right"; - input.style.backgroundColor = "white"; newDiv.appendChild(input); const valueMethods = { @@ -107,6 +184,45 @@ document._BLASEBALL_USERSCRIPT_OPTIONS_VERSION = "0.4"; }); }; + function applyCSS() { + const head = document.head || document.getElementsByTagName("head")[0]; + const css = ` + ._script_option { + overflow: hidden; + } + + /* all script options except first in each block */ + ._script_option + ._script_option { + margin-top: 1rem; + } + + ._script_option > label { + float: left; + } + + ._script_option > input { + float: right; + background-color: white; + } + + ._script_options { + overflow: hidden; + + transition: height 0.2s; + } + + ._hidden { + height: 0 !important; + } + `; + const style = document.createElement("style"); + style.id = "_blaseball-userscript-options_style"; + + head.appendChild(style); + style.appendChild(document.createTextNode(css)); + } + applyCSS(); + if (document.hasOwnProperty("_BLASEBALL_USERSCRIPT_REGISTER")) { document._BLASEBALL_USERSCRIPT_REGISTER("Userscript Options", callback, (mutations) => (document.querySelector(".account__content"))); } else {