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:
+26
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user