add ability to collapse the options for each userscript

main
Holly McFarland 4 months ago
parent be7fb86d4d
commit 7c17241a03

@ -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:
// <h3>Script Name</h3>
// <div> <!-- "outer" -->
// <div> <!-- "inner" -->
// <!-- Option 1 -->
// <!-- Option 2 -->
// </div>
// </div>
// 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 {

Loading…
Cancel
Save