Files
MetonaEditor/site/demo.html
T
thzxx 8d2e172289 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
2026-07-23 21:00:09 +08:00

401 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>MetonaEditor · 功能演示</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f5f6f8;
--card: #ffffff;
--text: #1f2937;
--muted: #6b7280;
--border: #e5e7eb;
--accent: #3b82f6;
--accent-soft: rgba(59, 130, 246, 0.1);
--danger: #ef4444;
--success: #10b981;
--code-bg: #1c2029;
--code-text: #e6e8eb;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
padding: 24px;
}
.wrap { max-width: 1120px; margin: 0 auto; }
/* 顶栏 */
.topbar {
display: flex; align-items: center; justify-content: space-between;
flex-wrap: wrap; gap: 12px;
margin-bottom: 20px;
}
.topbar h1 { font-size: 22px; font-weight: 700; display: flex; align-items: center; gap: 12px; }
.topbar h1 a {
color: var(--accent); text-decoration: none;
font-size: 13px; font-weight: 400;
padding: 4px 10px; border: 1px solid var(--border); border-radius: 6px;
transition: border-color 0.15s;
}
.topbar h1 a:hover { border-color: var(--accent); }
.controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.ctrl-label { font-size: 12px; color: var(--muted); margin-right: 4px; }
.ctrl-group {
display: inline-flex; gap: 0;
background: var(--card); border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
}
.ctrl-btn {
padding: 7px 14px; border: 0; background: transparent; cursor: pointer;
font-size: 13px; color: var(--text); transition: background 0.15s, color 0.15s;
}
.ctrl-btn:hover { background: #f3f4f6; }
.ctrl-btn.active { background: var(--accent); color: #fff; }
.ctrl-btn + .ctrl-btn { border-left: 1px solid var(--border); }
/* 编辑器 */
#editor { margin-bottom: 24px; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 20px -8px rgba(0,0,0,0.1); }
/* 网格布局 */
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 768px) {
.grid { grid-template-columns: 1fr; }
}
.panel {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px 18px;
}
.panel-title {
font-size: 13px; color: var(--muted);
margin-bottom: 10px; font-weight: 600;
display: flex; align-items: center; justify-content: space-between;
}
.panel-title .count {
background: var(--accent-soft); color: var(--accent);
padding: 2px 8px; border-radius: 999px; font-size: 11px;
}
/* 控制台 */
.console {
background: var(--code-bg); color: var(--code-text);
border-radius: 8px; padding: 14px 16px;
font-family: "SF Mono", "Cascadia Code", Consolas, monospace; font-size: 12.5px;
min-height: 140px; max-height: 240px; overflow: auto; white-space: pre-wrap; word-break: break-all;
line-height: 1.7;
}
.console:empty::before { content: "点击下方按钮查看输出..."; color: #6b7280; }
.console .log-time { color: #5c6370; }
.console .log-label { color: #82aaff; font-weight: 600; }
/* 事件流 */
.events {
background: var(--code-bg); color: var(--code-text);
border-radius: 8px; padding: 14px 16px;
font-family: "SF Mono", Consolas, monospace; font-size: 12px;
min-height: 140px; max-height: 240px; overflow: auto;
line-height: 1.7;
}
.events:empty::before { content: "事件流将在这里实时显示..."; color: #6b7280; }
.event-line { padding: 2px 0; }
.event-line .ev-name { color: #c3e88d; }
.event-line .ev-time { color: #5c6370; }
/* API 按钮 */
.api-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 8px;
margin-top: 12px;
}
.api-btn {
padding: 9px 12px; border: 1px solid var(--border); background: var(--card);
border-radius: 7px; cursor: pointer; font-size: 12.5px; color: var(--text);
transition: all 0.15s; text-align: center; font-family: "SF Mono", Consolas, monospace;
}
.api-btn:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-1px); }
.api-btn:active { transform: translateY(0); }
.api-btn.danger:hover { border-color: var(--danger); color: var(--danger); }
.api-btn.success:hover { border-color: var(--success); color: var(--success); }
.hint {
font-size: 12px; color: var(--muted);
margin-top: 14px; padding: 10px 14px;
background: var(--accent-soft); border-radius: 8px;
border-left: 3px solid var(--accent);
}
code { background: #f3f4f6; padding: 1px 6px; border-radius: 4px; font-size: 12px; font-family: "SF Mono", Consolas, monospace; }
/* 状态指示 */
.status-bar {
display: flex; gap: 16px; flex-wrap: wrap;
padding: 10px 14px;
background: var(--card); border: 1px solid var(--border);
border-radius: 8px; margin-bottom: 16px;
font-size: 12px; color: var(--muted);
}
.status-item { display: inline-flex; align-items: center; gap: 6px; }
.status-item strong { color: var(--text); font-weight: 600; }
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--success);
}
.status-dot.destroyed { background: var(--danger); }
</style>
</head>
<body>
<div class="wrap">
<div class="topbar">
<h1>
MetonaEditor 功能演示
<a href="index.html">← 返回首页</a>
</h1>
<div class="controls">
<span class="ctrl-label">主题</span>
<div class="ctrl-group" id="themeCtrl">
<button class="ctrl-btn" data-theme="light">亮色</button>
<button class="ctrl-btn" data-theme="dark">暗色</button>
<button class="ctrl-btn" data-theme="warm">暖色</button>
</div>
<span class="ctrl-label">语言</span>
<div class="ctrl-group" id="localeCtrl">
<button class="ctrl-btn" data-locale="zh-CN">中文</button>
<button class="ctrl-btn" data-locale="en-US">English</button>
</div>
</div>
</div>
<div class="status-bar" id="statusBar">
<div class="status-item">
<span class="status-dot" id="statusDot"></span>
<span>状态:</span><strong id="stState">运行中</strong>
</div>
<div class="status-item"><span>模式:</span><strong id="stMode">split</strong></div>
<div class="status-item"><span>主题:</span><strong id="stTheme">light</strong></div>
<div class="status-item"><span>语言:</span><strong id="stLocale">zh-CN</strong></div>
<div class="status-item"><span>字数:</span><strong id="stWords">0</strong></div>
<div class="status-item"><span>全屏:</span><strong id="stFs"></strong></div>
</div>
<div id="editor"></div>
<div class="grid">
<div class="panel">
<div class="panel-title">
<span>API 控制台</span>
<span class="count" id="apiCount">0 次调用</span>
</div>
<div class="console" id="console"></div>
<div class="api-grid">
<button class="api-btn" data-api="getValue">getValue()</button>
<button class="api-btn" data-api="getHTML">getHTML()</button>
<button class="api-btn" data-api="getStats">getStats()</button>
<button class="api-btn" data-api="getStatus">getStatus()</button>
<button class="api-btn" data-api="insert">insert()</button>
<button class="api-btn" data-api="exec-bold">exec('bold')</button>
<button class="api-btn" data-api="exec-h1">exec('h1')</button>
<button class="api-btn" data-api="setMode-edit">编辑模式</button>
<button class="api-btn" data-api="setMode-split">分屏模式</button>
<button class="api-btn" data-api="setMode-preview">预览模式</button>
<button class="api-btn" data-api="fullscreen">切换全屏</button>
<button class="api-btn" data-api="undo">undo()</button>
<button class="api-btn" data-api="redo">redo()</button>
<button class="api-btn success" data-api="exportMD">导出 .md</button>
<button class="api-btn success" data-api="exportHTML">导出 .html</button>
<button class="api-btn" data-api="restoreDraft">恢复草稿</button>
<button class="api-btn" data-api="clearDraft">清空草稿</button>
<button class="api-btn danger" data-api="destroy">destroy()</button>
</div>
</div>
<div class="panel">
<div class="panel-title">
<span>事件流(实时)</span>
<span class="count" id="evCount">0 个事件</span>
</div>
<div class="events" id="events"></div>
</div>
</div>
<div class="hint">
<strong>快捷键提示:</strong>
编辑区内按 <code>Ctrl+B</code> 加粗、<code>Ctrl+I</code> 斜体、<code>Ctrl+K</code> 插入链接、
<code>Ctrl+Z</code> 撤销、<code>Ctrl+S</code> 触发保存、<code>Ctrl+F</code> 查找、<code>Ctrl+H</code> 替换、
<code>Tab</code> 缩进、<code>Esc</code> 关闭面板。
</div>
</div>
<script src="../dist/metona-editor.js"></script>
<script>
let apiCalls = 0;
let eventCount = 0;
const log = (label, val) => {
apiCalls++;
document.getElementById('apiCount').textContent = apiCalls + ' 次调用';
const c = document.getElementById('console');
const now = new Date().toLocaleTimeString('zh-CN', { hour12: false });
const text = typeof val === 'string' ? val : JSON.stringify(val, null, 2);
const truncated = text.length > 500 ? text.slice(0, 500) + '\n... (已截断)' : text;
c.innerHTML = `<span class="log-time">[${now}]</span> <span class="log-label">${label}</span>\n${escapeHtml(truncated)}`;
};
const logEvent = (name, data) => {
eventCount++;
document.getElementById('evCount').textContent = eventCount + ' 个事件';
const el = document.getElementById('events');
const now = new Date().toLocaleTimeString('zh-CN', { hour12: false });
const dataStr = data !== undefined ? ' ' + (typeof data === 'string' ? data.slice(0, 40) : JSON.stringify(data).slice(0, 40)) : '';
const line = document.createElement('div');
line.className = 'event-line';
line.innerHTML = `<span class="ev-time">${now}</span> <span class="ev-name">${name}</span>${escapeHtml(dataStr)}`;
el.appendChild(line);
el.scrollTop = el.scrollHeight;
// 限制最多 50 条
while (el.children.length > 50) el.removeChild(el.firstChild);
};
function escapeHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function updateStatus() {
if (editor.isDestroyed()) {
document.getElementById('stState').textContent = '已销毁';
document.getElementById('statusDot').classList.add('destroyed');
return;
}
const s = editor.getStatus();
const stats = editor.getStats();
document.getElementById('stState').textContent = '运行中';
document.getElementById('stMode').textContent = s.mode;
document.getElementById('stTheme').textContent = s.theme;
document.getElementById('stLocale').textContent = s.locale;
document.getElementById('stWords').textContent = stats.words;
document.getElementById('stFs').textContent = s.fullscreen ? '是' : '否';
}
// 创建编辑器,启用全部预设插件
const editor = MeEditor.create('#editor', {
mode: 'split',
height: 420,
theme: 'light',
locale: 'zh-CN',
value: [
'# 功能演示',
'',
'这里是 MetonaEditor 的完整功能演示。',
'',
'## 行内格式',
'',
'- 支持 **加粗**、*斜体*、~~删除线~~、`行内代码`',
'- 支持 [链接](https://example.com) 与 ![图片](https://placehold.co/100)',
'',
'## 表格',
'',
'| 名称 | 值 |',
'| --- | --- |',
'| 版本 | 0.1.3 |',
'| 依赖 | 零 |',
'| 测试 | 390+ |',
'',
'## 任务列表',
'',
'- [x] 分屏预览',
'- [x] 插件系统',
'- [x] 主题与国际化',
'- [ ] 你的创意',
'',
'## 代码块',
'',
'```js',
'function hello(name) {',
' return `Hello, ${name}!`;',
'}',
'```',
'',
'> 试试顶部的主题/语言切换与下方的 API 按钮。',
'',
'右侧事件流会实时显示编辑器触发的事件。'
].join('\n'),
plugins: ['autoSave', 'exportTool', 'searchReplace'],
onChange: (val) => { logEvent('change', val); updateStatus(); },
onInput: (val) => logEvent('input', val),
onFocus: () => logEvent('focus'),
onBlur: () => logEvent('blur'),
onSave: () => logEvent('save'),
onModeChange: (mode) => { logEvent('modeChange', mode); updateStatus(); },
onFullscreen: (fs) => { logEvent('fullscreen', fs); updateStatus(); },
});
// 监听 autosave 事件(来自 autoSave 插件)
editor.on('autosave', (data) => logEvent('autosave', data));
// 主题切换
document.getElementById('themeCtrl').addEventListener('click', (e) => {
const btn = e.target.closest('[data-theme]');
if (!btn) return;
document.querySelectorAll('#themeCtrl .ctrl-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
MeEditor.setTheme(btn.dataset.theme);
editor.el.className = editor.el.className.replace(/me-theme-\w+/g, '') + ' me-theme-' + btn.dataset.theme;
updateStatus();
});
document.querySelector('#themeCtrl [data-theme="light"]').classList.add('active');
// 语言切换
document.getElementById('localeCtrl').addEventListener('click', (e) => {
const btn = e.target.closest('[data-locale]');
if (!btn) return;
document.querySelectorAll('#localeCtrl .ctrl-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
MeEditor.setLocale(btn.dataset.locale);
updateStatus();
});
document.querySelector('#localeCtrl [data-locale="zh-CN"]').classList.add('active');
// API 控制台
document.querySelector('.api-grid').addEventListener('click', (e) => {
const btn = e.target.closest('[data-api]');
if (!btn) return;
const api = btn.dataset.api;
if (editor.isDestroyed()) { log('error', '编辑器已销毁,请刷新页面'); return; }
switch (api) {
case 'getValue': log('getValue', editor.getValue().slice(0, 200) + '...'); break;
case 'getHTML': log('getHTML', editor.getHTML()); break;
case 'getStats': log('getStats', editor.getStats()); break;
case 'getStatus': log('getStatus', editor.getStatus()); break;
case 'insert': editor.insert('\n\n## 新插入的标题\n'); log('insert', '已插入内容'); break;
case 'exec-bold': editor.exec('bold'); log('exec', "执行 'bold'"); break;
case 'exec-h1': editor.exec('h1'); log('exec', "执行 'h1'"); break;
case 'setMode-edit': editor.setMode('edit'); log('setMode', '切换到 edit'); break;
case 'setMode-split': editor.setMode('split'); log('setMode', '切换到 split'); break;
case 'setMode-preview': editor.setMode('preview'); log('setMode', '切换到 preview'); break;
case 'fullscreen': editor.toggleFullscreen(); log('fullscreen', '已切换全屏'); break;
case 'undo': editor.undo(); log('undo', '撤销'); break;
case 'redo': editor.redo(); log('redo', '重做'); break;
case 'exportMD': editor.exportMarkdown(); log('export', '已触发 .md 下载'); break;
case 'exportHTML': editor.exportHTML('demo.html', { title: 'MetonaEditor 导出' }); log('export', '已触发 .html 下载'); break;
case 'restoreDraft': editor.restoreDraft(); log('restoreDraft', '已从 localStorage 恢复草稿'); break;
case 'clearDraft': editor.clearDraft(); log('clearDraft', '已清空草稿'); break;
case 'destroy': editor.destroy(); log('destroy', '编辑器已销毁'); updateStatus(); break;
}
});
// 初始状态
updateStatus();
logEvent('ready', '编辑器已就绪');
</script>
</body>
</html>