release: v0.1.6 — plugin v2, i18n instance isolation, shortcuts, toolbar, toast

feat(plugins): plugin system v2
- depends: declarative plugin dependencies with topological sort (Kahn algorithm)
- async plugins: install() returning Promise auto-await
- editor.unuse(name): uninstall individual plugins
- pluginUtils.validateConfig(schema, config): configuration validation
- plugin priority field controls install order within same dependency level

feat(i18n): instance-level locale isolation, pluralization, dynamic loading
- createInstanceI18n(): per-editor independent locale contexts
- editor.setLocale()/getLocale()/t() instance methods
- Plural rules: t('items', { count: 5 }) auto-selects one/other/few/many
- i18nUtils.loadRemote(url, locale): fetch translation packs from remote
- editor.on('localeChange') event

feat(core): toolbar & shortcut customization, context menu, toast
- editor.registerShortcut(combo, handler): custom keyboard shortcuts
- editor.configureToolbar(tools): dynamic toolbar rebuild
- editor.removeToolbarButton(action): remove single button
- editor.registerContextMenu(items): right-click context menu
- editor.toast(msg, {type, duration, animation}): toast notifications

style: toast notifications, context menu, dropdown CSS
- .me-toast with success/error/warning/info types, fade/slide animations
- .me-context-menu with items, separators, shortcut hints

test: 22 new tests covering plugin deps, unuse, shortcuts, toast, i18n plural (521 total)

chore: bump version 0.1.5 → 0.1.6 across all files, site, types
This commit is contained in:
2026-07-24 10:17:57 +08:00
parent d41da3c204
commit 45ed8d5351
20 changed files with 1038 additions and 405 deletions
+216 -10
View File
@@ -1,20 +1,29 @@
/**
* MetonaEditor Core - 编辑器核心
* @module core
* @version 0.1.5
* @version 0.1.6
* @description MarkdownEditor 类实现:DOM 构建、事件、渲染、历史栈、选区操作、模式切换
* v0.1.6: 插件依赖拓扑排序、实例 i18n、快捷键/工具栏定制、右键菜单、Toast
*/
import { generateId, escapeHTML, isBrowser } from './utils.js';
import { DEFAULTS, ICONS, THEMES } from './constants.js';
import { injectStyles } from './styles.js';
import { t as i18nT, getCurrentLocale, getLocaleDirection } from './i18n.js';
import { t as i18nT, getCurrentLocale, getLocaleDirection, createInstanceI18n } from './i18n.js';
import { parseMarkdown } from './parser.js';
import { presetPlugins } from './plugins.js';
import { presetPlugins, topologicalSort } from './plugins.js';
import { resolveTheme, getThemeConfig, setThemeVariables, createInstanceTheme } from './themes.js';
const MODES = ['edit', 'split', 'preview'];
/** Toast 图标 */
const TOAST_ICONS = {
success: '✓',
error: '✗',
warning: '⚠',
info: '',
};
/**
* 解析主题名称为实际主题(auto -> light/dark
*/
@@ -89,6 +98,9 @@ class MarkdownEditor {
this._fullscreen = false;
this._destroyed = false;
this._lastRenderedValue = null;
this._shortcuts = []; // v0.1.6: 快捷键注册表
this._contextMenuItems = []; // v0.1.6: 右键菜单项
this._customActions = {}; // 自定义工具栏动作
// 渲染函数:自定义覆盖内置解析器
this._renderFn = (typeof this.config.render === 'function') ? this.config.render : parseMarkdown;
@@ -99,12 +111,14 @@ class MarkdownEditor {
injectStyles();
this._buildDOM();
// 实例级主题管理(v0.1.5: 隔离 CSS 变量到 wrapper,支持独立主题
// 实例级主题管理(v0.1.5
this._themeCtx = createInstanceTheme(this);
// 向后兼容:用户显式传 theme 时同步到全局并持久化
// 实例级 i18n 管理(v0.1.6
this._i18nCtx = createInstanceI18n(this);
// 向后兼容:用户显式传 theme 时同步到全局
if (options.theme) {
// 全局同步(保留原有行为)
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-md-theme', resolveTheme(options.theme));
}
@@ -123,9 +137,14 @@ class MarkdownEditor {
this._render();
this._updateWordCount();
// 安装配置中的插件
// 安装配置中的插件v0.1.6: 拓扑排序)
if (Array.isArray(this.config.plugins)) {
this.config.plugins.forEach((p) => this.use(p));
const plugins = this.config.plugins.map((p) => {
if (typeof p === 'string') return { ...presetPlugins[p], _isStringRef: true };
return p;
}).filter(Boolean);
const sorted = topologicalSort(plugins);
sorted.forEach((p) => this.use(p));
}
if (this.config.autofocus && !this.config.readOnly) this.focus();
@@ -352,6 +371,24 @@ class MarkdownEditor {
return;
}
// v0.1.6: 检查自定义快捷键注册表
if (this._shortcuts.length) {
for (const sc of this._shortcuts) {
const ctrlOk = sc.ctrl ? (e.ctrlKey || e.metaKey) : !e.ctrlKey && !e.metaKey;
const shiftOk = sc.shift ? e.shiftKey : !e.shiftKey;
const altOk = sc.alt ? e.altKey : !e.altKey;
if (e.key.toLowerCase() === sc.key && ctrlOk && shiftOk && altOk) {
e.preventDefault();
if (typeof sc.handler === 'string') {
this.exec(sc.handler);
} else if (typeof sc.handler === 'function') {
try { sc.handler(this); } catch (err) { console.error('Shortcut handler error:', err); }
}
return;
}
}
}
const mod = e.ctrlKey || e.metaKey;
if (!mod) return;
@@ -890,7 +927,7 @@ class MarkdownEditor {
}
/**
* 安装插件
* 安装插件v0.1.6: 支持异步 install
* @param {string|Object} plugin - 预设插件名或插件对象
* @param {Object} options - 插件配置
*/
@@ -910,15 +947,40 @@ class MarkdownEditor {
const merged = { ...p, ...options };
if (typeof merged.install === 'function') {
try {
merged.install(this);
const result = merged.install(this);
// 异步插件支持
if (result && typeof result.then === 'function') {
result.catch((e) => {
console.error(`MeEditor: async plugin "${merged.name || 'custom'}" install error:`, e);
});
}
} catch (e) {
console.error(`MeEditor: plugin "${merged.name || 'custom'}" install error:`, e);
}
}
// 直接 push 原对象(保持运行时属性引用一致)
this._plugins.push(merged);
return this;
}
/**
* 卸载插件(v0.1.6 新增)
* @param {string} name - 插件名
*/
unuse(name) {
const idx = this._plugins.findIndex((p) => p.name === name);
if (idx === -1) {
console.warn(`MeEditor: plugin "${name}" not found`);
return this;
}
const plugin = this._plugins[idx];
if (typeof plugin.destroy === 'function') {
try { plugin.destroy(this); } catch (e) { console.error(`MeEditor: plugin "${name}" destroy error:`, e); }
}
this._plugins.splice(idx, 1);
return this;
}
getPlugins() { return [...this._plugins]; }
/**
@@ -944,6 +1006,142 @@ class MarkdownEditor {
return this;
}
// ============ v0.1.6: 快捷键注册 ============
/**
* 注册自定义快捷键
* @param {string} combo - 组合键如 'Ctrl+B' / 'Ctrl+Shift+K' / 'Alt+1'
* @param {Function|string} handler - 回调函数或 exec action 名
* @param {string} [description] - 描述
*/
registerShortcut(combo, handler, description = '') {
if (!combo) return this;
// 解析组合键
const parts = combo.toLowerCase().split('+');
const key = parts.pop();
const ctrl = parts.includes('ctrl') || parts.includes('cmd');
const shift = parts.includes('shift');
const alt = parts.includes('alt');
const meta = parts.includes('meta');
this._shortcuts.push({ combo, key, ctrl, shift, alt, meta, handler, description });
return this;
}
/**
* 注销快捷键
* @param {string} combo - 组合键字符串
*/
unregisterShortcut(combo) {
this._shortcuts = this._shortcuts.filter((s) => s.combo !== combo);
return this;
}
/**
* 获取已注册的快捷键列表
*/
getShortcuts() {
return [...this._shortcuts];
}
// ============ v0.1.6: 工具栏动态配置 ============
/**
* 动态重建工具栏按钮
* @param {Array} tools - 按钮数组(同 config.toolbar 格式)
*/
configureToolbar(tools) {
if (!this.toolbarEl) return this;
this.config.toolbar = tools;
this.toolbarEl.innerHTML = '';
this._buildToolbar();
return this;
}
/**
* 从工具栏移除指定按钮
* @param {string} action - 按钮 action 名
*/
removeToolbarButton(action) {
if (!this.toolbarEl) return this;
const btn = this.toolbarEl.querySelector(`.me-btn[data-action="${action}"], .me-btn[data-mode="${action}"]`);
if (btn) btn.remove();
return this;
}
// ============ v0.1.6: 右键菜单 ============
/**
* 注册右键上下文菜单
* @param {Array} items - [{ label, action, shortcut?, type?:'separator' }]
*/
registerContextMenu(items = []) {
this._contextMenuItems = items;
return this;
}
// ============ v0.1.6: Toast 通知 ============
/**
* 显示 Toast 通知
* @param {string} message - 消息内容
* @param {Object} [opts]
* @param {string} [opts.type='info'] - success / error / warning / info
* @param {number} [opts.duration=3000] - 自动消失时间(ms),0 不自动消失
* @param {string} [opts.animation='fade'] - 动画类型
*/
toast(message, opts = {}) {
if (!this.el || typeof document === 'undefined') return this;
const { type = 'info', duration = 3000, animation = 'fade' } = opts;
const toast = document.createElement('div');
toast.className = `me-toast me-toast-${type} me-toast-${animation}`;
toast.innerHTML = `<span class="me-toast-icon">${TOAST_ICONS[type] || ''}</span><span class="me-toast-msg">${escapeHTML(String(message))}</span>`;
this.el.appendChild(toast);
// 入场动画
requestAnimationFrame(() => { toast.classList.add('me-toast-enter'); });
const remove = () => {
toast.classList.remove('me-toast-enter');
toast.classList.add('me-toast-leave');
setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
};
if (duration > 0) setTimeout(remove, duration);
// 点击关闭
toast.addEventListener('click', remove);
return this;
}
// ============ v0.1.6: 实例级 i18n ============
/**
* 设置实例语言
*/
setLocale(locale) {
if (this._i18nCtx) this._i18nCtx.set(locale);
return this;
}
/**
* 获取实例当前语言
*/
getLocale() {
if (this._i18nCtx) return this._i18nCtx.get();
return getCurrentLocale();
}
/**
* 实例级翻译函数
*/
t(key, params) {
if (this._i18nCtx) return this._i18nCtx.t(key, params);
return i18nT(key, params);
}
/**
* 销毁编辑器
*/
@@ -956,6 +1154,14 @@ class MarkdownEditor {
this._cleanups.forEach((fn) => { try { fn(); } catch (_) {} });
this._cleanups = [];
this._shortcuts = [];
this._contextMenuItems = [];
this._customActions = {};
// 移除所有 toast
if (this.el) {
this.el.querySelectorAll('.me-toast').forEach((t) => t.remove());
}
// 销毁插件
this._plugins.forEach((p) => {