refactor(core): 拆分 4 模块 + 插件状态 Symbol 化 + eslint type-aware
- core.ts 从 1162 行降至 865 行:命令/浮动工具栏/右键菜单/大纲 拆至 commands.ts / floating-toolbar.ts / context-menu.ts / outline.ts (原型安装,API 与测试完全兼容) - 6 个预设插件状态改用 Symbol 键存储,定义 EditorLike 契约接口, 消灭 (editor as any).__xxx 魔法属性 - 实现 maxLength(textarea maxlength + 程序化入口截断)与 zenMode 初始状态(此前为 dead config) - 搜索面板补齐 matchCase/wholeWord(翻译键已有但未实现, 中文全字边界感知) - 分隔条 localStorage key 加入实例 id 隔离 - eslint 启用 type-aware 规则(consistent-type-imports / no-unnecessary-type-assertion),0 errors
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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[] = []): void {
|
||||
this._contextMenuItems = items;
|
||||
}
|
||||
|
||||
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 = `<span>${item.label}</span>${item.shortcut ? `<span class="me-context-menu-shortcut">${item.shortcut}</span>` : ''}`;
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user