- 浮动工具栏 mirror 镜像测量:隐藏镜像层复制 textarea 排版样式,选区锚点插零宽字符读像素坐标,CJK/软换行/跨行选区误差全部消除 - 实测尺寸替代魔法数字;滚动 rAF 节流重定位;滚出视口自动隐藏;a11y:role=toolbar + aria-label、visibility 移出 Tab 序列、键盘可达 - 修复死开关/监听器泄漏/销毁后 focus 崩溃/blur 焦点抖动误隐藏 - _afterProgrammaticEdit 统一 8 条程序化路径的渲染/行号/字数/大纲/事件刷新;右键菜单 label 转义、copyAsHTML Firefox 降级、paste 失败提示、Esc 退出全屏 - 测试 871 → 895,e2e 22 → 28 项断言(含真实浏览器浮动工具栏布局验证)
85 lines
5.2 KiB
TypeScript
85 lines
5.2 KiB
TypeScript
/**
|
||
* MetonaEditor — Type-safe, lightweight Markdown Editor
|
||
* @module metona-editor
|
||
* @version 0.4.3
|
||
*/
|
||
|
||
import { MarkdownEditor } from './core';
|
||
import { parseMarkdown, parseTokens, renderTokens, safeUrl, slugify, clearRenderCache, registerBlockHandler } from './parser';
|
||
import { themeUtils, exportCSSVars, getCSSVariable, followExternalTheme, adoptFromParent, createInstanceTheme } from './themes';
|
||
import { i18nUtils, createInstanceI18n, loadRemote } from './i18n';
|
||
import { pluginUtils, presetPlugins, topologicalSort, validateConfig } from './plugins';
|
||
import { animationUtils } from './animations';
|
||
import { highlight, normalizeLanguage, registerLanguage, getSupportedLanguages } from './highlight';
|
||
import { DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS } from './constants';
|
||
import type { EditMode, ThemeName, ToolbarItem, EditorOptions } from './constants';
|
||
|
||
const VERSION = '0.4.3';
|
||
|
||
const globalPlugins: any[] = [];
|
||
|
||
// 全局插件在 afterCreate 安装:此时 DOM 已构建完成,依赖 textarea/el 的插件
|
||
// (searchReplace / shortcutHelp / imagePaste)才能正常生效。
|
||
const injectGlobalPlugins = (editor: MarkdownEditor): void => {
|
||
if (!editor || editor.isDestroyed()) return;
|
||
globalPlugins.forEach((p) => { try { editor.use(p); } catch (e) { console.error('MeEditor global plugin install error:', e); } });
|
||
};
|
||
|
||
MarkdownEditor.on('afterCreate', injectGlobalPlugins);
|
||
|
||
function create(container: string | HTMLElement, options: EditorOptions = {}): MarkdownEditor {
|
||
return new MarkdownEditor(container, options);
|
||
}
|
||
|
||
function use(plugin: string | any, options: any = {}): typeof api {
|
||
let p = plugin;
|
||
if (typeof plugin === 'string') { p = presetPlugins[plugin]; if (!p) { console.warn(`MeEditor: preset plugin "${plugin}" not found`); return api; } }
|
||
if (!p || typeof p !== 'object') { console.warn('MeEditor: invalid plugin'); return api; }
|
||
const merged = (options && typeof options === 'object' && Object.keys(options).length > 0) ? { ...p, ...options } : p;
|
||
globalPlugins.push(merged); return api;
|
||
}
|
||
|
||
function on(name: string, fn: (editor: MarkdownEditor) => void): () => void { return MarkdownEditor.on(name, fn); }
|
||
function off(name: string, fn: (editor: MarkdownEditor) => void): typeof api { MarkdownEditor.off(name, fn); return api; }
|
||
|
||
function setTheme(theme: ThemeName, _options?: any): typeof api { if (themeUtils && typeof themeUtils.switchTheme === 'function') themeUtils.switchTheme(theme); return api; }
|
||
function setLocale(locale: string): typeof api { if (i18nUtils && typeof i18nUtils.setCurrentLocale === 'function') i18nUtils.setCurrentLocale(locale); return api; }
|
||
|
||
function destroy(): void {
|
||
if (themeUtils) { try { themeUtils.clearThemeListeners(); } catch (_) {} try { themeUtils.unwatchSystemTheme(); } catch (_) {} }
|
||
if (i18nUtils) { try { i18nUtils.clearLocaleListeners(); } catch (_) {} }
|
||
if (animationUtils) { try { animationUtils.cancelAll(); } catch (_) {} }
|
||
globalPlugins.length = 0;
|
||
// 清理全局静态钩子(beforeCreate/afterCreate/beforeChange 等),整体复位;
|
||
// 随后重建内部 afterCreate 插件注入钩子,保证 destroy 后 MeEditor.use() 仍可用。
|
||
try { MarkdownEditor._hooks.clear(); } catch (_) {}
|
||
MarkdownEditor.on('afterCreate', injectGlobalPlugins);
|
||
}
|
||
|
||
function getStatus() {
|
||
return { version: VERSION, theme: (themeUtils && typeof themeUtils.getCurrentTheme === 'function') ? themeUtils.getCurrentTheme() : 'auto', locale: (i18nUtils && typeof i18nUtils.getCurrentLocale === 'function') ? i18nUtils.getCurrentLocale() : 'zh-CN', globalPlugins: globalPlugins.map((p) => p.name || 'custom'), presetPlugins: Object.keys(presetPlugins || {}), };
|
||
}
|
||
|
||
try {
|
||
if (typeof themeUtils === 'object' && typeof themeUtils.initTheme === 'function') themeUtils.initTheme();
|
||
if (typeof i18nUtils === 'object' && typeof i18nUtils.initI18n === 'function') i18nUtils.initI18n();
|
||
} catch (_) {}
|
||
|
||
const api = {
|
||
VERSION, version: VERSION,
|
||
MarkdownEditor, Editor: MarkdownEditor,
|
||
create, use, on, off, setTheme, setLocale, destroy, getStatus,
|
||
parseMarkdown, parseTokens, renderTokens, safeUrl, slugify, clearRenderCache, registerBlockHandler,
|
||
highlight, normalizeLanguage, registerLanguage, getSupportedLanguages,
|
||
themes: themeUtils, i18n: i18nUtils, animations: animationUtils, plugins: pluginUtils, presetPlugins,
|
||
topologicalSort, validateConfig, createInstanceI18n, loadRemote,
|
||
exportCSSVars, getCSSVariable, followExternalTheme, adoptFromParent, createInstanceTheme,
|
||
DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS,
|
||
};
|
||
|
||
if (typeof window !== 'undefined') { (window as any).MeEditor = api; }
|
||
|
||
export default api;
|
||
export { api, api as meEditor, api as MeEditor, MarkdownEditor, MarkdownEditor as Editor, create, use, on, off, setTheme, setLocale, destroy, getStatus, parseMarkdown, parseTokens, renderTokens, safeUrl, slugify, clearRenderCache, registerBlockHandler, highlight, normalizeLanguage, registerLanguage, getSupportedLanguages, themeUtils, i18nUtils, pluginUtils, presetPlugins, animationUtils, topologicalSort, validateConfig, createInstanceI18n, loadRemote, exportCSSVars, getCSSVariable, followExternalTheme, adoptFromParent, createInstanceTheme, DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS, VERSION, };
|
||
|