Files
dees-catalog/ts_web/elements/dees-contextmenu.ts

299 lines
8.9 KiB
TypeScript
Raw Normal View History

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';
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;
public static accumulatedMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { 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();
// Get the target element of the right-click
let target: EventTarget | null = event.target;
// Clear previously accumulated items
DeesContextmenu.accumulatedMenuItems = [];
// Traverse up the DOM tree to accumulate menu items
while (target) {
if ((target as any).getContextMenuItems) {
const items = (target as any).getContextMenuItems();
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
}
target = (target as Node).parentNode;
}
// Open the context menu with the accumulated items
DeesContextmenu.openContextMenuWithOptions(event, DeesContextmenu.accumulatedMenuItems);
});
}
// allows opening of a contextmenu with options
public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { 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';
contextMenu.style.zIndex = '10000';
2023-01-13 02:15:30 +01:00
contextMenu.style.opacity = '0';
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();
contextMenu.windowLayer.addEventListener('click', async () => {
await contextMenu.destroy();
})
2023-01-13 02:15:30 +01:00
document.body.append(contextMenu);
// 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';
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,
})
public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; divider?: never } | { divider: true })[] = [];
2023-09-04 19:28:50 +02:00
windowLayer: DeesWindowLayer;
2023-01-12 18:14:59 +01:00
constructor() {
super();
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;
transition: opacity 0.2s, transform 0.2s;
outline: none;
2023-01-12 18:14:59 +01:00
}
.mainbox {
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;
padding: 4px 0;
font-size: 12px;
color: ${cssManager.bdTheme('#333', '#ccc')};
2023-01-13 00:30:56 +01:00
}
.menuitem {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: default;
transition: background 0.1s;
line-height: 1;
2023-01-13 00:30:56 +01: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
}
.menuitem:active {
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
}
.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) => {
if ('divider' in menuItemArg && menuItemArg.divider) {
return html`<div class="menu-divider"></div>`;
}
const menuItem = menuItemArg as plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean };
2023-01-13 00:30:56 +01:00
return html`
<div class="menuitem ${menuItem.disabled ? 'disabled' : ''}" @click=${() => !menuItem.disabled && this.handleClick(menuItem)}>
${menuItem.iconName ? html`
<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>
` : ''}
<span class="menuitem-text">${menuItem.name}</span>
${menuItem.shortcut ? html`
<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
}}>
<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() {
// 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
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
public async handleClick(menuItem: plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }) {
2023-01-13 02:15:30 +01:00
menuItem.action();
await this.destroy();
}
public async destroy() {
2023-09-04 19:28:50 +02:00
if (this.windowLayer) {
this.windowLayer.destroy();
}
2023-01-13 02:15:30 +01:00
this.style.opacity = '0';
this.style.transform = 'scale(0.95) translateY(-10px)';
2023-01-13 02:15:30 +01:00
await domtools.plugins.smartdelay.delayFor(100);
this.parentElement.removeChild(this);
}
2023-01-12 18:14:59 +01:00
}
2023-10-24 14:18:03 +02:00
DeesContextmenu.initializeGlobalListener();