diff --git a/static/add_instance_flow.mts b/static/add_instance_flow.mts index 6f22ae5..522a1cc 100644 --- a/static/add_instance_flow.mts +++ b/static/add_instance_flow.mts @@ -39,6 +39,7 @@ export class AddInstanceFlow { hostSecure: secure, software: "", iconURL: null, + preferredFor: [] }; try { diff --git a/static/config.html b/static/config.html index 6066eb4..ac206fb 100644 --- a/static/config.html +++ b/static/config.html @@ -101,9 +101,9 @@ Unchecking this is not recommended, and this option only exists for exceptional

- +

diff --git a/static/config.mts b/static/config.mts index 6f807b2..86015d5 100644 --- a/static/config.mts +++ b/static/config.mts @@ -1,6 +1,5 @@ -import { parseHost } from "./add_an_instance.mjs"; import { AddInstanceFlow } from "./add_instance_flow.mjs"; -import { dialogDetailsFromInstance, dialogDetailsToInstance, InstanceDetailsDialog, InstanceDetailsDialogData } from "./confirm_instance_details.mjs"; +import { dialogDetailsFromInstance, dialogDetailsToInstance, InstanceDetailsDialog } from "./confirm_instance_details.mjs"; import { findButtonOrFail, findDialogOrFail, findOlOrFail } from "./dom.mjs"; import storageManager, { Instance } from "./storage_manager.mjs"; diff --git a/static/confirm_instance_details.mts b/static/confirm_instance_details.mts index 689877a..ff3a273 100644 --- a/static/confirm_instance_details.mts +++ b/static/confirm_instance_details.mts @@ -2,7 +2,7 @@ import { parseHost } from "./add_an_instance.mjs"; import { FormDialog, ONCE } from "./dialog.mjs"; -import { findButtonOrFail, findFormOrFail, findImageOrFail, findInputOrFail, findSelectOrFail } from "./dom.mjs"; +import { findButtonOrFail, findFormOrFail, findImageOrFail, findInputOrFail, findOptionOrFail, findSelectOrFail } from "./dom.mjs"; import knownSoftware from "./known_software.mjs"; import { Instance } from "./storage_manager.mjs"; @@ -17,7 +17,8 @@ export type InstanceDetailsDialogData = { host: string, hostSecure: boolean, software: string, - iconURL: string | null + iconURL: string | null, + preferredFor: string[], }; export function dialogDetailsFromInstance(instance: Instance): InstanceDetailsDialogData { @@ -27,7 +28,8 @@ export function dialogDetailsFromInstance(instance: Instance): InstanceDetailsDi host: host.host, hostSecure: host.secure, software: instance.software, - iconURL: instance.iconURL ?? null + iconURL: instance.iconURL ?? null, + preferredFor: instance.preferredFor, }; } @@ -36,6 +38,7 @@ export function dialogDetailsToInstance(data: InstanceDetailsDialogData, instanc instance.origin = mergeHost(data.host, data.hostSecure); instance.software = data.software; instance.iconURL = data.iconURL ?? undefined; + instance.preferredFor = data.preferredFor; return instance as Instance; } @@ -46,6 +49,11 @@ export class InstanceDetailsDialog extends FormDialog { protected instanceSoftware: HTMLSelectElement; protected instanceIcon: HTMLImageElement; protected closeButton: HTMLButtonElement; + protected defaultsList?: { + list: HTMLSelectElement, + removeButton: HTMLButtonElement, + noDefaultsOption: HTMLOptionElement, + }; constructor(dialog: HTMLDialogElement, initializeDOM: boolean = true) { super(dialog, findFormOrFail(dialog, ".instanceDetailsForm")); @@ -56,6 +64,15 @@ export class InstanceDetailsDialog extends FormDialog { this.instanceSoftware = findSelectOrFail(this.form, "#instanceSoftware"); this.instanceIcon = findImageOrFail(this.form, "#instanceIcon"); this.closeButton = findButtonOrFail(this.form, ".close"); + try { + this.defaultsList = { + list: findSelectOrFail(this.form, "#defaultsList"), + removeButton: findButtonOrFail(this.form, "#removeDefaults"), + noDefaultsOption: findOptionOrFail(this.form, "#noDefaults"), + }; + } catch { + this.defaultsList = undefined; + } if (initializeDOM) this.initializeDOM(); } @@ -71,6 +88,70 @@ export class InstanceDetailsDialog extends FormDialog { this.instanceIcon.src = blankImage; this.closeButton.addEventListener("click", e => this.close()); + + if (this.defaultsList) { + this.defaultsList.list.addEventListener("change", e => this.#handleListSelectionChange()); + this.defaultsList.removeButton.addEventListener("click", e => this.#removeSelectedListOptions()); + } + } + + #getRemainingListOptions(): string[] { + if (!this.defaultsList) return []; + + const items: string[] = []; + + for (const option of this.defaultsList.list.options) { + if (option == this.defaultsList.noDefaultsOption) continue; + items.push(option.value); + } + + return items; + } + + #removeSelectedListOptions() { + if (!this.defaultsList) return; + + // Copy using spread because this breaks when the list changes mid-iteration + for (const option of [...this.defaultsList.list.selectedOptions]) { + option.remove(); + } + + this.#handleListChange(); + } + + #populateDefaultsList(items: string[]) { + if (!this.defaultsList) return; + + while (this.defaultsList.list.children.length > 1) + this.defaultsList.list.removeChild(this.defaultsList.list.lastChild!); + + for (const item of items) { + const option = document.createElement("option"); + option.value = item; + option.innerText = knownSoftware.software[item]?.name ?? knownSoftware.groups[item]?.name ?? item; + this.defaultsList.list.appendChild(option); + } + + this.#handleListChange(); + } + + #handleListChange() { + this.#handleListEmpty(); + this.#handleListSelectionChange(); + } + + #handleListSelectionChange() { + if (!this.defaultsList) return; + this.defaultsList.removeButton.disabled = this.defaultsList.list.selectedOptions.length === 0; + } + + #handleListEmpty() { + if (!this.defaultsList) return; + if (this.defaultsList.list.children.length == 1) { + this.defaultsList.noDefaultsOption.removeAttribute("hidden"); + } else { + this.defaultsList.noDefaultsOption.setAttribute("hidden", ""); + } } #handleSubmit(data: InstanceDetailsDialogData, resolve: (data: InstanceDetailsDialogData) => void) { @@ -79,6 +160,7 @@ export class InstanceDetailsDialog extends FormDialog { data.host = this.instanceHost.value; data.hostSecure = this.instanceHostSecure.checked; data.software = this.instanceSoftware.value; + data.preferredFor = this.#getRemainingListOptions(); resolve(data); this.close(); @@ -91,6 +173,7 @@ export class InstanceDetailsDialog extends FormDialog { this.instanceHostSecure.checked = data.hostSecure; this.instanceSoftware.value = data.software; this.instanceIcon.src = data.iconURL ?? blankImage; + this.#populateDefaultsList(data.preferredFor); return new Promise((resolve, reject) => { this.cancelOnceClosed(reject); diff --git a/static/dom.mts b/static/dom.mts index 2c4267c..b0ec231 100644 --- a/static/dom.mts +++ b/static/dom.mts @@ -1,6 +1,13 @@ // I would've LOVED to use generics for this but unfortunately that's not possible. // Type safety, but at what cost... >~< thanks TypeScript +export function findOptionOrFail(on: Element, selector: string): HTMLOptionElement { + const element = on.querySelector(selector); + if (!(element instanceof HTMLOptionElement)) + throw new Error(`${selector} isn't an option`); + return element; +} + export function findOlOrFail(on: Element, selector: string): HTMLOListElement { const element = on.querySelector(selector); if (!(element instanceof HTMLOListElement))