mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-06 22:10:14 -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)
|
||||
//
|
||||
// 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
|
||||
// persist with the workflow JSON.
|
||||
//
|
||||
@@ -107,7 +107,7 @@ function buildMenuItems(node) {
|
||||
return [
|
||||
null,
|
||||
{
|
||||
content: "Mark as",
|
||||
content: "\uD83C\uDFAF Mark as",
|
||||
has_submenu: true,
|
||||
submenu: {
|
||||
options: buildSubmenuOptions(node),
|
||||
|
||||
@@ -260,7 +260,6 @@ function createTagElement({
|
||||
}) {
|
||||
const tagEl = document.createElement("div");
|
||||
tagEl.className = "comfy-tag";
|
||||
tagEl.dataset.captureWheel = "true";
|
||||
|
||||
const baseStyles = {
|
||||
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);
|
||||
}
|
||||
|
||||
// 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 = {}) {
|
||||
const container = document.createElement("div");
|
||||
container.className = "comfy-tags-container";
|
||||
@@ -628,6 +657,29 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0
|
||||
forwardMiddleMouseToCanvas(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, {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
@@ -641,6 +693,7 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0
|
||||
overflow: "auto",
|
||||
alignItems: "flex-start",
|
||||
alignContent: "flex-start",
|
||||
outline: "none",
|
||||
});
|
||||
|
||||
const initialTagsData = opts?.defaultVal || [];
|
||||
|
||||
@@ -186,32 +186,59 @@ const createExtensionObject = (useActionBar) => {
|
||||
};
|
||||
injectStyles();
|
||||
|
||||
const replaceButtonIcon = () => {
|
||||
const buttons = document.querySelectorAll('button[aria-label="Launch LoRA Manager (Shift+Click opens in new window)"]');
|
||||
buttons.forEach(button => {
|
||||
button.classList.add('lm-top-menu-button');
|
||||
button.innerHTML = getLoraManagerIcon();
|
||||
button.style.borderRadius = '4px';
|
||||
button.style.padding = '6px';
|
||||
button.style.backgroundColor = 'var(--primary-bg)';
|
||||
const svg = button.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.style.width = '20px';
|
||||
svg.style.height = '20px';
|
||||
}
|
||||
});
|
||||
if (buttons.length === 0) {
|
||||
requestAnimationFrame(replaceButtonIcon);
|
||||
const applyIconToButton = (button) => {
|
||||
// Skip if the SVG icon is already in place
|
||||
if (button.querySelector('svg')) return;
|
||||
button.classList.add('lm-top-menu-button');
|
||||
button.innerHTML = getLoraManagerIcon();
|
||||
button.style.borderRadius = '4px';
|
||||
button.style.padding = '6px';
|
||||
button.style.backgroundColor = 'var(--primary-bg)';
|
||||
const svg = button.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.style.width = '20px';
|
||||
svg.style.height = '20px';
|
||||
}
|
||||
};
|
||||
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) {
|
||||
extensionObj.actionBarButtons = [
|
||||
{
|
||||
icon: "icon-[mdi--alpha-l-box] size-4",
|
||||
icon: "icon-[lucide--layers] size-4",
|
||||
tooltip: BUTTON_TOOLTIP,
|
||||
onClick: openLoraManager
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user