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 {