From f451b1fbc343da35ff5e16c92c42f49422b14ea7 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Mon, 3 Feb 2025 17:23:45 +0100 Subject: [PATCH] Rewrite InstanceDetailsDialog Also misc cleanup. Almost done with this --- static/add_an_instance.mts | 21 ++--- static/add_instance_flow.mts | 72 ++++++++-------- static/config.html | 3 +- static/config.mts | 25 ++---- static/confirm_instance_details.mts | 124 +++++++++++++--------------- static/crossroad.mts | 35 ++++---- static/dialog.mts | 5 ++ 7 files changed, 140 insertions(+), 145 deletions(-) diff --git a/static/add_an_instance.mts b/static/add_an_instance.mts index 4086f8e..bbb54de 100644 --- a/static/add_an_instance.mts +++ b/static/add_an_instance.mts @@ -14,7 +14,7 @@ export function parseHost(host: string): { host: string, secure: boolean } | nul }; } -type AddInstanceDialogData = { +export type AddInstanceDialogData = { host: string, secure: boolean, autoQueryMetadata: boolean, @@ -27,6 +27,7 @@ export class AddInstanceDialog extends FormDialog { constructor(dialog: HTMLDialogElement, initializeDOM: boolean = true) { super(dialog, findFormOrFail(dialog, ".addInstanceForm")); + this.instanceHost = findInputOrFail(this.form, "#instanceHost"); this.autoQueryMetadata = findInputOrFail(this.form, "#autoQueryMetadata"); this.closeButton = findButtonOrFail(this.form, ".close"); @@ -34,6 +35,13 @@ export class AddInstanceDialog extends FormDialog { if (initializeDOM) this.initializeDOM(); } + protected override initializeDOM() { + super.initializeDOM(); + + this.instanceHost.addEventListener("input", e => this.#getDataIfValid()); + this.closeButton.addEventListener("click", e => this.close()); + } + #getDataIfValid(): AddInstanceDialogData | null { const parsedHost = parseHost(this.instanceHost.value); if (parsedHost === null) { @@ -62,16 +70,9 @@ export class AddInstanceDialog extends FormDialog { }, ONCE); } - protected override initializeDOM() { - super.initializeDOM(); - - this.instanceHost.addEventListener("input", e => this.#getDataIfValid()); - this.closeButton.addEventListener("click", e => this.close()); - } - - async prompt(): Promise { + async present(): Promise { return new Promise((resolve, reject) => { - this.dialog.addEventListener("close", e => reject(), ONCE); + this.cancelOnceClosed(reject); this.#handleSubmit(resolve); this.open(); }); diff --git a/static/add_instance_flow.mts b/static/add_instance_flow.mts index 1877a87..e59c6c9 100644 --- a/static/add_instance_flow.mts +++ b/static/add_instance_flow.mts @@ -1,56 +1,45 @@ import { AddInstanceDialog } from "./add_an_instance.mjs"; -import { initializeInstanceDetailsDialog } from "./confirm_instance_details.mjs"; +import { InstanceDetailsDialog, InstanceDetailsDialogData } from "./confirm_instance_details.mjs"; import { Dialog } from "./dialog.mjs"; import storageManager, { Instance } from "./storage_manager.mjs"; export class AddInstanceFlow { addDialog: AddInstanceDialog; spinnerDialog: Dialog; - detailsDialog: HTMLDialogElement; + detailsDialog: InstanceDetailsDialog; constructor( addDialog: AddInstanceDialog | HTMLDialogElement, spinnerDialog: HTMLDialogElement, - detailsDialog: HTMLDialogElement, + detailsDialog: InstanceDetailsDialog | HTMLDialogElement, ) { if (addDialog instanceof AddInstanceDialog) this.addDialog = addDialog; else this.addDialog = new AddInstanceDialog(addDialog, true); + this.spinnerDialog = new Dialog(spinnerDialog); - this.detailsDialog = detailsDialog; + + if (detailsDialog instanceof InstanceDetailsDialog) + this.detailsDialog = detailsDialog; + else + this.detailsDialog = new InstanceDetailsDialog(detailsDialog, true); } async start() { - const instanceDetailsDialogCallback = ( - name: string, - host: string, - hostSecure: boolean, - software: string, - icon: string | null - ) => { - const instance: Instance = { - name, - origin: `http${hostSecure ? "s" : ""}://${host}`, - software, - iconURL: icon ?? undefined - }; - storageManager.storage.instances.push(instance); - storageManager.save(); - console.log("Successfully added new instance:", instance); - }; - - const { - showInstanceDetailsDialog, - hideInstanceDetailsDialog, - populateInstanceDetailsDialog - } = initializeInstanceDetailsDialog(this.detailsDialog, instanceDetailsDialogCallback); - const { autoQueryMetadata, host, secure, - } = await this.addDialog.prompt(); + } = await this.addDialog.present(); + + let detailsDialogData: InstanceDetailsDialogData = { + name: host, + host, + hostSecure: secure, + software: "", + iconURL: null, + }; try { if (!autoQueryMetadata) throw null; // Skip to catch block @@ -67,12 +56,23 @@ export class AddInstanceFlow { ) throw new Error("Invalid API response"); - populateInstanceDetailsDialog(name, host, secure, software, iconURL as string | null); - } catch { - populateInstanceDetailsDialog(host, host, secure, "", null); - } finally { - this.spinnerDialog.close(); - showInstanceDetailsDialog(); - } + detailsDialogData.name = name; + detailsDialogData.software = software; + detailsDialogData.iconURL = iconURL as string | null; + } catch { } + this.spinnerDialog.close(); + + detailsDialogData = await this.detailsDialog.present(detailsDialogData); + + const instance: Instance = { + name: detailsDialogData.name, + origin: `http${detailsDialogData.hostSecure ? "s" : ""}://${detailsDialogData.host}`, + software: detailsDialogData.software, + iconURL: detailsDialogData.iconURL ?? undefined + }; + + storageManager.storage.instances.push(instance); + storageManager.save(); + console.log("Successfully added new instance:", instance); } } diff --git a/static/config.html b/static/config.html index 868eba2..33297e0 100644 --- a/static/config.html +++ b/static/config.html @@ -25,7 +25,7 @@

    - +
    @@ -110,6 +110,7 @@ Unchecking this is not recommended, and this option only exists for exceptional + \ No newline at end of file diff --git a/static/config.mts b/static/config.mts index bb7e3d7..8a1c257 100644 --- a/static/config.mts +++ b/static/config.mts @@ -1,6 +1,6 @@ import { parseHost } from "./add_an_instance.mjs"; -import { initializeAddInstanceFlow } from "./add_instance_flow.mjs"; -import { initializeInstanceDetailsDialog } from "./confirm_instance_details.mjs"; +import { AddInstanceFlow } from "./add_instance_flow.mjs"; +import { InstanceDetailsDialog } from "./confirm_instance_details.mjs"; import { findButtonOrFail, findDialogOrFail, findOlOrFail } from "./dom.mjs"; import storageManager from "./storage_manager.mjs"; @@ -9,14 +9,18 @@ let reordering = false; let elementBeingDragged: HTMLLIElement | undefined; const mainDialog = findDialogOrFail(document.body, "#mainDialog"); -const showAddInstanceDialogButton = findButtonOrFail(document.body, "#showAddInstanceDialog"); -const detailsDialog = findDialogOrFail(document.body, "#instanceDetails"); +const startAddInstanceFlowButton = findButtonOrFail(document.body, "#startAddInstanceFlow"); const addDialog = findDialogOrFail(document.body, "#addInstance"); +const spinnerDialog = findDialogOrFail(document.body, "#spinner"); +const detailsDialog = findDialogOrFail(document.body, "#instanceDetails"); const instanceList = findOlOrFail(document.body, "#instanceList"); const saveButton = findButtonOrFail(document.body, "#save"); const reorderButton = findButtonOrFail(document.body, "#reorder"); -showAddInstanceDialogButton.addEventListener("click", e => showAddInstanceDialog()); +let instanceDetailsDialog = new InstanceDetailsDialog(detailsDialog, true); +let addInstanceFlow = new AddInstanceFlow(addDialog, spinnerDialog, instanceDetailsDialog); + +startAddInstanceFlowButton.addEventListener("click", e => addInstanceFlow.start()); saveButton.addEventListener("click", e => { storageManager.save(); @@ -29,17 +33,6 @@ reorderButton.addEventListener("click", () => { reorderButton.innerText = reordering ? "Finish reordering" : "Reorder"; }); -const { - showInstanceDetailsDialog, - hideInstanceDetailsDialog, - populateInstanceDetailsDialog, -} = initializeInstanceDetailsDialog(detailsDialog, () => { }); - -const { - showAddInstanceDialog, - hideAddInstanceDialog -} = initializeAddInstanceFlow(detailsDialog, addDialog); - updateInstanceList(); storageManager.addSaveCallback(updateInstanceList); diff --git a/static/confirm_instance_details.mts b/static/confirm_instance_details.mts index e376fee..530c1f6 100644 --- a/static/confirm_instance_details.mts +++ b/static/confirm_instance_details.mts @@ -1,81 +1,75 @@ // This file handles the "Confirm instance details" dialog +import { FormDialog, ONCE } from "./dialog.mjs"; import { findButtonOrFail, findFormOrFail, findImageOrFail, findInputOrFail, findSelectOrFail } from "./dom.mjs"; import knownSoftware from "./known_software.mjs"; const blankImage = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; -export function initializeInstanceDetailsDialog( - dialog: HTMLDialogElement, - callback: ( - instanceName: string, - instanceHost: string, - instanceHostSecure: boolean, - instanceSoftware: string, - instanceIcon: string | null - ) => void -): { - showInstanceDetailsDialog: () => void, - hideInstanceDetailsDialog: () => void, - populateInstanceDetailsDialog: ( - instanceNameValue: string, - instanceHostValue: string, - instanceHostSecureValue: boolean, - instanceSoftwareValue: string, - instanceIconValue: string | null - ) => void -} { - const showInstanceDetailsDialog = () => dialog.showModal(); - const hideInstanceDetailsDialog = () => dialog.close(); +export type InstanceDetailsDialogData = { + name: string, + host: string, + hostSecure: boolean, + software: string, + iconURL: string | null +}; - const form = findFormOrFail(dialog, ".instanceDetailsForm"); - const instanceName = findInputOrFail(form, "#instanceName"); - const instanceHost = findInputOrFail(form, "#instanceHost"); - const instanceHostSecure = findInputOrFail(form, "#instanceHostSecure"); - const instanceSoftware = findSelectOrFail(form, "#instanceSoftware"); - const instanceIcon = findImageOrFail(form, "#instanceIcon"); - const closeButton = findButtonOrFail(form, ".close"); +export class InstanceDetailsDialog extends FormDialog { + protected instanceName: HTMLInputElement; + protected instanceHost: HTMLInputElement; + protected instanceHostSecure: HTMLInputElement; + protected instanceSoftware: HTMLSelectElement; + protected instanceIcon: HTMLImageElement; + protected closeButton: HTMLButtonElement; - for (const [name, software] of Object.entries(knownSoftware.software)) { - const option = new Option(software.name, name); - instanceSoftware.appendChild(option); + 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"); + + if (initializeDOM) this.initializeDOM(); } - instanceIcon.src = blankImage; + protected override initializeDOM() { + super.initializeDOM(); - const populateInstanceDetailsDialog = ( - instanceNameValue: string, - instanceHostValue: string, - instanceHostSecureValue: boolean, - instanceSoftwareValue: string, - instanceIconValue: string | null - ) => { - instanceName.value = instanceNameValue; - instanceHost.value = instanceHostValue; - instanceHostSecure.checked = instanceHostSecureValue; - instanceSoftware.value = instanceSoftwareValue; - instanceIcon.src = instanceIconValue ?? blankImage; - }; + for (const [name, software] of Object.entries(knownSoftware.software)) { + const option = new Option(software.name, name); + this.instanceSoftware.appendChild(option); + } - form.addEventListener("submit", e => { - callback( - instanceName.value, - instanceHost.value, - instanceHostSecure.checked, - instanceSoftware.value, - instanceIcon.src - ); - form.reset(); - }); + this.instanceIcon.src = blankImage; - closeButton.addEventListener("click", e => { - instanceIcon.src = blankImage; - hideInstanceDetailsDialog(); - }); + this.closeButton.addEventListener("click", e => this.close()); + } - return { - showInstanceDetailsDialog, - hideInstanceDetailsDialog, - populateInstanceDetailsDialog - }; + #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; + 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; + + return new Promise((resolve, reject) => { + this.cancelOnceClosed(reject); + this.#handleSubmit(data, resolve); + this.open(); + }); + } } diff --git a/static/crossroad.mts b/static/crossroad.mts index 761e824..bcab3c1 100644 --- a/static/crossroad.mts +++ b/static/crossroad.mts @@ -3,9 +3,10 @@ import { findButtonOrFail, findDialogOrFail, findFormOrFail, findInputOrFail, fi import knownSoftware from "./known_software.mjs"; import storageManager from "./storage_manager.mjs"; -const radioButtonName = "instanceSelect"; +const RADIO_BUTTON_NAME = "instanceSelect"; let addInstanceFlow: AddInstanceFlow | undefined; + const mainDialog = findDialogOrFail(document.body, "#mainDialog"); const startAddInstanceFlowButton = findButtonOrFail(document.body, "#startAddInstanceFlow"); const addDialog = findDialogOrFail(document.body, "#addInstance"); @@ -16,20 +17,6 @@ const redirectButton = findButtonOrFail(document.body, "#redirect"); const redirectAlwaysButton = findButtonOrFail(document.body, "#redirectAlways"); const pathText = findPreOrFail(document.body, "#path"); -startAddInstanceFlowButton.addEventListener("click", e => addInstanceFlow?.start()); - -redirectButton.addEventListener("click", e => { - // Can be assumed to not fail because the button is disabled if there are no options and the first one is selected by default - redirect(getSelectedOption()!); -}); - -redirectAlwaysButton.addEventListener("click", e => { - // Can be assumed to not fail because the button is disabled if there are no options and the first one is selected by default - const option = getSelectedOption()!; - setAutoRedirect(option); - redirect(option); -}); - // Don't bother initializing if we're performing autoredirect if (!autoRedirect()) { createInstanceSelectOptions(); @@ -44,6 +31,20 @@ if (!autoRedirect()) { mainDialog.show(); }; +startAddInstanceFlowButton.addEventListener("click", e => addInstanceFlow?.start()); + +redirectButton.addEventListener("click", e => { + // Can be assumed to not fail because the button is disabled if there are no options and the first one is selected by default + redirect(getSelectedOption()!); +}); + +redirectAlwaysButton.addEventListener("click", e => { + // Can be assumed to not fail because the button is disabled if there are no options and the first one is selected by default + const option = getSelectedOption()!; + setAutoRedirect(option); + redirect(option); +}); + function updateNoInstanceHint() { findParagraphOrFail(document.body, "#no-instance").style.display = storageManager.storage.instances.length > 0 @@ -60,7 +61,7 @@ function createInstanceSelectOptions() { radio.id = instance.origin; radio.value = instance.origin; radio.type = "radio"; - radio.name = radioButtonName; + radio.name = RADIO_BUTTON_NAME; const label = document.createElement("label"); label.htmlFor = instance.origin; label.innerText = instance.name + " "; @@ -107,7 +108,7 @@ function getTargetPath(): string { function getSelectedOption(): string | null { try { - return findInputOrFail(instanceSelectForm, `input[name="${radioButtonName}"]:checked`).value; + return findInputOrFail(instanceSelectForm, `input[name="${RADIO_BUTTON_NAME}"]:checked`).value; } catch { return null; } diff --git a/static/dialog.mts b/static/dialog.mts index 84dc3f1..6704daf 100644 --- a/static/dialog.mts +++ b/static/dialog.mts @@ -1,4 +1,5 @@ export const ONCE = { once: true }; +export const CANCELLED = Symbol("Cancelled"); export class Dialog { protected dialog: HTMLDialogElement; @@ -19,6 +20,10 @@ export class Dialog { close() { this.dialog.close(); } + + protected cancelOnceClosed(reject: (reason?: any) => void) { + this.dialog.addEventListener("close", e => reject(CANCELLED), ONCE); + } }; export class FormDialog extends Dialog {