mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-12 03:07:52 -03:00
feat(bulk): reorganize context menu with sections and submenu for workflow actions
Group 15 flat menu items into 5 logical sections (Workflow, Metadata, Attributes, Organize, Download) with section headers to reduce cognitive load. Nest the three workflow-related actions (Append, Replace, Copy Syntax) into a single "Send to Workflow" hover-triggered submenu. Add submenu infrastructure to BaseContextMenu with mouseover/mouseout boundary detection, 250ms close delay, and viewport-aware positioning. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3,32 +3,113 @@ export class BaseContextMenu {
|
||||
this.menu = document.getElementById(menuId);
|
||||
this.cardSelector = cardSelector;
|
||||
this.currentCard = null;
|
||||
|
||||
this.submenuTimeout = null;
|
||||
this.openSubmenu = null;
|
||||
|
||||
if (!this.menu) {
|
||||
console.error(`Context menu element with ID ${menuId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Hide menu on regular clicks
|
||||
document.addEventListener('click', () => this.hideMenu());
|
||||
// Hide menu when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!this.menu.contains(e.target)) {
|
||||
this.hideMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle menu item clicks
|
||||
// Handle menu item clicks (including submenu items)
|
||||
this.menu.addEventListener('click', (e) => {
|
||||
const menuItem = e.target.closest('.context-menu-item');
|
||||
if (!menuItem || !this.currentCard) return;
|
||||
|
||||
// Ignore clicks on submenu trigger (has-submenu parent)
|
||||
if (menuItem.classList.contains('has-submenu')) return;
|
||||
|
||||
const action = menuItem.dataset.action;
|
||||
if (!action) return;
|
||||
|
||||
|
||||
this.handleMenuAction(action, menuItem);
|
||||
this.hideMenu();
|
||||
});
|
||||
|
||||
// Submenu hover handling
|
||||
// Use mouseover/mouseout (which bubble) with relatedTarget checks
|
||||
// to reliably detect crossing the .has-submenu boundary
|
||||
this.menu.addEventListener('mouseover', (e) => {
|
||||
const trigger = e.target.closest('.has-submenu');
|
||||
if (!trigger) return;
|
||||
|
||||
// Only act when entering from outside this trigger's tree
|
||||
if (e.relatedTarget && trigger.contains(e.relatedTarget)) return;
|
||||
|
||||
this._openSubmenu(trigger);
|
||||
});
|
||||
|
||||
this.menu.addEventListener('mouseout', (e) => {
|
||||
const trigger = e.target.closest('.has-submenu');
|
||||
if (!trigger) return;
|
||||
|
||||
// Only close when leaving the trigger's tree entirely
|
||||
if (e.relatedTarget && trigger.contains(e.relatedTarget)) return;
|
||||
|
||||
this._scheduleSubmenuClose(trigger);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
_openSubmenu(trigger) {
|
||||
// Clear any pending close
|
||||
if (this.submenuTimeout) {
|
||||
clearTimeout(this.submenuTimeout);
|
||||
this.submenuTimeout = null;
|
||||
}
|
||||
|
||||
// Hide any previously open submenu
|
||||
if (this.openSubmenu && this.openSubmenu !== trigger) {
|
||||
this._hideSubmenu(this.openSubmenu);
|
||||
}
|
||||
|
||||
const submenu = trigger.querySelector('.context-submenu');
|
||||
if (!submenu) return;
|
||||
|
||||
submenu.style.display = 'block';
|
||||
this.openSubmenu = trigger;
|
||||
this._positionSubmenu(submenu);
|
||||
}
|
||||
|
||||
_scheduleSubmenuClose(trigger) {
|
||||
this.submenuTimeout = setTimeout(() => {
|
||||
this._hideSubmenu(trigger);
|
||||
this.submenuTimeout = null;
|
||||
}, 250);
|
||||
}
|
||||
|
||||
_hideSubmenu(trigger) {
|
||||
const submenu = trigger.querySelector('.context-submenu');
|
||||
if (submenu) {
|
||||
submenu.style.display = 'none';
|
||||
submenu.classList.remove('flip-left');
|
||||
}
|
||||
if (this.openSubmenu === trigger) {
|
||||
this.openSubmenu = null;
|
||||
}
|
||||
}
|
||||
|
||||
_positionSubmenu(submenu) {
|
||||
const submenuRect = submenu.getBoundingClientRect();
|
||||
const viewportWidth = document.documentElement.clientWidth;
|
||||
|
||||
if (submenuRect.right > viewportWidth) {
|
||||
submenu.classList.add('flip-left');
|
||||
} else {
|
||||
submenu.classList.remove('flip-left');
|
||||
}
|
||||
}
|
||||
|
||||
handleMenuAction(action, menuItem) {
|
||||
// Override in subclass
|
||||
console.warn('handleMenuAction not implemented');
|
||||
@@ -40,34 +121,41 @@ export class BaseContextMenu {
|
||||
|
||||
// Get menu dimensions
|
||||
const menuRect = this.menu.getBoundingClientRect();
|
||||
|
||||
|
||||
// Get viewport dimensions
|
||||
const viewportWidth = document.documentElement.clientWidth;
|
||||
const viewportHeight = document.documentElement.clientHeight;
|
||||
|
||||
|
||||
// Calculate position
|
||||
let finalX = x;
|
||||
let finalY = y;
|
||||
|
||||
|
||||
// Ensure menu doesn't go offscreen right
|
||||
if (x + menuRect.width > viewportWidth) {
|
||||
finalX = x - menuRect.width;
|
||||
}
|
||||
|
||||
|
||||
// Ensure menu doesn't go offscreen bottom
|
||||
if (y + menuRect.height > viewportHeight) {
|
||||
finalY = y - menuRect.height;
|
||||
}
|
||||
|
||||
|
||||
// Position menu
|
||||
this.menu.style.left = `${finalX}px`;
|
||||
this.menu.style.top = `${finalY}px`;
|
||||
}
|
||||
|
||||
hideMenu() {
|
||||
if (this.submenuTimeout) {
|
||||
clearTimeout(this.submenuTimeout);
|
||||
this.submenuTimeout = null;
|
||||
}
|
||||
if (this.openSubmenu) {
|
||||
this._hideSubmenu(this.openSubmenu);
|
||||
}
|
||||
if (this.menu) {
|
||||
this.menu.style.display = 'none';
|
||||
}
|
||||
this.currentCard = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,14 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
if (copyAllItem) {
|
||||
copyAllItem.style.display = config.copyAll ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
// Submenu parent visibility
|
||||
const sendToWorkflowSubmenu = this.menu.querySelector('[data-has-submenu="send-to-workflow"]');
|
||||
if (sendToWorkflowSubmenu) {
|
||||
const hasWorkflowActions = config.sendToWorkflow || config.copyAll;
|
||||
sendToWorkflowSubmenu.style.display = hasWorkflowActions ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (refreshAllItem) {
|
||||
refreshAllItem.style.display = config.refreshAll ? 'flex' : 'none';
|
||||
}
|
||||
@@ -148,6 +156,14 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Hide empty sections
|
||||
this.menu.querySelectorAll('.context-menu-section').forEach(section => {
|
||||
const items = Array.from(section.querySelectorAll('.context-menu-item'))
|
||||
.filter(item => !item.closest('.context-submenu'));
|
||||
const allHidden = items.length > 0 && items.every(item => item.style.display === 'none');
|
||||
section.style.display = allHidden ? 'none' : '';
|
||||
});
|
||||
}
|
||||
|
||||
updateSelectedCountHeader() {
|
||||
|
||||
Reference in New Issue
Block a user