Rework js file with new version
This commit is contained in:
@@ -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 `<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);
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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 `<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);
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user