Rework js file with new version

This commit is contained in:
relhamdi
2026-05-17 13:41:18 +02:00
parent 7834e074d8
commit ca0456272d
2 changed files with 206 additions and 145 deletions
-145
View File
@@ -1,145 +0,0 @@
import { app } from "/scripts/app.js";
// Escape HTML to prevent injections
const escapeHtml = (str) =>
str.replace(/[&<>"]/g, c => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;"
}[c]));
// Hook into ComfyUI widget callback (reliable change detection)
const hookWidget = (widget, onChange) => {
if (!widget) return;
const originalCallback = widget.callback;
widget.callback = function (value) {
if (originalCallback) {
originalCallback.call(this, value);
}
onChange(value);
};
};
// Main function to attach the custom editor to the node
function attachEditor(node) {
// Only apply to PromptPresetSelector custom node
if (node.type !== "PromptPresetSelector") return;
const textWidget = node.widgets?.find(w => w.name === "text");
const separatorWidget = node.widgets?.find(w => w.name === "separator");
const presetWidget = node.widgets?.find(w => w.name === "preset_index");
if (!textWidget) return;
const textarea = textWidget.inputEl || textWidget.element;
if (!textarea || !textarea.parentNode) return;
// Prevent double initialization
if (textarea._presetEditorAttached) return;
textarea._presetEditorAttached = true;
// Hide the original textarea
textarea.style.display = "none";
// Create a contentEditable div for rich coloring
const editor = document.createElement("div");
editor.contentEditable = true;
editor.style.cssText = `
height: 100%;
padding: 6px;
border: 1px solid #444;
background: #111;
color: #eee;
font-family: monospace;
font-size: 10px;
white-space: pre-wrap;
overflow-y: auto;
outline: none;
box-sizing: border-box;
`;
textarea.parentNode.appendChild(editor);
editor.innerText = textarea.value;
// Used colors
const tagColor = "#ff9800";
const inactiveColor = "#777";
let lastRawText = textarea.value;
// Text recolor function
const recolor = (force = false) => {
const raw = editor.innerText;
// Skip if no tags found
if (!raw.includes("{%")) return;
// Prevent caret jump while typing
if (!force && document.activeElement === editor) return;
if (!force && raw !== lastRawText) return;
const separator = separatorWidget?.value || "|";
const presetIndex = Math.max(1, presetWidget?.value || 1) - 1;
const safe = escapeHtml(raw);
editor.innerHTML = safe.replace(/\{%(.*?)%\}/gs, (_, content) => {
const rawParts = content.split(separator);
const coloredParts = rawParts.map((part, i) => {
const color = i === presetIndex ? "inherit" : inactiveColor;
return `<span style="color:${color}">${escapeHtml(part)}</span>`;
});
const sepHtml = `<span style="color:${tagColor}">${escapeHtml(separator)}</span>`;
return `<span style="color:${tagColor}">{%</span>`
+ coloredParts.join(sepHtml)
+ `<span style="color:${tagColor}">%}</span>`
;
});
};
// Sync editor with textarea
editor.addEventListener("input", () => {
const raw = editor.innerText;
textarea.value = raw;
lastRawText = raw;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
});
// Raw text while editing
editor.addEventListener("focus", () => {
editor.innerText = textarea.value;
});
// Colored mode when leaving editor
editor.addEventListener("blur", () => recolor(true));
// React instantly to widget changes
hookWidget(separatorWidget, () => recolor(true));
hookWidget(presetWidget, () => recolor(true));
// Initial render
recolor(true);
}
// Register the extension
app.registerExtension({
name: "PromptPresetColorEditor",
beforeRegisterNodeDef(nodeType) {
nodeType.prototype.onNodeCreated = function () {
const node = this;
const tryAttach = () => {
attachEditor(node);
// Retry until widgets DOM is ready
if (!node.widgets?.find(w => w.name === "text")?.inputEl?.parentNode) {
requestAnimationFrame(tryAttach);
}
};
requestAnimationFrame(tryAttach);
};
}
});
+206
View File
@@ -0,0 +1,206 @@
import { app } from "/scripts/app.js";
// --- Helpers ---
// Escape HTML to prevent injections
const escapeHtml = (str) =>
str.replace(
/[&<>"]/g,
(c) =>
({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
})[c],
);
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// Hook into ComfyUI widget callback (reliable change detection)
const hookWidget = (widget, onChange) => {
if (!widget) return;
const original = widget.callback;
widget.callback = function (value) {
if (original) original.call(this, value);
onChange(value);
};
};
// Parse the syntax param to { openTag, separator, closeTag }
const parseSyntax = (syntax) => {
const parts = (syntax || "{% | %}").split(/\s+/);
if (parts.length !== 3)
return { openTag: "{%", separator: "|", closeTag: "%}" };
return { openTag: parts[0], separator: parts[1], closeTag: parts[2] };
};
// --- Colors ---
const COLORS = {
tag: "#ff9800",
sep: "#ff9800",
active: "#e0e0e0",
inactive: "#555",
};
// --- Editor ---
function buildHighlightedHtml(raw, syntax, presetIndex) {
const { openTag, separator, closeTag } = parseSyntax(syntax);
const pattern = new RegExp(
escapeRegex(openTag) + "\\s*(.*?)\\s*" + escapeRegex(closeTag),
"gs",
);
let result = "";
let lastIndex = 0;
for (const match of raw.matchAll(pattern)) {
// Text before the match, escaped
result += escapeHtml(raw.slice(lastIndex, match.index));
const parts = match[1].split(separator);
const coloredParts = parts.map((part, i) => {
const color = i === presetIndex ? COLORS.active : COLORS.inactive;
return `<span style="color:${color}">${escapeHtml(part)}</span>`;
});
const sepHtml = `<span style="color:${COLORS.sep}">${escapeHtml(separator)}</span>`;
result +=
`<span style="color:${COLORS.tag}">${escapeHtml(openTag)}</span> ` +
coloredParts.join(sepHtml) +
` <span style="color:${COLORS.tag}">${escapeHtml(closeTag)}</span>`;
lastIndex = match.index + match[0].length;
}
// Remaining text after last match
result += escapeHtml(raw.slice(lastIndex));
return result;
}
// Main function to attach the custom editor to the node
function attachEditor(node) {
// Only apply to PromptPresetSelector custom node
if (node.type !== "PromptPresetSelector") return;
const textWidget = node.widgets?.find((w) => w.name === "text");
const syntaxWidget = node.widgets?.find((w) => w.name === "syntax");
const presetWidget = node.widgets?.find((w) => w.name === "preset_index");
if (!textWidget) return;
const textarea = textWidget.inputEl || textWidget.element;
if (!textarea?.parentNode) return;
// Prevent double initialization
if (textarea._editorAttached) return;
textarea._editorAttached = true;
// Hide native textarea
textarea.style.display = "none";
// Create contentEditable div
const editor = document.createElement("div");
editor.contentEditable = "true";
editor.spellcheck = false;
editor.style.cssText = `
width: 100%;
height: 100%;
padding: 6px;
border: 1px solid #444;
border-radius: 4px;
background: #111;
color: ${COLORS.active};
font-family: monospace;
font-size: 10px;
white-space: pre-wrap;
overflow-y: auto;
outline: none;
box-sizing: border-box;
cursor: text;
line-height: 1.5;
`;
textarea.parentNode.appendChild(editor);
// State helpers
const getPresetIndex = () => presetWidget?.value ?? 0;
const getSyntax = () => syntaxWidget?.value ?? "{% | %}";
// Render modes
const renderColored = () => {
const raw = textarea.value;
editor.innerHTML = buildHighlightedHtml(
raw,
getSyntax(),
getPresetIndex(),
);
};
const renderRaw = () => {
editor.innerText = textarea.value;
};
// --- Event listener - Input: Sync editor -> textarea ---
editor.addEventListener("input", () => {
textarea.value = editor.innerText;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
});
// --- Event listener - Focus: Switch to raw text for editing ---
editor.addEventListener("focus", () => {
renderRaw();
// Place caret at end
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
});
// --- Event listener - Blur: Switch to colored view ---
editor.addEventListener("blur", () => renderColored());
// React to widget changes
hookWidget(syntaxWidget, () => {
if (document.activeElement !== editor) renderColored();
});
hookWidget(presetWidget, () => {
if (document.activeElement !== editor) renderColored();
});
// Initial render
renderColored();
}
// --- Registration ---
app.registerExtension({
name: "PromptPresetSelector",
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "PromptPresetSelector") return;
const original = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
if (original) original.call(this);
const node = this;
// Retry until widgets DOM is ready
const tryAttach = () => {
const textWidget = node.widgets?.find((w) => w.name === "text");
if (textWidget?.inputEl?.parentNode) {
attachEditor(node);
} else {
requestAnimationFrame(tryAttach);
}
};
requestAnimationFrame(tryAttach);
};
},
});