Files

254 lines
12 KiB
JavaScript

import { app } from "../../scripts/app.js";
app.registerExtension({
name: "ComfyUI-Text-Driven-Workflows.TextLinePrompt",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "TextDrivenTextLinePrompt") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const self = this;
self.is_updating_widgets = false;
const textWidget = self.widgets.find(w => w.name === "text");
const comboWidget = self.widgets.find(w => w.name === "selected_line");
const multWidget = self.widgets.find(w => w.name === "multiplier");
const actWidget = self.widgets.find(w => w.name === "active");
// Helper to safely extract the actual textarea DOM element across different ComfyUI V3 structures
const getTextareaElement = (w) => {
if (!w) return null;
if (w.inputEl) return w.inputEl;
if (w.element) {
if (w.element.tagName === "TEXTAREA") return w.element;
return w.element.querySelector("textarea");
}
return null;
};
// Helper to cleanly deconstruct a line into semantic metadata components
const parseLine = (line) => {
let trimmed = line.trim();
const active = !trimmed.startsWith("//");
let content = active ? trimmed : trimmed.replace(/^\s*\/\//, "").trim();
content = content.replace(/,\s*$/, "").trim();
let label = "";
let prompt = "";
if (content.includes("|-|")) {
const parts = content.split("|-|");
label = parts[0].trim();
prompt = parts.slice(1).join("|-|").trim();
} else {
label = content;
prompt = content;
}
const unescapedOpenCount = (prompt.match(/(?<!\\)\(/g) || []).length;
let multiplier = 1.0;
let isMultiParentheses = unescapedOpenCount >= 2;
if (!isMultiParentheses && unescapedOpenCount === 1) {
const multMatch = prompt.match(/^\((.*):(\d+(?:\.\d+)?)\)$/);
if (multMatch) {
const innerContent = multMatch[1];
const innerUnescapedCount = (innerContent.match(/(?<!\\)\(/g) || []).length;
if (innerUnescapedCount === 0) {
prompt = innerContent.trim();
multiplier = parseFloat(multMatch[2]);
} else {
isMultiParentheses = true;
}
}
}
return { active, label, prompt, multiplier, isMultiParentheses };
};
// Parse text block and dynamically populate combobox options list
self.updateComboOptions = function() {
if (!textWidget || !comboWidget) return;
const textValue = textWidget.value || "";
const lines = textValue.split("\n");
const options = [];
lines.forEach((line, index) => {
if (line.trim() === "") return;
const { active, label } = parseLine(line);
const prefix = active ? "" : "// ";
options.push(`${index + 1}: ${prefix}${label || "(Empty)"}`);
});
if (options.length === 0) options.push("none");
const currentVal = comboWidget.value;
comboWidget.options.values = options;
if (options.includes(currentVal)) {
comboWidget.value = currentVal;
} else {
// Trace and lock onto the row index prefix (e.g. '3:') to ensure robust follow-through when strings mutate
const match = currentVal ? currentVal.match(/^(\d+):/) : null;
if (match) {
const prefix = `${match[1]}:`;
const found = options.find(v => v.startsWith(prefix));
if (found) {
comboWidget.value = found;
return;
}
}
comboWidget.value = options[0];
}
};
// Read row string data and update standalone UI widgets values
self.syncWidgetsFromSelectedLine = function() {
if (self.is_updating_widgets || !textWidget || !comboWidget || !multWidget || !actWidget) return;
const selected = comboWidget.value;
if (!selected || selected === "none") return;
const match = selected.match(/^(\d+):/);
if (!match) return;
const lineIndex = parseInt(match[1], 10) - 1;
const lines = (textWidget.value || "").split("\n");
if (lineIndex < 0 || lineIndex >= lines.length) return;
const { active, multiplier, isMultiParentheses } = parseLine(lines[lineIndex]);
self.is_updating_widgets = true;
multWidget.value = isMultiParentheses ? 1.0 : multiplier;
actWidget.value = active;
self.is_updating_widgets = false;
app.graph.setDirtyCanvas(true, false);
};
// Directly rewrite rows within the multiline element when controls trigger
self.updateTextFromControls = function() {
if (self.is_updating_widgets || !textWidget || !comboWidget || !multWidget || !actWidget) return;
const selected = comboWidget.value;
if (!selected || selected === "none") return;
const match = selected.match(/^(\d+):/);
if (!match) return;
const lineIndex = parseInt(match[1], 10) - 1;
const lines = (textWidget.value || "").split("\n");
if (lineIndex < 0 || lineIndex >= lines.length) return;
const { label, prompt, isMultiParentheses } = parseLine(lines[lineIndex]);
const newActive = actWidget.value;
let newMultiplier = parseFloat(multWidget.value);
// Scope isolation turned on immediately before firing dispatch events to safely block feedback loops
self.is_updating_widgets = true;
if (isMultiParentheses) {
newMultiplier = 1.0;
multWidget.value = 1.0;
}
let promptPart = prompt;
if (!isMultiParentheses && newMultiplier !== 1.0) {
// Convert to string via parseFloat to automatically drop trailing zeros (e.g., 1.10 -> 1.1)
const formattedMultiplier = parseFloat(newMultiplier.toFixed(2)).toString();
promptPart = `(${prompt}:${formattedMultiplier})`;
}
const hasTrailingComma = lines[lineIndex].trim().endsWith(",");
let newLine = lines[lineIndex].includes("|-|") ? `${label}|-|${promptPart}` : promptPart;
if (hasTrailingComma) {
newLine = `${newLine},`;
}
if (!newActive) {
newLine = `//${newLine}`;
}
lines[lineIndex] = newLine;
const newTextValue = lines.join("\n");
textWidget.value = newTextValue;
if (textWidget.callback) {
textWidget.callback(newTextValue);
}
const textarea = getTextareaElement(textWidget);
if (textarea) {
textarea.value = newTextValue;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
textarea.dispatchEvent(new Event("change", { bubbles: true }));
}
self.updateComboOptions();
const updatedSelected = comboWidget.options.values.find(v => v.startsWith(`${lineIndex + 1}:`));
if (updatedSelected) comboWidget.value = updatedSelected;
// Release scope guard only after dropdown re-selection completes flawlessly
self.is_updating_widgets = false;
app.graph.setDirtyCanvas(true, true);
};
setTimeout(() => {
const textarea = getTextareaElement(textWidget);
if (textarea) {
textarea.addEventListener("input", () => {
if (!self.is_updating_widgets) {
self.updateComboOptions();
self.syncWidgetsFromSelectedLine();
}
});
}
}, 100);
if (comboWidget) {
const origComboCallback = comboWidget.callback;
comboWidget.callback = function (value) {
if (origComboCallback) origComboCallback.call(this, value);
self.syncWidgetsFromSelectedLine();
};
}
if (multWidget) {
const origMultCallback = multWidget.callback;
multWidget.callback = function (value) {
if (origMultCallback) origMultCallback.call(this, value);
self.updateTextFromControls();
};
}
if (actWidget) {
const origActCallback = actWidget.callback;
actWidget.callback = function (value) {
if (origActCallback) origActCallback.call(this, value);
self.updateTextFromControls();
};
}
setTimeout(() => {
self.updateComboOptions();
self.syncWidgetsFromSelectedLine();
}, 30);
return r;
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
const r = onConfigure ? onConfigure.apply(this, [info]) : undefined;
if (this.updateComboOptions) this.updateComboOptions();
return r;
};
}
}
});