Files
newtextdoc1111-ComfyUI-Auto…/web/js/main.js
T

401 lines
15 KiB
JavaScript

import { app } from "/scripts/app.js";
import { $el } from "/scripts/ui.js";
import { ComfyWidgets } from "/scripts/widgets.js";
import { settingValues } from "./settings.js";
import { loadCSS } from "./utils.js";
import { TagSource, loadDataAsync } from "./data.js";
import { AutocompleteEventHandler } from "./autocomplete.js";
import { RelatedTagsEventHandler } from "./related-tags.js";
// --- Constants ---
const id = "AutocompletePlus";
const name = "Autocomplete Plus";
// --- Functions ---
/**
* Initialize event handlers for the autocomplete and related tags features.
*/
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);
}
}
/**
* Add Miscellaneous settings to the settings screen
*/
async function addExtraSettings() {
// Function to perform the update check
async function performUpdateCheck(checkButton, lastCheckSpan) {
checkButton.textContent = "Checking...";
checkButton.disabled = true;
app.extensionManager.toast.add({
severity: "info",
summary: "Check new CSV",
detail: "Checking the CSV updates, see console for more details.",
life: 5000
});
try {
const response = await fetch('/autocomplete-plus/csv/force-check-updates', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
// Update last check time display using the response data
if (result.last_check_time) {
const newLastCheckDate = new Date(result.last_check_time);
lastCheckSpan.textContent = "Last checked: " + newLastCheckDate.toLocaleString();
} else {
lastCheckSpan.textContent = "Last checked: Never";
}
}
} catch (error) {
console.error("[Autocomplete-Plus] Error during force check:", error);
} finally {
checkButton.textContent = "Check now";
checkButton.disabled = false;
}
}
// Fetch last check time from API
let lastCheckTimeText = "Loading...";
try {
const response = await fetch('/autocomplete-plus/csv/last-check-time');
const data = await response.json();
if (data.last_check_time) {
const lastCheckDate = new Date(data.last_check_time);
lastCheckTimeText = "Last checked: " + lastCheckDate.toLocaleString();
} else {
lastCheckTimeText = "Last checked: Never";
}
} catch (error) {
console.error("[Autocomplete-Plus] Error fetching last check time:", error);
lastCheckTimeText = "Last checked: Error loading";
}
// Add extra setting for checking new CSV updates
// Note: Temporarily comment out until changes to remote files can be detected
// app.ui.settings.addSetting({
// id: id + ".check_new_csv",
// defaultValue: null,
// name: "Check CSV updates",
// category: [name, "Misc", "Check new CSV"],
// type: () => {
// const lastCheckSpan = $el("span", {
// textContent: lastCheckTimeText,
// className: "text-sm text-gray-500",
// style: {
// marginRight: "16px"
// }
// });
// const checkButton = $el("button", {
// textContent: "Check now",
// className: "p-button p-component p-button-primary",
// onclick: async () => {
// await performUpdateCheck(checkButton, lastCheckSpan);
// }
// });
// return $el("div", {
// className: "flex-row items-center gap-2",
// }, [
// $el("div", {
// className: "p-component",
// }, [
// lastCheckSpan,
// checkButton,
// ]),
// ]);
// }
// });
}
/**
* Registration of the extension
*/
app.registerExtension({
id: id,
name: name,
async setup() {
initializeEventHandlers();
addExtraSettings();
let rootPath = import.meta.url.replace("js/main.js", "");
loadCSS(rootPath + "css/autocomplete-plus.css"); // Load CSS for autocomplete
await loadDataAsync();
},
// One the Settings Screen, displays reverse order in same category
settings: [
// --- Tag source Settings ---
{
id: id + ".tag_source_icon_position",
name: "Tag Source Icon Position",
type: "combo",
options: ["left", "right", "hidden"],
defaultValue: "left",
category: [name, "Tag Source", "Tag Source Icon Position"],
onChange: (newVal, oldVal) => {
settingValues.tagSourceIconPosition = newVal;
}
},
{
id: id + ".primary_tag_source",
name: "Primary source for 'all' Source",
tooltip: "When 'Autocomplete Tag Source' is 'all', this determines which source's tags appear first in suggestions.",
type: "combo",
options: Object.values(TagSource),
defaultValue: TagSource.Danbooru,
category: [name, "Tag Source", "Prioritize Tag Source"],
onChange: (newVal, oldVal) => {
settingValues.primaryTagSource = newVal;
}
},
{
id: id + ".tag_source",
name: "Autocomplete Tag Source",
tooltip: "Select the tag source for autocomplete suggestions. 'all' includes tags from all loaded sources.",
type: "combo",
options: [...Object.values(TagSource), "all"],
defaultValue: "all",
category: [name, "Tag Source", "Tag Source"],
onChange: (newVal, oldVal) => {
settingValues.tagSource = newVal;
}
},
// --- Autocomplete Settings ---
{
id: id + ".use_fast_search",
name: "Use Fast Search",
tooltip: "Tag search processing during text input operates faster, improving responsiveness",
type: "boolean",
defaultValue: false,
category: [name, "Autocompletion", "Use Fast Search"],
onChange: (newVal, oldVal) => {
settingValues.useFastSearch = newVal;
}
},
{
id: id + ".enable_models",
name: "Enable Loras and Embeddings",
tooltip: "Enable Lora and Embedding suggestions",
type: "boolean",
defaultValue: true,
category: [name, "Autocompletion", "Enable Loras and Embeddings"],
onChange: (newVal, oldVal) => {
settingValues.enableModels = newVal;
}
},
{
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;
}
},
// --- Display settings ---
{
id: id + ".display_hide_alias",
name: "Hide Alias",
tooltip: "Hide alias in the autocomplete and related tags display",
type: "boolean",
defaultValue: false,
category: [name, "Display", "Hide Alias"],
onChange: (newVal, oldVal) => {
settingValues.hideAlias = newVal;
}
}
]
});