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))