From 9d517f591c6e535949d30cc8067925b0e063700e Mon Sep 17 00:00:00 2001 From: Alex Furer Date: Mon, 6 Oct 2025 20:27:46 +0200 Subject: [PATCH] Everything there - Html is rendering - Mouse interaction works and Comfy shortcuts like h, for hand tool are respected - Added double clicking to edit html --- AF_Enhanced_HTML_Note.py | 5 +- web/AF_Enhanced_HTML_Note.js | 350 +++++++++++++++++------------------ 2 files changed, 168 insertions(+), 187 deletions(-) diff --git a/AF_Enhanced_HTML_Note.py b/AF_Enhanced_HTML_Note.py index 51177f6..e0e70cf 100644 --- a/AF_Enhanced_HTML_Note.py +++ b/AF_Enhanced_HTML_Note.py @@ -23,8 +23,9 @@ # # Changelog: # v0.0.04 -# - Removed width and height parameters - using standard ComfyUI resizing -# - Simplified node to be just the HTML display +# - Fixed node selection and resizing by removing OUTPUT_NODE and using proper widget approach +# - Double clicking is now used to edit the html +# - Mouse interaction is now persistent to ComfyUI behaviour. Except middle mouse button for panning. # v0.0.03 # - Fixed node resizing issues on save/reload # - Improved selection and edit mode behavior to match ComfyUI core notes diff --git a/web/AF_Enhanced_HTML_Note.js b/web/AF_Enhanced_HTML_Note.js index 53c2e59..ad89df1 100644 --- a/web/AF_Enhanced_HTML_Note.js +++ b/web/AF_Enhanced_HTML_Note.js @@ -5,143 +5,119 @@ import { app } from "../../scripts/app.js"; // Define inline CSS styles const AF_HTML_NOTE_STYLES = ` .af-html-note-container { - position: relative; width: 100%; height: 100%; min-height: 100px; - pointer-events: none; /* Allow events to pass through to node */ - } - - .af-html-note-widget { background: var(--comfy-menu-bg, #2a2a2a); border: 1px solid var(--border-color, #555); padding: 16px; - margin: 0; + box-sizing: border-box; + overflow-y: auto; + cursor: default; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; line-height: 1.6; color: var(--input-text, #ffffff); - box-shadow: 0 2px 8px rgba(0,0,0,0.3); - min-height: 100px; - word-wrap: break-word; - width: 100%; - height: 100%; - box-sizing: border-box; - overflow-y: auto; - cursor: text; - pointer-events: auto; /* Re-enable events only for the content */ } - .af-html-note-widget:hover { - background: var(--comfy-menu-bg-hover, #333); - border-color: var(--border-color-hover, #777); + .af-html-note-content { + width: 100%; + height: 100%; + pointer-events: none; + } + + body.af-ctrl-active .af-html-note-content { + pointer-events: auto; } .af-html-note-editor { + width: 100%; + height: 100%; background: var(--comfy-menu-bg, #2a2a2a); border: 1px solid var(--border-color, #555); padding: 16px; - margin: 0; font-family: 'Monaco', 'Consolas', 'Courier New', monospace; line-height: 1.4; color: var(--input-text, #ffffff); - width: 100%; - height: 100%; box-sizing: border-box; resize: none; outline: none; font-size: 14px; display: none; - pointer-events: auto; /* Re-enable events only for the editor */ } - .af-html-note-editor:focus { - border-color: var(--border-color-focus, #0078d4); - } - - /* When in edit mode, hide the display and show the editor */ - .af-html-note-container.edit-mode .af-html-note-widget { - display: none !important; + .af-html-note-container.edit-mode .af-html-note-content { + display: none; } .af-html-note-container.edit-mode .af-html-note-editor { - display: block !important; + display: block; } - /* When not in edit mode, show display and hide editor */ - .af-html-note-container:not(.edit-mode) .af-html-note-widget { - display: block !important; - } - - .af-html-note-container:not(.edit-mode) .af-html-note-editor { - display: none !important; - } - - /* Rest of your styles remain the same */ - .af-html-note-widget h1, .af-html-note-widget h2, .af-html-note-widget h3, - .af-html-note-widget h4, .af-html-note-widget h5, .af-html-note-widget h6 { + /* HTML content styles */ + .af-html-note-content h1, .af-html-note-content h2, .af-html-note-content h3, + .af-html-note-content h4, .af-html-note-content h5, .af-html-note-content h6 { margin-top: 0; margin-bottom: 12px; font-weight: 600; } - .af-html-note-widget h1 { font-size: 1.5em; } - .af-html-note-widget h2 { font-size: 1.3em; } - .af-html-note-widget h3 { font-size: 1.1em; } + .af-html-note-content h1 { font-size: 1.5em; } + .af-html-note-content h2 { font-size: 1.3em; } + .af-html-note-content h3 { font-size: 1.1em; } - .af-html-note-widget .af-section { + .af-html-note-content .af-section { padding: 12px 16px; margin: 8px 0; border-radius: 6px; border-left: 4px solid; } - .af-html-note-widget .af-section.af-positive { + .af-html-note-content .af-section.af-positive { background: rgba(144, 238, 144, 0.1); border-left-color: #90EE90; } - .af-html-note-widget .af-section.af-negative { + .af-html-note-content .af-section.af-negative { background: rgba(255, 107, 107, 0.1); border-left-color: #FF6B6B; } - .af-html-note-widget .af-section.af-neutral { + .af-html-note-content .af-section.af-neutral { background: rgba(224, 224, 224, 0.1); border-left-color: #E0E0E0; } - .af-html-note-widget .af-section.af-info { + .af-html-note-content .af-section.af-info { background: rgba(135, 206, 235, 0.1); border-left-color: #87CEEB; } - .af-html-note-widget .af-section.af-warning { + .af-html-note-content .af-section.af-warning { background: rgba(255, 215, 0, 0.1); border-left-color: #FFD700; } - .af-html-note-widget .af-section.af-custom { + .af-html-note-content .af-section.af-custom { background: rgba(186, 85, 211, 0.1); border-left-color: #BA55D3; } - .af-html-note-widget .af-spacer { height: 16px; margin: 8px 0; } - .af-html-note-widget .af-spacer-small { height: 8px; margin: 4px 0; } - .af-html-note-widget .af-spacer-large { height: 32px; margin: 16px 0; } - .af-html-note-widget .af-spacer-xl { height: 48px; margin: 24px 0; } + .af-html-note-content .af-spacer { height: 16px; margin: 8px 0; } + .af-html-note-content .af-spacer-small { height: 8px; margin: 4px 0; } + .af-html-note-content .af-spacer-large { height: 32px; margin: 16px 0; } + .af-html-note-content .af-spacer-xl { height: 48px; margin: 24px 0; } - .af-html-note-widget a { + .af-html-note-content a { color: #58a6ff; text-decoration: none; - transition: color 0.2s ease; } - .af-html-note-widget a:hover { + body.af-ctrl-active .af-html-note-content a:hover { color: #79c0ff; text-decoration: underline; } - .af-html-note-widget code { + .af-html-note-content code { background: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 4px; @@ -150,7 +126,7 @@ const AF_HTML_NOTE_STYLES = ` color: #f0f6fc; } - .af-html-note-widget pre { + .af-html-note-content pre { background: rgba(0,0,0,0.4); border: 1px solid #444; border-radius: 6px; @@ -159,50 +135,50 @@ const AF_HTML_NOTE_STYLES = ` overflow-x: auto; } - .af-html-note-widget pre code { + .af-html-note-content pre code { background: transparent; padding: 0; } - .af-html-note-widget ul, .af-html-note-widget ol { + .af-html-note-content ul, .af-html-note-content ol { padding-left: 20px; margin: 12px 0; } - .af-html-note-widget li { margin: 4px 0; } - .af-html-note-widget p { margin: 8px 0; } + .af-html-note-content li { margin: 4px 0; } + .af-html-note-content p { margin: 8px 0; } - .af-html-note-widget table { + .af-html-note-content table { border-collapse: collapse; width: 100%; margin: 16px 0; } - .af-html-note-widget th, .af-html-note-widget td { + .af-html-note-content th, .af-html-note-content td { border: 1px solid var(--border-color, #555); padding: 8px 12px; text-align: left; } - .af-html-note-widget th { + .af-html-note-content th { background: rgba(255,255,255,0.1); font-weight: 600; } - .af-html-note-widget hr { + .af-html-note-content hr { border: none; border-top: 1px solid var(--border-color, #555); margin: 16px 0; opacity: 0.6; } - .af-html-note-widget::-webkit-scrollbar { width: 8px; } - .af-html-note-widget::-webkit-scrollbar-track { background: rgba(0,0,0,0.1); } - .af-html-note-widget::-webkit-scrollbar-thumb { + .af-html-note-container::-webkit-scrollbar { width: 8px; } + .af-html-note-container::-webkit-scrollbar-track { background: rgba(0,0,0,0.1); } + .af-html-note-container::-webkit-scrollbar-thumb { background: var(--border-color, #555); border-radius: 4px; } - .af-html-note-widget::-webkit-scrollbar-thumb:hover { background: #777; } + .af-html-note-container::-webkit-scrollbar-thumb:hover { background: #777; } .af-html-note-editor::-webkit-scrollbar { width: 8px; } .af-html-note-editor::-webkit-scrollbar-track { background: rgba(0,0,0,0.1); } @@ -231,84 +207,43 @@ app.registerExtension({ if (nodeData.name === "AF_Enhanced_HTML_Note") { injectStyles(); - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function(message) { - onExecuted?.apply(this, arguments); + // Store the original onNodeCreated + const originalOnNodeCreated = nodeType.prototype.onNodeCreated; + + nodeType.prototype.onNodeCreated = function() { + const result = originalOnNodeCreated?.apply(this, arguments); - if (message.af_html_note) { - const htmlData = message.af_html_note[0]; - this.updateHTMLDisplay(htmlData.content); - } - }; - - nodeType.prototype.updateHTMLDisplay = function(content) { - if (this.htmlNoteElement && !this.isEditMode) { - this.htmlNoteElement.innerHTML = content; - this.handleLinksInHTML(); - } - }; - - nodeType.prototype.handleLinksInHTML = function() { - if (!this.htmlNoteElement) return; - - const links = this.htmlNoteElement.querySelectorAll('a'); - links.forEach(link => { - link.onclick = (e) => { - e.stopPropagation(); - if (link.href && !link.href.includes('#')) { - window.open(link.href, '_blank'); - } - }; - }); - }; - - nodeType.prototype.enterEditMode = function() { - if (this.isEditMode || !this.containerElement) return; - - console.log('Entering edit mode'); - this.isEditMode = true; - this.containerElement.classList.add('edit-mode'); - this.editorElement.value = this.htmlContent; - - setTimeout(() => { - this.editorElement.focus(); - this.editorElement.setSelectionRange(0, 0); - }, 10); - }; - - nodeType.prototype.exitEditMode = function() { - if (!this.isEditMode || !this.containerElement) return; - - console.log('Exiting edit mode'); this.isEditMode = false; - this.containerElement.classList.remove('edit-mode'); - this.htmlContent = this.editorElement.value; - this.htmlNoteElement.innerHTML = this.htmlContent; - this.handleLinksInHTML(); + this.size = [400, 300]; - // Update the widget value - const htmlWidget = this.widgets?.find(w => w.name === "html_content"); - if (htmlWidget) { - htmlWidget.value = this.htmlContent; - // Trigger any change handlers - if (htmlWidget.callback) { - htmlWidget.callback(htmlWidget.value); + // Don't hide the original widget - instead, replace it with our custom display + setTimeout(() => { + const htmlWidget = this.widgets?.find(w => w.name === "html_content"); + if (htmlWidget && htmlWidget.element) { + // Store the original value + this.htmlContent = htmlWidget.value; + + // Create our custom container + this.createHTMLNoteDisplay(this.htmlContent); + + // Hide the original widget but keep its value + htmlWidget.computeSize = () => [0, 0]; + htmlWidget.element.style.display = 'none'; } - } + }, 0); + + return result; }; nodeType.prototype.createHTMLNoteDisplay = function(content) { - this.htmlContent = content; - this.isEditMode = false; - // Create container element this.containerElement = document.createElement("div"); this.containerElement.className = "af-html-note-container"; - // Create HTML display element - this.htmlNoteElement = document.createElement("div"); - this.htmlNoteElement.className = "af-html-note-widget"; - this.htmlNoteElement.innerHTML = content; + // Create HTML content display + this.contentElement = document.createElement("div"); + this.contentElement.className = "af-html-note-content"; + this.contentElement.innerHTML = content; // Create editor element this.editorElement = document.createElement("textarea"); @@ -316,36 +251,32 @@ app.registerExtension({ this.editorElement.value = content; // Append both elements to container - this.containerElement.appendChild(this.htmlNoteElement); + this.containerElement.appendChild(this.contentElement); this.containerElement.appendChild(this.editorElement); - // Add click handler for entering edit mode - only on content area - this.htmlNoteElement.addEventListener('click', (e) => { - // Don't enter edit mode if clicking on a link - if (e.target.tagName.toLowerCase() === 'a') { - return; + // Add double-click to edit + this.containerElement.addEventListener('dblclick', (e) => { + if (!this.isEditMode) { + this.enterEditMode(); + e.stopPropagation(); } - - e.preventDefault(); - e.stopPropagation(); - - console.log('HTML note clicked, entering edit mode'); - this.enterEditMode(); }); + // Handle links + this.handleLinksInHTML(); + // Handle editor events this.editorElement.addEventListener('keydown', (e) => { - e.stopPropagation(); // Prevent ComfyUI from handling these - - // Exit edit mode on Escape if (e.key === 'Escape') { this.exitEditMode(); e.preventDefault(); + e.stopPropagation(); + } else { + e.stopPropagation(); } }); - this.editorElement.addEventListener('blur', (e) => { - // Exit edit mode when editor loses focus + this.editorElement.addEventListener('blur', () => { setTimeout(() => { if (this.isEditMode && document.activeElement !== this.editorElement) { this.exitEditMode(); @@ -353,42 +284,91 @@ app.registerExtension({ }, 10); }); - this.handleLinksInHTML(); - - // Add the container as a DOM widget - this.htmlNoteWidget = this.addDOMWidget( - "html_note_container", - "div", - this.containerElement - ); - - return this.htmlNoteWidget; + // Add as DOM widget + this.addDOMWidget("html_note_display", "div", this.containerElement); }; - // Display initial content on node creation - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function() { - onNodeCreated?.apply(this, arguments); + nodeType.prototype.enterEditMode = function() { + if (this.isEditMode) return; + + this.isEditMode = true; + this.containerElement.classList.add('edit-mode'); + this.editorElement.value = this.htmlContent; + + setTimeout(() => { + this.editorElement.focus(); + this.editorElement.select(); + }, 10); + }; + + nodeType.prototype.exitEditMode = function() { + if (!this.isEditMode) return; this.isEditMode = false; + this.containerElement.classList.remove('edit-mode'); + this.htmlContent = this.editorElement.value; + this.contentElement.innerHTML = this.htmlContent; - // Set a reasonable default size - this.size = [400, 300]; + // Update the original widget value + const htmlWidget = this.widgets?.find(w => w.name === "html_content"); + if (htmlWidget) { + htmlWidget.value = this.htmlContent; + if (htmlWidget.callback) { + htmlWidget.callback(this.htmlContent); + } + } - // Hide the original input widget completely - setTimeout(() => { - const htmlWidget = this.widgets?.find(w => w.name === "html_content"); - if (htmlWidget) { - // Hide the widget completely - htmlWidget.computeSize = () => [0, -4]; - if (htmlWidget.element) { - htmlWidget.element.style.display = 'none'; + // Re-attach link handlers + this.handleLinksInHTML(); + }; + + nodeType.prototype.handleLinksInHTML = function() { + if (!this.contentElement) return; + + const links = this.contentElement.querySelectorAll('a'); + links.forEach(link => { + link.onclick = (e) => { + // Only handle links when Ctrl is active + if (!document.body.classList.contains('af-ctrl-active')) { + e.preventDefault(); + e.stopPropagation(); + return false; } - // Create our custom display - this.createHTMLNoteDisplay(htmlWidget.value); - } - }, 0); + // Handle external links + if (link.href && !link.href.includes('#') && !link.href.startsWith('javascript:')) { + e.preventDefault(); + window.open(link.href, '_blank'); + } + + e.stopPropagation(); + return false; + }; + }); + }; + + // Global Ctrl key handling + let ctrlKeyActive = false; + + document.addEventListener('keydown', (e) => { + if (e.key === 'Control') { + ctrlKeyActive = true; + document.body.classList.add('af-ctrl-active'); + } + }); + + document.addEventListener('keyup', (e) => { + if (e.key === 'Control') { + ctrlKeyActive = false; + document.body.classList.remove('af-ctrl-active'); + } + }); + + // Clean up when node is removed + const originalOnRemoved = nodeType.prototype.onRemoved; + nodeType.prototype.onRemoved = function() { + document.body.classList.remove('af-ctrl-active'); + originalOnRemoved?.apply(this, arguments); }; } }