2024-01-15 19:42:15 +01:00
|
|
|
|
import * as plugins from './00plugins.js';
|
2024-01-18 02:08:19 +01:00
|
|
|
|
import { demoFunc } from './dees-contextmenu.demo.js';
|
2023-01-12 18:14:59 +01:00
|
|
|
|
import {
|
|
|
|
|
customElement,
|
|
|
|
|
html,
|
|
|
|
|
DeesElement,
|
|
|
|
|
property,
|
2023-08-07 20:02:18 +02:00
|
|
|
|
type TemplateResult,
|
2023-01-12 18:14:59 +01:00
|
|
|
|
cssManager,
|
|
|
|
|
css,
|
2023-08-08 01:10:02 +02:00
|
|
|
|
type CSSResult,
|
2023-01-12 18:14:59 +01:00
|
|
|
|
unsafeCSS,
|
2023-08-07 19:13:29 +02:00
|
|
|
|
} from '@design.estate/dees-element';
|
2023-01-12 18:14:59 +01:00
|
|
|
|
|
2023-08-07 19:13:29 +02:00
|
|
|
|
import * as domtools from '@design.estate/dees-domtools';
|
2023-01-13 00:30:56 +01:00
|
|
|
|
import { DeesWindowLayer } from './dees-windowlayer.js';
|
2025-06-26 15:46:44 +00:00
|
|
|
|
import { zIndexLayers } from './00zindex.js';
|
2025-06-17 11:39:16 +00:00
|
|
|
|
import './dees-icon.js';
|
2023-01-12 18:14:59 +01:00
|
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
|
'dees-contextmenu': DeesContextmenu;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@customElement('dees-contextmenu')
|
|
|
|
|
export class DeesContextmenu extends DeesElement {
|
2023-01-13 00:30:56 +01:00
|
|
|
|
// DEMO
|
2023-09-09 13:34:46 +02:00
|
|
|
|
public static demo = demoFunc
|
2023-01-12 18:14:59 +01:00
|
|
|
|
|
2023-01-13 00:30:56 +01:00
|
|
|
|
// STATIC
|
2023-10-24 14:18:03 +02:00
|
|
|
|
// This will store all the accumulated menu items
|
|
|
|
|
public static contextMenuDeactivated = false;
|
2025-06-27 19:25:34 +00:00
|
|
|
|
public static accumulatedMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] } | { divider: true })[] = [];
|
2023-10-24 14:18:03 +02:00
|
|
|
|
|
|
|
|
|
// Add a global event listener for the right-click context menu
|
|
|
|
|
public static initializeGlobalListener() {
|
|
|
|
|
document.addEventListener('contextmenu', (event: MouseEvent) => {
|
|
|
|
|
if (this.contextMenuDeactivated) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
// Clear previously accumulated items
|
|
|
|
|
DeesContextmenu.accumulatedMenuItems = [];
|
|
|
|
|
|
2025-06-27 19:25:34 +00:00
|
|
|
|
// Use composedPath to properly traverse shadow DOM boundaries
|
|
|
|
|
const path = event.composedPath();
|
|
|
|
|
|
|
|
|
|
// Traverse the composed path to accumulate menu items
|
|
|
|
|
for (const element of path) {
|
|
|
|
|
if ((element as any).getContextMenuItems) {
|
|
|
|
|
const items = (element as any).getContextMenuItems();
|
2025-06-17 11:39:16 +00:00
|
|
|
|
if (items && items.length > 0) {
|
|
|
|
|
if (DeesContextmenu.accumulatedMenuItems.length > 0) {
|
|
|
|
|
DeesContextmenu.accumulatedMenuItems.push({ divider: true });
|
|
|
|
|
}
|
|
|
|
|
DeesContextmenu.accumulatedMenuItems.push(...items);
|
|
|
|
|
}
|
2023-10-24 14:18:03 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open the context menu with the accumulated items
|
|
|
|
|
DeesContextmenu.openContextMenuWithOptions(event, DeesContextmenu.accumulatedMenuItems);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// allows opening of a contextmenu with options
|
2025-06-27 19:25:34 +00:00
|
|
|
|
public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] } | { divider: true })[]) {
|
2023-10-24 14:18:03 +02:00
|
|
|
|
if (this.contextMenuDeactivated) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-01-13 02:15:30 +01:00
|
|
|
|
eventArg.preventDefault();
|
2023-09-08 11:44:03 +02:00
|
|
|
|
eventArg.stopPropagation();
|
2023-01-13 00:30:56 +01:00
|
|
|
|
const contextMenu = new DeesContextmenu();
|
2023-09-13 01:37:02 +02:00
|
|
|
|
contextMenu.style.position = 'fixed';
|
2025-06-26 15:46:44 +00:00
|
|
|
|
contextMenu.style.zIndex = String(zIndexLayers.overlay.contextMenu);
|
2023-01-13 02:15:30 +01:00
|
|
|
|
contextMenu.style.opacity = '0';
|
2025-06-17 11:39:16 +00:00
|
|
|
|
contextMenu.style.transform = 'scale(0.95) translateY(-10px)';
|
2023-01-13 02:15:30 +01:00
|
|
|
|
contextMenu.menuItems = menuItemsArg;
|
2023-09-04 19:28:50 +02:00
|
|
|
|
contextMenu.windowLayer = await DeesWindowLayer.createAndShow();
|
2025-06-27 19:25:34 +00:00
|
|
|
|
contextMenu.windowLayer.addEventListener('click', async (event) => {
|
|
|
|
|
// Check if click is on the context menu or its submenus
|
|
|
|
|
const clickedElement = event.target as HTMLElement;
|
|
|
|
|
const isContextMenu = clickedElement.closest('dees-contextmenu');
|
|
|
|
|
if (!isContextMenu) {
|
|
|
|
|
await contextMenu.destroy();
|
|
|
|
|
}
|
2023-09-04 19:28:50 +02:00
|
|
|
|
})
|
2023-01-13 02:15:30 +01:00
|
|
|
|
document.body.append(contextMenu);
|
2025-06-17 11:39:16 +00:00
|
|
|
|
|
|
|
|
|
// Get dimensions after adding to DOM
|
|
|
|
|
await domtools.plugins.smartdelay.delayFor(0);
|
|
|
|
|
const rect = contextMenu.getBoundingClientRect();
|
|
|
|
|
const windowWidth = window.innerWidth;
|
|
|
|
|
const windowHeight = window.innerHeight;
|
|
|
|
|
|
|
|
|
|
// Calculate position
|
|
|
|
|
let top = eventArg.clientY;
|
|
|
|
|
let left = eventArg.clientX;
|
|
|
|
|
|
|
|
|
|
// Adjust if menu would go off right edge
|
|
|
|
|
if (left + rect.width > windowWidth) {
|
|
|
|
|
left = windowWidth - rect.width - 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Adjust if menu would go off bottom edge
|
|
|
|
|
if (top + rect.height > windowHeight) {
|
|
|
|
|
top = windowHeight - rect.height - 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure menu doesn't go off left or top edge
|
|
|
|
|
if (left < 10) left = 10;
|
|
|
|
|
if (top < 10) top = 10;
|
|
|
|
|
|
|
|
|
|
contextMenu.style.top = `${top}px`;
|
|
|
|
|
contextMenu.style.left = `${left}px`;
|
|
|
|
|
contextMenu.style.transformOrigin = 'top left';
|
|
|
|
|
|
|
|
|
|
// Animate in
|
2023-01-13 02:15:30 +01:00
|
|
|
|
await domtools.plugins.smartdelay.delayFor(0);
|
|
|
|
|
contextMenu.style.opacity = '1';
|
2025-06-17 11:39:16 +00:00
|
|
|
|
contextMenu.style.transform = 'scale(1) translateY(0)';
|
2023-01-13 00:30:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-24 14:18:03 +02:00
|
|
|
|
// INSTANCE
|
2023-01-13 00:30:56 +01:00
|
|
|
|
@property({
|
|
|
|
|
type: Array,
|
|
|
|
|
})
|
2025-06-27 19:25:34 +00:00
|
|
|
|
public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[]; divider?: never } | { divider: true })[] = [];
|
2023-09-04 19:28:50 +02:00
|
|
|
|
windowLayer: DeesWindowLayer;
|
2025-06-27 19:25:34 +00:00
|
|
|
|
|
|
|
|
|
private submenu: DeesContextmenu | null = null;
|
|
|
|
|
private submenuTimeout: any = null;
|
|
|
|
|
private parentMenu: DeesContextmenu | null = null;
|
2023-01-12 18:14:59 +01:00
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
2025-06-17 11:39:16 +00:00
|
|
|
|
this.tabIndex = 0;
|
2023-01-12 18:14:59 +01:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-24 14:18:03 +02:00
|
|
|
|
/**
|
|
|
|
|
* STATIC STYLES
|
|
|
|
|
*/
|
2023-01-12 18:14:59 +01:00
|
|
|
|
public static styles = [
|
|
|
|
|
cssManager.defaultStyles,
|
|
|
|
|
css`
|
|
|
|
|
:host {
|
|
|
|
|
display: block;
|
2025-06-17 11:39:16 +00:00
|
|
|
|
transition: opacity 0.2s, transform 0.2s;
|
|
|
|
|
outline: none;
|
2023-01-12 18:14:59 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mainbox {
|
2025-06-17 11:39:16 +00:00
|
|
|
|
min-width: 200px;
|
|
|
|
|
max-width: 280px;
|
|
|
|
|
background: ${cssManager.bdTheme('#ffffff', '#000000')};
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
box-shadow: ${cssManager.bdTheme(
|
|
|
|
|
'0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
|
|
|
'0 4px 12px rgba(0, 0, 0, 0.3)'
|
|
|
|
|
)};
|
2023-01-13 00:30:56 +01:00
|
|
|
|
user-select: none;
|
2025-06-17 11:39:16 +00:00
|
|
|
|
padding: 4px 0;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
2023-01-13 00:30:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-17 11:39:16 +00:00
|
|
|
|
.menuitem {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
cursor: default;
|
|
|
|
|
transition: background 0.1s;
|
|
|
|
|
line-height: 1;
|
2025-06-27 19:25:34 +00:00
|
|
|
|
position: relative;
|
2023-01-13 00:30:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-17 11:39:16 +00:00
|
|
|
|
.menuitem:hover {
|
|
|
|
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
|
2023-01-12 18:14:59 +01:00
|
|
|
|
}
|
2025-06-27 19:25:34 +00:00
|
|
|
|
|
|
|
|
|
.menuitem.has-submenu::after {
|
|
|
|
|
content: '›';
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 8px;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
2023-01-12 18:14:59 +01:00
|
|
|
|
|
2025-06-27 19:25:34 +00:00
|
|
|
|
.menuitem:active:not(.has-submenu) {
|
2025-06-17 11:39:16 +00:00
|
|
|
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menuitem.disabled {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menuitem dees-icon {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menuitem-text {
|
|
|
|
|
flex: 1;
|
2023-01-13 00:30:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-17 11:39:16 +00:00
|
|
|
|
.menuitem-shortcut {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: ${cssManager.bdTheme('#999', '#666')};
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-divider {
|
|
|
|
|
height: 1px;
|
|
|
|
|
background: ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
|
|
|
|
margin: 4px 0;
|
2023-01-13 00:30:56 +01:00
|
|
|
|
}
|
2023-01-12 18:14:59 +01:00
|
|
|
|
`,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public render(): TemplateResult {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="mainbox">
|
2023-01-13 00:30:56 +01:00
|
|
|
|
${this.menuItems.map((menuItemArg) => {
|
2025-06-17 11:39:16 +00:00
|
|
|
|
if ('divider' in menuItemArg && menuItemArg.divider) {
|
|
|
|
|
return html`<div class="menu-divider"></div>`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 19:25:34 +00:00
|
|
|
|
const menuItem = menuItemArg as plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: any };
|
|
|
|
|
const hasSubmenu = menuItem.submenu && menuItem.submenu.length > 0;
|
2023-01-13 00:30:56 +01:00
|
|
|
|
return html`
|
2025-06-27 19:25:34 +00:00
|
|
|
|
<div
|
|
|
|
|
class="menuitem ${menuItem.disabled ? 'disabled' : ''} ${hasSubmenu ? 'has-submenu' : ''}"
|
|
|
|
|
@click=${() => !menuItem.disabled && !hasSubmenu && this.handleClick(menuItem)}
|
|
|
|
|
@mouseenter=${() => this.handleMenuItemHover(menuItem, hasSubmenu)}
|
|
|
|
|
@mouseleave=${() => this.handleMenuItemLeave()}
|
|
|
|
|
>
|
2025-06-17 11:39:16 +00:00
|
|
|
|
${menuItem.iconName ? html`
|
|
|
|
|
<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>
|
|
|
|
|
` : ''}
|
|
|
|
|
<span class="menuitem-text">${menuItem.name}</span>
|
2025-06-27 19:25:34 +00:00
|
|
|
|
${menuItem.shortcut && !hasSubmenu ? html`
|
2025-06-17 11:39:16 +00:00
|
|
|
|
<span class="menuitem-shortcut">${menuItem.shortcut}</span>
|
|
|
|
|
` : ''}
|
2023-01-13 00:30:56 +01:00
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
})}
|
2023-09-09 13:34:46 +02:00
|
|
|
|
${this.menuItems.length === 0 ? html`
|
|
|
|
|
<div class="menuitem" @click=${() => {
|
2023-10-24 14:18:03 +02:00
|
|
|
|
DeesContextmenu.contextMenuDeactivated = true;
|
|
|
|
|
this.destroy();
|
2023-09-09 13:34:46 +02:00
|
|
|
|
}}>
|
2025-06-17 11:39:16 +00:00
|
|
|
|
<dees-icon .icon="lucide:x"></dees-icon>
|
|
|
|
|
<span class="menuitem-text">Allow native context</span>
|
2023-09-09 13:34:46 +02:00
|
|
|
|
</div>
|
|
|
|
|
` : html``}
|
2023-01-12 18:14:59 +01:00
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async firstUpdated() {
|
2025-06-17 11:39:16 +00:00
|
|
|
|
// Focus on the menu for keyboard navigation
|
|
|
|
|
this.focus();
|
|
|
|
|
|
|
|
|
|
// Add keyboard event listeners
|
|
|
|
|
this.addEventListener('keydown', this.handleKeydown);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleKeydown = (event: KeyboardEvent) => {
|
|
|
|
|
const menuItems = Array.from(this.shadowRoot.querySelectorAll('.menuitem:not(.disabled)'));
|
|
|
|
|
const currentIndex = menuItems.findIndex(item => item.matches(':hover'));
|
2023-09-09 13:34:46 +02:00
|
|
|
|
|
2025-06-17 11:39:16 +00:00
|
|
|
|
switch (event.key) {
|
|
|
|
|
case 'ArrowDown':
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const nextIndex = currentIndex + 1 < menuItems.length ? currentIndex + 1 : 0;
|
|
|
|
|
(menuItems[nextIndex] as HTMLElement).dispatchEvent(new MouseEvent('mouseenter'));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'ArrowUp':
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const prevIndex = currentIndex - 1 >= 0 ? currentIndex - 1 : menuItems.length - 1;
|
|
|
|
|
(menuItems[prevIndex] as HTMLElement).dispatchEvent(new MouseEvent('mouseenter'));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'Enter':
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (currentIndex >= 0) {
|
|
|
|
|
(menuItems[currentIndex] as HTMLElement).click();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'Escape':
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
this.destroy();
|
|
|
|
|
break;
|
|
|
|
|
}
|
2023-01-12 18:14:59 +01:00
|
|
|
|
}
|
2023-01-13 02:15:30 +01:00
|
|
|
|
|
2025-06-17 11:39:16 +00:00
|
|
|
|
public async handleClick(menuItem: plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }) {
|
2023-01-13 02:15:30 +01:00
|
|
|
|
menuItem.action();
|
2025-06-27 19:25:34 +00:00
|
|
|
|
|
|
|
|
|
// Close all menus in the chain (this menu and all parent menus)
|
|
|
|
|
await this.destroyAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleMenuItemHover(menuItem: plugins.tsclass.website.IMenuItem & { submenu?: any }, hasSubmenu: boolean) {
|
|
|
|
|
// Clear any existing timeout
|
|
|
|
|
if (this.submenuTimeout) {
|
|
|
|
|
clearTimeout(this.submenuTimeout);
|
|
|
|
|
this.submenuTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hide any existing submenu if hovering a different item
|
|
|
|
|
if (this.submenu) {
|
|
|
|
|
await this.hideSubmenu();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show submenu if this item has one
|
|
|
|
|
if (hasSubmenu && menuItem.submenu) {
|
|
|
|
|
this.submenuTimeout = setTimeout(() => {
|
|
|
|
|
this.showSubmenu(menuItem);
|
|
|
|
|
}, 200); // Small delay to prevent accidental triggers
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleMenuItemLeave() {
|
|
|
|
|
// Add a delay before hiding to allow moving to submenu
|
|
|
|
|
if (this.submenuTimeout) {
|
|
|
|
|
clearTimeout(this.submenuTimeout);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.submenuTimeout = setTimeout(() => {
|
|
|
|
|
if (this.submenu && !this.submenu.matches(':hover')) {
|
|
|
|
|
this.hideSubmenu();
|
|
|
|
|
}
|
|
|
|
|
}, 300);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async showSubmenu(menuItem: plugins.tsclass.website.IMenuItem & { submenu?: any }) {
|
|
|
|
|
if (!menuItem.submenu || menuItem.submenu.length === 0) return;
|
|
|
|
|
|
|
|
|
|
// Find the menu item element
|
|
|
|
|
const menuItems = Array.from(this.shadowRoot.querySelectorAll('.menuitem'));
|
|
|
|
|
const menuItemElement = menuItems.find(el => el.querySelector('.menuitem-text')?.textContent === menuItem.name) as HTMLElement;
|
|
|
|
|
if (!menuItemElement) return;
|
|
|
|
|
|
|
|
|
|
// Create submenu
|
|
|
|
|
this.submenu = new DeesContextmenu();
|
|
|
|
|
this.submenu.menuItems = menuItem.submenu;
|
|
|
|
|
this.submenu.parentMenu = this;
|
|
|
|
|
this.submenu.style.position = 'fixed';
|
|
|
|
|
this.submenu.style.zIndex = String(parseInt(this.style.zIndex) + 1);
|
|
|
|
|
this.submenu.style.opacity = '0';
|
|
|
|
|
this.submenu.style.transform = 'scale(0.95)';
|
|
|
|
|
|
|
|
|
|
// Don't create a window layer for submenus
|
|
|
|
|
document.body.append(this.submenu);
|
|
|
|
|
|
|
|
|
|
// Position submenu
|
|
|
|
|
await domtools.plugins.smartdelay.delayFor(0);
|
|
|
|
|
const itemRect = menuItemElement.getBoundingClientRect();
|
|
|
|
|
const menuRect = this.getBoundingClientRect();
|
|
|
|
|
const submenuRect = this.submenu.getBoundingClientRect();
|
|
|
|
|
const windowWidth = window.innerWidth;
|
|
|
|
|
|
|
|
|
|
let left = menuRect.right - 4; // Slight overlap
|
|
|
|
|
let top = itemRect.top;
|
|
|
|
|
|
|
|
|
|
// Check if submenu would go off right edge
|
|
|
|
|
if (left + submenuRect.width > windowWidth - 10) {
|
|
|
|
|
// Show on left side instead
|
|
|
|
|
left = menuRect.left - submenuRect.width + 4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Adjust vertical position if needed
|
|
|
|
|
if (top + submenuRect.height > window.innerHeight - 10) {
|
|
|
|
|
top = window.innerHeight - submenuRect.height - 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.submenu.style.left = `${left}px`;
|
|
|
|
|
this.submenu.style.top = `${top}px`;
|
|
|
|
|
|
|
|
|
|
// Animate in
|
|
|
|
|
await domtools.plugins.smartdelay.delayFor(0);
|
|
|
|
|
this.submenu.style.opacity = '1';
|
|
|
|
|
this.submenu.style.transform = 'scale(1)';
|
|
|
|
|
|
|
|
|
|
// Handle submenu hover
|
|
|
|
|
this.submenu.addEventListener('mouseenter', () => {
|
|
|
|
|
if (this.submenuTimeout) {
|
|
|
|
|
clearTimeout(this.submenuTimeout);
|
|
|
|
|
this.submenuTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.submenu.addEventListener('mouseleave', () => {
|
|
|
|
|
this.handleMenuItemLeave();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async hideSubmenu() {
|
|
|
|
|
if (!this.submenu) return;
|
|
|
|
|
|
|
|
|
|
await this.submenu.destroy();
|
|
|
|
|
this.submenu = null;
|
2023-01-13 02:15:30 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async destroy() {
|
2025-06-27 19:25:34 +00:00
|
|
|
|
// Clear timeout
|
|
|
|
|
if (this.submenuTimeout) {
|
|
|
|
|
clearTimeout(this.submenuTimeout);
|
|
|
|
|
this.submenuTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Destroy submenu first
|
|
|
|
|
if (this.submenu) {
|
|
|
|
|
await this.submenu.destroy();
|
|
|
|
|
this.submenu = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only destroy window layer if this is not a submenu
|
|
|
|
|
if (this.windowLayer && !this.parentMenu) {
|
2023-09-04 19:28:50 +02:00
|
|
|
|
this.windowLayer.destroy();
|
|
|
|
|
}
|
2025-06-27 19:25:34 +00:00
|
|
|
|
|
2023-01-13 02:15:30 +01:00
|
|
|
|
this.style.opacity = '0';
|
2025-06-17 11:39:16 +00:00
|
|
|
|
this.style.transform = 'scale(0.95) translateY(-10px)';
|
2023-01-13 02:15:30 +01:00
|
|
|
|
await domtools.plugins.smartdelay.delayFor(100);
|
2025-06-27 19:25:34 +00:00
|
|
|
|
|
|
|
|
|
if (this.parentElement) {
|
|
|
|
|
this.parentElement.removeChild(this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Destroys this menu and all parent menus in the chain
|
|
|
|
|
*/
|
|
|
|
|
public async destroyAll() {
|
|
|
|
|
// First destroy parent menus if they exist
|
|
|
|
|
if (this.parentMenu) {
|
|
|
|
|
await this.parentMenu.destroyAll();
|
|
|
|
|
} else {
|
|
|
|
|
// If we're at the top level, just destroy this menu
|
|
|
|
|
await this.destroy();
|
|
|
|
|
}
|
2023-01-13 02:15:30 +01:00
|
|
|
|
}
|
2023-01-12 18:14:59 +01:00
|
|
|
|
}
|
2023-10-24 14:18:03 +02:00
|
|
|
|
|
|
|
|
|
DeesContextmenu.initializeGlobalListener();
|