139 lines
5.3 KiB
JavaScript
139 lines
5.3 KiB
JavaScript
export function createScope() {
|
|
const controller = new AbortController();
|
|
const cleanups = [];
|
|
return {
|
|
signal: controller.signal,
|
|
listen(target, event, callback) {
|
|
target.addEventListener(event, callback, { signal: controller.signal });
|
|
},
|
|
cleanup(callback) { cleanups.push(callback); },
|
|
debounce(callback, delay) {
|
|
let timer;
|
|
cleanups.push(() => clearTimeout(timer));
|
|
return (...args) => {
|
|
clearTimeout(timer);
|
|
if (!controller.signal.aborted) timer = setTimeout(() => callback(...args), delay);
|
|
};
|
|
},
|
|
dispose() {
|
|
if (controller.signal.aborted) return;
|
|
controller.abort();
|
|
cleanups.splice(0).forEach(callback => callback());
|
|
},
|
|
};
|
|
}
|
|
|
|
export function themeFromColor(color) {
|
|
let values;
|
|
const hex = color.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i)?.[1];
|
|
if (hex) {
|
|
const expanded = hex.length === 3 ? [...hex].map(value => value + value).join('') : hex;
|
|
values = expanded.match(/../g).map(value => parseInt(value, 16));
|
|
} else if (/^rgb\(/.test(color)) {
|
|
values = color.match(/[\d.]+/g)?.slice(0, 3).map(Number);
|
|
}
|
|
if (!values || values.length !== 3) return null;
|
|
const linear = values.map(value => value / 255).map(value => value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4);
|
|
return linear[0] * .2126 + linear[1] * .7152 + linear[2] * .0722 > .35 ? 'light' : 'dark';
|
|
}
|
|
|
|
export function mountTheme(element, settings, scope) {
|
|
const stylesheet = new URL('./controls.css', import.meta.url).href;
|
|
if (![...document.querySelectorAll('link[rel="stylesheet"]')].some(link => link.href === stylesheet)) {
|
|
const link = document.createElement('link');
|
|
link.rel = 'stylesheet';
|
|
link.href = stylesheet;
|
|
document.head.append(link);
|
|
}
|
|
const preference = matchMedia('(prefers-color-scheme: dark)');
|
|
const update = () => {
|
|
const background = getComputedStyle(document.body).getPropertyValue('--bg-color').trim();
|
|
element.dataset.caTheme = themeFromColor(background) || (preference.matches ? 'dark' : 'light');
|
|
};
|
|
const observer = new MutationObserver(update);
|
|
for (const target of [document.documentElement, document.body]) observer.observe(target, { attributes: true, attributeFilter: ['style', 'class', 'data-theme'] });
|
|
scope.cleanup(() => observer.disconnect());
|
|
scope.listen(preference, 'change', update);
|
|
if (settings?.addEventListener) scope.listen(settings, 'Comfy.ColorPalette.change', update);
|
|
update();
|
|
}
|
|
|
|
export function createButton(text, className = '') {
|
|
const element = document.createElement('button');
|
|
element.type = 'button';
|
|
element.className = className;
|
|
element.textContent = text;
|
|
return element;
|
|
}
|
|
|
|
export function createSlider(label, { id, min, max, step, value, unit }, scope, onInput) {
|
|
const field = document.createElement('div');
|
|
field.className = 'ca-range';
|
|
const caption = document.createElement('label');
|
|
caption.htmlFor = id;
|
|
caption.textContent = label;
|
|
const input = document.createElement('input');
|
|
Object.assign(input, { type: 'range', id, min, max, step, value });
|
|
const output = document.createElement('output');
|
|
output.setAttribute('for', id);
|
|
output.value = `${value} ${unit}`;
|
|
scope.listen(input, 'input', () => {
|
|
output.value = `${input.value} ${unit}`;
|
|
onInput(Number(input.value));
|
|
});
|
|
field.append(caption, input, output);
|
|
return field;
|
|
}
|
|
|
|
export function createField(label, input, id) {
|
|
const field = document.createElement('label');
|
|
field.className = 'ca-field';
|
|
field.htmlFor = id;
|
|
const caption = document.createElement('span');
|
|
caption.className = 'ca-field-label';
|
|
caption.textContent = label;
|
|
input.id = id;
|
|
field.append(caption, input);
|
|
return field;
|
|
}
|
|
|
|
export function createSwitch(label, className, id) {
|
|
const input = document.createElement('input');
|
|
input.type = 'checkbox';
|
|
input.role = 'switch';
|
|
input.checked = true;
|
|
input.className = className;
|
|
const field = createField(label, input, id);
|
|
field.className = 'ca-switch';
|
|
field.replaceChildren(input, document.createTextNode(label));
|
|
return field;
|
|
}
|
|
|
|
export function createSelect(options, className) {
|
|
const select = document.createElement('select');
|
|
select.className = className;
|
|
for (const [value, text] of options) select.add(new Option(text, value));
|
|
return select;
|
|
}
|
|
|
|
export function createSegments(label, options, name, scope, onChange) {
|
|
const fieldset = document.createElement('fieldset');
|
|
fieldset.className = 'ca-segments';
|
|
const legend = document.createElement('legend');
|
|
legend.className = 'ca-visually-hidden';
|
|
legend.textContent = label;
|
|
fieldset.append(legend);
|
|
options.forEach(([value, text], index) => {
|
|
const input = document.createElement('input');
|
|
input.type = 'radio';
|
|
input.name = name;
|
|
input.value = value;
|
|
input.checked = index === 0;
|
|
const field = createField(text, input, `${name}-${value}`);
|
|
field.replaceChildren(input, field.firstChild);
|
|
scope.listen(input, 'change', () => onChange(value));
|
|
fieldset.append(field);
|
|
});
|
|
return fieldset;
|
|
}
|