// This file handles the "Confirm instance details" dialog import { parseHost } from "./add_an_instance.mjs"; import { FormDialog, ONCE } from "./dialog.mjs"; import { findButtonOrFail, findFormOrFail, findImageOrFail, findInputOrFail, findOptionOrFail, findSelectOrFail } from "./dom.mjs"; import knownSoftware from "./known_software.mjs"; import { Instance } from "./storage_manager.mjs"; const blankImage = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; export function mergeHost(host: string, secure: boolean): string { return `http${secure ? "s" : ""}://${host}`; } export type InstanceDetailsDialogData = { name: string, host: string, hostSecure: boolean, software: string, iconURL: string | null, preferredFor: string[], }; export function dialogDetailsFromInstance(instance: Instance): InstanceDetailsDialogData { const host = parseHost(instance.origin)!; return { name: instance.name, host: host.host, hostSecure: host.secure, software: instance.software, iconURL: instance.iconURL ?? null, preferredFor: instance.preferredFor, }; } export function dialogDetailsToInstance(data: InstanceDetailsDialogData, instance: Partial): Instance { instance.name = data.name; instance.origin = mergeHost(data.host, data.hostSecure); instance.software = data.software; instance.iconURL = data.iconURL ?? undefined; instance.preferredFor = data.preferredFor; return instance as Instance; } export class InstanceDetailsDialog extends FormDialog { protected instanceName: HTMLInputElement; protected instanceHost: HTMLInputElement; protected instanceHostSecure: HTMLInputElement; 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")); this.instanceName = findInputOrFail(this.form, "#instanceName"); this.instanceHost = findInputOrFail(this.form, "#instanceHost"); this.instanceHostSecure = findInputOrFail(this.form, "#instanceHostSecure"); 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(); } protected override initializeDOM() { super.initializeDOM(); for (const [name, software] of Object.entries(knownSoftware.software)) { const option = new Option(software.name, name); this.instanceSoftware.appendChild(option); } 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) { this.form.addEventListener("submit", e => { data.name = this.instanceName.value; data.host = this.instanceHost.value; data.hostSecure = this.instanceHostSecure.checked; data.software = this.instanceSoftware.value; data.preferredFor = this.#getRemainingListOptions(); resolve(data); this.close(); }, ONCE); } async present(data: InstanceDetailsDialogData): Promise { this.instanceName.value = data.name; this.instanceHost.value = data.host; 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); this.#handleSubmit(data, resolve); this.open(); }); } }