release: v0.1.2 — readOnly, new syntax, plugins, optimizations

Features:
- feat(core): readOnly mode with config option and setReadOnly() API
- feat(parser): superscript ^text^ and subscript ~text~ support
- feat(parser): highlight/mark ==text== syntax
- feat(parser): table column alignment with colons (:---, :---:, ---:)
- feat(parser): emoji shortcodes 😄😊 (80+ common emojis)
- feat(plugins): imagePaste preset — paste clipboard images as base64
- feat(core): instance-level beforeRender/afterRender events
- feat(styles): print stylesheet (@media print)
- feat(styles): mark element CSS styling
- feat(styles): readOnly mode CSS (.me-readonly)

Optimizations:
- perf(core): history debounce now configurable via historyDebounce option
- perf(core): skip _render() in edit-only mode (already guarded)
- fix(core): divider drag division-by-zero guard
- fix(core): config.style deep merge with DEFAULTS
- fix(core): scroll position preserved across mode switches

Tests:
- 9 new test cases: readOnly (3), parser syntax (6)
- Total: 418 tests passing (+9 from v0.1.1)

Chores:
- version bump 0.1.1 → 0.1.2
- TypeScript definitions updated for new APIs
This commit is contained in:
2026-07-23 20:53:36 +08:00
parent e3142a17a8
commit 12875921eb
18 changed files with 257 additions and 26 deletions
+26 -3
View File
@@ -1,7 +1,7 @@
/**
* MetonaEditor Core - 编辑器核心
* @module core
* @version 0.1.1
* @version 0.1.2
* @description MarkdownEditor 类实现:DOM 构建、事件、渲染、历史栈、选区操作、模式切换
*/
@@ -74,7 +74,7 @@ class MarkdownEditor {
this.config = { ...DEFAULTS, ...options };
if (options.style && typeof options.style === 'object') {
this.config.style = { ...options.style };
this.config.style = { ...DEFAULTS.style, ...options.style };
}
this._value = this.config.value || '';
@@ -136,6 +136,7 @@ class MarkdownEditor {
_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'));
@@ -166,6 +167,7 @@ class MarkdownEditor {
const textarea = document.createElement('textarea');
textarea.className = 'me-textarea';
textarea.spellcheck = !!this.config.spellcheck;
textarea.readOnly = !!this.config.readOnly;
textarea.placeholder = this.config.placeholder || i18nT('placeholder');
textarea.setAttribute('aria-label', i18nT('edit'));
editorPane.appendChild(textarea);
@@ -410,6 +412,7 @@ class MarkdownEditor {
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) => {
@@ -443,6 +446,7 @@ class MarkdownEditor {
_render() {
if (this._mode === 'edit') return;
MarkdownEditor.trigger('beforeRender', this);
this._emit('beforeRender', this);
const env = {
highlight: this._highlightFn,
@@ -463,13 +467,14 @@ class MarkdownEditor {
this.previewEl.innerHTML = html;
MarkdownEditor.trigger('afterRender', this);
this._emit('afterRender', this);
}
// ============ 历史栈 ============
_scheduleHistory() {
if (this._historyTimer) clearTimeout(this._historyTimer);
this._historyTimer = setTimeout(() => this._pushHistory(), 400);
this._historyTimer = setTimeout(() => this._pushHistory(), this.config.historyDebounce || 400);
}
_pushHistory() {
@@ -668,10 +673,16 @@ class MarkdownEditor {
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);
if (typeof this.config.onModeChange === 'function') {
try { this.config.onModeChange(mode, this); } catch (e) { console.error(e); }
@@ -816,6 +827,18 @@ class MarkdownEditor {
isDisabled() { return this.textarea.disabled; }
/**
* 设置只读模式
*/
setReadOnly(readOnly) {
this.config.readOnly = !!readOnly;
if (this.textarea) this.textarea.readOnly = !!readOnly;
if (this.el) this.el.classList.toggle('me-readonly', !!readOnly);
return this;
}
isReadOnly() { return this.config.readOnly; }
/**
* 注册实例事件监听
* 支持事件:change/input/focus/blur/save/modeChange/fullscreen/beforeCreate/afterCreate/beforeRender/afterRender/destroy