/** * MetonaEditor Context Menu — right-click menu * @module context-menu * @version 0.2.5 */ import { t as i18nT } from './i18n'; import type { MarkdownEditor } from './core'; export function registerContextMenu(this: MarkdownEditor, items: any[] = []): MarkdownEditor { this._contextMenuItems = items; return this; } export function bindContextMenu(this: MarkdownEditor): void { if (!this.el) return; const onContextMenu = (e: MouseEvent) => { const existing = document.querySelector('.me-context-menu'); if (existing) existing.remove(); this._showContextMenu(e); }; this.el.addEventListener('contextmenu', onContextMenu); this._cleanups.push(() => this.el.removeEventListener('contextmenu', onContextMenu)); } export function showContextMenu(this: MarkdownEditor, e: MouseEvent): void { e.preventDefault(); const ta = this.textarea; const hasSelection = ta && ta.selectionStart !== ta.selectionEnd; const defaultItems: Array<{ label?: string; action?: string; shortcut?: string; sep?: boolean; disabled?: boolean; onClick?: () => void }> = [ { label: i18nT('undo') || 'Undo', action: 'undo', shortcut: 'Ctrl+Z', disabled: !this.canUndo() }, { label: i18nT('redo') || 'Redo', action: 'redo', shortcut: 'Ctrl+Y', disabled: !this.canRedo() }, { sep: true }, { label: i18nT('cut') || 'Cut', action: 'cut', shortcut: 'Ctrl+X', disabled: !hasSelection }, { label: i18nT('copy') || 'Copy', action: 'copy', shortcut: 'Ctrl+C', disabled: !hasSelection }, { label: i18nT('paste') || 'Paste', action: 'paste', shortcut: 'Ctrl+V', disabled: !!this.config.readOnly }, { label: i18nT('selectAll') || 'Select All', action: 'selectAll', shortcut: 'Ctrl+A' }, ]; const items = [...defaultItems]; if (this._contextMenuItems.length) { items.push({ sep: true }); this._contextMenuItems.forEach((item) => items.push(item)); } const menu = document.createElement('div'); menu.className = 'me-context-menu'; menu.style.left = e.clientX + 'px'; menu.style.top = e.clientY + 'px'; // Adjust if off-screen requestAnimationFrame(() => { const rect = menu.getBoundingClientRect(); if (rect.right > window.innerWidth) menu.style.left = (e.clientX - rect.width) + 'px'; if (rect.bottom > window.innerHeight) menu.style.top = (e.clientY - rect.height) + 'px'; }); items.forEach((item) => { if ((item as any).sep) { const sep = document.createElement('div'); sep.className = 'me-context-menu-sep'; menu.appendChild(sep); return; } const el = document.createElement('div'); el.className = 'me-context-menu-item'; if (item.disabled) el.classList.add('me-disabled'); el.innerHTML = `${item.label}${item.shortcut ? `${item.shortcut}` : ''}`; el.addEventListener('click', (ev) => { ev.stopPropagation(); if (item.disabled) return; if (item.onClick) { item.onClick(); } else if (item.action) this._execContextAction(item.action); this._hideContextMenu(); }); menu.appendChild(el); }); document.body.appendChild(menu); const close = (ev: Event) => { if (!menu.contains(ev.target as Node)) { this._hideContextMenu(); } }; document.addEventListener('click', close, { once: true }); document.addEventListener('keydown', (ev) => { if (ev.key === 'Escape') this._hideContextMenu(); }, { once: true }); } export function hideContextMenu(this: MarkdownEditor): void { const menu = document.querySelector('.me-context-menu'); if (menu) menu.remove(); } export function execContextAction(this: MarkdownEditor, action: string): void { const ta = this.textarea; if (!ta) return; switch (action) { case 'undo': this.undo(); break; case 'redo': this.redo(); break; case 'cut': document.execCommand('cut'); break; case 'copy': document.execCommand('copy'); break; case 'paste': document.execCommand('paste'); break; case 'selectAll': ta.focus(); ta.select(); break; default: this.exec(action); break; } } // ============ Prototype installation ============ export const installContextMenu = (proto: any): void => { proto.registerContextMenu = registerContextMenu; proto._bindContextMenu = bindContextMenu; proto._showContextMenu = showContextMenu; proto._hideContextMenu = hideContextMenu; proto._execContextAction = execContextAction; };