Speed up nextmatch context menus by creating them once & reuse them

This commit is contained in:
nathan 2025-01-06 13:30:17 -07:00
parent 2b5f15855b
commit 290dc59a09
3 changed files with 93 additions and 32 deletions

View File

@ -1,5 +1,5 @@
import {css, html, LitElement, nothing, PropertyValues} from "lit"; import {css, html, LitElement, nothing} from "lit";
import {SlIcon, SlMenu, SlMenuItem} from "@shoelace-style/shoelace"; import {SlMenu, SlMenuItem} from "@shoelace-style/shoelace";
import {egwMenuItem} from "./egw_menu"; import {egwMenuItem} from "./egw_menu";
import {customElement} from "lit/decorators/custom-element.js"; import {customElement} from "lit/decorators/custom-element.js";
import {repeat} from "lit/directives/repeat.js"; import {repeat} from "lit/directives/repeat.js";
@ -26,6 +26,12 @@ export class EgwMenuShoelace extends LitElement
box-shadow: var(--sl-shadow-x-large); box-shadow: var(--sl-shadow-x-large);
} }
/* sl-menu-item:host overrides display */
sl-menu-item[hidden] {
display: none !important;
}
sl-menu-item::part(base) { sl-menu-item::part(base) {
height: 1.7em; height: 1.7em;
line-height: var(--sl-line-height-dense); line-height: var(--sl-line-height-dense);
@ -60,7 +66,7 @@ export class EgwMenuShoelace extends LitElement
private structure = []; private structure = [];
private popup = null; private popup = null;
private removeCallback = null; private hideCallback = null;
private get menu() : SlMenu { return this.shadowRoot?.querySelector("sl-menu");} private get menu() : SlMenu { return this.shadowRoot?.querySelector("sl-menu");}
@ -91,16 +97,16 @@ export class EgwMenuShoelace extends LitElement
this.popup.remove(); this.popup.remove();
this.popup = null; this.popup = null;
} }
if(this.removeCallback) if(this.hideCallback)
{ {
this.removeCallback.call(); this.hideCallback.call();
} }
} }
public showAt(_x, _y, _onHide) public showAt(_x, _y, _onHide)
{ {
this.removeCallback = _onHide; this.hideCallback = _onHide;
if(this.popup == null) if(this.popup == null)
{ {
this.popup = Object.assign(document.createElement("sl-popup"), { this.popup = Object.assign(document.createElement("sl-popup"), {
@ -111,7 +117,10 @@ export class EgwMenuShoelace extends LitElement
}); });
this.popup.append(this); this.popup.append(this);
this.popup.classList.add("egw_menu"); this.popup.classList.add("egw_menu");
document.body.append(this.popup);
} }
// Open where instructed
let menu = this; let menu = this;
this.popup.anchor = { this.popup.anchor = {
getBoundingClientRect() getBoundingClientRect()
@ -129,7 +138,6 @@ export class EgwMenuShoelace extends LitElement
} }
}; };
this.popup.active = true; this.popup.active = true;
document.body.append(this.popup);
Promise.all([this.updateComplete, this.popup.updateComplete]).then(() => Promise.all([this.updateComplete, this.popup.updateComplete]).then(() =>
{ {
// Causes scroll issues if we don't position // Causes scroll issues if we don't position
@ -138,15 +146,34 @@ export class EgwMenuShoelace extends LitElement
}); });
} }
/**
* Update the menu items with current disabled / visible settings
*
* @param _links
*/
public applyContext(_links)
{
Object.keys(_links).forEach((actionId) =>
{
const menuItem = <SlMenuItem>this.shadowRoot.querySelector("[data-action-id='" + actionId + "']");
if(!menuItem)
{
return;
}
menuItem.disabled = !_links[actionId].enabled;
menuItem.hidden = !_links[actionId].visible;
});
}
public hide() public hide()
{ {
if(this.popup) if(this.popup)
{ {
this.popup.active = false; this.popup.active = false;
} }
if(this.hideCallback)
// egw_menu always creates a new menu {
this.remove(); this.hideCallback.call();
}
} }
handleSelect(event) handleSelect(event)
@ -230,14 +257,16 @@ export class EgwMenuShoelace extends LitElement
{ {
item.iconUrl = item.checked ? "toggle-on" : "toggle-off"; item.iconUrl = item.checked ? "toggle-on" : "toggle-off";
} }
const id = CSS.escape(item.id);
return html` return html`
<sl-menu-item <sl-menu-item
class=${classMap({ class=${classMap({
"default-item": item.default "default-item": item.default
})} })}
id=${item.id} id=${id}
type="${item.checkbox ? "checkbox" : "normal"}" type="${item.checkbox ? "checkbox" : "normal"}"
data-action-id="${item.id}"
?checked=${item.checkbox && item.checked} ?checked=${item.checkbox && item.checked}
?disabled=${!item.enabled} ?disabled=${!item.enabled}
.value=${item} .value=${item}

View File

@ -116,10 +116,24 @@ export class EgwPopupActionImplementation implements EgwActionImplementation {
_context = {"posx": x, "posy": y}; _context = {"posx": x, "posy": y};
} }
const menu = this._buildMenu(_links, _selected, _target); let menu = null;
menu.showAt(_context.posx, _context.posy); // Special handling for nextmatch context menu - reuse the same menu
if(!_target && !_context.menu && _selected[0].parent.manager.data.menu)
{
menu = _selected[0].parent.manager.data.menu
}
if(!menu)
{
menu = this._buildMenu(_links, _selected, _target);
}
else
{
menu.applyContext(_links, _selected, _target);
}
return true; menu.showAt(_context.posx, _context.posy);
return true;
} else { } else {
const defaultAction = this._getDefaultLink(_links); const defaultAction = this._getDefaultLink(_links);
if (defaultAction) { if (defaultAction) {
@ -303,6 +317,13 @@ export class EgwPopupActionImplementation implements EgwActionImplementation {
*/ */
private _registerContext = (_node, _callback, _context) => { private _registerContext = (_node, _callback, _context) => {
// Special handling for nextmatch: only build the menu once and just re-use it.
if(!_context.menu && _context.actionLinks && _context.parent?.manager?.data?.nextmatch && !_context.parent.manager.data.menu)
{
_context.parent.manager.data.menu = this._buildMenu(_context.actionLinks.filter(l => l.actionObj.type == "popup"), [_context], null);
_context.parent.manager.data.menu.showAt(0, 0);
_context.parent.manager.data.menu.hide();
}
const contextHandler = (e) => { const contextHandler = (e) => {
const x = _node const x = _node
//use different node and context for callback if event happens on parent //use different node and context for callback if event happens on parent
@ -526,7 +547,7 @@ export class EgwPopupActionImplementation implements EgwActionImplementation {
const tree = {"root": []}; const tree = {"root": []};
// Automatically add in Drag & Drop actions // Automatically add in Drag & Drop actions
if(this.auto_paste && !window.egwIsMobile() && !this._context.event?.type.match(/touch/)) if(this.auto_paste && !window.egwIsMobile() && this._context?.event && !this._context.event?.type.match(/touch/))
{ {
this._addCopyPaste(_links, _selected); this._addCopyPaste(_links, _selected);
} }
@ -828,6 +849,7 @@ export class EgwPopupActionImplementation implements EgwActionImplementation {
} }
}; };
private _context: any; private _context: any;
private menu : EgwMenu;
} }

View File

@ -9,15 +9,6 @@
* *
*/ */
import {EgwMenuShoelace} from "./EgwMenuShoelace"; import {EgwMenuShoelace} from "./EgwMenuShoelace";
import {egw_registeredShortcuts, egw_shortcutIdx} from './egw_keymanager';
import {
EGW_KEY_ARROW_DOWN,
EGW_KEY_ARROW_LEFT,
EGW_KEY_ARROW_RIGHT,
EGW_KEY_ARROW_UP,
EGW_KEY_ENTER,
EGW_KEY_ESCAPE
} from "./egw_action_constants";
//Global variable which is used to store the currently active menu so that it //Global variable which is used to store the currently active menu so that it
//may be closed when another menu opens //may be closed when another menu opens
@ -160,7 +151,6 @@ export class egwMenu
if (this.instance != null) if (this.instance != null)
{ {
this.instance.hide(); this.instance.hide();
this.instance = null;
} }
} }
@ -196,18 +186,38 @@ export class egwMenu
//Obtain a new egwMenuImpl object and pass this instance to it //Obtain a new egwMenuImpl object and pass this instance to it
this.instance = new EgwMenuShoelace(this.children); this.instance = new EgwMenuShoelace(this.children);
_egw_active_menu = this;
this.instance.showAt(_x, _y, () => {
this.instance = null;
_egw_active_menu = null;
});
return true;
} }
_egw_active_menu = this;
this.instance.showAt(_x, _y, () =>
{
_egw_active_menu = null;
});
return true;
return false; return false;
} }
/**
* Enable / disable menu items for the given selection & target
*
* @param _context
* @param _links
* @param _selected
* @param _target
* @private
*/
public applyContext(_links, _selected, _target)
{
if(!this.instance)
{
this.instance = new EgwMenuShoelace(this.children);
}
this.instance.applyContext(_links);
}
/** /**
* Adds a new menu item to the list and returns a reference to that object. * Adds a new menu item to the list and returns a reference to that object.
* *