mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-12 06:27:56 -03:00
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>
162 lines
4.8 KiB
JavaScript
162 lines
4.8 KiB
JavaScript
export class BaseContextMenu {
|
|
constructor(menuId, cardSelector) {
|
|
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 when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
if (!this.menu.contains(e.target)) {
|
|
this.hideMenu();
|
|
}
|
|
});
|
|
|
|
// 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');
|
|
}
|
|
|
|
showMenu(x, y, card) {
|
|
this.currentCard = card;
|
|
this.menu.style.display = 'block';
|
|
|
|
// 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;
|
|
}
|
|
}
|