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) }) const categories = ["KJNodes", "SUPIR", "VoiceCraft", "Marigold", "IC-Light"]; app.registerExtension({ name: "KJNodes.HelpPopup", async beforeRegisterNodeDef(nodeType, nodeData) { if (app.ui.settings.getSettingValue("KJNodes.helpPopup") === false) { return; } try { categories.forEach(category => { if (nodeData?.category?.startsWith(category)) { addDocumentation(nodeData, nodeType); } else return }); } 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 { color: yellow; } 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 } 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.preventDefault(); 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); }, { signal: this.docCtrl.signal }, ); // close button const closeButton = document.createElement('div'); closeButton.textContent = '❌'; closeButton.style.position = 'absolute'; closeButton.style.top = '0'; closeButton.style.right = '0'; closeButton.style.cursor = 'pointer'; closeButton.style.padding = '5px'; closeButton.style.color = 'red'; closeButton.style.fontSize = '12px'; docElement.appendChild(closeButton) closeButton.addEventListener('mousedown', (e) => { e.stopPropagation(); this.show_doc = !this.show_doc docElement.parentNode.removeChild(docElement) docElement = null if (contentWrapper) { contentWrapper.remove() contentWrapper = null } }, { signal: this.docCtrl.signal }, ); document.addEventListener('mousemove', function (e) { if (!isResizing) return; const scale = app.canvas.ds.scale; const newWidth = startWidth + (e.clientX - startX) / scale; const newHeight = startHeight + (e.clientY - startY) / scale;; docElement.style.width = `${newWidth}px`; docElement.style.height = `${newHeight}px`; }, { signal: this.docCtrl.signal }, ); document.addEventListener('mouseup', function () { isResizing = false }, { signal: this.docCtrl.signal }, ) 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 * Math.max(1.0,window.devicePixelRatio) , 0) .translateSelf(10, -32) const scale = new DOMMatrix() .scaleSelf(transform.a, transform.d); const bcr = app.canvas.canvas.getBoundingClientRect() const styleObject = { transformOrigin: '0 0', transform: scale, left: `${transform.a + bcr.x + transform.e}px`, top: `${transform.d + bcr.y + transform.f}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 } if (this.show_doc) { this.docCtrl = new AbortController() } else { this.docCtrl.abort() } return true; } return r; } const onRem = nodeType.prototype.onRemoved nodeType.prototype.onRemoved = function () { const r = onRem ? onRem.apply(this, []) : undefined if (docElement) { docElement.remove() docElement = null } if (contentWrapper) { contentWrapper.remove() contentWrapper = null } return r } }