release: v0.1.3 — bug fixes, performance, hardening
Bug Fixes:
- fix(core): exec() now supports _customActions registered via addToolbarButton
- fix(core): _emit('focus'/'blur') no longer passes editor argument twice
- fix(core): getHTML() now triggers beforeRender/afterRender hooks
- fix(core): _wrapSelection defaults to 'text' instead of i18n placeholder
- fix(core): addToolbarButton onClick+action no longer double-fires
- fix(core): null-safety on focus/blur/enable/disable after destroy
- fix(parser): empty headings (# ) no longer produce empty <h1> tags
- fix(parser): slugify returns 'heading' fallback for empty/special inputs
- fix(i18n): setCurrentLocale no longer mutates input parameter
- fix(core): autofocus skipped when readOnly is enabled
Performance:
- perf(core): cache last rendered value, skip parsing when content unchanged
- perf(core): add refresh() API to force re-render after theme/locale changes
Hardening:
- feat(core): tabSize dynamically applied to textarea via style.tabSize
- feat(core): getStatus() now includes readOnly field
Tests:
- 7 new test cases: customActions, refresh, focus args, destroy safety,
getStatus readOnly, autofocus+readOnly, slugify edge cases
- Total: 425 tests passing (+7 from v0.1.2)
Types:
- EditorStatus includes readOnly field
- MarkdownEditor.refresh() added to type definitions
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> 轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用,中文优先。
|
> 轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用,中文优先。
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/@metona-team/metona-editor)
|
[](https://www.npmjs.com/package/@metona-team/metona-editor)
|
||||||
[](./LICENSE)
|
[](./LICENSE)
|
||||||
[](./tests)
|
[](./tests)
|
||||||
[](./tests)
|
[](./tests)
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@metona-team/metona-editor",
|
"name": "@metona-team/metona-editor",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"description": "轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用。",
|
"description": "轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用。",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/metona-editor.js",
|
"main": "dist/metona-editor.js",
|
||||||
|
|||||||
+1
-1
@@ -305,7 +305,7 @@
|
|||||||
'',
|
'',
|
||||||
'| 名称 | 值 |',
|
'| 名称 | 值 |',
|
||||||
'| --- | --- |',
|
'| --- | --- |',
|
||||||
'| 版本 | 0.1.2 |',
|
'| 版本 | 0.1.3 |',
|
||||||
'| 依赖 | 零 |',
|
'| 依赖 | 零 |',
|
||||||
'| 测试 | 390+ |',
|
'| 测试 | 390+ |',
|
||||||
'',
|
'',
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Animations - 动画管理
|
* MetonaEditor Animations - 动画管理
|
||||||
* @module animations
|
* @module animations
|
||||||
* @version 0.1.2
|
* @version 0.1.3
|
||||||
* @description 动画注册与管理(当前为 CSS 动画元数据管理,供未来扩展如 Toast/通知组件的进出场动画使用;
|
* @description 动画注册与管理(当前为 CSS 动画元数据管理,供未来扩展如 Toast/通知组件的进出场动画使用;
|
||||||
* 内置 7 种预设动画曲线配置;编辑器核心当前未直接调用此模块)
|
* 内置 7 种预设动画曲线配置;编辑器核心当前未直接调用此模块)
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Constants - 常量定义
|
* MetonaEditor Constants - 常量定义
|
||||||
* @module constants
|
* @module constants
|
||||||
* @version 0.1.2
|
* @version 0.1.3
|
||||||
* @description 默认配置、主题、动画、工具栏等常量
|
* @description 默认配置、主题、动画、工具栏等常量
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+39
-16
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Core - 编辑器核心
|
* MetonaEditor Core - 编辑器核心
|
||||||
* @module core
|
* @module core
|
||||||
* @version 0.1.2
|
* @version 0.1.3
|
||||||
* @description MarkdownEditor 类实现:DOM 构建、事件、渲染、历史栈、选区操作、模式切换
|
* @description MarkdownEditor 类实现:DOM 构建、事件、渲染、历史栈、选区操作、模式切换
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -88,6 +88,7 @@ class MarkdownEditor {
|
|||||||
this._historyTimer = null;
|
this._historyTimer = null;
|
||||||
this._fullscreen = false;
|
this._fullscreen = false;
|
||||||
this._destroyed = false;
|
this._destroyed = false;
|
||||||
|
this._lastRenderedValue = null;
|
||||||
|
|
||||||
// 渲染函数:自定义覆盖内置解析器
|
// 渲染函数:自定义覆盖内置解析器
|
||||||
this._renderFn = (typeof this.config.render === 'function') ? this.config.render : parseMarkdown;
|
this._renderFn = (typeof this.config.render === 'function') ? this.config.render : parseMarkdown;
|
||||||
@@ -122,7 +123,7 @@ class MarkdownEditor {
|
|||||||
this.config.plugins.forEach((p) => this.use(p));
|
this.config.plugins.forEach((p) => this.use(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.autofocus) this.focus();
|
if (this.config.autofocus && !this.config.readOnly) this.focus();
|
||||||
|
|
||||||
if (typeof this.config.onCreate === 'function') {
|
if (typeof this.config.onCreate === 'function') {
|
||||||
try { this.config.onCreate(this); } catch (e) { console.error('onCreate error:', e); }
|
try { this.config.onCreate(this); } catch (e) { console.error('onCreate error:', e); }
|
||||||
@@ -168,6 +169,7 @@ class MarkdownEditor {
|
|||||||
textarea.className = 'me-textarea';
|
textarea.className = 'me-textarea';
|
||||||
textarea.spellcheck = !!this.config.spellcheck;
|
textarea.spellcheck = !!this.config.spellcheck;
|
||||||
textarea.readOnly = !!this.config.readOnly;
|
textarea.readOnly = !!this.config.readOnly;
|
||||||
|
textarea.style.tabSize = String(this.config.tabSize || 2);
|
||||||
textarea.placeholder = this.config.placeholder || i18nT('placeholder');
|
textarea.placeholder = this.config.placeholder || i18nT('placeholder');
|
||||||
textarea.setAttribute('aria-label', i18nT('edit'));
|
textarea.setAttribute('aria-label', i18nT('edit'));
|
||||||
editorPane.appendChild(textarea);
|
editorPane.appendChild(textarea);
|
||||||
@@ -294,13 +296,13 @@ class MarkdownEditor {
|
|||||||
ta.addEventListener('scroll', onScroll);
|
ta.addEventListener('scroll', onScroll);
|
||||||
|
|
||||||
const onFocus = () => {
|
const onFocus = () => {
|
||||||
this._emit('focus', this);
|
this._emit('focus');
|
||||||
if (typeof this.config.onFocus === 'function') {
|
if (typeof this.config.onFocus === 'function') {
|
||||||
try { this.config.onFocus(this); } catch (e) { console.error(e); }
|
try { this.config.onFocus(this); } catch (e) { console.error(e); }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onBlur = () => {
|
const onBlur = () => {
|
||||||
this._emit('blur', this);
|
this._emit('blur');
|
||||||
if (typeof this.config.onBlur === 'function') {
|
if (typeof this.config.onBlur === 'function') {
|
||||||
try { this.config.onBlur(this); } catch (e) { console.error(e); }
|
try { this.config.onBlur(this); } catch (e) { console.error(e); }
|
||||||
}
|
}
|
||||||
@@ -445,6 +447,9 @@ class MarkdownEditor {
|
|||||||
|
|
||||||
_render() {
|
_render() {
|
||||||
if (this._mode === 'edit') return;
|
if (this._mode === 'edit') return;
|
||||||
|
// 内容未变化时跳过解析(主题切换等场景触发重渲染但内容不变)
|
||||||
|
if (this._lastRenderedValue === this._value) return;
|
||||||
|
this._lastRenderedValue = this._value;
|
||||||
MarkdownEditor.trigger('beforeRender', this);
|
MarkdownEditor.trigger('beforeRender', this);
|
||||||
this._emit('beforeRender', this);
|
this._emit('beforeRender', this);
|
||||||
|
|
||||||
@@ -544,7 +549,11 @@ class MarkdownEditor {
|
|||||||
fullscreen: () => this.toggleFullscreen(),
|
fullscreen: () => this.toggleFullscreen(),
|
||||||
};
|
};
|
||||||
const fn = actions[action];
|
const fn = actions[action];
|
||||||
if (fn) fn.apply(this, args);
|
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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,7 +562,7 @@ class MarkdownEditor {
|
|||||||
const start = ta.selectionStart;
|
const start = ta.selectionStart;
|
||||||
const end = ta.selectionEnd;
|
const end = ta.selectionEnd;
|
||||||
const selected = ta.value.slice(start, end);
|
const selected = ta.value.slice(start, end);
|
||||||
const text = selected || (i18nT('placeholder') || 'text');
|
const text = selected || 'text';
|
||||||
const inserted = before + text + after;
|
const inserted = before + text + after;
|
||||||
|
|
||||||
ta.value = ta.value.slice(0, start) + inserted + ta.value.slice(end);
|
ta.value = ta.value.slice(0, start) + inserted + ta.value.slice(end);
|
||||||
@@ -776,14 +785,27 @@ class MarkdownEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getHTML() {
|
getHTML() {
|
||||||
|
MarkdownEditor.trigger('beforeRender', this);
|
||||||
|
this._emit('beforeRender', this);
|
||||||
const env = { highlight: this._highlightFn, locale: getCurrentLocale() };
|
const env = { highlight: this._highlightFn, locale: getCurrentLocale() };
|
||||||
let html = this._renderFn(this._value, env);
|
let html = this._renderFn(this._value, env);
|
||||||
if (typeof this.config.sanitize === 'function') {
|
if (typeof this.config.sanitize === 'function') {
|
||||||
try { html = this.config.sanitize(html); } catch (e) { console.error(e); }
|
try { html = this.config.sanitize(html); } catch (e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
MarkdownEditor.trigger('afterRender', this);
|
||||||
|
this._emit('afterRender', this);
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制刷新预览(内容未变但需重渲染时使用,如主题切换)
|
||||||
|
*/
|
||||||
|
refresh() {
|
||||||
|
this._lastRenderedValue = null;
|
||||||
|
this._render();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
insert(text, opts = {}) {
|
insert(text, opts = {}) {
|
||||||
const ta = this.textarea;
|
const ta = this.textarea;
|
||||||
const start = ta.selectionStart;
|
const start = ta.selectionStart;
|
||||||
@@ -810,22 +832,22 @@ class MarkdownEditor {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() { this.textarea.focus(); return this; }
|
focus() { if (this.textarea) this.textarea.focus(); return this; }
|
||||||
blur() { this.textarea.blur(); return this; }
|
blur() { if (this.textarea) this.textarea.blur(); return this; }
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
this.textarea.disabled = false;
|
if (this.textarea) this.textarea.disabled = false;
|
||||||
this.el.classList.remove('me-disabled');
|
if (this.el) this.el.classList.remove('me-disabled');
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
disable() {
|
disable() {
|
||||||
this.textarea.disabled = true;
|
if (this.textarea) this.textarea.disabled = true;
|
||||||
this.el.classList.add('me-disabled');
|
if (this.el) this.el.classList.add('me-disabled');
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
isDisabled() { return this.textarea.disabled; }
|
isDisabled() { return this.textarea ? this.textarea.disabled : false; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置只读模式
|
* 设置只读模式
|
||||||
@@ -902,14 +924,14 @@ class MarkdownEditor {
|
|||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.type = 'button';
|
btn.type = 'button';
|
||||||
btn.className = `me-btn me-btn-custom-${config.action}`;
|
btn.className = `me-btn me-btn-custom-${config.action}`;
|
||||||
btn.dataset.action = config.action;
|
|
||||||
btn.title = config.title || config.action;
|
btn.title = config.title || config.action;
|
||||||
btn.setAttribute('aria-label', config.title || config.action);
|
btn.setAttribute('aria-label', config.title || config.action);
|
||||||
btn.innerHTML = config.icon || `<span>${escapeHTML(config.text || config.action)}</span>`;
|
btn.innerHTML = config.icon || `<span>${escapeHTML(config.text || config.action)}</span>`;
|
||||||
if (typeof config.onClick === 'function') {
|
if (typeof config.onClick === 'function') {
|
||||||
btn.addEventListener('click', () => config.onClick(this));
|
// 直接绑定 click,不设置 data-action 避免工具栏委托重复触发
|
||||||
|
btn.addEventListener('click', (e) => { e.stopPropagation(); config.onClick(this); });
|
||||||
} else if (config.action) {
|
} else if (config.action) {
|
||||||
// 注册到 exec 动作
|
btn.dataset.action = config.action;
|
||||||
this._customActions = this._customActions || {};
|
this._customActions = this._customActions || {};
|
||||||
this._customActions[config.action] = config.handler;
|
this._customActions[config.action] = config.handler;
|
||||||
}
|
}
|
||||||
@@ -969,6 +991,7 @@ class MarkdownEditor {
|
|||||||
theme: this.config.theme,
|
theme: this.config.theme,
|
||||||
locale: getCurrentLocale(),
|
locale: getCurrentLocale(),
|
||||||
fullscreen: this._fullscreen,
|
fullscreen: this._fullscreen,
|
||||||
|
readOnly: this.config.readOnly || false,
|
||||||
disabled: this.textarea ? this.textarea.disabled : false,
|
disabled: this.textarea ? this.textarea.disabled : false,
|
||||||
destroyed: this._destroyed,
|
destroyed: this._destroyed,
|
||||||
plugins: this._plugins.map((p) => p.name || 'custom'),
|
plugins: this._plugins.map((p) => p.name || 'custom'),
|
||||||
|
|||||||
+8
-7
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor i18n - 国际化管理
|
* MetonaEditor i18n - 国际化管理
|
||||||
* @module i18n
|
* @module i18n
|
||||||
* @version 0.1.2
|
* @version 0.1.3
|
||||||
* @description 多语言支持、语言切换和翻译管理
|
* @description 多语言支持、语言切换和翻译管理
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -20,14 +20,15 @@ export const getCurrentLocale = () => currentLocale;
|
|||||||
* 设置当前语言
|
* 设置当前语言
|
||||||
*/
|
*/
|
||||||
export const setCurrentLocale = (locale) => {
|
export const setCurrentLocale = (locale) => {
|
||||||
if (!LOCALES[locale]) {
|
let target = locale;
|
||||||
console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`);
|
if (!LOCALES[target]) {
|
||||||
locale = fallbackLocale;
|
console.warn(`Locale "${target}" not found, falling back to "${fallbackLocale}"`);
|
||||||
|
target = fallbackLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentLocale = locale;
|
currentLocale = target;
|
||||||
notifyLocaleListeners(locale);
|
notifyLocaleListeners(target);
|
||||||
saveLocale(locale);
|
saveLocale(target);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Icons — 工具栏图标SVG定义
|
* MetonaEditor Icons — 工具栏图标SVG定义
|
||||||
* @module icons
|
* @module icons
|
||||||
* @version 0.1.2
|
* @version 0.1.3
|
||||||
* @description 工具栏格式化按钮 SVG 图标
|
* @description 工具栏格式化按钮 SVG 图标
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor - 轻量级 Markdown Editor 库
|
* MetonaEditor - 轻量级 Markdown Editor 库
|
||||||
* @module metona-editor
|
* @module metona-editor
|
||||||
* @version 0.1.2
|
* @version 0.1.3
|
||||||
* @author thzxx
|
* @author thzxx
|
||||||
* @description 现代化、零依赖、易扩展、易维护、易使用的 Markdown 编辑器库。单文件,开箱即用。
|
* @description 现代化、零依赖、易扩展、易维护、易使用的 Markdown 编辑器库。单文件,开箱即用。
|
||||||
* @license MIT
|
* @license MIT
|
||||||
@@ -29,7 +29,7 @@ import { animationUtils } from './animations.js';
|
|||||||
import { DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS } from './constants.js';
|
import { DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS } from './constants.js';
|
||||||
|
|
||||||
// 版本信息
|
// 版本信息
|
||||||
const VERSION = '0.1.2';
|
const VERSION = '0.1.3';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局默认插件队列
|
* 全局默认插件队列
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Locales — 国际化翻译数据
|
* MetonaEditor Locales — 国际化翻译数据
|
||||||
* @module locales
|
* @module locales
|
||||||
* @version 0.1.2
|
* @version 0.1.3
|
||||||
* @description 内置 zh-CN / en-US 完整翻译
|
* @description 内置 zh-CN / en-US 完整翻译
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Parser - 轻量 Markdown 解析器
|
* MetonaEditor Parser - 轻量 Markdown 解析器
|
||||||
* @module parser
|
* @module parser
|
||||||
* @version 0.1.2
|
* @version 0.1.3
|
||||||
* @description 自研零依赖 Markdown 解析器,覆盖 CommonMark 子集 + GFM 扩展
|
* @description 自研零依赖 Markdown 解析器,覆盖 CommonMark 子集 + GFM 扩展
|
||||||
*
|
*
|
||||||
* 支持语法:
|
* 支持语法:
|
||||||
@@ -100,8 +100,8 @@ export const parseMarkdown = (md, env = {}) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ATX 标题
|
// ATX 标题(非空标题文本)
|
||||||
const h = line.match(/^(#{1,6})\s+(.*?)(?:\s+#{1,6})?\s*$/);
|
const h = line.match(/^(#{1,6})\s+(.+?)(?:\s+#{1,6})?\s*$/);
|
||||||
if (h) {
|
if (h) {
|
||||||
tokens.push({ type: 'heading', level: h[1].length, text: h[2] });
|
tokens.push({ type: 'heading', level: h[1].length, text: h[2] });
|
||||||
i++;
|
i++;
|
||||||
@@ -364,12 +364,13 @@ const renderInline = (text, env) => {
|
|||||||
* 生成标题锚点 id(支持中文)
|
* 生成标题锚点 id(支持中文)
|
||||||
*/
|
*/
|
||||||
const slugify = (text) => {
|
const slugify = (text) => {
|
||||||
return String(text)
|
const slug = String(text)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^\w\u4e00-\u9fa5\s-]/g, '')
|
.replace(/[^\w\u4e00-\u9fa5\s-]/g, '')
|
||||||
.replace(/\s+/g, '-')
|
.replace(/\s+/g, '-')
|
||||||
.replace(/-+/g, '-')
|
.replace(/-+/g, '-')
|
||||||
.replace(/^-|-$/g, '');
|
.replace(/^-|-$/g, '');
|
||||||
|
return slug || 'heading';
|
||||||
};
|
};
|
||||||
|
|
||||||
export { safeUrl, slugify };
|
export { safeUrl, slugify };
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Plugins - 插件系统
|
* MetonaEditor Plugins - 插件系统
|
||||||
* @module plugins
|
* @module plugins
|
||||||
* @version 0.1.2
|
* @version 0.1.3
|
||||||
* @description 插件管理器 + 预设插件(autoSave / exportTool / searchReplace)
|
* @description 插件管理器 + 预设插件(autoSave / exportTool / searchReplace)
|
||||||
*
|
*
|
||||||
* 插件约定:
|
* 插件约定:
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Styles - 编辑器样式
|
* MetonaEditor Styles - 编辑器样式
|
||||||
* @module styles
|
* @module styles
|
||||||
* @version 0.1.2
|
* @version 0.1.3
|
||||||
* @description 编辑器 UI 样式注入与管理
|
* @description 编辑器 UI 样式注入与管理
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Themes - 主题管理
|
* MetonaEditor Themes - 主题管理
|
||||||
* @module themes
|
* @module themes
|
||||||
* @version 0.1.2
|
* @version 0.1.3
|
||||||
* @description 主题系统、自定义主题和主题切换
|
* @description 主题系统、自定义主题和主题切换
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Utils - 工具函数
|
* MetonaEditor Utils - 工具函数
|
||||||
* @module utils
|
* @module utils
|
||||||
* @version 0.1.2
|
* @version 0.1.3
|
||||||
* @description 通用工具函数集合
|
* @description 通用工具函数集合
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1278,4 +1278,77 @@ describe('MarkdownEditor - v0.1.2 readOnly 模式', () => {
|
|||||||
expect(ed.getValue()).toBe('# hello');
|
expect(ed.getValue()).toBe('# hello');
|
||||||
ed.destroy();
|
ed.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getStatus 包含 readOnly 字段', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { readOnly: true });
|
||||||
|
expect(ed.getStatus().readOnly).toBe(true);
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('autofocus 在 readOnly 模式下不聚焦', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { readOnly: true, autofocus: true });
|
||||||
|
// readOnly textarea 不应被 focus(autofocus 被跳过)
|
||||||
|
expect(ed.textarea.readOnly).toBe(true);
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MarkdownEditor - v0.1.3 修复验证', () => {
|
||||||
|
test('exec 支持 _customActions 自定义动作', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { value: 'hello' });
|
||||||
|
let called = false;
|
||||||
|
ed._customActions = { myAction: () => { called = true; } };
|
||||||
|
ed.exec('myAction');
|
||||||
|
expect(called).toBe(true);
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refresh 清除渲染缓存并强制重渲染', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { value: '# test', mode: 'split' });
|
||||||
|
const first = ed.previewEl.innerHTML;
|
||||||
|
// setValue 不改变内容,正常 _render 会跳过
|
||||||
|
ed.setValue('# test', { silent: true });
|
||||||
|
// refresh 强制重渲染
|
||||||
|
ed.refresh();
|
||||||
|
// 内容一致,预览应一致
|
||||||
|
expect(ed.previewEl.innerHTML).toBe(first);
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('focus/blur 监听器接收正确参数', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, {});
|
||||||
|
let focusArg;
|
||||||
|
ed.on('focus', (...args) => { focusArg = args; });
|
||||||
|
ed.textarea.dispatchEvent(new Event('focus', { bubbles: true }));
|
||||||
|
// focus 事件只传 editor 一次
|
||||||
|
expect(focusArg.length).toBe(1);
|
||||||
|
expect(focusArg[0]).toBe(ed);
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy 后 focus/blur 不抛错', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, {});
|
||||||
|
ed.destroy();
|
||||||
|
expect(() => ed.focus()).not.toThrow();
|
||||||
|
expect(() => ed.blur()).not.toThrow();
|
||||||
|
expect(ed.isDisabled()).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -350,6 +350,11 @@ describe('slugify', () => {
|
|||||||
test('去除首尾连字符', () => {
|
test('去除首尾连字符', () => {
|
||||||
expect(slugify('-hello-')).toBe('hello');
|
expect(slugify('-hello-')).toBe('hello');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('slugify 空/特殊字符输入返回兜底值', () => {
|
||||||
|
expect(slugify('')).toBe('heading');
|
||||||
|
expect(slugify('###')).toBe('heading');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseMarkdown - v0.1.2 新语法', () => {
|
describe('parseMarkdown - v0.1.2 新语法', () => {
|
||||||
|
|||||||
Vendored
+3
@@ -115,6 +115,7 @@ export interface EditorStatus {
|
|||||||
theme: ThemeName;
|
theme: ThemeName;
|
||||||
locale: string;
|
locale: string;
|
||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
|
readOnly: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
destroyed: boolean;
|
destroyed: boolean;
|
||||||
plugins: string[];
|
plugins: string[];
|
||||||
@@ -162,6 +163,8 @@ export class MarkdownEditor {
|
|||||||
getValue(): string;
|
getValue(): string;
|
||||||
setValue(markdown: string, opts?: { silent?: boolean }): this;
|
setValue(markdown: string, opts?: { silent?: boolean }): this;
|
||||||
getHTML(): string;
|
getHTML(): string;
|
||||||
|
/** 强制刷新预览(内容未变时手动触发重渲染) */
|
||||||
|
refresh(): this;
|
||||||
insert(text: string, opts?: { replace?: boolean }): this;
|
insert(text: string, opts?: { replace?: boolean }): this;
|
||||||
wrap(before: string, after?: string): this;
|
wrap(before: string, after?: string): this;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user