Move everything under public

This commit is contained in:
huchenlei
2024-06-13 15:59:56 -04:00
parent e0850bb82c
commit e50259cefb
23 changed files with 57671 additions and 8 deletions

View File

@@ -1,32 +0,0 @@
import { $el } from "../ui.js";
export class ComfyDialog {
constructor() {
this.element = $el("div.comfy-modal", { parent: document.body }, [
$el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]),
]);
}
createButtons() {
return [
$el("button", {
type: "button",
textContent: "Close",
onclick: () => this.close(),
}),
];
}
close() {
this.element.style.display = "none";
}
show(html) {
if (typeof html === "string") {
this.textElement.innerHTML = html;
} else {
this.textElement.replaceChildren(html);
}
this.element.style.display = "flex";
}
}

View File

@@ -1,287 +0,0 @@
// @ts-check
/*
Original implementation:
https://github.com/TahaSh/drag-to-reorder
MIT License
Copyright (c) 2023 Taha Shashtari
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { $el } from "../ui.js";
$el("style", {
parent: document.head,
textContent: `
.draggable-item {
position: relative;
will-change: transform;
user-select: none;
}
.draggable-item.is-idle {
transition: 0.25s ease transform;
}
.draggable-item.is-draggable {
z-index: 10;
}
`
});
export class DraggableList extends EventTarget {
listContainer;
draggableItem;
pointerStartX;
pointerStartY;
scrollYMax;
itemsGap = 0;
items = [];
itemSelector;
handleClass = "drag-handle";
off = [];
offDrag = [];
constructor(element, itemSelector) {
super();
this.listContainer = element;
this.itemSelector = itemSelector;
if (!this.listContainer) return;
this.off.push(this.on(this.listContainer, "mousedown", this.dragStart));
this.off.push(this.on(this.listContainer, "touchstart", this.dragStart));
this.off.push(this.on(document, "mouseup", this.dragEnd));
this.off.push(this.on(document, "touchend", this.dragEnd));
}
getAllItems() {
if (!this.items?.length) {
this.items = Array.from(this.listContainer.querySelectorAll(this.itemSelector));
this.items.forEach((element) => {
element.classList.add("is-idle");
});
}
return this.items;
}
getIdleItems() {
return this.getAllItems().filter((item) => item.classList.contains("is-idle"));
}
isItemAbove(item) {
return item.hasAttribute("data-is-above");
}
isItemToggled(item) {
return item.hasAttribute("data-is-toggled");
}
on(source, event, listener, options) {
listener = listener.bind(this);
source.addEventListener(event, listener, options);
return () => source.removeEventListener(event, listener);
}
dragStart(e) {
if (e.target.classList.contains(this.handleClass)) {
this.draggableItem = e.target.closest(this.itemSelector);
}
if (!this.draggableItem) return;
this.pointerStartX = e.clientX || e.touches[0].clientX;
this.pointerStartY = e.clientY || e.touches[0].clientY;
this.scrollYMax = this.listContainer.scrollHeight - this.listContainer.clientHeight;
this.setItemsGap();
this.initDraggableItem();
this.initItemsState();
this.offDrag.push(this.on(document, "mousemove", this.drag));
this.offDrag.push(this.on(document, "touchmove", this.drag, { passive: false }));
this.dispatchEvent(
new CustomEvent("dragstart", {
detail: { element: this.draggableItem, position: this.getAllItems().indexOf(this.draggableItem) },
})
);
}
setItemsGap() {
if (this.getIdleItems().length <= 1) {
this.itemsGap = 0;
return;
}
const item1 = this.getIdleItems()[0];
const item2 = this.getIdleItems()[1];
const item1Rect = item1.getBoundingClientRect();
const item2Rect = item2.getBoundingClientRect();
this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top);
}
initItemsState() {
this.getIdleItems().forEach((item, i) => {
if (this.getAllItems().indexOf(this.draggableItem) > i) {
item.dataset.isAbove = "";
}
});
}
initDraggableItem() {
this.draggableItem.classList.remove("is-idle");
this.draggableItem.classList.add("is-draggable");
}
drag(e) {
if (!this.draggableItem) return;
e.preventDefault();
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
const listRect = this.listContainer.getBoundingClientRect();
if (clientY > listRect.bottom) {
if (this.listContainer.scrollTop < this.scrollYMax) {
this.listContainer.scrollBy(0, 10);
this.pointerStartY -= 10;
}
} else if (clientY < listRect.top && this.listContainer.scrollTop > 0) {
this.pointerStartY += 10;
this.listContainer.scrollBy(0, -10);
}
const pointerOffsetX = clientX - this.pointerStartX;
const pointerOffsetY = clientY - this.pointerStartY;
this.updateIdleItemsStateAndPosition();
this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`;
}
updateIdleItemsStateAndPosition() {
const draggableItemRect = this.draggableItem.getBoundingClientRect();
const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2;
// Update state
this.getIdleItems().forEach((item) => {
const itemRect = item.getBoundingClientRect();
const itemY = itemRect.top + itemRect.height / 2;
if (this.isItemAbove(item)) {
if (draggableItemY <= itemY) {
item.dataset.isToggled = "";
} else {
delete item.dataset.isToggled;
}
} else {
if (draggableItemY >= itemY) {
item.dataset.isToggled = "";
} else {
delete item.dataset.isToggled;
}
}
});
// Update position
this.getIdleItems().forEach((item) => {
if (this.isItemToggled(item)) {
const direction = this.isItemAbove(item) ? 1 : -1;
item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`;
} else {
item.style.transform = "";
}
});
}
dragEnd() {
if (!this.draggableItem) return;
this.applyNewItemsOrder();
this.cleanup();
}
applyNewItemsOrder() {
const reorderedItems = [];
let oldPosition = -1;
this.getAllItems().forEach((item, index) => {
if (item === this.draggableItem) {
oldPosition = index;
return;
}
if (!this.isItemToggled(item)) {
reorderedItems[index] = item;
return;
}
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1;
reorderedItems[newIndex] = item;
});
for (let index = 0; index < this.getAllItems().length; index++) {
const item = reorderedItems[index];
if (typeof item === "undefined") {
reorderedItems[index] = this.draggableItem;
}
}
reorderedItems.forEach((item) => {
this.listContainer.appendChild(item);
});
this.items = reorderedItems;
this.dispatchEvent(
new CustomEvent("dragend", {
detail: { element: this.draggableItem, oldPosition, newPosition: reorderedItems.indexOf(this.draggableItem) },
})
);
}
cleanup() {
this.itemsGap = 0;
this.items = [];
this.unsetDraggableItem();
this.unsetItemState();
this.offDrag.forEach((f) => f());
this.offDrag = [];
}
unsetDraggableItem() {
this.draggableItem.style = null;
this.draggableItem.classList.remove("is-draggable");
this.draggableItem.classList.add("is-idle");
this.draggableItem = null;
}
unsetItemState() {
this.getIdleItems().forEach((item, i) => {
delete item.dataset.isAbove;
delete item.dataset.isToggled;
item.style.transform = "";
});
}
dispose() {
this.off.forEach((f) => f());
}
}

View File

@@ -1,97 +0,0 @@
import { $el } from "../ui.js";
export function calculateImageGrid(imgs, dw, dh) {
let best = 0;
let w = imgs[0].naturalWidth;
let h = imgs[0].naturalHeight;
const numImages = imgs.length;
let cellWidth, cellHeight, cols, rows, shiftX;
// compact style
for (let c = 1; c <= numImages; c++) {
const r = Math.ceil(numImages / c);
const cW = dw / c;
const cH = dh / r;
const scaleX = cW / w;
const scaleY = cH / h;
const scale = Math.min(scaleX, scaleY, 1);
const imageW = w * scale;
const imageH = h * scale;
const area = imageW * imageH * numImages;
if (area > best) {
best = area;
cellWidth = imageW;
cellHeight = imageH;
cols = c;
rows = r;
shiftX = c * ((cW - imageW) / 2);
}
}
return { cellWidth, cellHeight, cols, rows, shiftX };
}
export function createImageHost(node) {
const el = $el("div.comfy-img-preview");
let currentImgs;
let first = true;
function updateSize() {
let w = null;
let h = null;
if (currentImgs) {
let elH = el.clientHeight;
if (first) {
first = false;
// On first run, if we are small then grow a bit
if (elH < 190) {
elH = 190;
}
el.style.setProperty("--comfy-widget-min-height", elH);
} else {
el.style.setProperty("--comfy-widget-min-height", null);
}
const nw = node.size[0];
({ cellWidth: w, cellHeight: h } = calculateImageGrid(currentImgs, nw - 20, elH));
w += "px";
h += "px";
el.style.setProperty("--comfy-img-preview-width", w);
el.style.setProperty("--comfy-img-preview-height", h);
}
}
return {
el,
updateImages(imgs) {
if (imgs !== currentImgs) {
if (currentImgs == null) {
requestAnimationFrame(() => {
updateSize();
});
}
el.replaceChildren(...imgs);
currentImgs = imgs;
node.onResize(node.size);
node.graph.setDirtyCanvas(true, true);
}
},
getHeight() {
updateSize();
},
onDraw() {
// Element from point uses a hittest find elements so we need to toggle pointer events
el.style.pointerEvents = "all";
const over = document.elementFromPoint(app.canvas.mouse[0], app.canvas.mouse[1]);
el.style.pointerEvents = "none";
if(!over) return;
// Set the overIndex so Open Image etc work
const idx = currentImgs.indexOf(over);
node.overIndex = idx;
},
};
}

View File

@@ -1,317 +0,0 @@
import { $el } from "../ui.js";
import { api } from "../api.js";
import { ComfyDialog } from "./dialog.js";
export class ComfySettingsDialog extends ComfyDialog {
constructor(app) {
super();
this.app = app;
this.settingsValues = {};
this.settingsLookup = {};
this.element = $el(
"dialog",
{
id: "comfy-settings-dialog",
parent: document.body,
},
[
$el("table.comfy-modal-content.comfy-table", [
$el(
"caption",
{ textContent: "Settings" },
$el("button.comfy-btn", {
type: "button",
textContent: "\u00d7",
onclick: () => {
this.element.close();
},
})
),
$el("tbody", { $: (tbody) => (this.textElement = tbody) }),
$el("button", {
type: "button",
textContent: "Close",
style: {
cursor: "pointer",
},
onclick: () => {
this.element.close();
},
}),
]),
]
);
}
get settings() {
return Object.values(this.settingsLookup);
}
async load() {
if (this.app.storageLocation === "browser") {
this.settingsValues = localStorage;
} else {
this.settingsValues = await api.getSettings();
}
// Trigger onChange for any settings added before load
for (const id in this.settingsLookup) {
this.settingsLookup[id].onChange?.(this.settingsValues[this.getId(id)]);
}
}
getId(id) {
if (this.app.storageLocation === "browser") {
id = "Comfy.Settings." + id;
}
return id;
}
getSettingValue(id, defaultValue) {
let value = this.settingsValues[this.getId(id)];
if(value != null) {
if(this.app.storageLocation === "browser") {
try {
value = JSON.parse(value);
} catch (error) {
}
}
}
return value ?? defaultValue;
}
async setSettingValueAsync(id, value) {
const json = JSON.stringify(value);
localStorage["Comfy.Settings." + id] = json; // backwards compatibility for extensions keep setting in storage
let oldValue = this.getSettingValue(id, undefined);
this.settingsValues[this.getId(id)] = value;
if (id in this.settingsLookup) {
this.settingsLookup[id].onChange?.(value, oldValue);
}
await api.storeSetting(id, value);
}
setSettingValue(id, value) {
this.setSettingValueAsync(id, value).catch((err) => {
alert(`Error saving setting '${id}'`);
console.error(err);
});
}
addSetting({ id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", options = undefined }) {
if (!id) {
throw new Error("Settings must have an ID");
}
if (id in this.settingsLookup) {
throw new Error(`Setting ${id} of type ${type} must have a unique ID.`);
}
let skipOnChange = false;
let value = this.getSettingValue(id);
if (value == null) {
if (this.app.isNewUserSession) {
// Check if we have a localStorage value but not a setting value and we are a new user
const localValue = localStorage["Comfy.Settings." + id];
if (localValue) {
value = JSON.parse(localValue);
this.setSettingValue(id, value); // Store on the server
}
}
if (value == null) {
value = defaultValue;
}
}
// Trigger initial setting of value
if (!skipOnChange) {
onChange?.(value, undefined);
}
this.settingsLookup[id] = {
id,
onChange,
name,
render: () => {
const setter = (v) => {
if (onChange) {
onChange(v, value);
}
this.setSettingValue(id, v);
value = v;
};
value = this.getSettingValue(id, defaultValue);
let element;
const htmlID = id.replaceAll(".", "-");
const labelCell = $el("td", [
$el("label", {
for: htmlID,
classList: [tooltip !== "" ? "comfy-tooltip-indicator" : ""],
textContent: name,
}),
]);
if (typeof type === "function") {
element = type(name, setter, value, attrs);
} else {
switch (type) {
case "boolean":
element = $el("tr", [
labelCell,
$el("td", [
$el("input", {
id: htmlID,
type: "checkbox",
checked: value,
onchange: (event) => {
const isChecked = event.target.checked;
if (onChange !== undefined) {
onChange(isChecked);
}
this.setSettingValue(id, isChecked);
},
}),
]),
]);
break;
case "number":
element = $el("tr", [
labelCell,
$el("td", [
$el("input", {
type,
value,
id: htmlID,
oninput: (e) => {
setter(e.target.value);
},
...attrs,
}),
]),
]);
break;
case "slider":
element = $el("tr", [
labelCell,
$el("td", [
$el(
"div",
{
style: {
display: "grid",
gridAutoFlow: "column",
},
},
[
$el("input", {
...attrs,
value,
type: "range",
oninput: (e) => {
setter(e.target.value);
e.target.nextElementSibling.value = e.target.value;
},
}),
$el("input", {
...attrs,
value,
id: htmlID,
type: "number",
style: { maxWidth: "4rem" },
oninput: (e) => {
setter(e.target.value);
e.target.previousElementSibling.value = e.target.value;
},
}),
]
),
]),
]);
break;
case "combo":
element = $el("tr", [
labelCell,
$el("td", [
$el(
"select",
{
oninput: (e) => {
setter(e.target.value);
},
},
(typeof options === "function" ? options(value) : options || []).map((opt) => {
if (typeof opt === "string") {
opt = { text: opt };
}
const v = opt.value ?? opt.text;
return $el("option", {
value: v,
textContent: opt.text,
selected: value + "" === v + "",
});
})
),
]),
]);
break;
case "text":
default:
if (type !== "text") {
console.warn(`Unsupported setting type '${type}, defaulting to text`);
}
element = $el("tr", [
labelCell,
$el("td", [
$el("input", {
value,
id: htmlID,
oninput: (e) => {
setter(e.target.value);
},
...attrs,
}),
]),
]);
break;
}
}
if (tooltip) {
element.title = tooltip;
}
return element;
},
};
const self = this;
return {
get value() {
return self.getSettingValue(id, defaultValue);
},
set value(v) {
self.setSettingValue(id, v);
},
};
}
show() {
this.textElement.replaceChildren(
$el(
"tr",
{
style: { display: "none" },
},
[$el("th"), $el("th", { style: { width: "33%" } })]
),
...this.settings.sort((a, b) => a.name.localeCompare(b.name)).map((s) => s.render())
);
this.element.showModal();
}
}

View File

@@ -1,34 +0,0 @@
.lds-ring {
display: inline-block;
position: relative;
width: 1em;
height: 1em;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 100%;
height: 100%;
border: 0.15em solid #fff;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #fff transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -1,9 +0,0 @@
import { addStylesheet } from "../utils.js";
addStylesheet(import.meta.url);
export function createSpinner() {
const div = document.createElement("div");
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`;
return div.firstElementChild;
}

View File

@@ -1,60 +0,0 @@
import { $el } from "../ui.js";
/**
* @typedef { { text: string, value?: string, tooltip?: string } } ToggleSwitchItem
*/
/**
* Creates a toggle switch element
* @param { string } name
* @param { Array<string | ToggleSwitchItem } items
* @param { Object } [opts]
* @param { (e: { item: ToggleSwitchItem, prev?: ToggleSwitchItem }) => void } [opts.onChange]
*/
export function toggleSwitch(name, items, { onChange } = {}) {
let selectedIndex;
let elements;
function updateSelected(index) {
if (selectedIndex != null) {
elements[selectedIndex].classList.remove("comfy-toggle-selected");
}
onChange?.({ item: items[index], prev: selectedIndex == null ? undefined : items[selectedIndex] });
selectedIndex = index;
elements[selectedIndex].classList.add("comfy-toggle-selected");
}
elements = items.map((item, i) => {
if (typeof item === "string") item = { text: item };
if (!item.value) item.value = item.text;
const toggle = $el(
"label",
{
textContent: item.text,
title: item.tooltip ?? "",
},
$el("input", {
name,
type: "radio",
value: item.value ?? item.text,
checked: item.selected,
onchange: () => {
updateSelected(i);
},
})
);
if (item.selected) {
updateSelected(i);
}
return toggle;
});
const container = $el("div.comfy-toggle-switch", elements);
if (selectedIndex == null) {
elements[0].children[0].checked = true;
updateSelected(0);
}
return container;
}

View File

@@ -1,135 +0,0 @@
.comfy-user-selection {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
font-family: sans-serif;
background: linear-gradient(var(--tr-even-bg-color), var(--tr-odd-bg-color));
}
.comfy-user-selection-inner {
background: var(--comfy-menu-bg);
margin-top: -30vh;
padding: 20px 40px;
border-radius: 10px;
min-width: 365px;
position: relative;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
.comfy-user-selection-inner form {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.comfy-user-selection-inner h1 {
margin: 10px 0 30px 0;
font-weight: normal;
}
.comfy-user-selection-inner label {
display: flex;
flex-direction: column;
width: 100%;
}
.comfy-user-selection input,
.comfy-user-selection select {
background-color: var(--comfy-input-bg);
color: var(--input-text);
border: 0;
border-radius: 5px;
padding: 5px;
margin-top: 10px;
}
.comfy-user-selection input::placeholder {
color: var(--descrip-text);
opacity: 1;
}
.comfy-user-existing {
width: 100%;
}
.no-users .comfy-user-existing {
display: none;
}
.comfy-user-selection-inner .or-separator {
margin: 10px 0;
padding: 10px;
display: block;
text-align: center;
width: 100%;
color: var(--descrip-text);
}
.comfy-user-selection-inner .or-separator {
overflow: hidden;
text-align: center;
margin-left: -10px;
}
.comfy-user-selection-inner .or-separator::before,
.comfy-user-selection-inner .or-separator::after {
content: "";
background-color: var(--border-color);
position: relative;
height: 1px;
vertical-align: middle;
display: inline-block;
width: calc(50% - 20px);
top: -1px;
}
.comfy-user-selection-inner .or-separator::before {
right: 10px;
margin-left: -50%;
}
.comfy-user-selection-inner .or-separator::after {
left: 10px;
margin-right: -50%;
}
.comfy-user-selection-inner section {
width: 100%;
padding: 10px;
margin: -10px;
transition: background-color 0.2s;
}
.comfy-user-selection-inner section.selected {
background: var(--border-color);
border-radius: 5px;
}
.comfy-user-selection-inner footer {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
}
.comfy-user-selection-inner .comfy-user-error {
color: var(--error-text);
margin-bottom: 10px;
}
.comfy-user-button-next {
font-size: 16px;
padding: 6px 10px;
width: 100px;
display: flex;
gap: 5px;
align-items: center;
justify-content: center;
}

View File

@@ -1,114 +0,0 @@
import { api } from "../api.js";
import { $el } from "../ui.js";
import { addStylesheet } from "../utils.js";
import { createSpinner } from "./spinner.js";
export class UserSelectionScreen {
async show(users, user) {
// This will rarely be hit so move the loading to on demand
await addStylesheet(import.meta.url);
const userSelection = document.getElementById("comfy-user-selection");
userSelection.style.display = "";
return new Promise((resolve) => {
const input = userSelection.getElementsByTagName("input")[0];
const select = userSelection.getElementsByTagName("select")[0];
const inputSection = input.closest("section");
const selectSection = select.closest("section");
const form = userSelection.getElementsByTagName("form")[0];
const error = userSelection.getElementsByClassName("comfy-user-error")[0];
const button = userSelection.getElementsByClassName("comfy-user-button-next")[0];
let inputActive = null;
input.addEventListener("focus", () => {
inputSection.classList.add("selected");
selectSection.classList.remove("selected");
inputActive = true;
});
select.addEventListener("focus", () => {
inputSection.classList.remove("selected");
selectSection.classList.add("selected");
inputActive = false;
select.style.color = "";
});
select.addEventListener("blur", () => {
if (!select.value) {
select.style.color = "var(--descrip-text)";
}
});
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (inputActive == null) {
error.textContent = "Please enter a username or select an existing user.";
} else if (inputActive) {
const username = input.value.trim();
if (!username) {
error.textContent = "Please enter a username.";
return;
}
// Create new user
input.disabled = select.disabled = input.readonly = select.readonly = true;
const spinner = createSpinner();
button.prepend(spinner);
try {
const resp = await api.createUser(username);
if (resp.status >= 300) {
let message = "Error creating user: " + resp.status + " " + resp.statusText;
try {
const res = await resp.json();
if(res.error) {
message = res.error;
}
} catch (error) {
}
throw new Error(message);
}
resolve({ username, userId: await resp.json(), created: true });
} catch (err) {
spinner.remove();
error.textContent = err.message ?? err.statusText ?? err ?? "An unknown error occurred.";
input.disabled = select.disabled = input.readonly = select.readonly = false;
return;
}
} else if (!select.value) {
error.textContent = "Please select an existing user.";
return;
} else {
resolve({ username: users[select.value], userId: select.value, created: false });
}
});
if (user) {
const name = localStorage["Comfy.userName"];
if (name) {
input.value = name;
}
}
if (input.value) {
// Focus the input, do this separately as sometimes browsers like to fill in the value
input.focus();
}
const userIds = Object.keys(users ?? {});
if (userIds.length) {
for (const u of userIds) {
$el("option", { textContent: users[u], value: u, parent: select });
}
select.style.color = "var(--descrip-text)";
if (select.value) {
// Focus the select, do this separately as sometimes browsers like to fill in the value
select.focus();
}
} else {
userSelection.classList.add("no-users");
input.focus();
}
}).then((r) => {
userSelection.remove();
return r;
});
}
}