Files
arcum42-ComfyUI_SageUtils/js/components/formElements.js
T

637 lines
20 KiB
JavaScript

/**
* Form Elements Component Library
*
* Centralized form element creation utilities providing consistent styling
* and behavior across the application. All form elements follow common
* design patterns with customizable options.
*
* @module components/formElements
*/
/**
* Apply styles to an element from a style object
* @param {HTMLElement} element - Element to style
* @param {Object} styles - Style object
*/
function applyStyles(element, styles) {
Object.entries(styles).forEach(([key, value]) => {
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
element.style[key] = value;
});
}
/**
* Create a styled text input
*
* @param {Object} options - Configuration options
* @param {string} [options.type='text'] - Input type (text, number, email, password, etc.)
* @param {string} [options.placeholder=''] - Placeholder text
* @param {string|number} [options.value=''] - Initial value
* @param {string} [options.min] - Minimum value (for number type)
* @param {string} [options.max] - Maximum value (for number type)
* @param {string} [options.step] - Step value (for number type)
* @param {string} [options.className=''] - Additional CSS classes
* @param {Function} [options.onInput] - Input event handler
* @param {Function} [options.onChange] - Change event handler
* @param {Function} [options.onKeydown] - Keydown event handler
* @param {Object} [options.style] - Custom style overrides
* @param {boolean} [options.disabled=false] - Disabled state
* @param {string} [options.id] - Element ID
* @param {string} [options.name] - Element name attribute
* @param {string} [options.ariaLabel] - ARIA label for accessibility
* @returns {HTMLInputElement} The styled input element
*
* @example
* const input = createInput({
* type: 'number',
* placeholder: 'Enter value',
* min: '0',
* max: '100',
* value: '50',
* onInput: (e) => console.log(e.target.value)
* });
*/
export function createInput(options = {}) {
const {
type = 'text',
placeholder = '',
value = '',
min,
max,
step,
className = '',
onInput,
onChange,
onKeydown,
style = {},
disabled = false,
id,
name,
ariaLabel
} = options;
const input = document.createElement('input');
input.type = type;
if (placeholder) input.placeholder = placeholder;
if (value !== '') input.value = value;
if (min !== undefined) input.min = min;
if (max !== undefined) input.max = max;
if (step !== undefined) input.step = step;
if (id) input.id = id;
if (name) input.name = name;
if (ariaLabel) input.setAttribute('aria-label', ariaLabel);
if (disabled) input.disabled = disabled;
input.className = `sage-input ${className}`.trim();
// Apply custom style overrides only
if (Object.keys(style).length > 0) {
applyStyles(input, style);
}
// Event handlers
if (onInput) input.addEventListener('input', onInput);
if (onChange) input.addEventListener('change', onChange);
if (onKeydown) input.addEventListener('keydown', onKeydown);
return input;
}
/**
* Create a styled select dropdown
*
* @param {Object} options - Configuration options
* @param {Array} [options.items=[]] - Array of items (strings or {value, text} objects)
* @param {string|number} [options.value=''] - Initially selected value
* @param {string} [options.className=''] - Additional CSS classes
* @param {Function} [options.onChange] - Change event handler
* @param {Object} [options.style] - Custom style overrides
* @param {boolean} [options.disabled=false] - Disabled state
* @param {string} [options.id] - Element ID
* @param {string} [options.name] - Element name attribute
* @param {string} [options.ariaLabel] - ARIA label for accessibility
* @param {string} [options.placeholder] - Placeholder option text
* @returns {HTMLSelectElement} The styled select element
*
* @example
* const select = createSelect({
* items: [
* { value: 'opt1', text: 'Option 1' },
* { value: 'opt2', text: 'Option 2' }
* ],
* value: 'opt1',
* onChange: (e) => console.log(e.target.value)
* });
*
* // Or with simple string array
* const select2 = createSelect({
* items: ['Option 1', 'Option 2', 'Option 3'],
* placeholder: 'Select an option...'
* });
*/
export function createSelect(options = {}) {
const {
items = [],
value = '',
className = '',
onChange,
style = {},
disabled = false,
id,
name,
ariaLabel,
placeholder
} = options;
const select = document.createElement('select');
if (id) select.id = id;
if (name) select.name = name;
if (ariaLabel) select.setAttribute('aria-label', ariaLabel);
if (disabled) select.disabled = disabled;
select.className = `sage-select ${className}`.trim();
// Apply custom style overrides only
if (Object.keys(style).length > 0) {
applyStyles(select, style);
}
// Add placeholder option if provided
if (placeholder) {
const placeholderOption = document.createElement('option');
placeholderOption.value = '';
placeholderOption.textContent = placeholder;
placeholderOption.disabled = true;
placeholderOption.selected = !value;
select.appendChild(placeholderOption);
}
// Add items
items.forEach(item => {
const option = document.createElement('option');
if (typeof item === 'string') {
option.value = item;
option.textContent = item;
} else {
option.value = item.value;
option.textContent = item.text || item.value;
if (item.disabled) option.disabled = true;
}
if (option.value === value) {
option.selected = true;
}
select.appendChild(option);
});
// Event handlers
if (onChange) select.addEventListener('change', onChange);
return select;
}
/**
* Create a styled textarea
*
* @param {Object} options - Configuration options
* @param {string} [options.placeholder=''] - Placeholder text
* @param {string} [options.value=''] - Initial value
* @param {number} [options.rows=6] - Number of rows
* @param {number} [options.cols] - Number of columns
* @param {boolean} [options.monospace=true] - Use monospace font
* @param {string} [options.className=''] - Additional CSS classes
* @param {Function} [options.onInput] - Input event handler
* @param {Function} [options.onChange] - Change event handler
* @param {Function} [options.onKeydown] - Keydown event handler
* @param {Object} [options.style] - Custom style overrides
* @param {boolean} [options.disabled=false] - Disabled state
* @param {string} [options.id] - Element ID
* @param {string} [options.name] - Element name attribute
* @param {string} [options.ariaLabel] - ARIA label for accessibility
* @param {string} [options.resize='vertical'] - CSS resize property (vertical, horizontal, both, none)
* @returns {HTMLTextAreaElement} The styled textarea element
*
* @example
* const textarea = createTextarea({
* placeholder: 'Enter your text...',
* rows: 10,
* monospace: true,
* onInput: (e) => console.log(e.target.value.length, 'characters')
* });
*/
export function createTextarea(options = {}) {
const {
placeholder = '',
value = '',
rows = 6,
cols,
monospace = true,
className = '',
onInput,
onChange,
onKeydown,
style = {},
disabled = false,
id,
name,
ariaLabel,
resize = 'vertical'
} = options;
const textarea = document.createElement('textarea');
if (placeholder) textarea.placeholder = placeholder;
if (value) textarea.value = value;
textarea.rows = rows;
if (cols) textarea.cols = cols;
if (id) textarea.id = id;
if (name) textarea.name = name;
if (ariaLabel) textarea.setAttribute('aria-label', ariaLabel);
if (disabled) textarea.disabled = disabled;
textarea.className = `sage-textarea ${className}`.trim();
textarea.classList.add(monospace ? 'sage-textarea--monospace' : 'sage-textarea--inherit');
const allowedResizeModes = new Set(['vertical', 'horizontal', 'both', 'none']);
const normalizedResize = allowedResizeModes.has(resize) ? resize : 'vertical';
textarea.classList.add(`sage-textarea--resize-${normalizedResize}`);
// Apply any custom style overrides only
if (Object.keys(style).length > 0) {
applyStyles(textarea, style);
}
// Event handlers
if (onInput) textarea.addEventListener('input', onInput);
if (onChange) textarea.addEventListener('change', onChange);
if (onKeydown) textarea.addEventListener('keydown', onKeydown);
return textarea;
}
/**
* Create a checkbox with label
*
* @param {string} labelText - Label text
* @param {Object} options - Configuration options
* @param {boolean} [options.checked=false] - Initial checked state
* @param {string} [options.className=''] - Additional CSS classes for container
* @param {string} [options.checkboxClass=''] - Additional CSS classes for checkbox
* @param {string} [options.labelClass=''] - Additional CSS classes for label
* @param {Function} [options.onChange] - Change event handler
* @param {Object} [options.style] - Custom style overrides for container
* @param {boolean} [options.disabled=false] - Disabled state
* @param {string} [options.id] - Element ID for checkbox
* @param {string} [options.name] - Element name attribute
* @param {string} [options.value] - Checkbox value attribute
* @returns {Object} Object containing {container, checkbox, label}
*
* @example
* const { container, checkbox } = createCheckbox('Enable feature', {
* checked: true,
* onChange: (e) => console.log('Checked:', e.target.checked)
* });
*/
export function createCheckbox(labelText, options = {}) {
const {
checked = false,
className = '',
checkboxClass = '',
labelClass = '',
onChange,
style = {},
disabled = false,
id,
name,
value
} = options;
const container = document.createElement('div');
container.className = `sage-checkbox-row ${className}`.trim();
if (Object.keys(style).length > 0) {
applyStyles(container, style);
}
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = checked;
checkbox.className = `sage-checkbox-input ${checkboxClass}`.trim();
if (id) checkbox.id = id;
if (name) checkbox.name = name;
if (value !== undefined) checkbox.value = value;
if (disabled) checkbox.disabled = disabled;
label.className = `sage-checkbox-label ${labelClass}`.trim();
if (disabled) {
label.classList.add('sage-disabled');
}
if (onChange) checkbox.addEventListener('change', onChange);
if (onChange) checkbox.addEventListener('change', onChange);
container.appendChild(checkbox);
container.appendChild(label);
return { container, checkbox, label };
}
/**
* Create a slider with label and value display
*
* @param {string} labelText - Label text
* @param {Object} options - Configuration options
* @param {number} [options.min=0] - Minimum value
* @param {number} [options.max=100] - Maximum value
* @param {number} [options.step=1] - Step increment
* @param {number} [options.value=50] - Initial value
* @param {string} [options.className=''] - Additional CSS classes for container
* @param {string} [options.sliderClass=''] - Additional CSS classes for slider
* @param {string} [options.labelClass=''] - Additional CSS classes for label
* @param {string} [options.valueClass=''] - Additional CSS classes for value display
* @param {Function} [options.onInput] - Input event handler (fires during dragging)
* @param {Function} [options.onChange] - Change event handler (fires on release)
* @param {Function} [options.formatValue] - Function to format displayed value
* @param {Object} [options.style] - Custom style overrides for container
* @param {boolean} [options.disabled=false] - Disabled state
* @param {string} [options.id] - Element ID for slider
* @param {string} [options.name] - Element name attribute
* @param {boolean} [options.showValue=true] - Whether to show value display
* @returns {Object} Object containing {container, slider, valueDisplay, label}
*
* @example
* const { container, slider } = createSlider('Temperature', {
* min: 0,
* max: 2,
* step: 0.1,
* value: 0.7,
* formatValue: (v) => v.toFixed(1),
* onInput: (e) => console.log('Current:', e.target.value)
* });
*/
export function createSlider(labelText, options = {}) {
const {
min = 0,
max = 100,
step = 1,
value = 50,
className = '',
sliderClass = '',
labelClass = '',
valueClass = '',
onInput,
onChange,
formatValue = (v) => v,
style = {},
disabled = false,
id,
name,
showValue = true
} = options;
const container = document.createElement('div');
container.className = `sage-slider-row ${className}`.trim();
if (Object.keys(style).length > 0) {
applyStyles(container, style);
}
// Header with label and value
const header = document.createElement('div');
header.className = 'sage-slider-header';
const label = document.createElement('label');
label.textContent = labelText;
label.className = `sage-slider-label ${labelClass}`.trim();
if (id) label.setAttribute('for', id);
const valueDisplay = document.createElement('span');
valueDisplay.className = `sage-slider-value ${valueClass}`.trim();
valueDisplay.textContent = formatValue(value);
header.appendChild(label);
if (showValue) {
header.appendChild(valueDisplay);
}
// Slider
const slider = document.createElement('input');
slider.type = 'range';
slider.min = min;
slider.max = max;
slider.step = step;
slider.value = value;
slider.className = `sage-slider ${sliderClass}`.trim();
if (id) slider.id = id;
if (name) slider.name = name;
if (disabled) slider.disabled = disabled;
// Update value display on input
const updateValue = (e) => {
if (showValue) {
valueDisplay.textContent = formatValue(e.target.value);
}
if (onInput) onInput(e);
};
slider.addEventListener('input', updateValue);
if (onChange) slider.addEventListener('change', onChange);
container.appendChild(header);
container.appendChild(slider);
return { container, slider, valueDisplay, label };
}
/**
* Create a radio button group
*
* @param {string} name - Group name (same name for all radios in group)
* @param {Array} items - Array of {value, label, checked} objects or strings
* @param {Object} options - Configuration options
* @param {string} [options.selectedValue] - Initially selected value
* @param {string} [options.className=''] - Additional CSS classes for container
* @param {string} [options.radioClass=''] - Additional CSS classes for radio buttons
* @param {string} [options.labelClass=''] - Additional CSS classes for labels
* @param {Function} [options.onChange] - Change event handler
* @param {Object} [options.style] - Custom style overrides for container
* @param {boolean} [options.disabled=false] - Disabled state for all radios
* @param {string} [options.layout='vertical'] - Layout direction (vertical or horizontal)
* @returns {Object} Object containing {container, radios}
*
* @example
* const { container, radios } = createRadioGroup('theme', [
* { value: 'dark', label: 'Dark Theme' },
* { value: 'light', label: 'Light Theme' },
* { value: 'auto', label: 'Auto' }
* ], {
* selectedValue: 'dark',
* onChange: (e) => console.log('Selected:', e.target.value)
* });
*/
export function createRadioGroup(name, items = [], options = {}) {
const {
selectedValue,
className = '',
radioClass = '',
labelClass = '',
onChange,
style = {},
disabled = false,
layout = 'vertical'
} = options;
const container = document.createElement('div');
container.className = `sage-radio-group sage-radio-group--${layout} ${className}`.trim();
if (Object.keys(style).length > 0) {
applyStyles(container, style);
}
const radios = [];
items.forEach((item, index) => {
const itemValue = typeof item === 'string' ? item : item.value;
const itemLabel = typeof item === 'string' ? item : (item.label || item.value);
const itemChecked = typeof item === 'string' ? (itemValue === selectedValue) : (item.checked || itemValue === selectedValue);
const itemDisabled = typeof item === 'string' ? disabled : (item.disabled || disabled);
const radioContainer = document.createElement('div');
radioContainer.className = 'sage-radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = name;
radio.value = itemValue;
radio.id = `${name}-${index}`;
radio.checked = itemChecked;
radio.disabled = itemDisabled;
radio.className = `sage-radio-input ${radioClass}`.trim();
const label = document.createElement('label');
label.textContent = itemLabel;
label.setAttribute('for', radio.id);
label.className = `sage-radio-label ${labelClass}`.trim();
if (itemDisabled) label.classList.add('sage-disabled');
if (onChange) radio.addEventListener('change', onChange);
radioContainer.appendChild(radio);
radioContainer.appendChild(label);
container.appendChild(radioContainer);
radios.push({ radio, label, container: radioContainer });
});
return { container, radios };
}
/**
* Create a form row with label and element
*
* @param {string} labelText - Label text
* @param {HTMLElement} element - Form element
* @param {Object} options - Configuration options
* @param {string} [options.className=''] - Additional CSS classes for container
* @param {string} [options.labelClass=''] - Additional CSS classes for label
* @param {Object} [options.style] - Custom style overrides for container
* @param {string} [options.id] - ID for the label (will set for attribute)
* @param {boolean} [options.required=false] - Whether field is required
* @param {string} [options.helpText] - Optional help text below the element
* @param {string} [options.layout='vertical'] - Layout (vertical or horizontal)
* @returns {HTMLElement} Container with label and element
*
* @example
* const input = createInput({ placeholder: 'Enter name' });
* const row = createFormRow('Name', input, {
* required: true,
* helpText: 'Your full name'
* });
*/
export function createFormRow(labelText, element, options = {}) {
const {
className = '',
labelClass = '',
style = {},
id,
required = false,
helpText,
layout = 'vertical'
} = options;
const container = document.createElement('div');
container.className = `sage-form-row sage-form-row--${layout} ${className}`.trim();
if (Object.keys(style).length > 0) {
applyStyles(container, style);
}
const label = document.createElement('label');
label.textContent = labelText + (required ? ' *' : '');
label.className = `sage-form-label ${labelClass}`.trim();
if (id) label.setAttribute('for', id);
container.appendChild(label);
if (layout === 'horizontal') {
const elementWrapper = document.createElement('div');
elementWrapper.className = 'sage-form-row-element';
elementWrapper.appendChild(element);
container.appendChild(elementWrapper);
} else {
container.appendChild(element);
}
if (helpText) {
const help = document.createElement('div');
help.className = 'sage-form-help-text';
help.textContent = helpText;
container.appendChild(help);
}
return container;
}
/**
* Create a form group container
*
* @param {Array<HTMLElement>} elements - Array of form elements or rows
* @param {Object} options - Configuration options
* @param {string} [options.className=''] - Additional CSS classes
* @param {string} [options.title] - Optional group title
* @param {Object} [options.style] - Custom style overrides
* @returns {HTMLElement} Form group container
*
* @example
* const group = createFormGroup([
* createFormRow('Email', createInput({ type: 'email' })),
* createFormRow('Password', createInput({ type: 'password' }))
* ], {
* title: 'Login Credentials'
* });
*/
export function createFormGroup(elements, options = {}) {
const {
className = '',
title,
style = {}
} = options;
const container = document.createElement('div');
container.className = `sage-form-group ${className}`.trim();
if (Object.keys(style).length > 0) {
applyStyles(container, style);
}
if (title) {
const titleElement = document.createElement('h3');
titleElement.textContent = title;
titleElement.className = 'sage-form-group-title';
container.appendChild(titleElement);
}
elements.forEach(element => {
container.appendChild(element);
});
return container;
}