Files

684 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../../scripts/app.js";
app.registerExtension({
name: "Lera.TagTableNode",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "TagTableNode") {
return;
}
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
const originalOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onNodeCreated = function () {
const result = originalOnNodeCreated
? originalOnNodeCreated.apply(this, arguments)
: undefined;
setupTagTable.call(this);
return result;
};
nodeType.prototype.onConfigure = function () {
const result = originalOnConfigure
? originalOnConfigure.apply(this, arguments)
: undefined;
if (this.__tagTableRefresh) {
requestAnimationFrame(() => this.__tagTableRefresh?.());
}
return result;
};
function setupTagTable() {
const rowsWidget = this.widgets?.find(w => w.name === "rows");
const tableDataWidget = this.widgets?.find(w => w.name === "table_data");
if (!rowsWidget || !tableDataWidget) {
return;
}
if (this.__tagTableInitialized) {
this.__tagTableRefresh?.();
return;
}
this.__tagTableInitialized = true;
tableDataWidget.hidden = true;
this.properties = this.properties || {};
const MIN_WIDTH = 620;
const MIN_HEIGHT = 320;
const SIZE_STEP = 10;
const DEFAULT_WIDTH = 760;
const DEFAULT_HEIGHT = 520;
const safeParseArray = (value) => {
try {
const parsed = JSON.parse(value || "[]");
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
};
const snapSize = (value, minValue) => {
const n = Number(value);
if (!Number.isFinite(n)) return minValue;
return Math.max(minValue, Math.round(n / SIZE_STEP) * SIZE_STEP);
};
const loadState = () => {
let data = safeParseArray(this.properties.tag_table_data);
if (!data.length) {
data = safeParseArray(tableDataWidget.value);
}
return data.map(row => ({
enabled: row?.enabled !== false,
tag: String(row?.tag ?? ""),
comment: String(row?.comment ?? "")
}));
};
let state = loadState();
let rowRefs = [];
let dragIndex = null;
let saveTimer = null;
let layoutQueued = false;
let preferredWidth = snapSize(
Number(this.properties?.tag_table_width) ||
(Array.isArray(this.size) ? this.size[0] : DEFAULT_WIDTH),
MIN_WIDTH
);
let preferredHeight = snapSize(
Number(this.properties?.tag_table_height) ||
(Array.isArray(this.size) ? this.size[1] : DEFAULT_HEIGHT),
MIN_HEIGHT
);
const ensureRowCount = (count) => {
while (state.length < count) {
state.push({
enabled: true,
tag: "",
comment: ""
});
}
while (state.length > count) {
state.pop();
}
};
const buildFinalPrompt = () => {
return state
.filter(row => row.enabled && String(row.tag || "").trim())
.map(row => String(row.tag || "").trim())
.join(" ");
};
const autoResizeTextarea = (textarea) => {
if (!textarea) return;
textarea.style.height = "auto";
textarea.style.height = Math.max(textarea.scrollHeight, 30) + "px";
};
const syncSingleRowHeight = (tagInput, commentInput) => {
autoResizeTextarea(tagInput);
autoResizeTextarea(commentInput);
const rowHeight = Math.max(
tagInput?.scrollHeight || 30,
commentInput?.scrollHeight || 30,
30
);
if (tagInput) tagInput.style.height = `${rowHeight}px`;
if (commentInput) commentInput.style.height = `${rowHeight}px`;
};
const persistState = () => {
const json = JSON.stringify(state);
tableDataWidget.value = json;
this.properties.tag_table_data = json;
this.properties.tag_table_rows = Number(rowsWidget.value || 1);
this.properties.tag_table_width = preferredWidth;
this.properties.tag_table_height = preferredHeight;
if (tableDataWidget.callback) {
tableDataWidget.callback(tableDataWidget.value);
}
this.setDirtyCanvas(true, true);
app.graph.setDirtyCanvas(true, true);
};
const queueSave = () => {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
persistState();
}, 120);
};
const applyNodeSize = () => {
const currentWidth = Array.isArray(this.size) ? this.size[0] : 0;
const currentHeight = Array.isArray(this.size) ? this.size[1] : 0;
if (
Math.abs(currentWidth - preferredWidth) > 1 ||
Math.abs(currentHeight - preferredHeight) > 1
) {
this.setSize([preferredWidth, preferredHeight]);
this.setDirtyCanvas(true, true);
app.graph.setDirtyCanvas(true, true);
}
};
const queueLayout = () => {
if (layoutQueued) return;
layoutQueued = true;
requestAnimationFrame(() => {
layoutQueued = false;
for (const ref of rowRefs) {
syncSingleRowHeight(ref.tagInput, ref.commentInput);
}
finalPromptArea.value = buildFinalPrompt();
autoResizeTextarea(finalPromptArea);
this.setDirtyCanvas(true, true);
app.graph.setDirtyCanvas(true, true);
});
};
const root = document.createElement("div");
root.style.display = "flex";
root.style.flexDirection = "column";
root.style.gap = "6px";
root.style.padding = "6px";
root.style.minWidth = "520px";
root.style.width = "100%";
root.style.boxSizing = "border-box";
root.style.overflow = "visible";
const styleBlock = document.createElement("style");
styleBlock.textContent = `
.lera-tt-header-row,
.lera-tt-row {
display: grid;
grid-template-columns: 26px 52px minmax(0, 1fr) minmax(0, 1fr) 30px;
gap: 6px;
width: 100%;
box-sizing: border-box;
align-items: start;
}
.lera-tt-header-row {
font-weight: 700;
}
.lera-tt-row {
border-radius: 4px;
}
.lera-tt-row.is-disabled textarea,
.lera-tt-row.is-disabled .lera-tt-drag,
.lera-tt-row.is-disabled .lera-tt-delete-btn {
opacity: 0.45;
}
.lera-tt-row.is-dragover {
background: rgba(122,162,255,0.10);
outline: 1px solid #7aa2ff;
}
.lera-tt-drag {
cursor: grab;
user-select: none;
text-align: center;
padding-top: 6px;
font-size: 14px;
opacity: 0.8;
}
.lera-tt-toggle {
display: flex;
align-items: center;
justify-content: center;
min-height: 30px;
}
.lera-tt-toggle input {
transform: scale(1.05);
}
.lera-tt-input,
.lera-tt-note {
width: 100%;
min-height: 30px;
font: inherit;
line-height: 1.25;
padding: 4px 6px;
border-radius: 4px;
border: 1px solid var(--border-color, #666);
background: var(--comfy-input-bg, rgba(0,0,0,0.20));
color: var(--input-text, inherit);
box-sizing: border-box;
resize: none;
overflow: hidden;
display: block;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
}
.lera-tt-preview-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 8px;
}
.lera-tt-copy-btn {
font: inherit;
font-size: 12px;
line-height: 1;
padding: 6px 10px;
border-radius: 5px;
border: 1px solid var(--border-color, #666);
background: var(--comfy-input-bg, rgba(0,0,0,0.20));
color: var(--input-text, inherit);
cursor: pointer;
}
.lera-tt-copy-btn:hover {
filter: brightness(1.08);
}
.lera-tt-delete-wrap {
display: flex;
align-items: center;
justify-content: center;
min-height: 30px;
}
.lera-tt-delete-btn {
width: 26px;
height: 26px;
border-radius: 4px;
border: 1px solid var(--border-color, #666);
background: var(--comfy-input-bg, rgba(0,0,0,0.20));
color: var(--input-text, inherit);
cursor: pointer;
padding: 0;
line-height: 1;
font-size: 14px;
}
.lera-tt-delete-btn:hover {
filter: brightness(1.08);
}
`;
root.appendChild(styleBlock);
const header = document.createElement("div");
header.className = "lera-tt-header-row";
const hDrag = document.createElement("div");
hDrag.textContent = "≡";
const hOn = document.createElement("div");
hOn.textContent = "On";
const hTag = document.createElement("div");
hTag.textContent = "Tag";
const hComment = document.createElement("div");
hComment.textContent = "Comment";
const hDelete = document.createElement("div");
hDelete.textContent = "×";
hDelete.style.textAlign = "center";
header.appendChild(hDrag);
header.appendChild(hOn);
header.appendChild(hTag);
header.appendChild(hComment);
header.appendChild(hDelete);
root.appendChild(header);
const rowsWrap = document.createElement("div");
rowsWrap.style.display = "flex";
rowsWrap.style.flexDirection = "column";
rowsWrap.style.gap = "6px";
rowsWrap.style.width = "100%";
rowsWrap.style.boxSizing = "border-box";
root.appendChild(rowsWrap);
const previewTop = document.createElement("div");
previewTop.className = "lera-tt-preview-top";
const finalPromptLabel = document.createElement("div");
finalPromptLabel.textContent = "Final Prompt";
finalPromptLabel.style.fontWeight = "bold";
const copyButton = document.createElement("button");
copyButton.type = "button";
copyButton.className = "lera-tt-copy-btn";
copyButton.textContent = "Copy";
previewTop.appendChild(finalPromptLabel);
previewTop.appendChild(copyButton);
root.appendChild(previewTop);
const finalPromptArea = document.createElement("textarea");
finalPromptArea.readOnly = true;
finalPromptArea.placeholder = "Final prompt";
finalPromptArea.rows = 4;
finalPromptArea.style.width = "100%";
finalPromptArea.style.minHeight = "90px";
finalPromptArea.style.font = "inherit";
finalPromptArea.style.lineHeight = "1.25";
finalPromptArea.style.padding = "6px 8px";
finalPromptArea.style.borderRadius = "4px";
finalPromptArea.style.border = "1px solid var(--border-color, #666)";
finalPromptArea.style.background = "var(--comfy-input-bg, rgba(0,0,0,0.20))";
finalPromptArea.style.color = "var(--input-text, inherit)";
finalPromptArea.style.boxSizing = "border-box";
finalPromptArea.style.resize = "none";
finalPromptArea.style.overflow = "hidden";
finalPromptArea.style.display = "block";
finalPromptArea.style.whiteSpace = "pre-wrap";
finalPromptArea.style.wordBreak = "break-word";
finalPromptArea.style.overflowWrap = "anywhere";
root.appendChild(finalPromptArea);
copyButton.addEventListener("click", async () => {
const text = buildFinalPrompt();
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const temp = document.createElement("textarea");
temp.value = text;
document.body.appendChild(temp);
temp.select();
document.execCommand("copy");
document.body.removeChild(temp);
}
const oldText = copyButton.textContent;
copyButton.textContent = "Copied";
setTimeout(() => {
copyButton.textContent = oldText;
}, 900);
} catch {
const oldText = copyButton.textContent;
copyButton.textContent = "Failed";
setTimeout(() => {
copyButton.textContent = oldText;
}, 900);
}
});
const clearDragOverState = () => {
for (const ref of rowRefs) {
ref.row.classList.remove("is-dragover");
}
};
const deleteRowAt = (index) => {
if (index < 0 || index >= state.length) return;
if (state.length <= 1) {
state[0] = {
enabled: true,
tag: "",
comment: ""
};
rowsWidget.value = 1;
} else {
state.splice(index, 1);
rowsWidget.value = state.length;
}
this.properties.tag_table_rows = Number(rowsWidget.value || 1);
renderRows();
queueSave();
};
const moveRow = (fromIndex, toIndex) => {
if (
fromIndex == null ||
toIndex == null ||
fromIndex === toIndex ||
fromIndex < 0 ||
toIndex < 0 ||
fromIndex >= state.length ||
toIndex >= state.length
) {
return;
}
const [moved] = state.splice(fromIndex, 1);
state.splice(toIndex, 0, moved);
renderRows();
queueSave();
};
const renderRows = () => {
const count = Number(rowsWidget.value || 1);
ensureRowCount(count);
rowsWrap.innerHTML = "";
rowRefs = [];
dragIndex = null;
for (let i = 0; i < count; i++) {
const row = document.createElement("div");
row.className = "lera-tt-row";
row.draggable = true;
const dragHandle = document.createElement("div");
dragHandle.className = "lera-tt-drag";
dragHandle.textContent = "⋮⋮";
dragHandle.title = "Drag row";
const enabledWrap = document.createElement("div");
enabledWrap.className = "lera-tt-toggle";
const enabledInput = document.createElement("input");
enabledInput.type = "checkbox";
enabledInput.checked = state[i]?.enabled !== false;
enabledWrap.appendChild(enabledInput);
const tagInput = document.createElement("textarea");
tagInput.className = "lera-tt-input";
tagInput.value = state[i]?.tag || "";
tagInput.placeholder = "tag";
tagInput.rows = 1;
tagInput.wrap = "soft";
const commentInput = document.createElement("textarea");
commentInput.className = "lera-tt-note";
commentInput.value = state[i]?.comment || "";
commentInput.placeholder = "comment";
commentInput.rows = 1;
commentInput.wrap = "soft";
const deleteWrap = document.createElement("div");
deleteWrap.className = "lera-tt-delete-wrap";
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.className = "lera-tt-delete-btn";
deleteButton.title = "Delete row";
deleteButton.textContent = "×";
deleteWrap.appendChild(deleteButton);
const applyEnabledVisual = () => {
const enabled = enabledInput.checked;
row.classList.toggle("is-disabled", !enabled);
};
enabledInput.addEventListener("change", () => {
state[i].enabled = enabledInput.checked;
applyEnabledVisual();
queueLayout();
queueSave();
});
tagInput.addEventListener("input", () => {
state[i].tag = tagInput.value;
syncSingleRowHeight(tagInput, commentInput);
queueLayout();
queueSave();
});
commentInput.addEventListener("input", () => {
state[i].comment = commentInput.value;
syncSingleRowHeight(tagInput, commentInput);
queueLayout();
queueSave();
});
deleteButton.addEventListener("click", () => {
deleteRowAt(i);
});
row.addEventListener("dragstart", (e) => {
dragIndex = i;
row.style.opacity = "0.55";
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", String(i));
}
});
row.addEventListener("dragend", () => {
row.style.opacity = "";
dragIndex = null;
clearDragOverState();
});
row.addEventListener("dragover", (e) => {
e.preventDefault();
clearDragOverState();
row.classList.add("is-dragover");
});
row.addEventListener("dragleave", () => {
row.classList.remove("is-dragover");
});
row.addEventListener("drop", (e) => {
e.preventDefault();
row.classList.remove("is-dragover");
moveRow(dragIndex, i);
});
applyEnabledVisual();
row.appendChild(dragHandle);
row.appendChild(enabledWrap);
row.appendChild(tagInput);
row.appendChild(commentInput);
row.appendChild(deleteWrap);
rowsWrap.appendChild(row);
rowRefs.push({
row,
enabledInput,
tagInput,
commentInput,
deleteButton,
});
}
finalPromptArea.value = buildFinalPrompt();
for (const ref of rowRefs) {
syncSingleRowHeight(ref.tagInput, ref.commentInput);
}
autoResizeTextarea(finalPromptArea);
queueLayout();
};
const oldRowsCallback = rowsWidget.callback;
rowsWidget.callback = (value, ...args) => {
if (oldRowsCallback) {
oldRowsCallback.call(rowsWidget, value, ...args);
}
const count = Math.max(1, Number(value || 1));
ensureRowCount(count);
renderRows();
queueSave();
};
const domWidget = this.addDOMWidget("tag_table_dom", "tag_table_dom", root, {
serialize: false,
hideOnZoom: false,
});
if (domWidget) {
domWidget.computeSize = (width) => {
return [
Math.max(width || MIN_WIDTH, MIN_WIDTH),
Math.max(root.scrollHeight + 16, 260),
];
};
}
const oldOnResize = this.onResize?.bind(this);
this.onResize = (...args) => {
if (oldOnResize) {
oldOnResize(...args);
}
this.properties = this.properties || {};
const actualWidth = Array.isArray(this.size) ? this.size[0] : null;
const actualHeight = Array.isArray(this.size) ? this.size[1] : null;
if (actualWidth) {
preferredWidth = snapSize(actualWidth, MIN_WIDTH);
this.size[0] = preferredWidth;
this.properties.tag_table_width = preferredWidth;
}
if (actualHeight) {
preferredHeight = snapSize(actualHeight, MIN_HEIGHT);
this.size[1] = preferredHeight;
this.properties.tag_table_height = preferredHeight;
}
this.setDirtyCanvas(true, true);
app.graph.setDirtyCanvas(true, true);
queueSave();
};
this.__tagTableRefresh = () => {
state = loadState();
if (this.properties?.tag_table_rows != null) {
rowsWidget.value = Number(this.properties.tag_table_rows);
}
preferredWidth = snapSize(
Number(this.properties?.tag_table_width) || preferredWidth,
MIN_WIDTH
);
preferredHeight = snapSize(
Number(this.properties?.tag_table_height) || preferredHeight,
MIN_HEIGHT
);
applyNodeSize();
renderRows();
};
applyNodeSize();
renderRows();
queueSave();
}
}
});