From ca0456272d219edfd2ced3f2b92f7bd75c6c241c Mon Sep 17 00:00:00 2001 From: relhamdi Date: Sun, 17 May 2026 13:41:18 +0200 Subject: [PATCH] Rework js file with new version --- web/prompt_color_editor.js | 145 ------------------------ web/prompt_preset_selector.js | 206 ++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+), 145 deletions(-) delete mode 100644 web/prompt_color_editor.js create mode 100644 web/prompt_preset_selector.js diff --git a/web/prompt_color_editor.js b/web/prompt_color_editor.js deleted file mode 100644 index 8814b87..0000000 --- a/web/prompt_color_editor.js +++ /dev/null @@ -1,145 +0,0 @@ -import { app } from "/scripts/app.js"; - -// Escape HTML to prevent injections -const escapeHtml = (str) => - str.replace(/[&<>"]/g, c => ({ - "&": "&", - "<": "<", - ">": ">", - '"': """ - }[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 `${escapeHtml(part)}`; - }); - - const sepHtml = `${escapeHtml(separator)}`; - - return `{%` - + coloredParts.join(sepHtml) - + `%}` - ; - }); - }; - - // 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); - }; - } -}); diff --git a/web/prompt_preset_selector.js b/web/prompt_preset_selector.js new file mode 100644 index 0000000..885eda5 --- /dev/null +++ b/web/prompt_preset_selector.js @@ -0,0 +1,206 @@ +import { app } from "/scripts/app.js"; + +// --- Helpers --- + +// Escape HTML to prevent injections +const escapeHtml = (str) => + str.replace( + /[&<>"]/g, + (c) => + ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + })[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 `${escapeHtml(part)}`; + }); + + const sepHtml = `${escapeHtml(separator)}`; + + result += + `${escapeHtml(openTag)} ` + + coloredParts.join(sepHtml) + + ` ${escapeHtml(closeTag)}`; + + 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); + }; + }, +});