mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-08 15:00:15 -03:00
Compare commits
4 Commits
v1.1.5
...
0ac10dfd42
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ac10dfd42 | ||
|
|
9c95856b2f | ||
|
|
5ce4667d32 | ||
|
|
be53fda6df |
@@ -3,7 +3,7 @@ import { app } from "../../scripts/app.js";
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Node Marker – right-click node marking (no dedicated node required)
|
// Node Marker – right-click node marking (no dedicated node required)
|
||||||
//
|
//
|
||||||
// Adds a "Mark as →" submenu with role options to any node's context menu.
|
// Adds a "🎯 Mark as →" submenu with role options to any node's context menu.
|
||||||
// Roles are stored in ``node.properties.lm_marker_role`` and automatically
|
// Roles are stored in ``node.properties.lm_marker_role`` and automatically
|
||||||
// persist with the workflow JSON.
|
// persist with the workflow JSON.
|
||||||
//
|
//
|
||||||
@@ -107,7 +107,7 @@ function buildMenuItems(node) {
|
|||||||
return [
|
return [
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
content: "Mark as",
|
content: "\uD83C\uDFAF Mark as",
|
||||||
has_submenu: true,
|
has_submenu: true,
|
||||||
submenu: {
|
submenu: {
|
||||||
options: buildSubmenuOptions(node),
|
options: buildSubmenuOptions(node),
|
||||||
|
|||||||
@@ -260,7 +260,6 @@ function createTagElement({
|
|||||||
}) {
|
}) {
|
||||||
const tagEl = document.createElement("div");
|
const tagEl = document.createElement("div");
|
||||||
tagEl.className = "comfy-tag";
|
tagEl.className = "comfy-tag";
|
||||||
tagEl.dataset.captureWheel = "true";
|
|
||||||
|
|
||||||
const baseStyles = {
|
const baseStyles = {
|
||||||
padding: `${roundScaled(group ? 5 : 3, styleScale)}px ${roundScaled(group ? 8 : 10, styleScale)}px`,
|
padding: `${roundScaled(group ? 5 : 3, styleScale)}px ${roundScaled(group ? 8 : 10, styleScale)}px`,
|
||||||
@@ -619,6 +618,36 @@ function showTagContextMenu(event, tagData, index, widget, anchorEl) {
|
|||||||
setTimeout(() => document.addEventListener('click', closeMenu), 0);
|
setTimeout(() => document.addEventListener('click', closeMenu), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Singleton window capture-phase wheel hook: focuses the tags container when a
|
||||||
|
// wheel event occurs inside it, so that ComfyUI's wheelCapturedByFocusedElement
|
||||||
|
// recognises this zone and does NOT forward the event to canvas (which would
|
||||||
|
// trigger zoom and stopPropagation, preventing the strength-adjustment handler).
|
||||||
|
/** @type {boolean} */
|
||||||
|
let tagWheelCaptureHookInstalled = false;
|
||||||
|
function installTagWheelCaptureHook() {
|
||||||
|
if (tagWheelCaptureHookInstalled) return;
|
||||||
|
tagWheelCaptureHookInstalled = true;
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
"wheel",
|
||||||
|
(event) => {
|
||||||
|
// Only handle vertical mouse wheel (not pinch-zoom or horizontal swipe)
|
||||||
|
if (event.ctrlKey || event.metaKey) return;
|
||||||
|
if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) return;
|
||||||
|
|
||||||
|
const target = /** @type {Element} */ (event.target);
|
||||||
|
if (!target?.closest) return;
|
||||||
|
const targetContainer = target.closest(
|
||||||
|
'.comfy-tags-container[data-capture-wheel="true"]'
|
||||||
|
);
|
||||||
|
if (!targetContainer) return;
|
||||||
|
|
||||||
|
targetContainer.focus({ preventScroll: true });
|
||||||
|
},
|
||||||
|
{ capture: true, passive: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.02, options = {}) {
|
export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.02, options = {}) {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.className = "comfy-tags-container";
|
container.className = "comfy-tags-container";
|
||||||
@@ -628,6 +657,29 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0
|
|||||||
forwardMiddleMouseToCanvas(container);
|
forwardMiddleMouseToCanvas(container);
|
||||||
forwardWheelToCanvas(container);
|
forwardWheelToCanvas(container);
|
||||||
|
|
||||||
|
// Vue render mode: ComfyUI's TransformPane uses a capture-phase wheel handler
|
||||||
|
// (TransformPane @wheel.capture) that checks wheelCapturedByFocusedElement.
|
||||||
|
// For that check to return true (preventing canvas zoom and allowing our
|
||||||
|
// strength-adjustment wheel handler to fire), the container needs both
|
||||||
|
// data-capture-wheel AND document.activeElement inside it.
|
||||||
|
// We make the container focusable and auto-focus it on wheel events via a
|
||||||
|
// window capture-phase hook.
|
||||||
|
container.dataset.captureWheel = "true";
|
||||||
|
container.tabIndex = -1;
|
||||||
|
|
||||||
|
// Blur on mouseleave to avoid lingering focus side effects.
|
||||||
|
container.addEventListener("mouseleave", () => {
|
||||||
|
if (document.activeElement === container) {
|
||||||
|
container.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Singleton window capture-phase wheel handler: focuses our container when
|
||||||
|
// a wheel event occurs inside it, so that wheelCapturedByFocusedElement
|
||||||
|
// recognises this zone and does NOT forward the event to canvas (which would
|
||||||
|
// trigger zoom and stopPropagation, preventing our strength handler).
|
||||||
|
installTagWheelCaptureHook();
|
||||||
|
|
||||||
Object.assign(container.style, {
|
Object.assign(container.style, {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
@@ -641,6 +693,7 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0
|
|||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
alignContent: "flex-start",
|
alignContent: "flex-start",
|
||||||
|
outline: "none",
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialTagsData = opts?.defaultVal || [];
|
const initialTagsData = opts?.defaultVal || [];
|
||||||
|
|||||||
@@ -186,32 +186,59 @@ const createExtensionObject = (useActionBar) => {
|
|||||||
};
|
};
|
||||||
injectStyles();
|
injectStyles();
|
||||||
|
|
||||||
const replaceButtonIcon = () => {
|
const applyIconToButton = (button) => {
|
||||||
const buttons = document.querySelectorAll('button[aria-label="Launch LoRA Manager (Shift+Click opens in new window)"]');
|
// Skip if the SVG icon is already in place
|
||||||
buttons.forEach(button => {
|
if (button.querySelector('svg')) return;
|
||||||
button.classList.add('lm-top-menu-button');
|
button.classList.add('lm-top-menu-button');
|
||||||
button.innerHTML = getLoraManagerIcon();
|
button.innerHTML = getLoraManagerIcon();
|
||||||
button.style.borderRadius = '4px';
|
button.style.borderRadius = '4px';
|
||||||
button.style.padding = '6px';
|
button.style.padding = '6px';
|
||||||
button.style.backgroundColor = 'var(--primary-bg)';
|
button.style.backgroundColor = 'var(--primary-bg)';
|
||||||
const svg = button.querySelector('svg');
|
const svg = button.querySelector('svg');
|
||||||
if (svg) {
|
if (svg) {
|
||||||
svg.style.width = '20px';
|
svg.style.width = '20px';
|
||||||
svg.style.height = '20px';
|
svg.style.height = '20px';
|
||||||
}
|
|
||||||
});
|
|
||||||
if (buttons.length === 0) {
|
|
||||||
requestAnimationFrame(replaceButtonIcon);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
requestAnimationFrame(replaceButtonIcon);
|
|
||||||
|
// Initial application — retry until the button is rendered by Vue
|
||||||
|
const pollUntilFound = () => {
|
||||||
|
const buttons = document.querySelectorAll('button[aria-label="Launch LoRA Manager (Shift+Click opens in new window)"]');
|
||||||
|
if (buttons.length > 0) {
|
||||||
|
buttons.forEach(applyIconToButton);
|
||||||
|
} else {
|
||||||
|
requestAnimationFrame(pollUntilFound);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
requestAnimationFrame(pollUntilFound);
|
||||||
|
|
||||||
|
// MutationObserver: keep the SVG icon in place after Vue re-renders
|
||||||
|
// (e.g. when the properties panel is toggled inside a subgraph)
|
||||||
|
if (typeof MutationObserver !== 'undefined') {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const buttons = document.querySelectorAll('button[aria-label="Launch LoRA Manager (Shift+Click opens in new window)"]');
|
||||||
|
buttons.forEach(button => {
|
||||||
|
// Only re-apply when Vue has reset innerHTML back to <i>
|
||||||
|
if (button.querySelector('i')) {
|
||||||
|
applyIconToButton(button);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Watch the action bar and a broad ancestor so we cover re-mounts
|
||||||
|
const watchNode = document.querySelector('[data-testid="action-bar-buttons"]')
|
||||||
|
|| document.querySelector('.actionbar-container')
|
||||||
|
|| document.body;
|
||||||
|
observer.observe(watchNode, { childList: true, subtree: true });
|
||||||
|
// Store reference for potential cleanup
|
||||||
|
window.__lmIconObserver = observer;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (useActionBar) {
|
if (useActionBar) {
|
||||||
extensionObj.actionBarButtons = [
|
extensionObj.actionBarButtons = [
|
||||||
{
|
{
|
||||||
icon: "icon-[mdi--alpha-l-box] size-4",
|
icon: "icon-[lucide--layers] size-4",
|
||||||
tooltip: BUTTON_TOOLTIP,
|
tooltip: BUTTON_TOOLTIP,
|
||||||
onClick: openLoraManager
|
onClick: openLoraManager
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user