mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-01-10 06:10:50 +08:00
fix issues with zooming in editor, simplify, improve list inputs and outputs
This commit is contained in:
parent
cc5f16caeb
commit
37048fc1a2
@ -1,769 +0,0 @@
|
||||
/**
|
||||
* Uses code adapted from https://github.com/yorkane/ComfyUI-KYNode
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Kevin Yuan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
// Make modal window
|
||||
function makeModal({ title = "Message", text = "No text", type = "info", parent = null, stylePos = "fixed", classes = [] } = {}) {
|
||||
const overlay = document.createElement("div");
|
||||
Object.assign(overlay.style, {
|
||||
display: "none",
|
||||
position: stylePos,
|
||||
background: "rgba(0 0 0 / 0.8)",
|
||||
opacity: 0,
|
||||
top: "0",
|
||||
left: "0",
|
||||
right: "0",
|
||||
bottom: "0",
|
||||
zIndex: "500",
|
||||
transition: "all .8s",
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
const boxModal = document.createElement("div");
|
||||
Object.assign(boxModal.style, {
|
||||
transition: "all 0.5s",
|
||||
opacity: 0,
|
||||
display: "none",
|
||||
position: stylePos,
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%,-50%)",
|
||||
background: "#525252",
|
||||
minWidth: "300px",
|
||||
fontFamily: "sans-serif",
|
||||
zIndex: "501",
|
||||
border: "1px solid rgb(255 255 255 / 45%)",
|
||||
});
|
||||
|
||||
boxModal.className = "alekpet_modal_window";
|
||||
boxModal.classList.add(...classes);
|
||||
|
||||
const boxModalBody = document.createElement("div");
|
||||
Object.assign(boxModalBody.style, {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
textAlign: "center",
|
||||
});
|
||||
|
||||
boxModalBody.className = "alekpet_modal_body";
|
||||
|
||||
const boxModalHtml = `
|
||||
<div class="alekpet_modal_header" style="display: flex; align-items: center; background: #222; width: 100%;justify-content: center;">
|
||||
<div class="alekpet_modal_title" style="flex-basis: 85%; text-align: center;padding: 5px;">${title}</div>
|
||||
<div class="alekpet_modal_close">✕</div>
|
||||
</div>
|
||||
<div class="alekpet_modal_description" style="padding: 8px;">${text}</div>`;
|
||||
boxModalBody.innerHTML = boxModalHtml;
|
||||
|
||||
const alekpet_modal_header = boxModalBody.querySelector(".alekpet_modal_header");
|
||||
Object.assign(alekpet_modal_header.style, {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
const close = boxModalBody.querySelector(".alekpet_modal_close");
|
||||
Object.assign(close.style, {
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
let parentElement = document.body;
|
||||
if (parent && parent.nodeType === 1) {
|
||||
parentElement = parent;
|
||||
}
|
||||
|
||||
boxModal.append(boxModalBody);
|
||||
parentElement.append(overlay, boxModal);
|
||||
|
||||
const removeEvent = new Event("removeElements");
|
||||
const remove = () => {
|
||||
animateTransitionProps(boxModal, { opacity: 0 }).then(() =>
|
||||
animateTransitionProps(overlay, { opacity: 0 }).then(() => {
|
||||
parentElement.removeChild(boxModal);
|
||||
parentElement.removeChild(overlay);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
boxModal.addEventListener("removeElements", remove);
|
||||
overlay.addEventListener("removeElements", remove);
|
||||
|
||||
animateTransitionProps(overlay)
|
||||
.then(() => {
|
||||
overlay.addEventListener("click", () => {
|
||||
overlay.dispatchEvent(removeEvent);
|
||||
});
|
||||
animateTransitionProps(boxModal);
|
||||
})
|
||||
.then(() => boxModal.querySelector(".alekpet_modal_close").addEventListener("click", () => boxModal.dispatchEvent(removeEvent)));
|
||||
}
|
||||
|
||||
function findWidget(node, value, attr = "name", func = "find") {
|
||||
return node?.widgets ? node.widgets[func]((w) => (Array.isArray(value) ? value.includes(w[attr]) : w[attr] === value)) : null;
|
||||
}
|
||||
|
||||
function animateTransitionProps(el, props = { opacity: 1 }, preStyles = { display: "block" }) {
|
||||
Object.assign(el.style, preStyles);
|
||||
|
||||
el.style.transition = !el.style.transition || !window.getComputedStyle(el).getPropertyValue("transition") ? "all .8s" : el.style.transition;
|
||||
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => {
|
||||
Object.assign(el.style, props);
|
||||
|
||||
const transstart = () => (el.isAnimating = true);
|
||||
const transchancel = () => (el.isAnimating = false);
|
||||
el.addEventListener("transitionstart", transstart);
|
||||
el.addEventListener("transitioncancel", transchancel);
|
||||
|
||||
el.addEventListener("transitionend", function transend() {
|
||||
el.isAnimating = false;
|
||||
el.removeEventListener("transitionend", transend);
|
||||
el.removeEventListener("transitionend", transchancel);
|
||||
el.removeEventListener("transitionend", transstart);
|
||||
res(el);
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
function animateClick(target, params = {}) {
|
||||
const { opacityVal = 0.9, callback = () => {} } = params;
|
||||
if (target?.isAnimating) return;
|
||||
|
||||
const hide = +target.style.opacity === 0;
|
||||
return animateTransitionProps(target, {
|
||||
opacity: hide ? opacityVal : 0,
|
||||
}).then((el) => {
|
||||
const isHide = hide || el.style.display === "none";
|
||||
showHide({ elements: [target], hide: !hide });
|
||||
callback();
|
||||
return isHide;
|
||||
});
|
||||
}
|
||||
|
||||
function showHide({ elements = [], hide = null, displayProp = "block" } = {}) {
|
||||
Array.from(elements).forEach((el) => {
|
||||
if (hide !== null) {
|
||||
el.style.display = !hide ? displayProp : "none";
|
||||
} else {
|
||||
el.style.display = !el.style.display || el.style.display === "none" ? displayProp : "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isEmptyObject(obj) {
|
||||
if (!obj) return true;
|
||||
return Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||
}
|
||||
|
||||
function makeElement(tag, attrs = {}) {
|
||||
if (!tag) tag = "div";
|
||||
const element = document.createElement(tag);
|
||||
Object.keys(attrs).forEach((key) => {
|
||||
const currValue = attrs[key];
|
||||
if (key === "class") {
|
||||
if (Array.isArray(currValue)) {
|
||||
element.classList.add(...currValue);
|
||||
} else if (currValue instanceof String || typeof currValue === "string") {
|
||||
element.className = currValue;
|
||||
}
|
||||
} else if (key === "dataset") {
|
||||
try {
|
||||
if (Array.isArray(currValue)) {
|
||||
currValue.forEach((datasetArr) => {
|
||||
const [prop, propval] = Object.entries(datasetArr)[0];
|
||||
element.dataset[prop] = propval;
|
||||
});
|
||||
} else {
|
||||
Object.entries(currValue).forEach((datasetArr) => {
|
||||
const [prop, propval] = datasetArr;
|
||||
element.dataset[prop] = propval;
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
} else if (key === "style") {
|
||||
if (typeof currValue === "object" && !Array.isArray(currValue) && Object.keys(currValue).length) {
|
||||
Object.assign(element[key], currValue);
|
||||
} else if (typeof currValue === "object" && Array.isArray(currValue) && currValue.length) {
|
||||
element[key] = [...currValue];
|
||||
} else if (currValue instanceof String || typeof currValue === "string") {
|
||||
element[key] = currValue;
|
||||
}
|
||||
} else if (["for"].includes(key)) {
|
||||
element.setAttribute(key, currValue);
|
||||
} else if (key === "children") {
|
||||
element.append(...(currValue instanceof Array ? currValue : [currValue]));
|
||||
} else if (key === "parent") {
|
||||
currValue.append(element);
|
||||
} else {
|
||||
element[key] = currValue;
|
||||
}
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
function isValidStyle(opt, strColor) {
|
||||
let op = new Option().style;
|
||||
if (!op.hasOwnProperty(opt)) return { result: false, color: "", color_hex: "" };
|
||||
|
||||
op[opt] = strColor;
|
||||
|
||||
return {
|
||||
result: op[opt] !== "",
|
||||
color_rgb: op[opt],
|
||||
color_hex: rgbToHex(op[opt]),
|
||||
};
|
||||
}
|
||||
|
||||
function rgbToHex(rgb) {
|
||||
const regEx = new RegExp(/\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
|
||||
if (regEx.test(rgb)) {
|
||||
let [, r, g, b] = regEx.exec(rgb);
|
||||
r = parseInt(r).toString(16);
|
||||
g = parseInt(g).toString(16);
|
||||
b = parseInt(b).toString(16);
|
||||
|
||||
r = r.length === 1 ? r + "0" : r;
|
||||
g = g.length === 1 ? g + "0" : g;
|
||||
b = b.length === 1 ? b + "0" : b;
|
||||
|
||||
return `#${r}${g}${b}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function getDataJSON(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const jsonData = await response.json();
|
||||
return jsonData;
|
||||
} catch (err) {
|
||||
return new Error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function deepMerge(target, source) {
|
||||
if (source?.nodeType) return;
|
||||
for (let key in source) {
|
||||
if (source[key] instanceof Object && key in target) {
|
||||
Object.assign(source[key], deepMerge(target[key], source[key]));
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(target || {}, source);
|
||||
return target;
|
||||
}
|
||||
|
||||
const THEME_MODAL_WINDOW_BASE = {
|
||||
stylesTitle: {
|
||||
background: "auto",
|
||||
padding: "5px",
|
||||
borderRadius: "6px",
|
||||
marginBottom: "5px",
|
||||
alignSelf: "stretch",
|
||||
},
|
||||
stylesWrapper: {
|
||||
display: "none",
|
||||
opacity: 0,
|
||||
minWidth: "220px",
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
transition: "all .8s",
|
||||
fontFamily: "monospace",
|
||||
zIndex: 99999,
|
||||
},
|
||||
stylesBox: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: "#0e0e0e",
|
||||
padding: "6px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
textAlign: "center",
|
||||
borderRadius: "6px",
|
||||
color: "white",
|
||||
border: "2px solid silver",
|
||||
boxShadow: "2px 2px 4px silver",
|
||||
maxWidth: "300px",
|
||||
},
|
||||
stylesClose: {
|
||||
position: "absolute",
|
||||
top: "-10px",
|
||||
right: "-10px",
|
||||
background: "silver",
|
||||
borderRadius: "50%",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
fontSize: "0.8rem",
|
||||
},
|
||||
};
|
||||
|
||||
const THEMES_MODAL_WINDOW = {
|
||||
error: {
|
||||
stylesTitle: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||
background: "#8f210f",
|
||||
},
|
||||
stylesBox: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||
background: "#3b2222",
|
||||
boxShadow: "3px 3px 6px #141414",
|
||||
border: "1px solid #f91b1b",
|
||||
},
|
||||
stylesWrapper: { ...THEME_MODAL_WINDOW_BASE.stylesWrapper },
|
||||
stylesClose: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesClose,
|
||||
background: "#3b2222",
|
||||
},
|
||||
},
|
||||
warning: {
|
||||
stylesTitle: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||
background: "#e99818",
|
||||
},
|
||||
stylesBox: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||
background: "#594e32",
|
||||
boxShadow: "3px 3px 6px #141414",
|
||||
border: "1px solid #e99818",
|
||||
},
|
||||
stylesWrapper: { ...THEME_MODAL_WINDOW_BASE.stylesWrapper },
|
||||
stylesClose: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesClose,
|
||||
background: "#594e32",
|
||||
},
|
||||
},
|
||||
normal: {
|
||||
stylesTitle: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||
background: "#108f0f",
|
||||
},
|
||||
stylesBox: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||
background: "#223b2a",
|
||||
boxShadow: "3px 3px 6px #141414",
|
||||
border: "1px solid #108f0f",
|
||||
},
|
||||
stylesWrapper: { ...THEME_MODAL_WINDOW_BASE.stylesWrapper },
|
||||
stylesClose: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesClose,
|
||||
background: "#223b2a",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
auto: {
|
||||
autohide: false,
|
||||
autoshow: false,
|
||||
autoremove: false,
|
||||
propStyles: { opacity: 0 },
|
||||
propPreStyles: {},
|
||||
timewait: 2000,
|
||||
},
|
||||
overlay: {
|
||||
overlay_enabled: false,
|
||||
overlayClasses: [],
|
||||
overlayStyles: {},
|
||||
},
|
||||
close: { closeRemove: false, showClose: true },
|
||||
parent: null,
|
||||
};
|
||||
|
||||
function createWindowModal({ textTitle = "Message", textBody = "Hello world!", textFooter = null, classesWrapper = [], stylesWrapper = {}, classesBox = [], stylesBox = {}, classesTitle = [], stylesTitle = {}, classesBody = [], stylesBody = {}, classesClose = [], stylesClose = {}, classesFooter = [], stylesFooter = {}, options = defaultOptions } = {}) {
|
||||
// Check all options exist
|
||||
const _options = deepMerge(JSON.parse(JSON.stringify(defaultOptions)), options);
|
||||
|
||||
const {
|
||||
parent,
|
||||
overlay: { overlay_enabled, overlayClasses, overlayStyles },
|
||||
close: { closeRemove, showClose },
|
||||
auto: { autohide, autoshow, autoremove, timewait, propStyles, propPreStyles },
|
||||
} = _options;
|
||||
|
||||
// Function past text(html)
|
||||
function addText(text, parent) {
|
||||
if (!parent) return;
|
||||
|
||||
switch (typeof text) {
|
||||
case "string":
|
||||
if (/^\<.*\/?\>$/.test(text)) {
|
||||
parent.innerHTML = text;
|
||||
} else {
|
||||
parent.textContent = text;
|
||||
}
|
||||
break;
|
||||
case "object":
|
||||
default:
|
||||
if (Array.isArray(text)) {
|
||||
text.forEach((element) => (element.nodeType === 1 || element.nodeType === 3) && parent.append(element));
|
||||
} else if (text.nodeType === 1 || text.nodeType === 3) parent.append(text);
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay
|
||||
let overlayElement = null;
|
||||
if (overlay_enabled) {
|
||||
overlayElement = makeElement("div", {
|
||||
class: [...overlayClasses],
|
||||
style: {
|
||||
display: "none",
|
||||
position: "fixed",
|
||||
background: "rgba(0 0 0 / 0.8)",
|
||||
opacity: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 99999,
|
||||
transition: "all .8s",
|
||||
cursor: "pointer",
|
||||
...overlayStyles,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Wrapper
|
||||
const wrapper_settings = makeElement("div", {
|
||||
class: ["alekpet__wrapper__window", ...classesWrapper],
|
||||
});
|
||||
|
||||
Object.assign(wrapper_settings.style, {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesWrapper,
|
||||
...stylesWrapper,
|
||||
});
|
||||
|
||||
// Box
|
||||
const box__settings = makeElement("div", {
|
||||
class: ["alekpet__window__box", ...classesBox],
|
||||
});
|
||||
Object.assign(box__settings.style, {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||
...stylesBox,
|
||||
});
|
||||
|
||||
// Title
|
||||
let box_settings_title = "";
|
||||
if (textTitle) {
|
||||
box_settings_title = makeElement("div", {
|
||||
class: ["alekpet__window__title", ...classesTitle],
|
||||
});
|
||||
|
||||
Object.assign(box_settings_title.style, {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||
...stylesTitle,
|
||||
});
|
||||
|
||||
// Add text (html) to title
|
||||
addText(textTitle, box_settings_title);
|
||||
}
|
||||
// Body
|
||||
let box_settings_body = "";
|
||||
if (textBody) {
|
||||
box_settings_body = makeElement("div", {
|
||||
class: ["alekpet__window__body", ...classesBody],
|
||||
});
|
||||
|
||||
Object.assign(box_settings_body.style, {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-end",
|
||||
gap: "5px",
|
||||
textWrap: "wrap",
|
||||
...stylesBody,
|
||||
});
|
||||
|
||||
// Add text (html) to body
|
||||
addText(textBody, box_settings_body);
|
||||
}
|
||||
|
||||
// Close button
|
||||
const close__box__button = makeElement("div", {
|
||||
class: ["close__box__button", ...classesClose],
|
||||
textContent: "✖",
|
||||
});
|
||||
|
||||
Object.assign(close__box__button.style, {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesClose,
|
||||
...stylesClose,
|
||||
});
|
||||
|
||||
if (!showClose) close__box__button.style.display = "none";
|
||||
|
||||
const closeEvent = new Event("closeModal");
|
||||
const closeModalWindow = function () {
|
||||
overlay_enabled
|
||||
? animateTransitionProps(overlayElement, {
|
||||
opacity: 0,
|
||||
})
|
||||
.then(() =>
|
||||
animateTransitionProps(wrapper_settings, {
|
||||
opacity: 0,
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
if (closeRemove) {
|
||||
parent.removeChild(wrapper_settings);
|
||||
parent.removeChild(overlayElement);
|
||||
} else {
|
||||
showHide({ elements: [wrapper_settings, overlayElement] });
|
||||
}
|
||||
})
|
||||
: animateTransitionProps(wrapper_settings, {
|
||||
opacity: 0,
|
||||
}).then(() => {
|
||||
showHide({ elements: [wrapper_settings] });
|
||||
});
|
||||
};
|
||||
|
||||
close__box__button.addEventListener("closeModal", closeModalWindow);
|
||||
|
||||
close__box__button.addEventListener("click", () => close__box__button.dispatchEvent(closeEvent));
|
||||
|
||||
close__box__button.onmouseenter = () => {
|
||||
close__box__button.style.opacity = 0.8;
|
||||
};
|
||||
|
||||
close__box__button.onmouseleave = () => {
|
||||
close__box__button.style.opacity = 1;
|
||||
};
|
||||
|
||||
box__settings.append(box_settings_title, box_settings_body);
|
||||
|
||||
// Footer
|
||||
if (textFooter) {
|
||||
const box_settings_footer = makeElement("div", {
|
||||
class: [...classesFooter],
|
||||
});
|
||||
Object.assign(box_settings_footer.style, {
|
||||
...stylesFooter,
|
||||
});
|
||||
|
||||
// Add text (html) to body
|
||||
addText(textFooter, box_settings_footer);
|
||||
|
||||
box__settings.append(box_settings_footer);
|
||||
}
|
||||
|
||||
wrapper_settings.append(close__box__button, box__settings);
|
||||
|
||||
if (parent && parent.nodeType === 1) {
|
||||
if (overlay_enabled) parent.append(overlayElement);
|
||||
parent.append(wrapper_settings);
|
||||
|
||||
if (autoshow) {
|
||||
overlay_enabled
|
||||
? animateClick(overlayElement).then(() =>
|
||||
animateClick(wrapper_settings).then(
|
||||
() =>
|
||||
autohide &&
|
||||
setTimeout(
|
||||
() =>
|
||||
animateTransitionProps(wrapper_settings, { ...propStyles }, { ...propPreStyles })
|
||||
.then(() => animateTransitionProps(overlayElement, { ...propStyles }, { ...propPreStyles }))
|
||||
.then(() => {
|
||||
if (autoremove) {
|
||||
parent.removeChild(wrapper_settings);
|
||||
parent.removeChild(overlayElement);
|
||||
}
|
||||
}),
|
||||
timewait,
|
||||
),
|
||||
),
|
||||
)
|
||||
: animateClick(wrapper_settings).then(() => autohide && setTimeout(() => animateTransitionProps(wrapper_settings, { ...propStyles }, { ...propPreStyles }).then(() => autoremove && parent.removeChild(wrapper_settings)), timewait));
|
||||
}
|
||||
}
|
||||
|
||||
return wrapper_settings;
|
||||
}
|
||||
|
||||
// Prompt
|
||||
async function comfyuiDesktopPrompt(title, message, defaultValue) {
|
||||
try {
|
||||
return await app.extensionManager.dialog.prompt({
|
||||
title,
|
||||
message,
|
||||
defaultValue,
|
||||
});
|
||||
} catch (err) {
|
||||
return prompt(title, message);
|
||||
}
|
||||
}
|
||||
|
||||
// Alert
|
||||
function comfyuiDesktopAlert(message) {
|
||||
try {
|
||||
app.extensionManager.toast.addAlert(message);
|
||||
} catch (err) {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm
|
||||
function confirmModal({ title, message }) {
|
||||
return new Promise((res) => {
|
||||
const overlay = makeElement("div", {
|
||||
class: ["alekpet_confOverlay"],
|
||||
style: {
|
||||
background: "rgba(0, 0, 0, 0.7)",
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 9999,
|
||||
userSelect: "none",
|
||||
},
|
||||
});
|
||||
|
||||
const modal = makeElement("div", {
|
||||
class: ["alekpet_confModal"],
|
||||
style: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||
position: "fixed",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
fontFamily: "monospace",
|
||||
background: "rgb(92 186 255 / 20%)",
|
||||
transform: "translate(-50%, -50%)",
|
||||
borderColor: "rgba(92, 186, 255, 0.63)",
|
||||
boxShadow: "rgba(92, 186, 255, 0.63) 2px 2px 4px",
|
||||
},
|
||||
});
|
||||
|
||||
const titleEl = makeElement("div", {
|
||||
class: ["alekpet_confTitle"],
|
||||
style: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||
background: "rgba(92, 186, 255, 0.63)",
|
||||
},
|
||||
textContent: title,
|
||||
});
|
||||
|
||||
const messageEl = makeElement("div", {
|
||||
class: ["alekpet_confMessage"],
|
||||
style: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-end",
|
||||
gap: "5px",
|
||||
textWrap: "wrap",
|
||||
},
|
||||
textContent: message,
|
||||
});
|
||||
|
||||
const action_box = makeElement("div", {
|
||||
class: ["alekpet_confActions"],
|
||||
style: {
|
||||
display: "flex",
|
||||
gap: "5px",
|
||||
width: "100%",
|
||||
padding: "4px",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
});
|
||||
|
||||
const remove = () => {
|
||||
modal.remove();
|
||||
overlay.remove();
|
||||
};
|
||||
|
||||
const ok = makeElement("div", {
|
||||
class: ["alekpet_confButtons", "alekpet_confButtonOk"],
|
||||
style: {
|
||||
background: "linear-gradient(45deg, green, limegreen) rgb(21, 100, 6)",
|
||||
},
|
||||
textContent: "Ok",
|
||||
onclick: (e) => {
|
||||
res(true);
|
||||
remove();
|
||||
},
|
||||
});
|
||||
|
||||
const Cancel = makeElement("div", {
|
||||
class: ["alekpet_confButtons", "alekpet_confButtonCancel"],
|
||||
style: {
|
||||
background: "linear-gradient(45deg, #b64396, #a52a8b) rgb(135 3 161)",
|
||||
},
|
||||
textContent: "Cancel",
|
||||
onclick: (e) => {
|
||||
res(false);
|
||||
remove();
|
||||
},
|
||||
});
|
||||
|
||||
action_box.append(ok, Cancel);
|
||||
modal.append(titleEl, messageEl, action_box);
|
||||
overlay.append(modal);
|
||||
document.body.append(overlay);
|
||||
});
|
||||
}
|
||||
|
||||
async function comfyuiDesktopConfirm(message) {
|
||||
try {
|
||||
const result = await confirmModal({
|
||||
title: "Confirm",
|
||||
message: message,
|
||||
});
|
||||
|
||||
// Wait update comfyui frontend! Confirm Cancel not return value! Fixed in ComfyUI_frontend ver. v1.10.8
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/2649
|
||||
// const result = await app.extensionManager.dialog.confirm({
|
||||
// title: "Confirm",
|
||||
// message: message,
|
||||
// });
|
||||
return result;
|
||||
} catch (err) {
|
||||
return confirm(message);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
makeModal,
|
||||
createWindowModal,
|
||||
animateTransitionProps,
|
||||
animateClick,
|
||||
showHide,
|
||||
makeElement,
|
||||
getDataJSON,
|
||||
isEmptyObject,
|
||||
isValidStyle,
|
||||
rgbToHex,
|
||||
findWidget,
|
||||
THEMES_MODAL_WINDOW,
|
||||
//
|
||||
comfyuiDesktopConfirm,
|
||||
comfyuiDesktopPrompt,
|
||||
comfyuiDesktopAlert,
|
||||
};
|
||||
@ -24,7 +24,6 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { makeElement, findWidget } from "./ace_utils.js";
|
||||
|
||||
// Load Ace editor using script tag for Safari compatibility
|
||||
// The noconflict build includes AMD loader that works in all browsers
|
||||
@ -34,7 +33,7 @@ const aceLoadPromise = new Promise((resolve) => {
|
||||
ace = window.ace;
|
||||
resolve();
|
||||
} else {
|
||||
const script = document.createElement('script');
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://cdn.jsdelivr.net/npm/ace-builds@1.43.4/src-noconflict/ace.js";
|
||||
script.onload = () => {
|
||||
ace = window.ace;
|
||||
@ -45,44 +44,108 @@ const aceLoadPromise = new Promise((resolve) => {
|
||||
}
|
||||
});
|
||||
|
||||
// todo: do we really want to do this here?
|
||||
await aceLoadPromise;
|
||||
const findWidget = (node, value, attr = "name", func = "find") => {
|
||||
return node?.widgets ? node.widgets[func]((w) => (Array.isArray(value) ? value.includes(w[attr]) : w[attr] === value)) : null;
|
||||
};
|
||||
|
||||
const makeElement = (tag, attrs = {}) => {
|
||||
if (!tag) tag = "div";
|
||||
const element = document.createElement(tag);
|
||||
Object.keys(attrs).forEach((key) => {
|
||||
const currValue = attrs[key];
|
||||
if (key === "class") {
|
||||
if (Array.isArray(currValue)) {
|
||||
element.classList.add(...currValue);
|
||||
} else if (currValue instanceof String || typeof currValue === "string") {
|
||||
element.className = currValue;
|
||||
}
|
||||
} else if (key === "dataset") {
|
||||
try {
|
||||
if (Array.isArray(currValue)) {
|
||||
currValue.forEach((datasetArr) => {
|
||||
const [prop, propval] = Object.entries(datasetArr)[0];
|
||||
element.dataset[prop] = propval;
|
||||
});
|
||||
} else {
|
||||
Object.entries(currValue).forEach((datasetArr) => {
|
||||
const [prop, propval] = datasetArr;
|
||||
element.dataset[prop] = propval;
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// todo: what is this trying to do?
|
||||
}
|
||||
} else if (key === "style") {
|
||||
if (typeof currValue === "object" && !Array.isArray(currValue) && Object.keys(currValue).length) {
|
||||
Object.assign(element[key], currValue);
|
||||
} else if (typeof currValue === "object" && Array.isArray(currValue) && currValue.length) {
|
||||
element[key] = [...currValue];
|
||||
} else if (currValue instanceof String || typeof currValue === "string") {
|
||||
element[key] = currValue;
|
||||
}
|
||||
} else if (["for"].includes(key)) {
|
||||
element.setAttribute(key, currValue);
|
||||
} else if (key === "children") {
|
||||
element.append(...(currValue instanceof Array ? currValue : [currValue]));
|
||||
} else if (key === "parent") {
|
||||
currValue.append(element);
|
||||
} else {
|
||||
element[key] = currValue;
|
||||
}
|
||||
});
|
||||
return element;
|
||||
};
|
||||
|
||||
function getPosition(node, ctx, w_width, y, n_height) {
|
||||
const getPosition = (node, ctx, w_width, y, n_height) => {
|
||||
const margin = 5;
|
||||
|
||||
const rect = ctx.canvas.getBoundingClientRect();
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(rect.width / ctx.canvas.width, rect.height / ctx.canvas.height)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(margin, margin + y);
|
||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
|
||||
const transform = ctx.getTransform();
|
||||
const scale = app.canvas.ds.scale;
|
||||
|
||||
// The context is already transformed to draw at the widget position
|
||||
// transform.e and transform.f give us the canvas coordinates (in canvas pixels)
|
||||
// We need to convert these to screen pixels by accounting for the canvas scale
|
||||
// rect gives us the canvas element's position on the page
|
||||
|
||||
// The transform matrix has scale baked in (transform.a = transform.d = scale)
|
||||
// transform.e and transform.f are the translation in canvas-pixel space
|
||||
const canvasPixelToScreenPixel = rect.width / ctx.canvas.width;
|
||||
|
||||
const x = transform.e * canvasPixelToScreenPixel + rect.left;
|
||||
const y_pos = transform.f * canvasPixelToScreenPixel + rect.top;
|
||||
|
||||
// Convert widget dimensions from canvas coordinates to screen pixels
|
||||
const scaledWidth = w_width * scale;
|
||||
const scaledHeight = (n_height - y - 15) * scale;
|
||||
const scaledMargin = margin * scale;
|
||||
const scaledY = y * scale;
|
||||
|
||||
return {
|
||||
transformOrigin: "0 0",
|
||||
transform: scale,
|
||||
left: `${transform.a + transform.e + rect.left}px`,
|
||||
top: `${transform.d + transform.f + rect.top}px`,
|
||||
maxWidth: `${w_width - margin * 2}px`,
|
||||
maxHeight: `${n_height - margin * 2 - y - 15}px`,
|
||||
width: `${w_width - margin * 2}px`,
|
||||
height: "90%",
|
||||
left: `${x + scaledMargin}px`,
|
||||
top: `${y_pos + scaledY + scaledMargin}px`,
|
||||
width: `${scaledWidth - scaledMargin * 2}px`,
|
||||
maxWidth: `${scaledWidth - scaledMargin * 2}px`,
|
||||
height: `${scaledHeight - scaledMargin * 2}px`,
|
||||
maxHeight: `${scaledHeight - scaledMargin * 2}px`,
|
||||
position: "absolute",
|
||||
scrollbarColor: "var(--descrip-text) var(--bg-color)",
|
||||
scrollbarWidth: "thin",
|
||||
zIndex: app.graph._nodes.indexOf(node),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Create code editor widget
|
||||
function codeEditor(node, inputName, inputData) {
|
||||
const codeEditor = (node, inputName, inputData) => {
|
||||
const widget = {
|
||||
type: "pycode",
|
||||
type: "code_block_python",
|
||||
name: inputName,
|
||||
options: { hideOnZoom: true },
|
||||
value: inputData[1]?.default || "",
|
||||
draw(ctx, node, widgetWidth, y) {
|
||||
const hidden = node.flags?.collapsed || (!!this.options.hideOnZoom && app.canvas.ds.scale < 0.5) || this.type === "converted-widget" || this.type === "hidden";
|
||||
const hidden = node.flags?.collapsed || (!!this.options.hideOnZoom && app.canvas.ds.scale < 0.5) || this.type === "converted-widget" || this.type === "hidden" || this.type === "converted-widget";
|
||||
|
||||
this.codeElement.hidden = hidden;
|
||||
|
||||
@ -122,19 +185,19 @@ function codeEditor(node, inputName, inputData) {
|
||||
};
|
||||
|
||||
return widget;
|
||||
}
|
||||
};
|
||||
|
||||
// Trigger workflow change tracking
|
||||
function markWorkflowChanged() {
|
||||
const markWorkflowChanged = () => {
|
||||
app?.extensionManager?.workflow?.activeWorkflow?.changeTracker?.checkState();
|
||||
}
|
||||
};
|
||||
|
||||
// Register extensions
|
||||
app.registerExtension({
|
||||
name: "Comfy.EvalPython",
|
||||
getCustomWidgets(app) {
|
||||
return {
|
||||
PYCODE: (node, inputName, inputData) => {
|
||||
CODE_BLOCK_PYTHON: (node, inputName, inputData) => {
|
||||
const widget = codeEditor(node, inputName, inputData);
|
||||
|
||||
widget.editor.getSession().on("change", () => {
|
||||
@ -165,7 +228,7 @@ app.registerExtension({
|
||||
originalOnConfigure?.apply(this, arguments);
|
||||
|
||||
if (info?.widgets_values?.length) {
|
||||
const widgetCodeIndex = findWidget(this, "pycode", "type", "findIndex");
|
||||
const widgetCodeIndex = findWidget(this, "code_block_python", "type", "findIndex");
|
||||
const editor = this.widgets[widgetCodeIndex]?.editor;
|
||||
|
||||
if (editor) {
|
||||
|
||||
@ -36,7 +36,7 @@ return {", ".join([f"value{i}" for i in range(inputs)])}
|
||||
return {
|
||||
"required": {
|
||||
"pycode": (
|
||||
"PYCODE",
|
||||
"CODE_BLOCK_PYTHON",
|
||||
{
|
||||
"default": default_code
|
||||
},
|
||||
@ -47,16 +47,19 @@ return {", ".join([f"value{i}" for i in range(inputs)])}
|
||||
|
||||
RETURN_TYPES = tuple(IO.ANY for _ in range(outputs))
|
||||
RETURN_NAMES = tuple(f"item{i}" for i in range(outputs))
|
||||
OUTPUT_IS_LIST = output_is_list
|
||||
INPUT_IS_LIST = input_is_list is not None
|
||||
FUNCTION = "exec_py"
|
||||
DESCRIPTION = ""
|
||||
CATEGORY = "eval"
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, *args, **kwargs):
|
||||
ctx = current_execution_context()
|
||||
|
||||
return ctx.configuration.enable_eval
|
||||
|
||||
def exec_py(self, pycode, **kwargs):
|
||||
ctx = current_execution_context()
|
||||
|
||||
# Ensure all value inputs have a default of None
|
||||
kwargs = {
|
||||
**{f"value{i}": None for i in range(inputs)},
|
||||
**kwargs,
|
||||
@ -68,11 +71,9 @@ return {", ".join([f"value{i}" for i in range(inputs)])}
|
||||
if not ctx.configuration.enable_eval:
|
||||
raise ValueError("Python eval is disabled")
|
||||
|
||||
# Extract value arguments in order
|
||||
value_args = [kwargs.pop(f"value{i}") for i in range(inputs)]
|
||||
arg_names = ", ".join(f"value{i}=None" for i in range(inputs))
|
||||
|
||||
# Wrap pycode in a function to support return statements
|
||||
wrapped_code = f"def _eval_func({arg_names}):\n"
|
||||
for line in pycode.splitlines():
|
||||
wrapped_code += " " + line + "\n"
|
||||
@ -83,13 +84,8 @@ return {", ".join([f"value{i}" for i in range(inputs)])}
|
||||
"print": print,
|
||||
}
|
||||
|
||||
# Execute wrapped function definition
|
||||
exec(wrapped_code, globals_for_eval)
|
||||
|
||||
# Call the function with value arguments
|
||||
results = globals_for_eval["_eval_func"](*value_args)
|
||||
|
||||
# Normalize results to match output count
|
||||
if not isinstance(results, tuple):
|
||||
results = (results,)
|
||||
|
||||
@ -100,22 +96,24 @@ return {", ".join([f"value{i}" for i in range(inputs)])}
|
||||
|
||||
return results
|
||||
|
||||
# Set the class name for better debugging/introspection
|
||||
# todo: interact better with the weird comfyui machinery for this
|
||||
if input_is_list is not None:
|
||||
setattr(EvalPythonNode, "INPUT_IS_LIST", input_is_list)
|
||||
|
||||
if output_is_list is not None:
|
||||
setattr(EvalPythonNode, "OUTPUT_IS_LIST", output_is_list)
|
||||
|
||||
EvalPythonNode.__name__ = name
|
||||
EvalPythonNode.__qualname__ = name
|
||||
|
||||
return EvalPythonNode
|
||||
|
||||
|
||||
# Create the default EvalPython node with 5 inputs and 5 outputs
|
||||
EvalPython_1_1 = eval_python(inputs=1, outputs=1, name="EvalPython_1_1")
|
||||
EvalPython_5_5 = eval_python(inputs=5, outputs=5, name="EvalPython_5_5")
|
||||
EvalPython = EvalPython_5_5 # Backward compatibility alias
|
||||
|
||||
# Create list variants
|
||||
EvalPython_List_1 = eval_python(inputs=1, outputs=1, name="EvalPython_List_1", input_is_list=True, output_is_list=None)
|
||||
EvalPython_1_List = eval_python(inputs=1, outputs=1, name="EvalPython_1_List", input_is_list=None, output_is_list=(True,))
|
||||
EvalPython_List_List = eval_python(inputs=1, outputs=1, name="EvalPython_List_List", input_is_list=True, output_is_list=(True,))
|
||||
|
||||
|
||||
export_custom_nodes()
|
||||
export_package_as_web_directory("comfy_extras.eval_web")
|
||||
|
||||
@ -4,9 +4,8 @@ from unittest.mock import Mock, patch
|
||||
from comfy.cli_args import default_configuration
|
||||
from comfy.execution_context import context_configuration
|
||||
from comfy_extras.nodes.nodes_eval import (
|
||||
EvalPython,
|
||||
EvalPython_5_5,
|
||||
eval_python,
|
||||
EvalPython_5_5,
|
||||
EvalPython_List_1,
|
||||
EvalPython_1_List,
|
||||
EvalPython_List_List,
|
||||
@ -447,7 +446,7 @@ def test_eval_python_input_types():
|
||||
assert "required" in input_types
|
||||
assert "optional" in input_types
|
||||
assert "pycode" in input_types["required"]
|
||||
assert input_types["required"]["pycode"][0] == "PYCODE"
|
||||
assert input_types["required"]["pycode"][0] == "CODE_BLOCK_PYTHON"
|
||||
|
||||
# Check optional inputs
|
||||
for i in range(5):
|
||||
@ -560,7 +559,6 @@ def test_eval_python_list_1_input_is_list(eval_context):
|
||||
|
||||
# Verify INPUT_IS_LIST is set
|
||||
assert EvalPython_List_1.INPUT_IS_LIST is True
|
||||
assert EvalPython_List_1.OUTPUT_IS_LIST is None
|
||||
|
||||
# Test that value0 receives a list
|
||||
result = node.exec_py(
|
||||
@ -586,7 +584,6 @@ def test_eval_python_1_list_output_is_list(eval_context):
|
||||
node = EvalPython_1_List()
|
||||
|
||||
# Verify OUTPUT_IS_LIST is set
|
||||
assert EvalPython_1_List.INPUT_IS_LIST is False
|
||||
assert EvalPython_1_List.OUTPUT_IS_LIST == (True,)
|
||||
|
||||
# Test that returns a list
|
||||
@ -652,7 +649,6 @@ def test_eval_python_factory_with_list_flags(eval_context):
|
||||
ListInputNode = eval_python(inputs=1, outputs=1, input_is_list=True, output_is_list=None)
|
||||
|
||||
assert ListInputNode.INPUT_IS_LIST is True
|
||||
assert ListInputNode.OUTPUT_IS_LIST is None
|
||||
|
||||
node = ListInputNode()
|
||||
result = node.exec_py(
|
||||
@ -666,7 +662,6 @@ def test_eval_python_factory_scalar_output_list(eval_context):
|
||||
"""Test factory function with scalar input and list output"""
|
||||
ScalarToListNode = eval_python(inputs=1, outputs=1, input_is_list=None, output_is_list=(True,))
|
||||
|
||||
assert ScalarToListNode.INPUT_IS_LIST is False
|
||||
assert ScalarToListNode.OUTPUT_IS_LIST == (True,)
|
||||
|
||||
node = ScalarToListNode()
|
||||
@ -686,8 +681,3 @@ def test_eval_python_list_empty_list(eval_context):
|
||||
value0=[]
|
||||
)
|
||||
assert result == ([],)
|
||||
|
||||
|
||||
def test_eval_python_backward_compatibility():
|
||||
"""Test that EvalPython is an alias for EvalPython_5_5"""
|
||||
assert EvalPython is EvalPython_5_5
|
||||
|
||||
Loading…
Reference in New Issue
Block a user