Files
2026-06-26 23:56:52 +09:00

1018 lines
29 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
const IMAGE_SEQUENCE_CLASS = "ComfyUIPromptImageSequence";
const IMAGE_MASK_SEQUENCE_CLASS = "ComfyUIPromptImageMaskSequence";
const COMBO_CLASS = "ComfyUIPromptSequenceCombo";
const JOIN_CLASS = "ComfyUIPromptSequenceJoin";
const MAX_JOIN_INPUTS = 32;
const ALL_TOPICS = "__all_topics__";
const STYLE_ID = "prompt-sequence-style";
const WIDGET_TOOLTIPS = {
ComfyUIPromptSequenceText: {
prompts: "Each non-empty line becomes one prompt item.",
mode: "sequential: output top to bottom. random: shuffle before output.",
seed: "For random mode: -1 makes a new order each queue; a fixed value repeats the same order.",
max_items: "0 outputs all prompts. A positive value outputs up to that many prompts.",
},
ComfyUIPromptSequenceCombo: {
prompts: "Used only when source is not connected. Hidden and ignored while source is connected.",
mode: "sequential: keep source order. random: shuffle before max_items is applied.",
max_items: "0 outputs all available prompts. A positive value outputs up to that many prompts.",
seed: "Used only in random mode. -1 makes a new order each queue; a fixed value repeats the same order.",
},
ComfyUIPromptSequenceJoin: {
input_count: "Number of visible text inputs. Click update inputs after changing it.",
separator: "Text inserted between parts. Use \\n or /n for a newline, including ,\\n or ,/n.",
skip_empty: "When enabled, blank missing parts are removed before joining, including their separator.",
length_policy: "longest: emit up to the longest input. shortest: stop at the shortest connected input.",
missing_policy: "blank: missing values become empty text. repeat_last: reuse the shorter input's final value.",
max_items: "0 keeps all joined prompts. A positive value limits the final joined output count.",
"update inputs": "Applies input_count by adding or removing visible text input slots.",
},
ComfyUIPromptTagFormatter: {
text: "Normalizes line breaks and commas into one comma-space-separated tag line.",
},
ComfyUIPromptImageSequence: {
selection_json: "Hidden picker state. Use the thumbnail grid instead of editing this manually.",
mode: "sequential: use checked cards in order. random: shuffle checked cards before output.",
seed: "For random mode: -1 makes a new order each queue; a fixed value repeats the same order.",
max_items: "0 outputs all checked prompts. A positive value outputs up to that many prompts.",
},
ComfyUIPromptImageMaskSequence: {
selection_json: "Hidden picker state. Use the thumbnail grid instead of editing this manually.",
mode: "sequential: use checked cards in order. random: shuffle checked cards before output.",
seed: "For random mode: -1 makes a new order each queue; a fixed value repeats the same order.",
max_items: "0 outputs all checked records. A positive value outputs up to that many records.",
},
};
function injectStyle() {
if (document.getElementById(STYLE_ID)) {
return;
}
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
.ps-picker {
box-sizing: border-box;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-width: 320px;
min-height: 0;
padding: 8px;
color: #ddd;
font: 12px/1.4 Arial, sans-serif;
background: #222;
border: 1px solid #444;
border-radius: 6px;
overflow: hidden;
}
.ps-toolbar {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto auto;
gap: 6px;
align-items: center;
margin-bottom: 8px;
}
.ps-picker select,
.ps-picker input,
.ps-picker textarea,
.ps-picker button {
box-sizing: border-box;
width: 100%;
min-height: 26px;
color: #eee;
background: #171717;
border: 1px solid #555;
border-radius: 4px;
font: inherit;
}
.ps-picker textarea {
min-height: 64px;
resize: vertical;
}
.ps-picker button {
width: auto;
padding: 0 8px;
cursor: pointer;
background: #303030;
}
.ps-picker button:disabled {
opacity: 0.55;
cursor: wait;
}
.ps-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
align-content: start;
gap: 6px;
min-height: 0;
max-height: 100%;
flex: 1 1 auto;
overflow-x: hidden;
overflow-y: auto;
padding-right: 2px;
}
.ps-card {
position: relative;
display: grid;
grid-template-rows: auto 22px;
gap: 4px;
min-width: 0;
padding: 5px;
background: #191919;
border: 1px solid #3d3d3d;
border-radius: 6px;
cursor: pointer;
user-select: none;
}
.ps-card.ps-selected {
border-color: #8fb7ff;
box-shadow: inset 0 0 0 1px rgba(143, 183, 255, 0.45);
}
.ps-media {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
overflow: hidden;
background: #101010;
border-radius: 4px;
}
.ps-thumb {
display: block;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.ps-picker.ps-contain .ps-thumb {
object-fit: contain;
}
.ps-placeholder {
display: grid;
place-items: center;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
color: #888;
}
.ps-check {
position: absolute;
top: 8px;
left: 8px;
z-index: 30;
width: auto !important;
min-height: 0 !important;
}
.ps-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #f2f2f2;
font-weight: 600;
line-height: 22px;
}
.ps-detail {
display: none;
position: absolute;
inset: 5px;
z-index: 20;
overflow: auto;
padding: 8px;
color: #eee;
white-space: pre-wrap;
background: rgba(24, 24, 24, 0.98);
border: 1px solid #666;
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}
.ps-card:hover .ps-detail,
.ps-card:focus-within .ps-detail {
display: block;
}
.ps-empty {
grid-column: 1 / -1;
color: #999;
padding: 10px 2px;
}
.ps-status {
min-height: 18px;
color: #aaa;
margin-top: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ps-editor {
display: none;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #444;
}
.ps-editor.ps-open {
display: block;
}
.ps-editor-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-bottom: 6px;
}
.ps-file-field {
display: grid;
grid-template-rows: auto auto;
gap: 3px;
min-width: 0;
color: #bbb;
}
.ps-file-field span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
}
.ps-editor-actions {
display: flex;
gap: 6px;
justify-content: flex-end;
margin-top: 6px;
}
.ps-menu {
position: fixed;
z-index: 999999;
display: none;
min-width: 128px;
padding: 4px;
background: #202020;
border: 1px solid #555;
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}
.ps-menu button {
display: block;
width: 100%;
min-height: 28px;
padding: 0 8px;
color: #eee;
text-align: left;
background: transparent;
border: 0;
border-radius: 4px;
cursor: pointer;
font: 12px/1.4 Arial, sans-serif;
}
.ps-menu button:hover {
background: #383838;
}
@media (max-width: 520px) {
.ps-grid {
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
}
.ps-toolbar {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
}
`;
document.head.appendChild(style);
}
function stopCanvasEvents(root) {
[
"pointerdown",
"pointerup",
"mousedown",
"mouseup",
"click",
"dblclick",
"contextmenu",
"wheel",
"keydown",
"keyup",
"input",
"change",
].forEach((eventName) => {
root.addEventListener(eventName, (event) => event.stopPropagation());
});
}
function readSelection(widget) {
try {
const parsed = JSON.parse(widget?.value || "{}");
return {
project: parsed.project || "",
topic: parsed.topic || "",
selected: Array.isArray(parsed.selected) ? parsed.selected : [],
};
} catch {
return { project: "", topic: "", selected: [] };
}
}
function writeSelection(node, widget, state) {
widget.value = JSON.stringify({
project: state.project || "",
topic: state.topic || "",
selected: Array.from(new Set(state.selected || [])),
});
if (typeof node.setDirtyCanvas === "function") {
node.setDirtyCanvas(true, true);
} else {
app.graph?.setDirtyCanvas(true, true);
}
}
function findProject(library, projectId) {
return library.projects.find((project) => project.id === projectId) || null;
}
function findTopic(project, topicId) {
return project?.topics?.find((topic) => topic.id === topicId) || null;
}
function projectItems(project) {
return (project?.topics || []).flatMap((topic) => topic.items || []);
}
function topicForItem(project, item) {
const topicId = String(item?.id || "").split("/")[1] || "";
return findTopic(project, topicId);
}
function currentItems(library, state) {
const project = findProject(library, state.project);
if (state.topic === ALL_TOPICS) {
return projectItems(project);
}
const topic = findTopic(project, state.topic);
return topic?.items || [];
}
function hideRawSelectionWidget(node) {
const widget = node.widgets?.find((item) => item.name === "selection_json");
if (!widget) {
return null;
}
widget.type = "hidden";
widget.computeSize = () => [0, -4];
return widget;
}
function option(value, text, selected) {
const item = document.createElement("option");
item.value = value;
item.textContent = text;
item.selected = selected;
return item;
}
function makeInput(placeholder) {
const input = document.createElement("input");
input.placeholder = placeholder;
return input;
}
function makeFileField(labelText) {
const field = document.createElement("label");
field.className = "ps-file-field";
const label = document.createElement("span");
label.textContent = labelText;
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
field.append(label, input);
return { field, input };
}
function makeButton(text) {
const button = document.createElement("button");
button.type = "button";
button.textContent = text;
return button;
}
function normalizeState(library, state) {
const projects = library.projects || [];
if (!projects.length) {
return { project: "", topic: "", selected: [] };
}
const project = findProject(library, state.project) || projects[0];
const topics = project.topics || [];
if (!topics.length) {
return { project: project.id, topic: "", selected: [] };
}
const topic = state.topic === ALL_TOPICS ? null : findTopic(project, state.topic);
const nextTopic = state.topic === ALL_TOPICS || !topic ? ALL_TOPICS : topic.id;
const items = nextTopic === ALL_TOPICS ? projectItems(project) : topic.items || [];
const validIds = new Set(items.map((item) => item.id));
return {
project: project.id,
topic: nextTopic,
selected: (state.selected || []).filter((id) => validIds.has(id)),
};
}
async function fetchLibrary() {
const response = await api.fetchApi("/prompt_sequence/assets");
if (!response.ok) {
throw new Error(`Library request failed: ${response.status}`);
}
return response.json();
}
async function savePromptAsset(form) {
const response = await api.fetchApi("/prompt_sequence/assets", {
method: "POST",
body: form,
});
if (!response.ok) {
const message = await response.text();
throw new Error(message || `Save failed: ${response.status}`);
}
return response.json();
}
async function deletePromptAsset(itemId) {
const params = new URLSearchParams({ id: itemId });
const response = await api.fetchApi(`/prompt_sequence/assets?${params}`, {
method: "DELETE",
});
if (!response.ok) {
const message = await response.text();
throw new Error(message || `Delete failed: ${response.status}`);
}
return response.json();
}
function clampJoinInputCount(value) {
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed)) {
return 2;
}
return Math.max(1, Math.min(MAX_JOIN_INPUTS, parsed));
}
function getWidget(node, name) {
return node.widgets?.find((widget) => widget.name === name) || null;
}
function setWidgetTooltip(widget, text) {
if (!widget || !text) {
return;
}
widget.tooltip = text;
widget.options = widget.options || {};
widget.options.tooltip = text;
}
function setupNodeWidgetTooltips(node, nodeName) {
const tooltips = WIDGET_TOOLTIPS[nodeName];
if (!tooltips) {
return;
}
for (const widget of node.widgets || []) {
setWidgetTooltip(widget, tooltips[widget.name]);
}
}
function setWidgetHidden(widget, hidden) {
if (!widget) {
return;
}
if (!widget._promptSequenceOriginalType) {
widget._promptSequenceOriginalType = widget.type;
widget._promptSequenceOriginalComputeSize = widget.computeSize;
}
if (hidden) {
widget.type = "hidden";
widget.computeSize = () => [0, -4];
} else {
widget.type = widget._promptSequenceOriginalType;
widget.computeSize = widget._promptSequenceOriginalComputeSize;
}
}
function isInputConnected(node, inputName) {
return Boolean(
(node.inputs || []).find((input) => input.name === inputName && input.link != null),
);
}
function setupComboNode(node) {
if (node._promptSequenceComboReady) {
return;
}
node._promptSequenceComboReady = true;
const syncWidgets = () => {
const promptsWidget = getWidget(node, "prompts");
const sourceWidget = getWidget(node, "source");
const sourceConnected = isInputConnected(node, "source");
setWidgetHidden(promptsWidget, sourceConnected);
setWidgetHidden(sourceWidget, true);
if (promptsWidget) {
setWidgetTooltip(
promptsWidget,
sourceConnected
? "Source is connected. Local prompts are hidden, ignored, and saved only for later reuse."
: WIDGET_TOOLTIPS.ComfyUIPromptSequenceCombo.prompts,
);
}
node.setSize?.(node.computeSize?.() || node.size);
app.graph?.setDirtyCanvas(true, true);
};
const previousOnConfigure = node.onConfigure;
node.onConfigure = function () {
const result = previousOnConfigure?.apply(this, arguments);
syncWidgets();
return result;
};
const previousOnConnectionsChange = node.onConnectionsChange;
node.onConnectionsChange = function () {
const result = previousOnConnectionsChange?.apply(this, arguments);
syncWidgets();
return result;
};
syncWidgets();
}
function isJoinTextInput(input) {
return /^text_\d+$/.test(input?.name || "");
}
function joinInputNumber(input) {
const match = /^text_(\d+)$/.exec(input?.name || "");
return match ? Number.parseInt(match[1], 10) : 0;
}
function resizeJoinInputs(node, requestedCount) {
const count = clampJoinInputCount(requestedCount);
for (let index = (node.inputs?.length || 0) - 1; index >= 0; index -= 1) {
const input = node.inputs[index];
if (isJoinTextInput(input) && joinInputNumber(input) > count) {
node.removeInput(index);
}
}
const existing = new Set(
(node.inputs || []).filter(isJoinTextInput).map((input) => joinInputNumber(input)),
);
for (let index = 1; index <= count; index += 1) {
if (!existing.has(index)) {
node.addInput(`text_${index}`, "STRING");
}
}
const inputCountWidget = getWidget(node, "input_count");
if (inputCountWidget) {
inputCountWidget.value = count;
}
node.setSize?.(node.computeSize?.() || node.size);
app.graph?.setDirtyCanvas(true, true);
}
function setupJoinNode(node) {
if (node._promptSequenceJoinReady) {
return;
}
node._promptSequenceJoinReady = true;
const update = () => {
const inputCountWidget = getWidget(node, "input_count");
resizeJoinInputs(node, inputCountWidget?.value ?? 2);
};
const updateWidget = node.addWidget("button", "update inputs", null, update);
setWidgetTooltip(updateWidget, WIDGET_TOOLTIPS.ComfyUIPromptSequenceJoin["update inputs"]);
const previousOnConfigure = node.onConfigure;
node.onConfigure = function () {
const result = previousOnConfigure?.apply(this, arguments);
update();
return result;
};
update();
}
function setupPicker(node, pickerOptions = {}) {
const options = {
containThumb: false,
preserveImage: false,
requireImage: false,
supportsMask: false,
...pickerOptions,
};
injectStyle();
const selectionWidget = hideRawSelectionWidget(node);
if (!selectionWidget) {
return;
}
const root = document.createElement("div");
root.className = `ps-picker${options.containThumb ? " ps-contain" : ""}`;
stopCanvasEvents(root);
const state = readSelection(selectionWidget);
const editor = { item: null };
let library = { projects: [] };
const menu = document.createElement("div");
menu.className = "ps-menu";
document.body.append(menu);
const toolbar = document.createElement("div");
toolbar.className = "ps-toolbar";
const projectSelect = document.createElement("select");
const topicSelect = document.createElement("select");
const refreshButton = makeButton("Refresh");
const addButton = makeButton("Add");
toolbar.append(projectSelect, topicSelect, refreshButton, addButton);
const grid = document.createElement("div");
grid.className = "ps-grid";
const editorPanel = document.createElement("div");
editorPanel.className = "ps-editor";
const editorGrid = document.createElement("div");
editorGrid.className = "ps-editor-grid";
const projectInput = makeInput("Project");
const topicInput = makeInput("Topic");
const nameInput = makeInput("Prompt name");
const imageField = makeFileField(options.supportsMask ? "Image file" : "Preview image");
const imageInput = imageField.input;
const maskField = makeFileField("Mask file");
const maskInput = maskField.input;
if (options.supportsMask) {
editorGrid.append(projectInput, topicInput, nameInput, imageField.field, maskField.field);
} else {
editorGrid.append(projectInput, topicInput, nameInput, imageField.field);
}
const promptInput = document.createElement("textarea");
promptInput.placeholder = "Prompt";
const editorActions = document.createElement("div");
editorActions.className = "ps-editor-actions";
const cancelButton = makeButton("Cancel");
const saveButton = makeButton("Save");
editorActions.append(cancelButton, saveButton);
editorPanel.append(editorGrid, promptInput, editorActions);
const status = document.createElement("div");
status.className = "ps-status";
root.append(toolbar, grid, editorPanel, status);
const hideMenu = () => {
menu.style.display = "none";
menu.textContent = "";
};
const openEditor = (item = null) => {
const project = findProject(library, state.project);
const topic = item
? topicForItem(project, item)
: findTopic(project, state.topic) || project?.topics?.[0] || null;
editor.item = item;
projectInput.value = project?.name || "";
topicInput.value = topic?.name || "";
nameInput.value = item?.name || "";
promptInput.value = item?.prompt || "";
imageInput.value = "";
maskInput.value = "";
editorPanel.classList.add("ps-open");
nameInput.focus();
};
const closeEditor = () => {
editor.item = null;
editorPanel.classList.remove("ps-open");
projectInput.value = "";
topicInput.value = "";
nameInput.value = "";
promptInput.value = "";
imageInput.value = "";
maskInput.value = "";
};
const renderProjects = () => {
projectSelect.textContent = "";
if (!library.projects.length) {
projectSelect.append(option("", "No projects", true));
return;
}
for (const project of library.projects) {
projectSelect.append(option(project.id, project.name, project.id === state.project));
}
};
const renderTopics = () => {
topicSelect.textContent = "";
const project = findProject(library, state.project);
const topics = project?.topics || [];
if (!topics.length) {
topicSelect.append(option("", "No topics", true));
return;
}
topicSelect.append(option(ALL_TOPICS, "All topics", state.topic === ALL_TOPICS));
for (const topic of topics) {
topicSelect.append(option(topic.id, topic.name, topic.id === state.topic));
}
};
const renderMenu = (event, item) => {
event.preventDefault();
event.stopPropagation();
hideMenu();
const editButton = makeButton("Edit");
const deleteButton = makeButton("Delete");
menu.append(editButton, deleteButton);
menu.style.left = `${event.clientX}px`;
menu.style.top = `${event.clientY}px`;
menu.style.display = "block";
editButton.addEventListener("click", () => {
hideMenu();
openEditor(item);
});
deleteButton.addEventListener("click", async () => {
hideMenu();
if (!confirm(`Delete "${item.name}"?`)) {
return;
}
status.textContent = "Deleting";
try {
const result = await deletePromptAsset(item.id);
library = result.library || (await fetchLibrary());
state.selected = (state.selected || []).filter((id) => id !== item.id);
writeSelection(node, selectionWidget, state);
render();
status.textContent = "Deleted";
} catch (error) {
status.textContent = error.message || "Delete failed";
}
});
};
const toggleItem = (item, checked) => {
const selected = new Set(state.selected || []);
if (checked) {
selected.add(item.id);
} else {
selected.delete(item.id);
}
state.selected = Array.from(selected);
writeSelection(node, selectionWidget, state);
renderItems();
};
const renderItems = () => {
grid.textContent = "";
const items = currentItems(library, state);
if (!items.length) {
const empty = document.createElement("div");
empty.className = "ps-empty";
empty.textContent = state.topic === ALL_TOPICS ? "No prompts in this project." : "No prompts in this topic.";
grid.append(empty);
return;
}
const selected = new Set(state.selected || []);
for (const item of items) {
const card = document.createElement("label");
card.className = `ps-card${selected.has(item.id) ? " ps-selected" : ""}`;
card.tabIndex = 0;
card.addEventListener("contextmenu", (event) => renderMenu(event, item));
const checkbox = document.createElement("input");
checkbox.className = "ps-check";
checkbox.type = "checkbox";
checkbox.checked = selected.has(item.id);
checkbox.addEventListener("change", () => toggleItem(item, checkbox.checked));
const mediaWrap = document.createElement("div");
mediaWrap.className = "ps-media";
const media = item.image_url ? document.createElement("img") : document.createElement("div");
if (item.image_url) {
media.className = "ps-thumb";
media.src = `${item.image_url}&t=${encodeURIComponent(item.updated_at || "")}`;
media.alt = item.name;
} else {
media.className = "ps-placeholder";
media.textContent = "No image";
}
mediaWrap.append(media);
const name = document.createElement("div");
name.className = "ps-name";
name.textContent = item.name;
const detail = document.createElement("div");
detail.className = "ps-detail";
detail.textContent = item.prompt || "(empty prompt)";
card.append(checkbox, mediaWrap, name, detail);
grid.append(card);
}
};
const render = () => {
Object.assign(state, normalizeState(library, state));
writeSelection(node, selectionWidget, state);
renderProjects();
renderTopics();
renderItems();
};
const refresh = async () => {
refreshButton.disabled = true;
status.textContent = "Loading";
try {
library = await fetchLibrary();
render();
status.textContent = "";
} catch (error) {
status.textContent = error.message || "Failed";
} finally {
refreshButton.disabled = false;
}
};
projectSelect.addEventListener("change", () => {
state.project = projectSelect.value;
state.topic = ALL_TOPICS;
state.selected = [];
closeEditor();
render();
});
topicSelect.addEventListener("change", () => {
state.topic = topicSelect.value;
state.selected = [];
closeEditor();
render();
});
refreshButton.addEventListener("click", refresh);
addButton.addEventListener("click", () => openEditor(null));
cancelButton.addEventListener("click", closeEditor);
saveButton.addEventListener("click", async () => {
const form = new FormData();
if (editor.item?.id) {
form.set("id", editor.item.id);
}
if (options.preserveImage) {
form.set("preserve_image", "1");
}
form.set("project", projectInput.value.trim());
form.set("topic", topicInput.value.trim());
form.set("name", nameInput.value.trim());
form.set("prompt", promptInput.value.trim());
if (imageInput.files?.[0]) {
form.set("image", imageInput.files[0]);
}
if (options.supportsMask && maskInput.files?.[0]) {
form.set("mask", maskInput.files[0]);
}
if (!form.get("project") || !form.get("topic") || !form.get("name")) {
status.textContent = "Project, topic, and name are required.";
return;
}
if (options.requireImage && !editor.item?.image && !imageInput.files?.[0]) {
status.textContent = "Image is required.";
return;
}
saveButton.disabled = true;
status.textContent = "Saving";
try {
const result = await savePromptAsset(form);
library = result.library || (await fetchLibrary());
state.project = result.item?.project?.id || state.project;
state.topic = result.item?.topic?.id || state.topic;
state.selected = [result.item?.id].filter(Boolean);
closeEditor();
render();
status.textContent = "Saved";
} catch (error) {
status.textContent = error.message || "Save failed";
} finally {
saveButton.disabled = false;
}
});
document.addEventListener("click", hideMenu);
const previousOnRemoved = node.onRemoved;
node.onRemoved = function () {
previousOnRemoved?.apply(this, arguments);
hideMenu();
document.removeEventListener("click", hideMenu);
menu.remove();
};
const domWidget = node.addDOMWidget("prompt_assets", "custom", root, {
getValue: () => selectionWidget.value,
setValue: (value) => {
selectionWidget.value = value || "{}";
Object.assign(state, readSelection(selectionWidget));
render();
},
});
const previousOnResize = node.onResize;
const resizePicker = (size = node.size || [420, 500]) => {
const widgetTop =
Number.isFinite(domWidget?.last_y) && domWidget.last_y > 0
? domWidget.last_y
: Number.isFinite(domWidget?.y) && domWidget.y > 0
? domWidget.y
: 120;
root.style.width = `${Math.max(320, size[0] - 20)}px`;
root.style.height = `${Math.max(220, size[1] - widgetTop - 18)}px`;
};
node.onResize = function (size) {
previousOnResize?.apply(this, arguments);
resizePicker(size);
};
node.setSize([Math.max(node.size[0], 420), Math.max(node.size[1], 500)]);
resizePicker();
requestAnimationFrame(() => resizePicker());
refresh();
}
app.registerExtension({
name: "comfyui.prompt_sequence",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (!WIDGET_TOOLTIPS[nodeData.name]) {
return;
}
const baseOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = baseOnNodeCreated?.apply(this, arguments);
setupNodeWidgetTooltips(this, nodeData.name);
return result;
};
if (nodeData.name === IMAGE_SEQUENCE_CLASS) {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
setupPicker(this, {
containThumb: true,
});
return result;
};
}
if (nodeData.name === IMAGE_MASK_SEQUENCE_CLASS) {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
setupPicker(this, {
containThumb: true,
preserveImage: true,
requireImage: true,
supportsMask: true,
});
return result;
};
}
if (nodeData.name === JOIN_CLASS) {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
setupJoinNode(this);
return result;
};
}
if (nodeData.name === COMBO_CLASS) {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
setupComboNode(this);
return result;
};
}
},
});