release: v0.1.12 — accessibility, Zen mode, scroll sync v2, shortcutHelp plugin
feat(a11y): toolbar keyboard navigation (Arrow keys, Home, End)
feat(a11y): aria-live region for screen reader announcements on mode change
feat(a11y): enhance focus-visible style (outline-offset: 2px, hide on mouse focus)
feat(core): Zen mode (editor.toggleZen() / exec('zen'))
- Toggle with Ctrl+Shift+Z shortcut concept
- Auto-hide toolbar and statusbar, mouse to top edge reveals toolbar
- Centered editor pane with max-width constraint
feat(core): heading-aware scroll sync v2
- Sync preview scroll to nearest heading before cursor position
- Falls back to proportional sync when no headings found
feat(core): Word wrap toggle (editor.toggleWordWrap() / setWordWrap(bool))
feat(plugins): shortcutHelp preset plugin
- Press '?' to open keyboard shortcuts overlay panel
- Lists built-in + user-registered shortcuts
- Click overlay or press Escape to close
style: Zen mode CSS, sr-only class, focus-visible refinement
test: 3 new tests for Zen mode, WordWrap, shortcutHelp (535 total)
chore: bump version 0.1.11 → 0.1.12 across all files
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.11",
|
"version": "0.1.12",
|
||||||
"description": "轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用。",
|
"description": "轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用。",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/metona-editor.js",
|
"main": "dist/metona-editor.js",
|
||||||
|
|||||||
+4
-4
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📝</text></svg>"/>
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📝</text></svg>"/>
|
||||||
<title>MetonaEditor v0.1.11 — 在线演示</title>
|
<title>MetonaEditor v0.1.12 — 在线演示</title>
|
||||||
<style>
|
<style>
|
||||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
:root {
|
:root {
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
<header class="header">
|
<header class="header">
|
||||||
<h1><span class="grad">Metona</span>Editor</h1>
|
<h1><span class="grad">Metona</span>Editor</h1>
|
||||||
<div class="badges">
|
<div class="badges">
|
||||||
<span class="badge">v0.1.11</span>
|
<span class="badge">v0.1.12</span>
|
||||||
<span class="badge">零依赖</span>
|
<span class="badge">零依赖</span>
|
||||||
<span class="badge">532 tests</span>
|
<span class="badge">532 tests</span>
|
||||||
<span class="badge">MIT</span>
|
<span class="badge">MIT</span>
|
||||||
@@ -291,7 +291,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
MetonaEditor v0.1.11 ·
|
MetonaEditor v0.1.12 ·
|
||||||
<a href="index.html">首页</a> ·
|
<a href="index.html">首页</a> ·
|
||||||
<a href="docs.html">API 文档</a> ·
|
<a href="docs.html">API 文档</a> ·
|
||||||
<a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> ·
|
<a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> ·
|
||||||
@@ -329,7 +329,7 @@
|
|||||||
'',
|
'',
|
||||||
'| 版本 | 日期 | 测试数 |',
|
'| 版本 | 日期 | 测试数 |',
|
||||||
'| :--- | :---: | ---: |',
|
'| :--- | :---: | ---: |',
|
||||||
'| v0.1.11 | 2026-07 | 532+ |',
|
'| v0.1.12 | 2026-07 | 532+ |',
|
||||||
'| v0.1.6 | 2026-07 | 521 |',
|
'| v0.1.6 | 2026-07 | 521 |',
|
||||||
'| v0.1.5 | 2026-07 | 499 |',
|
'| v0.1.5 | 2026-07 | 499 |',
|
||||||
'',
|
'',
|
||||||
|
|||||||
+2
-2
@@ -176,7 +176,7 @@
|
|||||||
|
|
||||||
<header class="docs-header">
|
<header class="docs-header">
|
||||||
<h1><span class="grad">文档</span></h1>
|
<h1><span class="grad">文档</span></h1>
|
||||||
<p class="sub">MetonaEditor v0.1.11 完整 API、配置与使用指南</p>
|
<p class="sub">MetonaEditor v0.1.12 完整 API、配置与使用指南</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -619,7 +619,7 @@ i18nUtils<span class="c-punc">.</span><span class="c-fn">formatDate</span><span
|
|||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
MetonaEditor v0.1.11 · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT License
|
MetonaEditor v0.1.12 · <a href="https://git.metona.cn/MetonaTeam/MetonaEditor" target="_blank" rel="noopener">源码仓库</a> · MIT License
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Animations - 动画管理
|
* MetonaEditor Animations - 动画管理
|
||||||
* @module animations
|
* @module animations
|
||||||
* @version 0.1.11
|
* @version 0.1.12
|
||||||
* @description 动画注册与管理(当前为 CSS 动画元数据管理,供未来扩展如 Toast/通知组件的进出场动画使用;
|
* @description 动画注册与管理(当前为 CSS 动画元数据管理,供未来扩展如 Toast/通知组件的进出场动画使用;
|
||||||
* 内置 7 种预设动画曲线配置;编辑器核心当前未直接调用此模块)
|
* 内置 7 种预设动画曲线配置;编辑器核心当前未直接调用此模块)
|
||||||
*/
|
*/
|
||||||
|
|||||||
+5
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Constants - 常量定义
|
* MetonaEditor Constants - 常量定义
|
||||||
* @module constants
|
* @module constants
|
||||||
* @version 0.1.11
|
* @version 0.1.12
|
||||||
* @description 默认配置、主题、动画、工具栏等常量
|
* @description 默认配置、主题、动画、工具栏等常量
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -56,6 +56,10 @@ export const DEFAULTS = Object.freeze({
|
|||||||
outline: false,
|
outline: false,
|
||||||
// 自动括号闭合(v0.1.7)
|
// 自动括号闭合(v0.1.7)
|
||||||
autoBrackets: true,
|
autoBrackets: true,
|
||||||
|
// Zen 专注模式(v0.1.12)
|
||||||
|
zenMode: false,
|
||||||
|
// 自动换行(v0.1.12)
|
||||||
|
wordWrap: true,
|
||||||
// 主题:light / dark / auto / warm / 自定义
|
// 主题:light / dark / auto / warm / 自定义
|
||||||
theme: 'auto',
|
theme: 'auto',
|
||||||
// 国际化
|
// 国际化
|
||||||
|
|||||||
+160
-12
@@ -1,10 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Core - 编辑器核心
|
* MetonaEditor Core - 编辑器核心
|
||||||
* @module core
|
* @module core
|
||||||
* @version 0.1.11
|
* @version 0.1.12
|
||||||
* @description MarkdownEditor 类实现:DOM 构建、事件、渲染、历史栈、选区操作、模式切换
|
* @description MarkdownEditor 类实现:DOM 构建、事件、渲染、历史栈、选区操作、模式切换
|
||||||
* v0.1.6: 插件依赖拓扑排序、实例 i18n、快捷键/工具栏定制、右键菜单、Toast
|
* v0.1.6: 插件依赖拓扑排序、实例 i18n、快捷键/工具栏定制、右键菜单、Toast
|
||||||
* v0.1.7: 行号装订线、自动格式化、括号自动闭合、拖放、大纲面板
|
* v0.1.7: 行号装订线、自动格式化、括号自动闭合、拖放、大纲面板
|
||||||
|
* v0.1.12: 工具栏键盘导航、aria-live、Zen模式、滚动同步v2、WordWrap
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { generateId, escapeHTML, isBrowser } from './utils.js';
|
import { generateId, escapeHTML, isBrowser } from './utils.js';
|
||||||
@@ -110,6 +111,8 @@ class MarkdownEditor {
|
|||||||
this._contextMenuItems = []; // v0.1.6: 右键菜单项
|
this._contextMenuItems = []; // v0.1.6: 右键菜单项
|
||||||
this._customActions = {}; // 自定义工具栏动作
|
this._customActions = {}; // 自定义工具栏动作
|
||||||
this._outlineTimer = null; // 大纲防抖计时器
|
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._renderFn = (typeof this.config.render === 'function') ? this.config.render : parseMarkdown;
|
||||||
@@ -139,6 +142,8 @@ class MarkdownEditor {
|
|||||||
|
|
||||||
this._buildToolbar();
|
this._buildToolbar();
|
||||||
this._updateModeButtons(); // 初始化按钮状态
|
this._updateModeButtons(); // 初始化按钮状态
|
||||||
|
this._bindToolbarKeyboard(); // v0.1.12: 键盘导航
|
||||||
|
this._initAriaLive(); // v0.1.12: 屏幕阅读器
|
||||||
this._bindEvents();
|
this._bindEvents();
|
||||||
|
|
||||||
// 初始内容
|
// 初始内容
|
||||||
@@ -352,17 +357,11 @@ class MarkdownEditor {
|
|||||||
ta.addEventListener('keyup', onCursorActivity);
|
ta.addEventListener('keyup', onCursorActivity);
|
||||||
|
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
// 同步行号滚动(始终同步,不限于分屏模式)
|
if (this.gutter) this.gutter.scrollTop = ta.scrollTop;
|
||||||
if (this.gutter) {
|
// v0.1.12: 标题感知同步
|
||||||
this.gutter.scrollTop = ta.scrollTop;
|
if (this.config.syncScroll && this._mode === 'split') {
|
||||||
|
this._syncScrollByHeading();
|
||||||
}
|
}
|
||||||
// 同步预览滚动(仅分屏模式)
|
|
||||||
if (!this.config.syncScroll || this._mode !== 'split') return;
|
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
ta.addEventListener('scroll', onScroll);
|
ta.addEventListener('scroll', onScroll);
|
||||||
|
|
||||||
@@ -913,6 +912,8 @@ class MarkdownEditor {
|
|||||||
split: () => this.setMode('split'),
|
split: () => this.setMode('split'),
|
||||||
preview: () => this.setMode('preview'),
|
preview: () => this.setMode('preview'),
|
||||||
fullscreen: () => this.toggleFullscreen(),
|
fullscreen: () => this.toggleFullscreen(),
|
||||||
|
zen: () => this.toggleZen(), // v0.1.12
|
||||||
|
wordwrap: () => this.toggleWordWrap(), // v0.1.12
|
||||||
};
|
};
|
||||||
const fn = actions[action];
|
const fn = actions[action];
|
||||||
if (fn) { fn.apply(this, args); return this; }
|
if (fn) { fn.apply(this, args); return this; }
|
||||||
@@ -1059,6 +1060,7 @@ class MarkdownEditor {
|
|||||||
if (this.textarea) this.textarea.scrollTop = taScroll;
|
if (this.textarea) this.textarea.scrollTop = taScroll;
|
||||||
if (this.previewPane) this.previewPane.scrollTop = pvScroll;
|
if (this.previewPane) this.previewPane.scrollTop = pvScroll;
|
||||||
this._emit('modeChange', mode);
|
this._emit('modeChange', mode);
|
||||||
|
this._announce(`${i18nT(mode) || mode} ${i18nT('mode') || 'mode'}`); // v0.1.12
|
||||||
if (typeof this.config.onModeChange === 'function') {
|
if (typeof this.config.onModeChange === 'function') {
|
||||||
try { this.config.onModeChange(mode, this); } catch (e) { console.error(e); }
|
try { this.config.onModeChange(mode, this); } catch (e) { console.error(e); }
|
||||||
}
|
}
|
||||||
@@ -1126,7 +1128,153 @@ class MarkdownEditor {
|
|||||||
return this;
|
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() {
|
_updateWordCount() {
|
||||||
if (!this.config.wordCount || !this.statusEl) return;
|
if (!this.config.wordCount || !this.statusEl) return;
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor i18n — 国际化管理(增强版)
|
* MetonaEditor i18n — 国际化管理(增强版)
|
||||||
* @module i18n
|
* @module i18n
|
||||||
* @version 0.1.11
|
* @version 0.1.12
|
||||||
* @description 多语言支持、实例级语言隔离、复数规则、动态加载、命名空间
|
* @description 多语言支持、实例级语言隔离、复数规则、动态加载、命名空间
|
||||||
*
|
*
|
||||||
* v0.1.6 增强:
|
* v0.1.6 增强:
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Icons — 工具栏图标SVG定义
|
* MetonaEditor Icons — 工具栏图标SVG定义
|
||||||
* @module icons
|
* @module icons
|
||||||
* @version 0.1.11
|
* @version 0.1.12
|
||||||
* @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.11
|
* @version 0.1.12
|
||||||
* @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.11';
|
const VERSION = '0.1.12';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局默认插件队列
|
* 全局默认插件队列
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Locales — 国际化翻译数据
|
* MetonaEditor Locales — 国际化翻译数据
|
||||||
* @module locales
|
* @module locales
|
||||||
* @version 0.1.11
|
* @version 0.1.12
|
||||||
* @description 内置 zh-CN / en-US 完整翻译
|
* @description 内置 zh-CN / en-US 完整翻译
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Parser - 轻量 Markdown 解析器(增强版)
|
* MetonaEditor Parser - 轻量 Markdown 解析器(增强版)
|
||||||
* @module parser
|
* @module parser
|
||||||
* @version 0.1.11
|
* @version 0.1.12
|
||||||
* @description 自研零依赖 Markdown 解析器,覆盖 CommonMark 子集 + GFM 扩展
|
* @description 自研零依赖 Markdown 解析器,覆盖 CommonMark 子集 + GFM 扩展
|
||||||
*
|
*
|
||||||
* v0.1.4 增强:
|
* v0.1.4 增强:
|
||||||
|
|||||||
+91
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Plugins — 插件系统 v2
|
* MetonaEditor Plugins — 插件系统 v2
|
||||||
* @module plugins
|
* @module plugins
|
||||||
* @version 0.1.11
|
* @version 0.1.12
|
||||||
* @description 插件管理器 + 预设插件 + 依赖/生命周期/异步/卸载
|
* @description 插件管理器 + 预设插件 + 依赖/生命周期/异步/卸载
|
||||||
*
|
*
|
||||||
* v0.1.6 增强:
|
* v0.1.6 增强:
|
||||||
@@ -510,6 +510,95 @@ const imagePastePlugin = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快捷键帮助插件(v0.1.12)
|
||||||
|
* 按 ? 弹出快捷键列表面板
|
||||||
|
*/
|
||||||
|
const shortcutHelpPlugin = {
|
||||||
|
name: 'shortcutHelp',
|
||||||
|
version: '0.1.0',
|
||||||
|
description: '按 ? 查看快捷键列表',
|
||||||
|
priority: 200,
|
||||||
|
|
||||||
|
_onKeydown: null,
|
||||||
|
_panel: null,
|
||||||
|
|
||||||
|
install(editor) {
|
||||||
|
if (!editor || !editor.textarea || typeof document === 'undefined') return;
|
||||||
|
|
||||||
|
this._injectStyle();
|
||||||
|
this._onKeydown = (e) => {
|
||||||
|
// ? 键(Shift+/),且不在输入框中时
|
||||||
|
if (e.key === '?' && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
this._open(editor);
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape' && this._panel) {
|
||||||
|
this._close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
editor.textarea.addEventListener('keydown', this._onKeydown);
|
||||||
|
},
|
||||||
|
|
||||||
|
_injectStyle() {
|
||||||
|
if (document.getElementById('me-shortcut-style')) return;
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'me-shortcut-style';
|
||||||
|
style.textContent = `.me-shortcut-overlay{position:fixed;top:0;left:0;right:0;bottom:0;z-index:50;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center}.me-shortcut-panel{background:var(--md-bg,#fff);border-radius:12px;padding:24px;max-width:560px;width:90%;max-height:80vh;overflow-y:auto;box-shadow:0 12px 40px rgba(0,0,0,0.3)}.me-shortcut-panel h3{font-size:16px;margin:0 0 16px;color:var(--md-text)}.me-shortcut-panel table{width:100%;border-collapse:collapse;font-size:13px}.me-shortcut-panel td{padding:6px 10px;border-bottom:1px solid var(--md-border)}.me-shortcut-panel td:first-child{font-family:var(--md-mono);font-size:12px;color:var(--md-accent);white-space:nowrap;width:40%}.me-shortcut-panel td:last-child{color:var(--md-text)}.me-shortcut-panel .me-shortcut-close{position:absolute;top:16px;right:20px;background:none;border:none;font-size:20px;cursor:pointer;color:var(--md-muted)}`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
},
|
||||||
|
|
||||||
|
_open(editor) {
|
||||||
|
if (this._panel) { this._close(); return; }
|
||||||
|
|
||||||
|
const builtin = [
|
||||||
|
['Ctrl+B', '粗体'], ['Ctrl+I', '斜体'], ['Ctrl+U', '下划线'],
|
||||||
|
['Ctrl+K', '链接'], ['Ctrl+E', '行内代码'],
|
||||||
|
['Ctrl+1/2/3', '标题 H1/H2/H3'], ['Ctrl+Q', '引用'],
|
||||||
|
['Ctrl+Z', '撤销'], ['Ctrl+Y / Ctrl+Shift+Z', '重做'],
|
||||||
|
['Ctrl+S', '保存'], ['Ctrl+F', '查找'], ['Ctrl+H', '替换'],
|
||||||
|
['Tab', '缩进'], ['Shift+Tab', '反缩进'],
|
||||||
|
['Ctrl+Shift+T', 'Zen 模式演示快捷键'],
|
||||||
|
['?', '显示/隐藏快捷键帮助'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const custom = (editor.getShortcuts && editor.getShortcuts()) || [];
|
||||||
|
const customRows = custom.map((s) => [s.combo, s.description || s.combo]);
|
||||||
|
const allRows = [...builtin, ...customRows];
|
||||||
|
|
||||||
|
let rows = '';
|
||||||
|
allRows.forEach(([combo, desc]) => {
|
||||||
|
rows += `<tr><td>${combo}</td><td>${desc}</td></tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'me-shortcut-overlay';
|
||||||
|
overlay.innerHTML = `<div class="me-shortcut-panel"><h3>⌨️ 快捷键</h3><button class="me-shortcut-close">×</button><table>${rows}</table></div>`;
|
||||||
|
overlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === overlay || e.target.classList.contains('me-shortcut-close')) {
|
||||||
|
this._close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
this._panel = overlay;
|
||||||
|
},
|
||||||
|
|
||||||
|
_close() {
|
||||||
|
if (this._panel) {
|
||||||
|
this._panel.remove();
|
||||||
|
this._panel = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy(editor) {
|
||||||
|
this._close();
|
||||||
|
if (this._onKeydown && editor && editor.textarea) {
|
||||||
|
editor.textarea.removeEventListener('keydown', this._onKeydown);
|
||||||
|
}
|
||||||
|
this._onKeydown = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// ============ 预设插件表 ============
|
// ============ 预设插件表 ============
|
||||||
|
|
||||||
const presetPlugins = {
|
const presetPlugins = {
|
||||||
@@ -517,6 +606,7 @@ const presetPlugins = {
|
|||||||
exportTool: exportToolPlugin,
|
exportTool: exportToolPlugin,
|
||||||
searchReplace: searchReplacePlugin,
|
searchReplace: searchReplacePlugin,
|
||||||
imagePaste: imagePastePlugin,
|
imagePaste: imagePastePlugin,
|
||||||
|
shortcutHelp: shortcutHelpPlugin,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============ 实用工具 ============
|
// ============ 实用工具 ============
|
||||||
|
|||||||
+13
-2
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Styles - 编辑器样式
|
* MetonaEditor Styles - 编辑器样式
|
||||||
* @module styles
|
* @module styles
|
||||||
* @version 0.1.11
|
* @version 0.1.12
|
||||||
* @description 编辑器 UI 样式注入与管理
|
* @description 编辑器 UI 样式注入与管理
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -94,7 +94,8 @@ const generateCSS = () => {
|
|||||||
.me-btn:active { transform: scale(0.92); }
|
.me-btn:active { transform: scale(0.92); }
|
||||||
.me-btn.me-active { background: var(--md-accent); color: #fff; }
|
.me-btn.me-active { background: var(--md-accent); color: #fff; }
|
||||||
.me-btn:disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; }
|
.me-btn:disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; }
|
||||||
.me-btn:focus-visible { outline: 2px solid var(--md-accent); outline-offset: 1px; }
|
.me-btn:focus-visible { outline: 2px solid var(--md-accent); outline-offset: 2px; }
|
||||||
|
.me-btn:focus:not(:focus-visible) { outline: none; }
|
||||||
.me-btn svg { width: 17px; height: 17px; display: block; }
|
.me-btn svg { width: 17px; height: 17px; display: block; }
|
||||||
.me-btn span { font-size: 12px; font-weight: 500; }
|
.me-btn span { font-size: 12px; font-weight: 500; }
|
||||||
|
|
||||||
@@ -521,6 +522,16 @@ const generateCSS = () => {
|
|||||||
.me-outline-l3 a { padding-left: 20px; font-size: 12px; }
|
.me-outline-l3 a { padding-left: 20px; font-size: 12px; }
|
||||||
.me-outline-l4 a { padding-left: 28px; font-size: 12px; }
|
.me-outline-l4 a { padding-left: 28px; font-size: 12px; }
|
||||||
|
|
||||||
|
/* ============ Zen 模式(v0.1.12) ============ */
|
||||||
|
.me-wrapper.me-zen .me-body { max-width: 820px; margin: 0 auto; }
|
||||||
|
.me-wrapper.me-zen .me-textarea { font-size: 15px; line-height: 1.8; }
|
||||||
|
|
||||||
|
/* ============ 屏幕阅读器专用(v0.1.12) ============ */
|
||||||
|
.me-sr-only {
|
||||||
|
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
||||||
|
overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============ 打印样式 ============ */
|
/* ============ 打印样式 ============ */
|
||||||
@media print {
|
@media print {
|
||||||
.me-wrapper { border: 0 !important; box-shadow: none !important; }
|
.me-wrapper { border: 0 !important; box-shadow: none !important; }
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Themes — 主题系统(增强版)
|
* MetonaEditor Themes — 主题系统(增强版)
|
||||||
* @module themes
|
* @module themes
|
||||||
* @version 0.1.11
|
* @version 0.1.12
|
||||||
* @description 全局+实例级主题管理、CSS 变量隔离、外部主题跟随、主题继承
|
* @description 全局+实例级主题管理、CSS 变量隔离、外部主题跟随、主题继承
|
||||||
*
|
*
|
||||||
* v0.1.5 增强:
|
* v0.1.5 增强:
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaEditor Utils - 工具函数
|
* MetonaEditor Utils - 工具函数
|
||||||
* @module utils
|
* @module utils
|
||||||
* @version 0.1.11
|
* @version 0.1.12
|
||||||
* @description 通用工具函数集合
|
* @description 通用工具函数集合
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1482,3 +1482,53 @@ describe('MarkdownEditor - v0.1.7 outline', () => {
|
|||||||
ed.destroy();
|
ed.destroy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============ v0.1.12 新增测试 ============
|
||||||
|
|
||||||
|
describe('MarkdownEditor - v0.1.12 Zen 模式', () => {
|
||||||
|
test('toggleZen 切换状态', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, {});
|
||||||
|
expect(ed.isZen()).toBe(false);
|
||||||
|
ed.toggleZen();
|
||||||
|
expect(ed.isZen()).toBe(true);
|
||||||
|
expect(ed.el.classList.contains('me-zen')).toBe(true);
|
||||||
|
ed.toggleZen();
|
||||||
|
expect(ed.isZen()).toBe(false);
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MarkdownEditor - v0.1.12 Word Wrap', () => {
|
||||||
|
test('toggleWordWrap 切换换行', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, {});
|
||||||
|
expect(ed.isWordWrap()).toBe(true);
|
||||||
|
ed.toggleWordWrap();
|
||||||
|
expect(ed.isWordWrap()).toBe(false);
|
||||||
|
expect(ed.textarea.style.whiteSpace).toBe('pre');
|
||||||
|
ed.toggleWordWrap();
|
||||||
|
expect(ed.isWordWrap()).toBe(true);
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MarkdownEditor - v0.1.12 shortcutHelp 插件', () => {
|
||||||
|
test('按 ? 弹出快捷键面板', () => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const c = document.createElement('div');
|
||||||
|
document.body.appendChild(c);
|
||||||
|
const ed = new MarkdownEditor(c, { value: 'test' });
|
||||||
|
ed.use('shortcutHelp');
|
||||||
|
ed.textarea.dispatchEvent(new KeyboardEvent('keydown', { key: '?', bubbles: true }));
|
||||||
|
expect(document.querySelector('.me-shortcut-overlay')).not.toBeNull();
|
||||||
|
// cleanup
|
||||||
|
const panel = document.querySelector('.me-shortcut-overlay');
|
||||||
|
if (panel) panel.remove();
|
||||||
|
ed.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// Type definitions for MetonaEditor v0.1.11
|
// Type definitions for MetonaEditor v0.1.12
|
||||||
// Project: https://git.metona.cn/MetonaTeam/MetonaEditor
|
// Project: https://git.metona.cn/MetonaTeam/MetonaEditor
|
||||||
// Author: thzxx
|
// Author: thzxx
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user