Files
olivv-cs-ComfyUI-FunPack/app/ui/composer/elements/slider.js
T

135 lines
5.2 KiB
JavaScript

// Sliders.
//
// The track and thumb are drawn. v4 left them fully native with only
// accent-color, so a slider looked like the operating system rather than like
// the app, and its hit target was whatever the browser decided.
import { define } from "../internals/register.js";
import { el } from "../internals/el.js";
import { uid } from "../internals/ids.js";
const pct = (v, min, max) => (max === min ? 0 : ((v - min) / (max - min)) * 100);
function makeRange({ value, min, max, step, disabled, label, describedBy }) {
const input = el("input", {
cls: ["cx-range", "cx-focusable"],
attrs: { type: "range", min, max, step, disabled, "aria-label": label, "aria-describedby": describedBy },
});
input.value = String(value);
// The filled portion is drawn from a variable rather than a background image,
// so it follows the theme's accent without the JS knowing a colour.
const paint = () => input.style.setProperty("--fill", `${pct(Number(input.value), min, max)}%`);
paint();
input.addEventListener("input", paint);
return { input, paint };
}
function sliderBase(cls, { value = 0, min = 0, max = 1, step = 0.01, onChange, onCommit, disabled, label } = {}) {
const { input, paint } = makeRange({ value, min, max, step, disabled, label });
if (onChange) input.addEventListener("input", () => onChange(Number(input.value)));
if (onCommit) input.addEventListener("change", () => onCommit(Number(input.value)));
const node = el("div", { cls, children: input });
return {
node, input, paint,
get value() { return Number(input.value); },
setValue(v) { input.value = String(v); paint(); },
destroy: () => node.remove(),
};
}
for (const size of ["md", "sm"]) {
define("slider", size, (props = {}) => sliderBase(`cx-slider cx-slider-${size}`, props));
}
define("slider", "readout", ({ unit = "", precision = 2, ...props } = {}) => {
const base = sliderBase("cx-slider cx-slider-md cx-slider-readout", props);
const format = (v) => `${Number(v).toFixed(precision)}${unit}`;
const out = el("output", { cls: "cx-readout", text: format(base.value) });
base.input.addEventListener("input", () => { out.textContent = format(base.input.value); });
base.node.append(out);
const setValue = base.setValue;
return {
...base,
setValue(v) { setValue(v); out.textContent = format(v); },
node: base.node,
};
});
/**
* A slider with named landing points. The presets are the point: most people
* want "Chill" or "Chaos", not 0.62, and the number is there for the ones who do.
*/
define("slider", "macro", ({ presets = [], unit = "", precision = 2, ...props } = {}) => {
const base = sliderBase("cx-slider cx-slider-md cx-slider-macro", props);
const format = (v) => `${Number(v).toFixed(precision)}${unit}`;
const out = el("output", { cls: "cx-readout", text: format(base.value) });
base.node.append(out);
const row = el("div", { cls: "cx-macro-presets" });
const buttons = presets.map((preset) => {
const b = el("button", {
cls: ["cx-btn", "cx-btn-sm", "cx-btn-ghost", "cx-focusable"],
text: preset.label,
attrs: { type: "button" },
on: { click: () => {
base.setValue(preset.value);
out.textContent = format(preset.value);
sync();
if (props.onChange) props.onChange(preset.value);
if (props.onCommit) props.onCommit(preset.value);
} },
});
row.append(b);
return { preset, b };
});
function sync() {
for (const { preset, b } of buttons) b.classList.toggle("cx-on", Number(base.value) === preset.value);
}
base.input.addEventListener("input", () => { out.textContent = format(base.input.value); sync(); });
sync();
const node = el("div", { cls: "cx-macro", children: [base.node, row] });
return {
node,
get value() { return base.value; },
setValue(v) { base.setValue(v); out.textContent = format(v); sync(); },
destroy: () => node.remove(),
};
});
/** Two thumbs. Trim handles and any "between x and y" setting are built on this. */
define("range", "md", ({ from = 0, to = 1, min = 0, max = 1, step = 0.01, onChange, label } = {}) => {
const lo = makeRange({ value: from, min, max, step, label: label ? `${label} from` : "from" });
const hi = makeRange({ value: to, min, max, step, label: label ? `${label} to` : "to" });
// Thumbs must not pass each other: a range whose start is after its end is a
// state every consumer would then have to defend against.
const clampPair = (moved) => {
let a = Number(lo.input.value);
let b = Number(hi.input.value);
if (a > b) {
if (moved === "lo") a = b; else b = a;
lo.input.value = String(a);
hi.input.value = String(b);
lo.paint(); hi.paint();
}
if (onChange) onChange({ from: a, to: b });
};
lo.input.addEventListener("input", () => clampPair("lo"));
hi.input.addEventListener("input", () => clampPair("hi"));
const node = el("div", { cls: "cx-range-pair", attrs: { role: "group", "aria-label": label },
children: [lo.input, hi.input] });
return {
node,
get value() { return { from: Number(lo.input.value), to: Number(hi.input.value) }; },
setValue({ from: a, to: b }) {
lo.input.value = String(a); hi.input.value = String(b);
lo.paint(); hi.paint();
},
destroy: () => node.remove(),
};
});