Files
olivv-cs-ComfyUI-FunPack/app/composer/elements/input.js
T
digital-garbage 563b1c6f02 app: Settings looks designed instead of default -- icon chips, real sidebar, centred About
A bare glyph character had no framing at all; filterList's icon now gets a
coloured chip, drawn from a fixed tone (accent/danger/warn/good/neutral) --
never a raw colour from data, the same rule el()'s style refusal already
holds everywhere else. Each Settings section picks one.

The sidebar was the filterList element's small dropdown-list shape (a
240px-capped box with its own border, floating in a large pane) rather
than a real full-height sidebar; .cx-filter-full opts a host into the
pane version without changing the compact one pipeline_window's own node
picker still uses.

Round one had a per-row hint line under the icon+label, which doesn't
exist in v4's own sidebar and broke alignment the moment a hint wrapped
(icon and label baseline-aligned against a hint floating as an independent
sibling rather than stacked under its own label). Fixed the real bug --
label+hint are a column now, not loose flex siblings -- then removed the
hint entirely to match v4: a section's subtitle belongs in its own head
once picked, not repeated under every row whether picked or not.

About is centred -- mark, name, version/branch -- rather than a plain
settings-row list, the one section built fresh rather than mounting an
existing window.
2026-09-07 01:22:35 +03:00

207 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Text entry.
//
// onInput fires on every keystroke; onCommit fires on blur and Enter. Panels
// almost always want onCommit -- a settings value that updates per keystroke
// writes nine intermediate values while you type "0.65".
import { define } from "../internals/register.js";
import { el } from "../internals/el.js";
import { uid } from "../internals/ids.js";
function wire(node, { onInput, onCommit }) {
if (onInput) node.addEventListener("input", () => onInput(node.value));
if (onCommit) {
let last = node.value;
const commit = () => { if (node.value !== last) { last = node.value; onCommit(node.value); } };
node.addEventListener("blur", commit);
// And `change`, which is what autofill, a password manager and a script
// setting the value all fire without ever taking focus. commit() only acts
// on a real difference, so the pair costs nothing when both arrive.
node.addEventListener("change", commit);
// Enter commits a one-line field. In a TEXTAREA it is a newline, and
// stealing it there means a multi-line value cannot be typed at all -- the
// prompt included, which is the longest multi-line thing in the app.
if (node.tagName !== "TEXTAREA") {
node.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); commit(); } });
}
}
}
function textLike(tag, cls, { value = "", placeholder, onInput, onCommit, disabled, id, type, label } = {}) {
const node = el(tag, {
cls: [cls, "cx-focusable"],
attrs: { placeholder, disabled, id: id || uid("input"), type, "aria-label": label },
});
node.value = value;
wire(node, { onInput, onCommit });
return {
node,
get value() { return node.value; },
setValue(v) { node.value = v ?? ""; },
focus: () => node.focus(),
destroy: () => node.remove(),
};
}
for (const size of ["md", "sm"]) {
define("input", size, (props = {}) => textLike("input", `cx-input cx-input-${size}`, { type: "text", ...props }));
}
define("search", "md", (props = {}) => {
const handle = textLike("input", "cx-input cx-input-md cx-search", { type: "search", placeholder: "Search", ...props });
return handle;
});
define("number", "md", ({ value = 0, min, max, step = 1, precision, unit, onChange, disabled, id, label } = {}) => {
const input = el("input", {
cls: ["cx-input", "cx-input-md", "cx-number", "cx-focusable"],
attrs: { type: "number", min, max, step, disabled, id: id || uid("num"), "aria-label": label },
});
input.value = String(value);
let committed = Number(value);
const clamp = (n) => {
if (min !== undefined && n < min) return min;
if (max !== undefined && n > max) return max;
return precision === undefined ? n : Number(n.toFixed(precision));
};
// Clamping on commit rather than on input: clamping mid-typing makes "-" or
// a leading "0." impossible to type.
//
// An emptied or unparseable field reverts to the last committed value rather
// than being read as a number. A browser normalises invalid text in a number
// input to "", and Number("") is 0 -- which is finite, so it sails past any
// isFinite guard and lands on min, silently discarding what the user had.
const commit = () => {
const raw = input.value.trim();
const parsed = raw === "" ? NaN : Number(raw);
const next = Number.isFinite(parsed) ? clamp(parsed) : committed;
committed = next;
input.value = String(next);
if (onChange) onChange(next);
};
input.addEventListener("blur", commit);
input.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); commit(); } });
const node = unit
? el("span", { cls: "cx-number-wrap", children: [input, el("span", { cls: "cx-unit", text: unit })] })
: input;
return {
node,
get value() { return committed; },
setValue(v) {
const parsed = Number(v);
committed = Number.isFinite(parsed) ? clamp(parsed) : committed;
input.value = String(committed);
},
destroy: () => node.remove(),
};
});
define("stepper", "md", ({ value = 0, min, max, step = 1, onChange, label } = {}) => {
let current = value;
const out = el("input", { cls: ["cx-input", "cx-input-md", "cx-number", "cx-focusable"],
attrs: { type: "number", min, max, step, "aria-label": label } });
out.value = String(current);
const set = (next) => {
if (min !== undefined) next = Math.max(min, next);
if (max !== undefined) next = Math.min(max, next);
current = Number(next.toFixed(6)); // 0.1 + 0.2 must not become 0.30000000000000004
out.value = String(current);
if (onChange) onChange(current);
};
const bump = (dir) => el("button", {
cls: ["cx-step-btn", "cx-focusable"],
text: dir > 0 ? "+" : "−",
attrs: { type: "button", "aria-label": `${dir > 0 ? "Increase" : "Decrease"}${label ? ` ${label}` : ""}` },
on: { click: () => set(current + dir * step) },
});
out.addEventListener("change", () => set(Number(out.value)));
const node = el("span", { cls: "cx-stepper", children: [bump(-1), out, bump(1)] });
return { node, get value() { return current; }, setValue: set, destroy: () => node.remove() };
});
define("textarea", "md", ({ value = "", rows = 4, autoGrow = false, placeholder, onInput, onCommit, label } = {}) => {
const node = el("textarea", {
cls: ["cx-textarea", "cx-focusable", autoGrow ? "cx-autogrow" : null],
attrs: { rows, placeholder, "aria-label": label },
});
node.value = value;
const grow = () => { node.style.height = "auto"; node.style.height = `${node.scrollHeight}px`; };
if (autoGrow) { node.addEventListener("input", grow); queueMicrotask(grow); }
wire(node, { onInput, onCommit });
return {
node,
get value() { return node.value; },
setValue(v) { node.value = v ?? ""; if (autoGrow) grow(); },
destroy: () => node.remove(),
};
});
/**
* A search box over a list. v4 had seven of these, each with its own filter.
* Items are DATA -- {id, label, hint, icon} -- never a render callback, so a
* module cannot smuggle markup in through a "just render this" escape hatch.
*/
define("filterList", "md", ({ items = [], value, onChange, placeholder = "Search", empty = "Nothing matches" } = {}) => {
let selected = value;
const search = el("input", { cls: ["cx-input", "cx-input-sm", "cx-search", "cx-focusable"],
attrs: { type: "search", placeholder } });
const list = el("div", { cls: "cx-filter-list", attrs: { role: "listbox" } });
function draw() {
const q = search.value.trim().toLowerCase();
// `keywords` is searched but never drawn -- a way in that does not name
// itself in the visible label/hint (a section called "About FunPack"
// found by typing "commit", say).
const shown = items.filter((i) =>
!q || `${i.label} ${i.hint || ""} ${i.keywords || ""}`.toLowerCase().includes(q));
list.replaceChildren();
if (!shown.length) { list.append(el("div", { cls: "cx-filter-empty", text: empty })); return; }
for (const item of shown) {
const row = el("button", {
cls: ["cx-filter-row", "cx-focusable", item.id === selected ? "cx-on" : null],
attrs: { type: "button", role: "option", "aria-selected": String(item.id === selected) },
children: [
// `tone` picks one of a FIXED set of chip colours -- never a raw
// colour from data, which is the rule `el()`'s style refusal exists
// to hold everywhere else too.
item.icon ? el("span", {
cls: ["cx-filter-icon", item.tone ? `cx-filter-icon-${item.tone}` : null],
text: item.icon,
}) : null,
// A column, not two more flex siblings beside the icon -- a hint
// long enough to wrap needs to wrap UNDER its own label, not float
// as its own baseline-aligned sibling next to it.
el("div", { cls: "cx-filter-text", children: [
el("span", { cls: "cx-filter-label", text: item.label }),
item.hint ? el("span", { cls: "cx-filter-hint", text: item.hint }) : null,
].filter(Boolean) }),
].filter(Boolean),
});
row.addEventListener("click", () => { selected = item.id; draw(); if (onChange) onChange(item.id); });
list.append(row);
}
}
search.addEventListener("input", draw);
draw();
const node = el("div", { cls: "cx-filter", children: [search, list] });
return {
node,
get value() { return selected; },
setValue(v) { selected = v; draw(); },
destroy: () => node.remove(),
};
});