import { app } from "/scripts/app.js"; import { ComfyWidgets } from "/scripts/widgets.js"; import { settingValues } from "./settings.js"; import { loadCSS } from "./utils.js"; import { initializeData } from "./data.js"; import { AutocompleteEventHandler } from "./autocomplete.js"; import { RelatedTagsEventHandler } from "./related-tags.js"; function initializeEventHandlers() { const autocompleteEventHandler = new AutocompleteEventHandler(); const relatedTagsEventHandler = new RelatedTagsEventHandler(); const attachedElements = new WeakSet(); // Keep track of elements that have listeners attached // Function to attach listeners function attachListeners(element) { if (attachedElements.has(element)) return; // Prevent double attachment element.addEventListener('input', handleInput); element.addEventListener('focus', handleFocus); element.addEventListener('blur', handleBlur); element.addEventListener('keydown', handleKeyDown); element.addEventListener('keyup', handleKeyUp); // element.addEventListener('keypress', handleKeyPress); // keypress is deprecated // Add new event listeners for related tags feature element.addEventListener('mousemove', handleMouseMove); element.addEventListener('click', handleClick); attachedElements.add(element); // Mark as attached } // Attempt Widget Override as the primary method // The original ComfyWidgets.STRING arguments are (node, inputName, inputData, app) // inputData is often an array like [type, config] if (ComfyWidgets && ComfyWidgets.STRING) { const originalStringWidget = ComfyWidgets.STRING; ComfyWidgets.STRING = function (node, inputName, inputData, appInstance) { // Use appInstance to avoid conflict with global app const result = originalStringWidget.apply(this, arguments); // Check if the widget has an inputEl and if it's a TEXTAREA // This is to ensure we are targeting multiline text inputs, related to '.comfy-multiline-input' if (result && result.widget && result.widget.inputEl && result.widget.inputEl.tagName === 'TEXTAREA') { const widgetConfig = inputData && inputData[1] ? inputData[1] : {}; // Future: Add checks for Autocomplete Plus specific configurations if needed // e.g., if (widgetConfig["AutocompletePlus.enabled"] === false) return result; attachListeners(result.widget.inputEl); } return result; }; } if (settingValues._useFallbackAttachmentForEventListener) { // Fallback and for dynamically added elements not caught by widget override: MutationObserver const targetSelectors = [ '.comfy-multiline-input', // Add other selectors if needed ]; const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { targetSelectors.forEach(selector => { // Check if the added node itself matches or contains matching elements if (node.matches(selector)) { attachListeners(node); } else { node.querySelectorAll(selector).forEach(attachListeners); } }); } }); }); }); // Initial scan for existing elements targetSelectors.forEach(selector => { document.querySelectorAll(selector).forEach(attachListeners); }); // Start observing the document body for changes observer.observe(document.body, { childList: true, subtree: true }); } function handleInput(event) { autocompleteEventHandler.handleInput(event); relatedTagsEventHandler.handleInput(event); } function handleFocus(event) { autocompleteEventHandler.handleFocus(event); relatedTagsEventHandler.handleFocus(event); } function handleBlur(event) { autocompleteEventHandler.handleBlur(event); relatedTagsEventHandler.handleBlur(event); } function handleKeyDown(event) { autocompleteEventHandler.handleKeyDown(event); relatedTagsEventHandler.handleKeyDown(event); } function handleKeyUp(event) { autocompleteEventHandler.handleKeyUp(event); relatedTagsEventHandler.handleKeyUp(event); } // New event handler for mousemove to show related tags on hover function handleMouseMove(event) { autocompleteEventHandler.handleMouseMove(event); relatedTagsEventHandler.handleMouseMove(event); } // New event handler for click to show related tags function handleClick(event) { autocompleteEventHandler.handleClick(event); relatedTagsEventHandler.handleClick(event); } } const id = "AutocompletePlus"; const name = "Autocomplete Plus"; app.registerExtension({ id: id, name: name, setup() { initializeEventHandlers(); let rootPath = import.meta.url.replace("js/main.js", ""); loadCSS(rootPath + "css/autocomplete-plus.css"); // Load CSS for autocomplete initializeData(); }, //One the Settings Screen, displays reverse order in same category settings: [ // --- Autocomplete Settings --- { id: id + ".max_suggestions", name: "Max suggestions", type: "slider", attrs: { min: 5, max: 50, step: 5, }, defaultValue: 10, category: [name, "Autocompletion", "Max suggestions"], onChange: (newVal, oldVal) => { settingValues.maxSuggestions = newVal; } }, { id: id + ".boolean", name: "Enable Autocomplete", description: "Enable or disable the autocomplete feature.", type: "boolean", defaultValue: true, category: [name, "Autocompletion", "Enable Autocomplete"], onChange: (newVal, oldVal) => { settingValues.enabled = newVal; } }, // --- Related Tags Settings --- { id: id + ".related_tags_trigger_mode", name: "Related Tags Trigger Mode", description: "Trigger mode for related tags (click or ctrl+click)", type: "combo", options: ["click", "ctrl+Click"], defaultValue: "click", category: [name, "Related Tags", "Trigger Mode"], onChange: (newVal, oldVal) => { settingValues.relatedTagsTriggerMode = newVal; } }, { id: id + ".related_tags_position", name: "Default Display Position", description: "Display position (relative to Textarea)", type: "combo", options: ["horizontal", "vertical"], defaultValue: "horizontal", category: [name, "Related Tags", "Display Position"], onChange: (newVal, oldVal) => { settingValues.relatedTagsDisplayPosition = newVal; } }, { id: id + ".max_related_tags", name: "Max related tags", description: "Maximum number of related tags to display", type: "slider", attrs: { min: 5, max: 100, step: 5, }, defaultValue: 20, category: [name, "Related Tags", "Max related tags"], onChange: (newVal, oldVal) => { settingValues.maxRelatedTags = newVal; } }, { id: id + ".related_tags_enable", name: "Enable Related Tags", description: "Enable or disable the related tags feature.", type: "boolean", defaultValue: true, category: [name, "Related Tags", "Enable Related Tags"], onChange: (newVal, oldVal) => { settingValues.enableRelatedTags = newVal; } } ] });