update wysiwyg

This commit is contained in:
2025-06-27 19:25:34 +00:00
parent fe3cd0591f
commit 24957f02d4
11 changed files with 934 additions and 178 deletions

View File

@@ -31,7 +31,7 @@ export class DeesContextmenu extends DeesElement {
// STATIC
// 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 })[] = [];
public static accumulatedMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] } | { divider: true })[] = [];
// Add a global event listener for the right-click context menu
public static initializeGlobalListener() {
@@ -41,16 +41,16 @@ export class DeesContextmenu extends DeesElement {
}
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();
// 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();
if (items && items.length > 0) {
if (DeesContextmenu.accumulatedMenuItems.length > 0) {
DeesContextmenu.accumulatedMenuItems.push({ divider: true });
@@ -58,7 +58,6 @@ export class DeesContextmenu extends DeesElement {
DeesContextmenu.accumulatedMenuItems.push(...items);
}
}
target = (target as Node).parentNode;
}
// Open the context menu with the accumulated items
@@ -67,7 +66,7 @@ export class DeesContextmenu extends DeesElement {
}
// allows opening of a contextmenu with options
public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[]) {
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 })[]) {
if (this.contextMenuDeactivated) {
return;
}
@@ -80,8 +79,13 @@ export class DeesContextmenu extends DeesElement {
contextMenu.style.transform = 'scale(0.95) translateY(-10px)';
contextMenu.menuItems = menuItemsArg;
contextMenu.windowLayer = await DeesWindowLayer.createAndShow();
contextMenu.windowLayer.addEventListener('click', async () => {
await contextMenu.destroy();
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();
}
})
document.body.append(contextMenu);
@@ -123,8 +127,12 @@ export class DeesContextmenu extends DeesElement {
@property({
type: Array,
})
public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; divider?: never } | { divider: true })[] = [];
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 })[] = [];
windowLayer: DeesWindowLayer;
private submenu: DeesContextmenu | null = null;
private submenuTimeout: any = null;
private parentMenu: DeesContextmenu | null = null;
constructor() {
super();
@@ -167,13 +175,22 @@ export class DeesContextmenu extends DeesElement {
cursor: default;
transition: background 0.1s;
line-height: 1;
position: relative;
}
.menuitem:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
}
.menuitem.has-submenu::after {
content: '';
position: absolute;
right: 8px;
font-size: 16px;
opacity: 0.5;
}
.menuitem:active {
.menuitem:active:not(.has-submenu) {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
}
@@ -215,14 +232,20 @@ export class DeesContextmenu extends DeesElement {
return html`<div class="menu-divider"></div>`;
}
const menuItem = menuItemArg as plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean };
const menuItem = menuItemArg as plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: any };
const hasSubmenu = menuItem.submenu && menuItem.submenu.length > 0;
return html`
<div class="menuitem ${menuItem.disabled ? 'disabled' : ''}" @click=${() => !menuItem.disabled && this.handleClick(menuItem)}>
<div
class="menuitem ${menuItem.disabled ? 'disabled' : ''} ${hasSubmenu ? 'has-submenu' : ''}"
@click=${() => !menuItem.disabled && !hasSubmenu && this.handleClick(menuItem)}
@mouseenter=${() => this.handleMenuItemHover(menuItem, hasSubmenu)}
@mouseleave=${() => this.handleMenuItemLeave()}
>
${menuItem.iconName ? html`
<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>
` : ''}
<span class="menuitem-text">${menuItem.name}</span>
${menuItem.shortcut ? html`
${menuItem.shortcut && !hasSubmenu ? html`
<span class="menuitem-shortcut">${menuItem.shortcut}</span>
` : ''}
</div>
@@ -282,17 +305,151 @@ export class DeesContextmenu extends DeesElement {
public async handleClick(menuItem: plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }) {
menuItem.action();
await this.destroy();
// 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;
}
public async destroy() {
if (this.windowLayer) {
// 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) {
this.windowLayer.destroy();
}
this.style.opacity = '0';
this.style.transform = 'scale(0.95) translateY(-10px)';
await domtools.plugins.smartdelay.delayFor(100);
this.parentElement.removeChild(this);
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();
}
}
}