/** * MetonaEditor Core - 编辑器核心 * @module core * @version 0.1.12 * @description MarkdownEditor 类实现:DOM 构建、事件、渲染、历史栈、选区操作、模式切换 * v0.1.6: 插件依赖拓扑排序、实例 i18n、快捷键/工具栏定制、右键菜单、Toast * v0.1.7: 行号装订线、自动格式化、括号自动闭合、拖放、大纲面板 * v0.1.12: 工具栏键盘导航、aria-live、Zen模式、滚动同步v2、WordWrap */ 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, createInstanceI18n } from './i18n.js'; import { parseMarkdown } from './parser.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: 'ℹ', }; /** 括号/引号自动闭合映射 */ const BRACKET_PAIRS = { '(': ')', '[': ']', '{': '}', '"': '"', "'": "'", '`': '`', '*': '*', '_': '_', }; /** * 解析主题名称为实际主题(auto -> light/dark) */ const resolveThemeName = (theme) => { if (theme && theme !== 'auto') return theme; return resolveTheme('auto'); }; /** * MarkdownEditor 编辑器类 */ class MarkdownEditor { /** * 静态事件钩子(全局,所有实例共享,供插件使用) */ static _hooks = new Map(); static on(name, fn) { if (!this._hooks.has(name)) this._hooks.set(name, []); this._hooks.get(name).push(fn); return () => this.off(name, fn); } static off(name, fn) { const list = this._hooks.get(name); if (list) this._hooks.set(name, list.filter((f) => f !== fn)); } static trigger(name, instance) { const list = this._hooks.get(name); if (list) list.forEach((fn) => { try { fn(instance); } catch (e) { console.error(`MeEditor hook "${name}" error:`, e); } }); } /** * @param {string|HTMLElement} container - 容器选择器或元素 * @param {Object} options - 配置项(见 DEFAULTS) */ constructor(container, options = {}) { if (!isBrowser()) { this._destroyed = true; this._value = options.value || ''; return this; } this.id = options.id || generateId(); this.container = typeof container === 'string' ? document.querySelector(container) : container; if (!this.container) { console.error('MeEditor: container not found:', container); this._destroyed = true; return this; } this.config = { ...DEFAULTS, ...options }; if (options.style && typeof options.style === 'object') { this.config.style = { ...DEFAULTS.style, ...options.style }; } this._value = this.config.value || ''; this._mode = MODES.includes(this.config.mode) ? this.config.mode : 'split'; this._history = []; this._historyIndex = -1; this._cleanups = []; this._plugins = []; this._listeners = {}; this._renderRaf = null; this._historyTimer = null; this._fullscreen = false; this._destroyed = false; this._lastRenderedValue = null; this._shortcuts = []; // v0.1.6: 快捷键注册表 this._contextMenuItems = []; // v0.1.6: 右键菜单项 this._customActions = {}; // 自定义工具栏动作 this._outlineTimer = null; // 大纲防抖计时器 this._zenMode = false; // v0.1.12: Zen 模式 this._wordWrap = true; // v0.1.12: 自动换行 // 渲染函数:自定义覆盖内置解析器 this._renderFn = (typeof this.config.render === 'function') ? this.config.render : parseMarkdown; this._highlightFn = (typeof this.config.highlight === 'function') ? this.config.highlight : null; MarkdownEditor.trigger('beforeCreate', this); injectStyles(); this._buildDOM(); // 实例级主题管理(v0.1.5) this._themeCtx = createInstanceTheme(this); // 实例级 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)); } } // 确保 CSS 变量作用到实例 wrapper const resolvedThemeConfig = getThemeConfig(this.config.theme); setThemeVariables(resolvedThemeConfig, this.el); this._buildToolbar(); this._updateModeButtons(); // 初始化按钮状态 this._bindToolbarKeyboard(); // v0.1.12: 键盘导航 this._initAriaLive(); // v0.1.12: 屏幕阅读器 this._bindEvents(); // 初始内容 this.textarea.value = this._value; this._pushHistory(); this._render(); this._updateWordCount(); // 安装配置中的插件(v0.1.6: 拓扑排序) if (Array.isArray(this.config.plugins)) { 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(); if (typeof this.config.onCreate === 'function') { try { this.config.onCreate(this); } catch (e) { console.error('onCreate error:', e); } } MarkdownEditor.trigger('afterCreate', this); } // ============ DOM 构建 ============ _buildDOM() { const wrapper = document.createElement('div'); wrapper.className = `me-wrapper me-theme-${resolveThemeName(this.config.theme)}`; if (this.config.readOnly) wrapper.classList.add('me-readonly'); wrapper.dataset.id = this.id; wrapper.setAttribute('role', 'application'); wrapper.setAttribute('aria-label', i18nT('edit')); if (this.config.className) { this.config.className.split(/\s+/).filter(Boolean).forEach((c) => wrapper.classList.add(c)); } // 高度 const h = this.config.height; wrapper.style.height = typeof h === 'number' ? `${h}px` : (h || '400px'); // 自定义样式 if (this.config.style && typeof this.config.style === 'object') { Object.entries(this.config.style).forEach(([k, v]) => { wrapper.style[k] = v; }); } const toolbar = document.createElement('div'); toolbar.className = 'me-toolbar'; toolbar.setAttribute('role', 'toolbar'); const body = document.createElement('div'); body.className = `me-body me-mode-${this._mode}`; const editorPane = document.createElement('div'); editorPane.className = 'me-editor-pane'; // 编辑区内层容器(行号 + textarea 水平排列) const editorInner = document.createElement('div'); editorInner.className = 'me-editor-inner'; // 行号装订线(v0.1.7) const gutter = document.createElement('div'); gutter.className = 'me-gutter'; if (!this.config.lineNumbers) gutter.style.display = 'none'; editorInner.appendChild(gutter); const textarea = document.createElement('textarea'); textarea.className = 'me-textarea'; textarea.spellcheck = !!this.config.spellcheck; textarea.readOnly = !!this.config.readOnly; textarea.style.tabSize = String(this.config.tabSize || 2); textarea.placeholder = this.config.placeholder || i18nT('placeholder'); textarea.setAttribute('aria-label', i18nT('edit')); editorInner.appendChild(textarea); editorPane.appendChild(editorInner); const divider = document.createElement('div'); divider.className = 'me-divider'; divider.setAttribute('role', 'separator'); const previewPane = document.createElement('div'); previewPane.className = 'me-preview-pane'; previewPane.setAttribute('role', 'region'); previewPane.setAttribute('aria-label', i18nT('preview')); const preview = document.createElement('div'); preview.className = 'me-preview'; previewPane.appendChild(preview); body.appendChild(editorPane); body.appendChild(divider); body.appendChild(previewPane); wrapper.appendChild(toolbar); wrapper.appendChild(body); // 状态栏 let statusbar = null; if (this.config.wordCount) { statusbar = document.createElement('div'); statusbar.className = 'me-statusbar'; wrapper.appendChild(statusbar); } this.container.appendChild(wrapper); this.el = wrapper; this.toolbarEl = toolbar; this.bodyEl = body; this.editorPane = editorPane; this.editorInner = editorInner; this.gutter = gutter; this.previewPane = previewPane; this.dividerEl = divider; this.textarea = textarea; this.previewEl = preview; this.statusEl = statusbar; } _buildToolbar() { const tools = this.config.toolbar; if (!tools || !Array.isArray(tools) || tools.length === 0) return; const modeActions = []; tools.forEach((item) => { if (item === '|') { const sep = document.createElement('span'); sep.className = 'me-toolbar-sep'; this.toolbarEl.appendChild(sep); return; } // 模式与全屏按钮归入右侧组 if (item === 'edit' || item === 'split' || item === 'preview' || item === 'fullscreen') { modeActions.push(item); return; } this.toolbarEl.appendChild(this._createBtn(item)); }); if (modeActions.length) { const group = document.createElement('div'); group.className = 'me-toolbar-group'; modeActions.forEach((item) => { const btn = this._createBtn(item); if (item === 'edit' || item === 'split' || item === 'preview') { btn.dataset.mode = item; if (item === this._mode) btn.classList.add('me-active'); } group.appendChild(btn); }); this.toolbarEl.appendChild(group); } } _createBtn(item) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = `me-btn me-btn-${item}`; btn.dataset.action = item; const label = i18nT(item) || item; btn.title = label; btn.setAttribute('aria-label', label); btn.innerHTML = ICONS[item] || `${escapeHTML(item)}`; return btn; } // ============ 事件绑定 ============ _bindEvents() { const ta = this.textarea; const onInput = () => { this._value = ta.value; this._scheduleRender(); this._scheduleHistory(); this._updateWordCount(); this._renderGutter(); this._updateOutline(); this._emit('input', this._value); this._emit('change', this._value); if (typeof this.config.onInput === 'function') { try { this.config.onInput(this._value, this); } catch (e) { console.error(e); } } if (typeof this.config.onChange === 'function') { try { this.config.onChange(this._value, this); } catch (e) { console.error(e); } } }; ta.addEventListener('input', onInput); const onKeydown = (e) => { // v0.1.7: 智能 Enter if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) { this._handleSmartEnter(e); } this._handleKeydown(e); }; ta.addEventListener('keydown', onKeydown); // v0.1.7: 括号/引号自动闭合 const onKeypress = (e) => this._handleBracketAutoClose(e); ta.addEventListener('keypress', onKeypress); // v0.1.7: 当前行高亮 const onCursorActivity = () => { this._updateCurrentLine(); }; ta.addEventListener('click', onCursorActivity); ta.addEventListener('keyup', onCursorActivity); const onScroll = () => { if (this.gutter) this.gutter.scrollTop = ta.scrollTop; // v0.1.12: 标题感知同步 if (this.config.syncScroll && this._mode === 'split') { this._syncScrollByHeading(); } }; ta.addEventListener('scroll', onScroll); const onFocus = () => { this._emit('focus'); if (typeof this.config.onFocus === 'function') { try { this.config.onFocus(this); } catch (e) { console.error(e); } } }; const onBlur = () => { this._emit('blur'); if (typeof this.config.onBlur === 'function') { try { this.config.onBlur(this); } catch (e) { console.error(e); } } }; ta.addEventListener('focus', onFocus); ta.addEventListener('blur', onBlur); // 工具栏点击 const onToolbarClick = (e) => { const btn = e.target.closest('.me-btn'); if (!btn) return; const mode = btn.dataset.mode; if (mode) { this.setMode(mode); return; } const action = btn.dataset.action; if (action) this.exec(action); }; this.toolbarEl.addEventListener('click', onToolbarClick); // 分隔条拖拽(分屏模式) const onDividerDown = (e) => this._bindDividerDrag(e); this.dividerEl.addEventListener('pointerdown', onDividerDown); // v0.1.7: 拖放支持 this._bindDragDrop(); this._cleanups.push(() => { ta.removeEventListener('input', onInput); ta.removeEventListener('keydown', onKeydown); ta.removeEventListener('keypress', onKeypress); ta.removeEventListener('scroll', onScroll); ta.removeEventListener('click', onCursorActivity); ta.removeEventListener('keyup', onCursorActivity); ta.removeEventListener('focus', onFocus); ta.removeEventListener('blur', onBlur); this.toolbarEl.removeEventListener('click', onToolbarClick); this.dividerEl.removeEventListener('pointerdown', onDividerDown); }); } _handleKeydown(e) { // Tab 缩进 if (e.key === 'Tab') { e.preventDefault(); this._handleTab(e.shiftKey); 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; const key = e.key.toLowerCase(); const shortcuts = { b: 'bold', i: 'italic', k: 'link', u: 'underline', e: 'code', '1': 'h1', '2': 'h2', '3': 'h3', q: 'quote', }; if (key === 'z' && !e.shiftKey) { e.preventDefault(); this.undo(); } else if ((key === 'z' && e.shiftKey) || key === 'y') { e.preventDefault(); this.redo(); } else if (key === 's') { e.preventDefault(); this._emit('save', this._value); if (typeof this.config.onSave === 'function') { try { this.config.onSave(this._value, this); } catch (err) { console.error(err); } } } else if (shortcuts[key]) { e.preventDefault(); this.exec(shortcuts[key]); } } _handleTab(shift) { const ta = this.textarea; const start = ta.selectionStart; const end = ta.selectionEnd; const tabSize = this.config.tabSize || 0; const tabStr = tabSize > 0 ? ' '.repeat(tabSize) : '\t'; if (start === end) { // 无选区:插入缩进 ta.value = ta.value.slice(0, start) + tabStr + ta.value.slice(end); ta.selectionStart = ta.selectionEnd = start + tabStr.length; } else { // 有选区:整块缩进/反缩进 const lineStart = ta.value.lastIndexOf('\n', start - 1) + 1; const block = ta.value.slice(lineStart, end); const lines = block.split('\n'); let newBlock; if (shift) { newBlock = lines.map((l) => l.replace(/^(\t| {1,4})/, '')).join('\n'); } else { newBlock = lines.map((l) => tabStr + l).join('\n'); } ta.value = ta.value.slice(0, lineStart) + newBlock + ta.value.slice(end); ta.selectionStart = lineStart; ta.selectionEnd = lineStart + newBlock.length; } this._value = ta.value; this._pushHistory(); this._render(); this._emit('change', this._value); } _bindDividerDrag(e) { if (this._mode !== 'split') return; e.preventDefault(); const startX = e.clientX; const editorWidth = this.editorPane.getBoundingClientRect().width; const totalWidth = this.bodyEl.getBoundingClientRect().width; if (totalWidth <= 0) return; const divider = this.dividerEl; const onMove = (ev) => { const dx = ev.clientX - startX; let newEditorWidth = editorWidth + dx; const min = 80; const max = totalWidth - 80 - divider.offsetWidth; newEditorWidth = Math.max(min, Math.min(max, newEditorWidth)); const pct = (newEditorWidth / totalWidth) * 100; this.editorPane.style.flex = `0 0 ${pct}%`; this.previewPane.style.flex = `1 1 ${100 - pct}%`; }; const onUp = () => { window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerup', onUp); }; window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onUp); } // ============ 渲染 ============ _scheduleRender() { if (this._renderRaf) return; this._renderRaf = requestAnimationFrame(() => { this._renderRaf = null; this._render(); }); } _render() { if (this._mode === 'edit') return; // 内容未变化时跳过解析(主题切换等场景触发重渲染但内容不变) if (this._lastRenderedValue === this._value) return; this._lastRenderedValue = this._value; MarkdownEditor.trigger('beforeRender', this); this._emit('beforeRender', this); const env = { highlight: this._highlightFn, locale: getCurrentLocale(), }; let html; try { html = this._renderFn(this._value, env); } catch (err) { console.error('MeEditor render error:', err); html = `

${i18nT('renderError')}: ${escapeHTML(err.message)}

`; } if (typeof this.config.sanitize === 'function') { try { html = this.config.sanitize(html); } catch (err) { console.error('sanitize error:', err); } } this.previewEl.innerHTML = html; this._renderGutter(); MarkdownEditor.trigger('afterRender', this); this._emit('afterRender', this); } // ============ 行号渲染(v0.1.7) ============ _renderGutter() { if (!this.config.lineNumbers || !this.gutter) return; const lines = this._value ? this._value.split('\n').length : 1; const cur = this.gutter.children.length; if (cur === lines) return; // 行数未变,跳过 let html = ''; for (let i = 1; i <= lines; i++) { html += `
${i}
`; } this.gutter.innerHTML = html; } _updateCurrentLine() { if (!this.gutter) return; const pos = this.textarea.selectionStart; const lineNum = this._value.substring(0, pos).split('\n').length; const prev = this.gutter.querySelector('.me-gutter-active'); if (prev) prev.classList.remove('me-gutter-active'); const cur = this.gutter.children[lineNum - 1]; if (cur) cur.classList.add('me-gutter-active'); } // ============ 自动格式化(v0.1.7) ============ /** * 智能 Enter:在列表/引用行尾自动延续标记 */ _handleSmartEnter(e) { const ta = this.textarea; const start = ta.selectionStart; const val = ta.value; const lineStart = val.lastIndexOf('\n', start - 1) + 1; const line = val.slice(lineStart, start); // 列表项延续:匹配 "- " / "* " / "+ " / "1. " / "2. " 等 const listMatch = line.match(/^(\s*)([-*+]|\d+\.)\s(.*)/); if (listMatch) { const indent = listMatch[1]; const marker = listMatch[2]; const content = listMatch[3]; // 空列表项 → 结束列表(移除标记) if (!content.trim()) { e.preventDefault(); ta.value = val.slice(0, lineStart) + '\n' + val.slice(start); ta.selectionStart = ta.selectionEnd = lineStart; this._value = ta.value; this._pushHistory(); this._renderGutter(); this._emit('change', this._value); return; } // 有序列表自动递增 let nextMarker = marker; if (/^\d+\.$/.test(marker)) { const num = parseInt(marker, 10); if (!isNaN(num)) nextMarker = (num + 1) + '.'; } e.preventDefault(); const insert = '\n' + indent + nextMarker + ' '; ta.value = val.slice(0, start) + insert + val.slice(ta.selectionEnd); ta.selectionStart = ta.selectionEnd = start + insert.length; this._value = ta.value; this._pushHistory(); this._renderGutter(); this._emit('change', this._value); return; } // 引用块延续 const quoteMatch = line.match(/^(\s*>+\s?)(.*)/); if (quoteMatch) { const prefix = quoteMatch[1]; const content = quoteMatch[2]; if (!content.trim()) { e.preventDefault(); ta.value = val.slice(0, lineStart) + '\n' + val.slice(start); ta.selectionStart = ta.selectionEnd = lineStart; this._value = ta.value; this._pushHistory(); this._renderGutter(); this._emit('change', this._value); return; } e.preventDefault(); const insert = '\n' + prefix; ta.value = val.slice(0, start) + insert + val.slice(ta.selectionEnd); ta.selectionStart = ta.selectionEnd = start + insert.length; this._value = ta.value; this._pushHistory(); this._renderGutter(); this._emit('change', this._value); return; } } // ============ 括号/引号自动闭合(v0.1.7) ============ _handleBracketAutoClose(e) { if (!this.config.autoBrackets) return; const ta = this.textarea; const key = e.key; const close = BRACKET_PAIRS[key]; if (!close) return; const start = ta.selectionStart; const end = ta.selectionEnd; const hasSelection = start !== end; // 有选区:包裹选中文本 if (hasSelection) { e.preventDefault(); const selected = ta.value.slice(start, end); const wrap = key === '*' || key === '_' ? key + selected + close : key + selected + close; ta.value = ta.value.slice(0, start) + wrap + ta.value.slice(end); ta.selectionStart = start + 1; ta.selectionEnd = start + 1 + selected.length; this._value = ta.value; this._pushHistory(); this._emit('change', this._value); return; } // 无选区:判断是否应插入闭合对 const nextChar = ta.value[start] || ''; const isQuotePair = key === close; // 引号自己闭合自己 if (isQuotePair) { // 引号:下一个字符是同种引号 → 跳过而非插入 if (nextChar === close) { e.preventDefault(); ta.selectionStart = ta.selectionEnd = start + 1; return; } // 光标在单词中间 → 不自动闭合 if (/\w/.test(nextChar)) return; } // 括号:下一个字符非空白/换行/闭合括号 → 可能不需要闭合 // 简单策略:始终闭合 e.preventDefault(); const pair = key + close; ta.value = ta.value.slice(0, start) + pair + ta.value.slice(end); ta.selectionStart = ta.selectionEnd = start + 1; this._value = ta.value; this._pushHistory(); this._emit('change', this._value); } // ============ 拖放支持(v0.1.7) ============ _bindDragDrop() { const ta = this.textarea; if (!ta) return; const onDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }; const onDrop = (e) => { e.preventDefault(); const files = e.dataTransfer.files; if (files && files.length) { Array.from(files).forEach((file) => { if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = () => { const name = file.name || `image-${Date.now().toString(36)}.png`; this.insert(`![${name}](${reader.result})\n`); }; reader.readAsDataURL(file); } else if (file.type.startsWith('text/') || file.name.match(/\.(md|txt|js|ts|json|css|html|xml|yml|yaml)$/i)) { const reader = new FileReader(); reader.onload = () => this.insert(reader.result); reader.readAsText(file); } }); return; } // 拖入外部文本 const text = e.dataTransfer.getData('text/plain'); if (text) { this.insert(text); } }; ta.addEventListener('dragover', onDragOver); ta.addEventListener('drop', onDrop); this._cleanups.push(() => { ta.removeEventListener('dragover', onDragOver); ta.removeEventListener('drop', onDrop); }); } // ============ 大纲面板(v0.1.7) ============ _buildOutline() { if (!this.config.outline || !this.previewEl) return; // 移除旧面板 const old = this.el.querySelector('.me-outline'); if (old) old.remove(); const headings = []; const headingRe = /]*>(.+?)<\/h\1>/gi; const html = this.previewEl.innerHTML; let m; while ((m = headingRe.exec(html)) !== null) { headings.push({ level: parseInt(m[1], 10), id: m[2], text: m[3].replace(/<[^>]+>/g, '') }); } if (!headings.length) return; const panel = document.createElement('div'); panel.className = 'me-outline'; panel.innerHTML = '
' + (i18nT('outline') || '大纲') + '
'; const buildTree = (items, minLevel) => { let h = ''; return h; }; panel.innerHTML += buildTree(headings, 1); this.el.appendChild(panel); // 点击跳转 panel.addEventListener('click', (e) => { const a = e.target.closest('a'); if (!a) return; e.preventDefault(); const id = a.getAttribute('href').slice(1); const target = this.previewEl.querySelector('#' + CSS.escape(id)); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); // 同步滚动 textarea const headingText = target.textContent; const idx = this._value.indexOf(headingText); if (idx !== -1) { this.textarea.focus(); this.textarea.setSelectionRange(idx, idx); const lineNum = this._value.substring(0, idx).split('\n').length - 1; this.textarea.scrollTop = lineNum * 20; } } }); } _updateOutline() { if (!this.config.outline) return; // 防抖重建 clearTimeout(this._outlineTimer); this._outlineTimer = setTimeout(() => this._buildOutline(), 300); } _scheduleHistory() { if (this._historyTimer) clearTimeout(this._historyTimer); this._historyTimer = setTimeout(() => this._pushHistory(), this.config.historyDebounce || 400); } _pushHistory() { const cur = this._history[this._historyIndex]; if (cur === this._value) return; this._history = this._history.slice(0, this._historyIndex + 1); this._history.push(this._value); const limit = this.config.historyLimit > 0 ? this.config.historyLimit : 100; while (this._history.length > limit) this._history.shift(); this._historyIndex = this._history.length - 1; } undo() { if (this._historyIndex <= 0) return this; this._historyIndex--; this._applyHistory(); return this; } redo() { if (this._historyIndex >= this._history.length - 1) return this; this._historyIndex++; this._applyHistory(); return this; } _applyHistory() { this._value = this._history[this._historyIndex]; this.textarea.value = this._value; this._render(); this._renderGutter(); this._updateWordCount(); // 同步行号滚动位置 if (this.gutter) this.gutter.scrollTop = this.textarea.scrollTop; this._emit('change', this._value); if (typeof this.config.onChange === 'function') { try { this.config.onChange(this._value, this); } catch (e) { console.error(e); } } } canUndo() { return this._historyIndex > 0; } canRedo() { return this._historyIndex < this._history.length - 1; } // ============ 命令执行 ============ exec(action, ...args) { const actions = { bold: () => this._wrapSelection('**', '**'), italic: () => this._wrapSelection('*', '*'), strikethrough: () => this._wrapSelection('~~', '~~'), underline: () => this._wrapSelection('', ''), code: () => this._wrapSelection('`', '`'), h1: () => this._toggleLinePrefix('# '), h2: () => this._toggleLinePrefix('## '), h3: () => this._toggleLinePrefix('### '), quote: () => this._toggleLinePrefix('> '), ul: () => this._toggleLinePrefix('- '), ol: () => this._toggleLinePrefix('1. '), indent: () => this._handleTab(false), outdent: () => this._handleTab(true), hr: () => this._insertBlock('\n---\n'), link: () => this._insertLink(), image: () => this._insertImage(), table: () => this._insertTable(), undo: () => this.undo(), redo: () => this.redo(), edit: () => this.setMode('edit'), split: () => this.setMode('split'), preview: () => this.setMode('preview'), fullscreen: () => this.toggleFullscreen(), zen: () => this.toggleZen(), // v0.1.12 wordwrap: () => this.toggleWordWrap(), // v0.1.12 }; const fn = actions[action]; if (fn) { fn.apply(this, args); return this; } // 检查自定义动作(通过 addToolbarButton 注册) if (this._customActions && typeof this._customActions[action] === 'function') { try { this._customActions[action].apply(this, args); } catch (e) { console.error('custom action error:', e); } } return this; } _wrapSelection(before, after) { const ta = this.textarea; const start = ta.selectionStart; const end = ta.selectionEnd; const selected = ta.value.slice(start, end); const text = selected || 'text'; const inserted = before + text + after; ta.value = ta.value.slice(0, start) + inserted + ta.value.slice(end); ta.focus(); if (selected) { ta.selectionStart = start + before.length; ta.selectionEnd = start + before.length + text.length; } else { ta.selectionStart = ta.selectionEnd = start + before.length; } this._value = ta.value; this._pushHistory(); this._render(); this._updateWordCount(); this._emit('change', this._value); } _toggleLinePrefix(prefix) { const ta = this.textarea; const start = ta.selectionStart; const lineStart = ta.value.lastIndexOf('\n', start - 1) + 1; const lineEndPos = ta.value.indexOf('\n', start); const lineEnd = lineEndPos === -1 ? ta.value.length : lineEndPos; const line = ta.value.slice(lineStart, lineEnd); const existingMatch = line.match(/^(#{1,6}\s*|>\s*|[-*+]\s*|\d+\.\s*)/); let newLine; if (existingMatch && existingMatch[0] === prefix) { newLine = line.slice(prefix.length); } else if (existingMatch) { newLine = prefix + line.slice(existingMatch[0].length); } else { newLine = prefix + line; } ta.value = ta.value.slice(0, lineStart) + newLine + ta.value.slice(lineEnd); ta.focus(); ta.selectionStart = ta.selectionEnd = lineStart + newLine.length; this._value = ta.value; this._pushHistory(); this._render(); this._updateWordCount(); this._emit('change', this._value); } _insertBlock(text) { const ta = this.textarea; const start = ta.selectionStart; const before = ta.value.slice(0, start); const needNL = before && !before.endsWith('\n'); const insert = (needNL ? '\n' : '') + text; ta.value = ta.value.slice(0, start) + insert + ta.value.slice(ta.selectionEnd); ta.focus(); const pos = start + insert.length; ta.selectionStart = ta.selectionEnd = pos; this._value = ta.value; this._pushHistory(); this._render(); this._updateWordCount(); this._emit('change', this._value); } _insertLink() { const ta = this.textarea; const start = ta.selectionStart; const end = ta.selectionEnd; const sel = ta.value.slice(start, end) || i18nT('link') || 'link'; const url = 'https://'; const insert = `[${sel}](${url})`; ta.value = ta.value.slice(0, start) + insert + ta.value.slice(end); ta.focus(); // 选中 url 便于替换 const urlStart = start + sel.length + 3; ta.selectionStart = urlStart; ta.selectionEnd = urlStart + url.length; this._value = ta.value; this._pushHistory(); this._render(); this._emit('change', this._value); } _insertImage() { const ta = this.textarea; const start = ta.selectionStart; const end = ta.selectionEnd; const sel = ta.value.slice(start, end) || i18nT('image') || 'image'; const url = 'https://'; const insert = `![${sel}](${url})`; ta.value = ta.value.slice(0, start) + insert + ta.value.slice(end); ta.focus(); const urlStart = start + sel.length + 4; ta.selectionStart = urlStart; ta.selectionEnd = urlStart + url.length; this._value = ta.value; this._pushHistory(); this._render(); this._emit('change', this._value); } _insertTable(rows = 3, cols = 3) { const header = Array.from({ length: cols }, (_, i) => `${i18nT('tableCols') || '列'}${i + 1}`).join(' | '); const sep = Array.from({ length: cols }, () => '---').join(' | '); let md = `| ${header} |\n| ${sep} |\n`; for (let r = 1; r < rows; r++) { const row = Array.from({ length: cols }, () => ' ').join(' | '); md += `| ${row} |\n`; } this._insertBlock('\n' + md); } // ============ 模式与全屏 ============ setMode(mode) { if (!MODES.includes(mode) || mode === this._mode) return this; // 保存当前滚动位置 const taScroll = this.textarea ? this.textarea.scrollTop : 0; const pvScroll = this.previewPane ? this.previewPane.scrollTop : 0; this._mode = mode; this.bodyEl.className = `me-body me-mode-${mode}`; this._updateModeButtons(); if (mode !== 'edit') this._render(); // 恢复滚动位置 if (this.textarea) this.textarea.scrollTop = taScroll; if (this.previewPane) this.previewPane.scrollTop = pvScroll; this._emit('modeChange', mode); this._announce(`${i18nT(mode) || mode} ${i18nT('mode') || 'mode'}`); // v0.1.12 if (typeof this.config.onModeChange === 'function') { try { this.config.onModeChange(mode, this); } catch (e) { console.error(e); } } return this; } getMode() { return this._mode; } _updateModeButtons() { const isPreview = this._mode === 'preview'; const isReadonly = this.config.readOnly; this.toolbarEl.querySelectorAll('.me-btn').forEach((b) => { const mode = b.dataset.mode; const action = b.dataset.action; // 模式切换按钮始终可用,高亮当前模式 if (mode) { b.classList.toggle('me-active', mode === this._mode); b.disabled = false; return; } // fullscreen 始终可用 if (action === 'fullscreen') { b.disabled = false; return; } // readonly 模式:禁用编辑操作按钮,保留 undo/redo if (isReadonly) { b.disabled = !(action === 'undo' || action === 'redo'); return; } // preview 模式:禁用编辑操作按钮 if (isPreview) { b.disabled = !(action === 'undo' || action === 'redo'); return; } b.disabled = false; }); } toggleFullscreen() { this._fullscreen = !this._fullscreen; this.el.classList.toggle('me-fullscreen', this._fullscreen); // 更新全屏按钮图标/标题 const fsBtn = this.toolbarEl.querySelector('.me-btn-fullscreen'); if (fsBtn) { fsBtn.title = this._fullscreen ? (i18nT('fullscreenExit') || '退出全屏') : (i18nT('fullscreen') || '全屏'); } this._emit('fullscreen', this._fullscreen); if (typeof this.config.onFullscreen === 'function') { try { this.config.onFullscreen(this._fullscreen, this); } catch (e) { console.error(e); } } return this; } isFullscreen() { return this._fullscreen; } exitFullscreen() { if (this._fullscreen) this.toggleFullscreen(); return this; } // ============ Zen 模式(v0.1.12) ============ toggleZen() { this._zenMode = !this._zenMode; if (this.el) this.el.classList.toggle('me-zen', this._zenMode); // 鼠标移到顶部时显示工具栏 if (this._zenMode && this.toolbarEl) { this._zenMouseHandler = (e) => { this.toolbarEl.style.opacity = e.clientY < 40 ? '1' : '0'; this.toolbarEl.style.pointerEvents = e.clientY < 40 ? 'auto' : 'none'; }; this.toolbarEl.style.transition = 'opacity 0.2s'; this.toolbarEl.style.opacity = '0'; this.toolbarEl.style.pointerEvents = 'none'; document.addEventListener('mousemove', this._zenMouseHandler); // 隐藏状态栏 if (this.statusEl) this.statusEl.style.display = 'none'; } else { if (this._zenMouseHandler) { document.removeEventListener('mousemove', this._zenMouseHandler); this._zenMouseHandler = null; } if (this.toolbarEl) { this.toolbarEl.style.opacity = ''; this.toolbarEl.style.pointerEvents = ''; this.toolbarEl.style.transition = ''; } if (this.statusEl) this.statusEl.style.display = ''; } this._emit('zenChange', this._zenMode); return this; } isZen() { return this._zenMode; } // ============ Word Wrap(v0.1.12) ============ toggleWordWrap() { this._wordWrap = !this._wordWrap; if (this.textarea) { this.textarea.style.whiteSpace = this._wordWrap ? 'pre-wrap' : 'pre'; } return this; } setWordWrap(on) { this._wordWrap = !!on; if (this.textarea) { this.textarea.style.whiteSpace = this._wordWrap ? 'pre-wrap' : 'pre'; } return this; } isWordWrap() { return this._wordWrap; } // ============ 工具栏键盘导航(v0.1.12 无障碍) ============ _bindToolbarKeyboard() { if (!this.toolbarEl) return; const onKeydown = (e) => { const btns = [...this.toolbarEl.querySelectorAll('.me-btn:not(:disabled)')]; if (!btns.length) return; const idx = btns.indexOf(document.activeElement); if (idx === -1) return; if (e.key === 'ArrowRight') { e.preventDefault(); const next = (idx + 1) % btns.length; btns[next].focus(); } else if (e.key === 'ArrowLeft') { e.preventDefault(); const prev = (idx - 1 + btns.length) % btns.length; btns[prev].focus(); } else if (e.key === 'Home') { e.preventDefault(); btns[0].focus(); } else if (e.key === 'End') { e.preventDefault(); btns[btns.length - 1].focus(); } }; this.toolbarEl.addEventListener('keydown', onKeydown); this._cleanups.push(() => this.toolbarEl.removeEventListener('keydown', onKeydown)); } // ============ aria-live 区域(v0.1.12 无障碍) ============ _initAriaLive() { if (!this.el) return; const live = document.createElement('div'); live.className = 'me-sr-only'; live.setAttribute('aria-live', 'polite'); live.setAttribute('aria-atomic', 'true'); this.el.appendChild(live); this._ariaLive = live; } _announce(msg) { if (this._ariaLive) { this._ariaLive.textContent = ''; requestAnimationFrame(() => { this._ariaLive.textContent = msg; }); } } // ============ 滚动同步 v2 — 标题感知(v0.1.12) ============ _syncScrollByHeading() { if (!this.config.syncScroll || this._mode !== 'split') return; const ta = this.textarea; if (!ta) return; // 找到光标所在位置最近的标题 const pos = ta.selectionStart; const before = this._value.substring(0, pos); const headings = [...before.matchAll(/^(#{1,6})\s+(.+)$/gm)]; if (!headings.length) { // 无标题,回退到比例同步 const max = ta.scrollHeight - ta.clientHeight; if (max <= 0) return; const ratio = ta.scrollTop / max; const pmax = this.previewPane.scrollHeight - this.previewPane.clientHeight; this.previewPane.scrollTop = ratio * pmax; return; } // 取最后一个标题(光标之前最近的标题) const lastH = headings[headings.length - 1]; const headingText = lastH[2].trim(); // 在预览区查找对应标题元素 const previewEls = this.previewEl.querySelectorAll('h1, h2, h3, h4, h5, h6'); for (const el of previewEls) { if (el.textContent.trim() === headingText) { el.scrollIntoView({ block: 'start', behavior: 'instant' }); return; } } // fallback const max = ta.scrollHeight - ta.clientHeight; if (max <= 0) return; this.previewPane.scrollTop = (ta.scrollTop / max) * (this.previewPane.scrollHeight - this.previewPane.clientHeight); } _updateWordCount() { if (!this.config.wordCount || !this.statusEl) return; const text = this._value || ''; const chars = text.length; // 中文字符数 + 英文单词数 const cnChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length; const enWords = (text.replace(/[\u4e00-\u9fa5]/g, ' ').match(/[a-zA-Z0-9]+/g) || []).length; const words = cnChars + enWords; const lines = text ? text.split('\n').length : 0; const readingMin = Math.max(1, Math.ceil(words / 300)); const parts = [ `${i18nT('characters')}: ${chars}`, `${i18nT('words')}: ${words}`, `${i18nT('lines')}: ${lines}`, `${i18nT('readingTime')}: ${readingMin} ${i18nT('minutes')}`, ]; this.statusEl.textContent = parts.join(' · '); } getStats() { const text = this._value || ''; const cnChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length; const enWords = (text.replace(/[\u4e00-\u9fa5]/g, ' ').match(/[a-zA-Z0-9]+/g) || []).length; return { characters: text.length, words: cnChars + enWords, chineseChars: cnChars, englishWords: enWords, lines: text ? text.split('\n').length : 0, readingTime: Math.max(1, Math.ceil((cnChars + enWords) / 300)), }; } // ============ 公共 API ============ getValue() { return this._destroyed ? '' : this._value; } setValue(md, opts = {}) { if (this._destroyed) return this; this._value = md || ''; this.textarea.value = this._value; if (!opts.silent) this._pushHistory(); this._render(); this._renderGutter(); this._updateWordCount(); this._updateOutline(); // 同步行号滚动 if (this.gutter) this.gutter.scrollTop = 0; if (!opts.silent) { this._emit('change', this._value); if (typeof this.config.onChange === 'function') { try { this.config.onChange(this._value, this); } catch (e) { console.error(e); } } } return this; } getHTML() { MarkdownEditor.trigger('beforeRender', this); this._emit('beforeRender', this); const env = { highlight: this._highlightFn, locale: getCurrentLocale() }; let html = this._renderFn(this._value, env); if (typeof this.config.sanitize === 'function') { try { html = this.config.sanitize(html); } catch (e) { console.error(e); } } MarkdownEditor.trigger('afterRender', this); this._emit('afterRender', this); return html; } /** * 强制刷新预览(内容未变但需重渲染时使用,如主题切换) */ refresh() { this._lastRenderedValue = null; this._render(); return this; } insert(text, opts = {}) { const ta = this.textarea; const start = ta.selectionStart; const end = ta.selectionEnd; const replace = opts.replace === true; // replace=true: 替换选区内容;replace=false: 在光标处插入,保留选区 ta.value = ta.value.slice(0, start) + text + ta.value.slice(replace ? end : start); ta.focus(); const pos = start + text.length; ta.selectionStart = ta.selectionEnd = pos; this._value = ta.value; this._pushHistory(); this._render(); this._updateWordCount(); this._emit('change', this._value); return this; } /** * 在选区两侧包裹文本 */ wrap(before, after) { this._wrapSelection(before, after || before); return this; } focus() { if (this.textarea) this.textarea.focus(); return this; } blur() { if (this.textarea) this.textarea.blur(); return this; } enable() { if (this.textarea) this.textarea.disabled = false; if (this.el) this.el.classList.remove('me-disabled'); return this; } disable() { if (this.textarea) this.textarea.disabled = true; if (this.el) this.el.classList.add('me-disabled'); return this; } isDisabled() { return this.textarea ? this.textarea.disabled : false; } /** * 设置只读模式 */ setReadOnly(readOnly) { this.config.readOnly = !!readOnly; if (this.textarea) this.textarea.readOnly = !!readOnly; if (this.el) this.el.classList.toggle('me-readonly', !!readOnly); this._updateModeButtons(); return this; } isReadOnly() { return this.config.readOnly; } /** * 注册实例事件监听 * 支持事件:change/input/focus/blur/save/modeChange/fullscreen/beforeCreate/afterCreate/beforeRender/afterRender/destroy */ on(name, fn) { if (!this._listeners[name]) this._listeners[name] = []; this._listeners[name].push(fn); return () => this.off(name, fn); } off(name, fn) { const list = this._listeners[name]; if (list) this._listeners[name] = list.filter((f) => f !== fn); return this; } _emit(name, ...args) { const list = this._listeners[name]; if (list) list.forEach((fn) => { try { fn(...args, this); } catch (e) { console.error(`MeEditor event "${name}" error:`, e); } }); } /** * 安装插件(v0.1.6: 支持异步 install) * @param {string|Object} plugin - 预设插件名或插件对象 * @param {Object} options - 插件配置 */ use(plugin, options = {}) { let p = plugin; if (typeof plugin === 'string') { p = presetPlugins[plugin]; if (!p) { console.warn(`MeEditor: preset plugin "${plugin}" not found`); return this; } } if (!p || typeof p !== 'object') { console.warn('MeEditor: invalid plugin'); return this; } const merged = { ...p, ...options }; if (typeof merged.install === 'function') { try { 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]; } /** * 向工具栏追加自定义按钮 */ addToolbarButton(config) { if (!config || !config.action) return this; const btn = document.createElement('button'); btn.type = 'button'; btn.className = `me-btn me-btn-custom-${config.action}`; btn.title = config.title || config.action; btn.setAttribute('aria-label', config.title || config.action); btn.innerHTML = config.icon || `${escapeHTML(config.text || config.action)}`; if (typeof config.onClick === 'function') { // 直接绑定 click,不设置 data-action 避免工具栏委托重复触发 btn.addEventListener('click', (e) => { e.stopPropagation(); config.onClick(this); }); } else if (config.action) { btn.dataset.action = config.action; this._customActions = this._customActions || {}; this._customActions[config.action] = config.handler; } this.toolbarEl.insertBefore(btn, this.toolbarEl.querySelector('.me-toolbar-group') || null); 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 = `${TOAST_ICONS[type] || ''}${escapeHTML(String(message))}`; 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); } /** * 销毁编辑器 */ destroy() { if (this._destroyed) return; MarkdownEditor.trigger('beforeDestroy', this); if (this._renderRaf) cancelAnimationFrame(this._renderRaf); if (this._historyTimer) clearTimeout(this._historyTimer); if (this._outlineTimer) clearTimeout(this._outlineTimer); 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) => { if (typeof p.destroy === 'function') { try { p.destroy(this); } catch (_) {} } }); this._plugins = []; if (this.el && this.el.parentNode) { this.el.parentNode.removeChild(this.el); } this.el = null; this.textarea = null; this.previewEl = null; this._destroyed = true; if (typeof this.config.onDestroy === 'function') { try { this.config.onDestroy(this); } catch (e) { console.error(e); } } // 先触发 destroy 事件,再清空监听器(否则 destroy 监听器永远收不到事件) this._emit('destroy'); this._listeners = {}; MarkdownEditor.trigger('afterDestroy', this); } isDestroyed() { return this._destroyed; } /** * 设置实例主题(v0.1.5: 实例隔离,不影响其他编辑器) * @param {string} theme - 主题名 * @returns {MarkdownEditor} */ setTheme(theme) { if (!theme) return this; this.config.theme = theme; if (this._themeCtx) { this._themeCtx.set(theme); } return this; } /** * 获取实例当前主题名 * @returns {string} */ getTheme() { if (this._themeCtx) return this._themeCtx.get(); return this.config.theme || 'auto'; } /** * 获取实例主题上下文(高级 API) * 提供 syncWithElement / adopt / watch 等外部跟随能力 * @returns {Object|null} */ getThemeContext() { return this._themeCtx || null; } /** * 获取编辑器状态 */ getStatus() { return { id: this.id, mode: this._mode, theme: this.getTheme(), locale: getCurrentLocale(), fullscreen: this._fullscreen, readOnly: this.config.readOnly || false, disabled: this.textarea ? this.textarea.disabled : false, destroyed: this._destroyed, plugins: this._plugins.map((p) => p.name || 'custom'), }; } } export { MarkdownEditor as Editor, MarkdownEditor }; export default MarkdownEditor;