Add context menu
This commit is contained in:
13
clipboard.js
13
clipboard.js
@@ -6,11 +6,18 @@ export const clipboard = function(superClass) {
|
||||
};
|
||||
};
|
||||
|
||||
export const copyToClipboard = (content) => {
|
||||
export const copyToClipboard = async (content) => {
|
||||
const txtEl = document.createElement('input');
|
||||
txtEl.type = 'hidden';
|
||||
document.body.appendChild(txtEl);
|
||||
txtEl.value = content;
|
||||
navigator.clipboard.writeText(content);
|
||||
document.body.removeChild(txtEl);
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
} finally {
|
||||
document.body.removeChild(txtEl);
|
||||
}
|
||||
}
|
||||
|
||||
174
context-menu.js
Normal file
174
context-menu.js
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
@license
|
||||
Copyright (c) 2026 trading_peter
|
||||
This program is available under Apache License Version 2.0
|
||||
*/
|
||||
|
||||
import '@tp/tp-popup/tp-popup-menu.js';
|
||||
import '@tp/tp-popup/tp-popup-menu-item.js';
|
||||
import { html } from 'lit';
|
||||
|
||||
/**
|
||||
* General-purpose context menu mixin for LitElement components.
|
||||
*
|
||||
* The host must also use the Position mixin (from helpers/position.js)
|
||||
* so that `_posFixed()` is available.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Mix into your element: ContextMenu(Position(LitElement))
|
||||
* 2. Call `this._openContextMenu(e, actions, context)` from a
|
||||
* `contextmenu` event handler, where:
|
||||
* - e: the original MouseEvent
|
||||
* - actions: array of { label, action, icon?, disabled? }
|
||||
* - context: arbitrary data passed through to the event detail
|
||||
* 3. Include `${this._renderCtxMenu()}` somewhere in your render().
|
||||
* 4. Listen for the `ctx-action` event on the host to handle actions:
|
||||
* detail: { action, context, originalEvent }
|
||||
*
|
||||
* The mixin also dispatches a cancelable `ctx-open` event before showing
|
||||
* the menu. Calling `e.preventDefault()` on it suppresses the menu.
|
||||
*/
|
||||
export const ContextMenu = (superClass) => class ContextMenuHost extends superClass {
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
/** @private */
|
||||
_ctxMenu: { type: Object, state: true },
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._ctxMenu = null;
|
||||
this._ctxOutsideHandler = null;
|
||||
this._ctxKeyHandler = null;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._removeCtxHandlers();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Open a context menu at the pointer position.
|
||||
*
|
||||
* @param {MouseEvent} e - The contextmenu event.
|
||||
* @param {Array} actions - Menu items: [{ label, action, icon?, disabled? }]
|
||||
* @param {*} [context] - Arbitrary data forwarded in events.
|
||||
*/
|
||||
async _openContextMenu(e, actions, context) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const openEvt = new CustomEvent('ctx-open', {
|
||||
detail: { actions, context, originalEvent: e },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
cancelable: true,
|
||||
});
|
||||
this.dispatchEvent(openEvt);
|
||||
if (openEvt.defaultPrevented) return;
|
||||
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
|
||||
this._ctxMenu = { actions, context };
|
||||
this.requestUpdate();
|
||||
await this.updateComplete;
|
||||
|
||||
const menu = this.shadowRoot.querySelector('.ctx-menu');
|
||||
if (menu) {
|
||||
const anchor = {
|
||||
getBoundingClientRect: () => ({
|
||||
top: y, bottom: y, left: x, right: x, width: 0, height: 0,
|
||||
}),
|
||||
};
|
||||
this._posFixed(anchor, menu, { valign: 'bottom', halign: 'left' });
|
||||
}
|
||||
|
||||
this._attachCtxHandlers();
|
||||
}
|
||||
|
||||
/** Programmatically close the context menu. */
|
||||
_closeContextMenu() {
|
||||
this._ctxMenu = null;
|
||||
this.requestUpdate();
|
||||
this._removeCtxHandlers();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rendering — include in your host's render()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Returns the context-menu template. Place inside your host render(). */
|
||||
_renderCtxMenu() {
|
||||
if (!this._ctxMenu) return null;
|
||||
const { actions, context } = this._ctxMenu;
|
||||
|
||||
return html`
|
||||
<tp-popup-menu class="ctx-menu" part="ctx-menu">
|
||||
${(actions || []).map((a) => a.separator
|
||||
? html`<tp-popup-menu-divider part="ctx-separator"></tp-popup-menu-divider>`
|
||||
: html`
|
||||
<tp-popup-menu-item
|
||||
part="ctx-menu-item"
|
||||
.icon=${a.icon || null}
|
||||
?disabled=${a.disabled}
|
||||
@click=${(e) => this._onCtxAction(a, context, e)}>
|
||||
${a.label}
|
||||
</tp-popup-menu-item>
|
||||
`
|
||||
)}
|
||||
</tp-popup-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** @private */
|
||||
_onCtxAction(action, context, e) {
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(new CustomEvent('ctx-action', {
|
||||
detail: { action: action.action, context, originalEvent: e },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
this._closeContextMenu();
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_attachCtxHandlers() {
|
||||
if (this._ctxOutsideHandler) return;
|
||||
|
||||
this._ctxOutsideHandler = (e) => {
|
||||
const menu = this.shadowRoot.querySelector('.ctx-menu');
|
||||
if (menu && e.composedPath().includes(menu)) return;
|
||||
this._closeContextMenu();
|
||||
};
|
||||
|
||||
this._ctxKeyHandler = (e) => {
|
||||
if (e.key === 'Escape') this._closeContextMenu();
|
||||
};
|
||||
|
||||
window.addEventListener('pointerdown', this._ctxOutsideHandler, true);
|
||||
window.addEventListener('keydown', this._ctxKeyHandler, true);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_removeCtxHandlers() {
|
||||
if (this._ctxOutsideHandler) {
|
||||
window.removeEventListener('pointerdown', this._ctxOutsideHandler, true);
|
||||
this._ctxOutsideHandler = null;
|
||||
}
|
||||
if (this._ctxKeyHandler) {
|
||||
window.removeEventListener('keydown', this._ctxKeyHandler, true);
|
||||
this._ctxKeyHandler = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user