ruffle/web/packages/extension/src/common.ts

173 lines
4.7 KiB
TypeScript
Raw Normal View History

2021-03-09 19:51:03 +00:00
import * as utils from "./utils";
import type { BaseLoadOptions, Duration } from "ruffle-core";
2021-03-09 19:51:03 +00:00
export interface Options extends BaseLoadOptions {
ruffleEnable: boolean;
ignoreOptout: boolean;
autostart: boolean;
2021-03-09 19:51:03 +00:00
}
2021-09-25 10:02:01 +00:00
interface OptionElement<T> {
readonly input: Element;
readonly label: HTMLLabelElement;
value: T;
}
class CheckboxOption implements OptionElement<boolean> {
2023-02-20 14:20:50 +00:00
constructor(
private readonly checkbox: HTMLInputElement,
readonly label: HTMLLabelElement
) {}
2021-09-25 10:02:01 +00:00
get input() {
return this.checkbox;
}
get value() {
return this.checkbox.checked;
}
set value(value: boolean) {
this.checkbox.checked = value;
}
}
/**
* Number input option for 'maxExecutionDuration'.
*/
class MaxExecutionDurationOption implements OptionElement<Duration> {
constructor(
private readonly numberInput: HTMLInputElement,
readonly label: HTMLLabelElement
) {}
get input() {
return this.numberInput;
}
get value() {
return Math.max(1, Math.round(this.numberInput.valueAsNumber));
}
set value(value: Duration) {
// Ignoring the 'nanos' part from the old format type ObsoleteDuration.
const newValue = typeof value === "number" ? value : value.secs;
this.numberInput.value = newValue.toString();
}
}
2021-09-25 10:02:01 +00:00
class SelectOption implements OptionElement<string> {
2023-02-20 14:20:50 +00:00
constructor(
private readonly select: HTMLSelectElement,
readonly label: HTMLLabelElement
) {}
2021-09-25 10:02:01 +00:00
get input() {
return this.select;
}
get value() {
const index = this.select.selectedIndex;
2023-02-20 14:20:50 +00:00
const option = this.select.options[index]!;
2021-09-25 10:02:01 +00:00
return option.value;
}
set value(value: string) {
const options = Array.from(this.select.options);
const index = options.findIndex((option) => option.value === value);
this.select.selectedIndex = index;
}
}
function getElement(option: Element): OptionElement<unknown> {
2023-02-20 14:20:50 +00:00
const label = option.getElementsByTagName("label")[0]!;
2021-09-25 10:02:01 +00:00
const [input] = option.getElementsByTagName("input");
if (input) {
if (input.type === "checkbox") {
return new CheckboxOption(input, label);
}
if (input.type === "number") {
if (input.id === "max_execution_duration") {
return new MaxExecutionDurationOption(input, label);
}
}
2021-09-25 10:02:01 +00:00
}
const [select] = option.getElementsByTagName("select");
if (select) {
return new SelectOption(select, label);
}
throw new Error("Unknown option element");
}
function findOptionElements() {
const camelize = (s: string) =>
s.replace(/[^a-z\d](.)/gi, (_, char) => char.toUpperCase());
2021-09-25 10:02:01 +00:00
const elements = new Map<keyof Options, OptionElement<unknown>>();
2021-03-09 19:51:03 +00:00
for (const option of document.getElementsByClassName("option")) {
2021-09-25 10:02:01 +00:00
const element = getElement(option);
const key = camelize(element.input.id) as keyof Options;
elements.set(key, element);
2021-03-09 19:51:03 +00:00
}
return elements;
}
2021-09-25 10:02:01 +00:00
export async function bindOptions(
onChange?: (options: Options) => void
): Promise<void> {
2021-09-25 10:02:01 +00:00
const elements = findOptionElements();
const options = await utils.getOptions();
2021-03-09 19:51:03 +00:00
2021-09-25 10:02:01 +00:00
for (const [key, element] of elements.entries()) {
// Bind initial value.
2021-09-25 10:02:01 +00:00
element.value = options[key];
// Prevent transition on load.
// Method from https://stackoverflow.com/questions/11131875.
2021-09-25 10:02:01 +00:00
element.label.classList.add("notransition");
element.label.offsetHeight; // Trigger a reflow, flushing the CSS changes.
element.label.classList.remove("notransition");
2021-03-09 19:51:03 +00:00
// Localize label.
2021-09-25 10:02:01 +00:00
const message = utils.i18n.getMessage(`settings_${element.input.id}`);
if (message) {
2021-09-25 10:02:01 +00:00
element.label.textContent = message;
}
// Listen for user input.
2021-09-25 10:02:01 +00:00
element.input.addEventListener("change", () => {
const value = element.value;
options[key] = value as never;
utils.storage.sync.set({ [key]: value });
});
2021-03-09 19:51:03 +00:00
}
// Listen for future changes.
utils.storage.onChanged.addListener((changes, namespace) => {
2021-03-09 19:51:03 +00:00
if (namespace !== "sync") {
return;
}
for (const [key, option] of Object.entries(changes)) {
const element = elements.get(key as keyof Options);
if (!element) {
2021-03-09 19:51:03 +00:00
continue;
}
2021-09-25 10:02:01 +00:00
element.value = option.newValue;
options[key as keyof Options] = option.newValue as never;
2021-03-09 19:51:03 +00:00
}
if (onChange) {
onChange(options);
}
});
if (onChange) {
onChange(options);
}
}