mirror of
https://git.datalinker.icu/kijai/ComfyUI-KJNodes.git
synced 2025-12-08 20:34:35 +08:00
Added some js code (thanks melMass) to allow help popups on the nodes, and initial documentation on some nodes.
287 lines
9.2 KiB
JavaScript
287 lines
9.2 KiB
JavaScript
import { app } from "../../../scripts/app.js";
|
||
|
||
// code based on mtb nodes by Mel Massadian https://github.com/melMass/comfy_mtb/
|
||
export const loadScript = (
|
||
FILE_URL,
|
||
async = true,
|
||
type = 'text/javascript',
|
||
) => {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
// Check if the script already exists
|
||
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`)
|
||
if (existingScript) {
|
||
resolve({ status: true, message: 'Script already loaded' })
|
||
return
|
||
}
|
||
|
||
const scriptEle = document.createElement('script')
|
||
scriptEle.type = type
|
||
scriptEle.async = async
|
||
scriptEle.src = FILE_URL
|
||
|
||
scriptEle.addEventListener('load', (ev) => {
|
||
resolve({ status: true })
|
||
})
|
||
|
||
scriptEle.addEventListener('error', (ev) => {
|
||
reject({
|
||
status: false,
|
||
message: `Failed to load the script ${FILE_URL}`,
|
||
})
|
||
})
|
||
|
||
document.body.appendChild(scriptEle)
|
||
} catch (error) {
|
||
reject(error)
|
||
}
|
||
})
|
||
}
|
||
|
||
loadScript('/kjweb_async/marked.min.js').catch((e) => {
|
||
console.log(e)
|
||
})
|
||
loadScript('/kjweb_async/purify.min.js').catch((e) => {
|
||
console.log(e)
|
||
})
|
||
|
||
app.registerExtension({
|
||
name: "KJNodes.HelpPopup",
|
||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||
try {
|
||
if (nodeData?.category?.startsWith("KJNodes")) {
|
||
addDocumentation(nodeData, nodeType);
|
||
}
|
||
} catch (error) {
|
||
console.error("Error in registering KJNodes.HelpPopup", error);
|
||
}
|
||
},
|
||
});
|
||
|
||
const create_documentation_stylesheet = () => {
|
||
const tag = 'kj-documentation-stylesheet'
|
||
|
||
let styleTag = document.head.querySelector(tag)
|
||
|
||
if (!styleTag) {
|
||
styleTag = document.createElement('style')
|
||
styleTag.type = 'text/css'
|
||
styleTag.id = tag
|
||
styleTag.innerHTML = `
|
||
.kj-documentation-popup {
|
||
background: var(--comfy-menu-bg);
|
||
position: absolute;
|
||
color: var(--fg-color);
|
||
font: 12px monospace;
|
||
line-height: 1.5em;
|
||
padding: 10px;
|
||
border-radius: 10px;
|
||
border-style: solid;
|
||
border-width: medium;
|
||
border-color: var(--border-color);
|
||
z-index: 5;
|
||
overflow: hidden;
|
||
}
|
||
.content-wrapper {
|
||
overflow: auto;
|
||
max-height: 100%;
|
||
/* Scrollbar styling for Chrome */
|
||
&::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
&::-webkit-scrollbar-track {
|
||
background: var(--bg-color);
|
||
}
|
||
&::-webkit-scrollbar-thumb {
|
||
background-color: var(--fg-color);
|
||
border-radius: 6px;
|
||
border: 3px solid var(--bg-color);
|
||
}
|
||
|
||
/* Scrollbar styling for Firefox */
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--fg-color) var(--bg-color);
|
||
a:visited {
|
||
color: orange;
|
||
}
|
||
a:hover {
|
||
color: red;
|
||
}
|
||
}
|
||
`
|
||
document.head.appendChild(styleTag)
|
||
}
|
||
}
|
||
|
||
/** Add documentation widget to the selected node */
|
||
export const addDocumentation = (
|
||
nodeData,
|
||
nodeType,
|
||
opts = { icon_size: 14, icon_margin: 4 },) => {
|
||
|
||
opts = opts || {}
|
||
const iconSize = opts.icon_size ? opts.icon_size : 14
|
||
const iconMargin = opts.icon_margin ? opts.icon_margin : 4
|
||
let docElement = null
|
||
let contentWrapper = null
|
||
//if no description in the node python code, don't do anything
|
||
if (!nodeData.description) {
|
||
return
|
||
}
|
||
let hasResized = false //track if the popup has been resized manually
|
||
|
||
const drawFg = nodeType.prototype.onDrawForeground
|
||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||
const r = drawFg ? drawFg.apply(this, arguments) : undefined
|
||
if (this.flags.collapsed) return r
|
||
|
||
// icon position
|
||
const x = this.size[0] - iconSize - iconMargin
|
||
|
||
// create the popup
|
||
if (this.show_doc && docElement === null) {
|
||
docElement = document.createElement('div')
|
||
contentWrapper = document.createElement('div');
|
||
docElement.appendChild(contentWrapper);
|
||
|
||
create_documentation_stylesheet()
|
||
contentWrapper.classList.add('content-wrapper');
|
||
docElement.classList.add('kj-documentation-popup')
|
||
|
||
//parse the string from the python node code to html with marked, and sanitize the html with DOMPurify
|
||
contentWrapper.innerHTML = DOMPurify.sanitize(marked.parse(nodeData.description,))
|
||
|
||
// resize handle
|
||
const resizeHandle = document.createElement('div');
|
||
resizeHandle.style.width = '0';
|
||
resizeHandle.style.height = '0';
|
||
resizeHandle.style.position = 'absolute';
|
||
resizeHandle.style.bottom = '0';
|
||
resizeHandle.style.right = '0';
|
||
resizeHandle.style.cursor = 'se-resize';
|
||
|
||
// Add pseudo-elements to create a triangle shape
|
||
const borderColor = getComputedStyle(document.documentElement).getPropertyValue('--border-color').trim();
|
||
resizeHandle.style.borderTop = '10px solid transparent';
|
||
resizeHandle.style.borderLeft = '10px solid transparent';
|
||
resizeHandle.style.borderBottom = `10px solid ${borderColor}`;
|
||
resizeHandle.style.borderRight = `10px solid ${borderColor}`;
|
||
|
||
docElement.appendChild(resizeHandle)
|
||
let isResizing = false
|
||
let startX, startY, startWidth, startHeight
|
||
|
||
resizeHandle.addEventListener('mousedown', function (e) {
|
||
e.stopPropagation();
|
||
isResizing = true;
|
||
startX = e.clientX;
|
||
startY = e.clientY;
|
||
startWidth = parseInt(document.defaultView.getComputedStyle(docElement).width, 10);
|
||
startHeight = parseInt(document.defaultView.getComputedStyle(docElement).height, 10);
|
||
});
|
||
|
||
// close button
|
||
const closeButton = document.createElement('div');
|
||
closeButton.textContent = '❌'; // Use an emoji or text for the close button
|
||
closeButton.style.position = 'absolute';
|
||
closeButton.style.top = '0';
|
||
closeButton.style.right = '0';
|
||
closeButton.style.cursor = 'pointer';
|
||
closeButton.style.padding = '5px'; // Add some padding around the text
|
||
closeButton.style.color = 'red'; // Set the text color
|
||
closeButton.style.fontSize = '16px'; // Adjust the font size as needed
|
||
|
||
docElement.appendChild(closeButton)
|
||
|
||
closeButton.addEventListener('mousedown', (e) => {
|
||
e.stopPropagation();
|
||
this.show_doc = !this.show_doc
|
||
docElement.parentNode.removeChild(docElement)
|
||
docElement = null
|
||
});
|
||
|
||
document.addEventListener('mousemove', function (e) {
|
||
if (!isResizing) return;
|
||
const newWidth = startWidth + e.clientX - startX;
|
||
const newHeight = startHeight + e.clientY - startY;
|
||
docElement.style.width = `${newWidth}px`;
|
||
docElement.style.height = `${newHeight}px`;
|
||
});
|
||
|
||
document.addEventListener('mouseup', function () {
|
||
isResizing = false
|
||
hasResized = true
|
||
})
|
||
|
||
document.body.appendChild(docElement)
|
||
}
|
||
// close the popup
|
||
else if (!this.show_doc && docElement !== null) {
|
||
docElement.parentNode.removeChild(docElement)
|
||
docElement = null
|
||
}
|
||
// update position of the popup
|
||
if (this.show_doc && docElement !== null) {
|
||
const rect = ctx.canvas.getBoundingClientRect()
|
||
const scaleX = rect.width / ctx.canvas.width
|
||
const scaleY = rect.height / ctx.canvas.height
|
||
|
||
const transform = new DOMMatrix()
|
||
.scaleSelf(scaleX, scaleY)
|
||
.multiplySelf(ctx.getTransform())
|
||
.translateSelf(this.size[0] * scaleX, 0)
|
||
.translateSelf(10, -32)
|
||
|
||
const scale = new DOMMatrix()
|
||
.scaleSelf(transform.a, transform.d);
|
||
|
||
const styleObject = {
|
||
transformOrigin: '0 0',
|
||
transform: scale,
|
||
left: `${transform.a + transform.e}px`,
|
||
top: `${transform.d + transform.f}px`,
|
||
};
|
||
// keep possible manual resize
|
||
if (!hasResized) {
|
||
//styleObject.height = `${this.size[1] || this.parent?.inputHeight || 32}px`;
|
||
//styleObject.height = `${docElement.offsetHeight || 32}px`;
|
||
styleObject.width = `${this.size[0] * 1.5}px`;
|
||
}
|
||
Object.assign(docElement.style, styleObject);
|
||
}
|
||
|
||
ctx.save()
|
||
ctx.translate(x - 2, iconSize - 34)
|
||
ctx.scale(iconSize / 32, iconSize / 32)
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.3)'
|
||
ctx.lineCap = 'round'
|
||
ctx.lineJoin = 'round'
|
||
ctx.lineWidth = 2.4
|
||
ctx.font = 'bold 36px monospace'
|
||
ctx.fillStyle = 'orange';
|
||
ctx.fillText('?', 0, 24)
|
||
ctx.restore()
|
||
return r
|
||
}
|
||
// handle clicking of the icon
|
||
const mouseDown = nodeType.prototype.onMouseDown
|
||
nodeType.prototype.onMouseDown = function (e, localPos, canvas) {
|
||
const r = mouseDown ? mouseDown.apply(this, arguments) : undefined
|
||
const iconX = this.size[0] - iconSize - iconMargin
|
||
const iconY = iconSize - 34
|
||
if (
|
||
localPos[0] > iconX &&
|
||
localPos[0] < iconX + iconSize &&
|
||
localPos[1] > iconY &&
|
||
localPos[1] < iconY + iconSize
|
||
) {
|
||
if (this.show_doc === undefined) {
|
||
this.show_doc = true
|
||
} else {
|
||
this.show_doc = !this.show_doc
|
||
}
|
||
return true;
|
||
}
|
||
return r;
|
||
}
|
||
} |