The commit introduces a new `include_none` boolean parameter for both the Style and Palette selection widgets. When set to `True`, it forces "none" as the first item in the dropdown menu, allowing users to explicitly opt-out of applying any specific style or palette.
191 lines
7.6 KiB
JavaScript
191 lines
7.6 KiB
JavaScript
/**
|
|
* File : user_input_combo_controller.js
|
|
* Purpose : Controller for combo boxes that automatically updates options
|
|
* based on user-defined text input.
|
|
* Author : Martin Rizzo | <martinrizzo@gmail.com>
|
|
* Date : Aug 23, 2026
|
|
* Repo : https://github.com/martin-rizzo/ComfyUI-ZImagePowerNodes
|
|
* License : MIT
|
|
*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
* ComfyUI-ZImagePowerNodes
|
|
* ComfyUI nodes designed specifically for the "Z-Image" model.
|
|
*_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
|
*/
|
|
export { UserInputComboController };
|
|
import { schedulePeriodicCallback } from '../common_timer.js';
|
|
import { getInputNode } from '../common.js';
|
|
|
|
/**
|
|
* Controller class to manage dynamic combo box options in ComfyUI nodes
|
|
* based on plain text user input.
|
|
*/
|
|
class UserInputComboController {
|
|
/**
|
|
* Creates an instance of this controller.
|
|
*
|
|
* @param {Object} widget - The combo widget to be controlled.
|
|
* @param {ComfyNode} node - The ComfyUI node instance where the widget is attached.
|
|
* @param {string} userInput - The name of the node input from where to read the user-created items.
|
|
* @param {Array<string>} defaultItems - A list of item names to display when the node input is not connected.
|
|
* @param {string} [forcedFirstItem=""] - The name of the item that must always appear first in the list.
|
|
* @param {string} [itemTag=">>>"] - The custom tag used to capture item names from the text input.
|
|
*/
|
|
constructor(widget, node, userInput, defaultItems, forcedFirstItem = "", itemTag = ">>>") {
|
|
this.node = node;
|
|
this.widget = widget;
|
|
this.userInput = userInput;
|
|
this.forcedFirstItem = forcedFirstItem.trim();
|
|
this.comboBoxOptions = [];
|
|
this.defaultOptions = [];
|
|
this.defaultLowercaseOptions = new Set();
|
|
|
|
// escape special regular expression characters in `itemTag`;
|
|
// adds a backslash (\) before symbols like '*' or '$' so
|
|
// they are treated as literal text
|
|
const escapedItemTag = itemTag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
this.itemNameRegex = new RegExp(`^${escapedItemTag}\\s*(.*?)\\s*$`, 'gm');
|
|
|
|
if( Array.isArray(defaultItems) ) {
|
|
this.defaultOptions = defaultItems;
|
|
this.defaultLowercaseOptions = new Set(defaultItems.map(s => s.toLowerCase()));
|
|
}
|
|
|
|
this.lastInputText = null;
|
|
this.lastInputItemNames = null;
|
|
|
|
// bind widget options to the dynamic array `this.comboBoxOptions`,
|
|
// allowing real-time updates of the dropdown list
|
|
this.widget.options.values = () => this.comboBoxOptions;
|
|
|
|
// schedule periodic checks to update the list
|
|
// of available custom items based on node input
|
|
schedulePeriodicCallback(node, () => this.checkAndSyncOptions());
|
|
}
|
|
|
|
/**
|
|
* Monitors the node input for changes and updates the combo widget options accordingly.
|
|
*
|
|
* This method reads the text of the node connected to the input, extracts
|
|
* the item names from it using the configured regex, and then updates
|
|
* the combo widget options when any name changes.
|
|
*/
|
|
checkAndSyncOptions() {
|
|
const userInputNode = getInputNode(this.node, this.userInput);
|
|
const inputText = userInputNode?.widgets[0].value || "";
|
|
|
|
// check if the input text has actually changed
|
|
if( this.lastInputText === inputText ) {
|
|
return;
|
|
}
|
|
this.lastInputText = inputText;
|
|
|
|
// extract item names based on `this.itemNameRegex`
|
|
const inputItemArray = [...inputText.matchAll(this.itemNameRegex)]
|
|
.map(match => match[1].trim())
|
|
.filter(name => name.length > 0);
|
|
|
|
const inputItemNames = inputItemArray.join("\n");
|
|
|
|
// check if the extracted custom item names have changed
|
|
if( this.lastInputItemNames === inputItemNames ) {
|
|
return;
|
|
}
|
|
this.lastInputItemNames = inputItemNames;
|
|
|
|
// update the combo box options with the custom item names
|
|
try {
|
|
this.updateComboBoxOptions(inputItemArray);
|
|
} catch (error) {
|
|
this.resetToDefaultOptions();
|
|
console.error("UserInputComboController cannot update options:", error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the combo box options by processing a string or an array of strings.
|
|
*
|
|
* @param {string|string[]} options - The new options to be set for the combo box.
|
|
* If a string is provided, items should be
|
|
* separated by newline characters.
|
|
*/
|
|
updateComboBoxOptions(options) {
|
|
// ensure the input is converted into an array of strings
|
|
const optionArray =
|
|
Array.isArray(options) ? options :
|
|
typeof options === 'string' ? options.split('\n') :
|
|
null;
|
|
|
|
if( !optionArray ) {
|
|
throw new Error("Invalid input type, expected string or array of strings.");
|
|
}
|
|
|
|
// include any forced option at the beginning of the list
|
|
if( this.forcedFirstItem !== '' ) {
|
|
optionArray.unshift(this.forcedFirstItem);
|
|
}
|
|
|
|
// update options (removing whitespace and filtering out empty options)
|
|
this.comboBoxOptions = optionArray
|
|
.map(item => String(item).trim())
|
|
.filter(item => item.length > 0);
|
|
|
|
// ensure the widget value remains valid after updating the option set.
|
|
this.validateAndSyncWidgetValue();
|
|
}
|
|
|
|
/**
|
|
* Validates the widget value against the current combo options.
|
|
* If the current value is not present in the options list, it
|
|
* replaces the value with the best matching option.
|
|
*/
|
|
validateAndSyncWidgetValue() {
|
|
const options = this.comboBoxOptions;
|
|
const widget = this.widget;
|
|
|
|
// if the current value is already a valid option, no action is needed
|
|
if( options.includes(widget.value) ) {
|
|
return;
|
|
}
|
|
|
|
// since the current value is missing from the options,
|
|
// search for the option that shares the longest common prefix with 'value'
|
|
const value = String(widget.value ?? '').toLowerCase();
|
|
let bestMatch = undefined;
|
|
let maxPrefixLen = 0;
|
|
|
|
for( const option of options ) {
|
|
const optionLower = option.toLowerCase();
|
|
let prefixLen = 0;
|
|
const minLen = Math.min(value.length, optionLower.length);
|
|
|
|
while( prefixLen < minLen && value[prefixLen] === optionLower[prefixLen] ) {
|
|
prefixLen++;
|
|
}
|
|
|
|
if( prefixLen > maxPrefixLen ) {
|
|
maxPrefixLen = prefixLen;
|
|
bestMatch = option;
|
|
}
|
|
}
|
|
|
|
// update value with the best match.
|
|
// NOTE: This doesn't work for sub-graphs! value updates applied here
|
|
// will not reflect on the outer sub-graph's promoted widget
|
|
const newValue = bestMatch || (options.length > 0 ? options[0] : "");
|
|
widget.value = newValue;
|
|
|
|
if( typeof widget.callback === 'function' ) {
|
|
widget.callback(newValue);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets the widget options to the default names that were passed
|
|
* in the constructor.
|
|
*/
|
|
resetToDefaultOptions() {
|
|
this.comboBoxOptions = this.defaultOptions;
|
|
}
|
|
|
|
}
|