build: 提交 dist 构建产物(v0.4.2 完整 5 格式 + sourcemap + 类型声明)
CI / test-parser (push) Successful in 9m47s
CI / test-core (push) Successful in 12m44s
CI / test-rest (push) Failing after 31s
CI / e2e (push) Successful in 9m45s
CI / verify (18.x) (push) Successful in 9m54s
CI / verify (20.x) (push) Failing after 11s
CI / verify (24.x) (push) Successful in 10m11s
CI / test-parser (push) Successful in 9m47s
CI / test-core (push) Successful in 12m44s
CI / test-rest (push) Failing after 31s
CI / e2e (push) Successful in 9m45s
CI / verify (18.x) (push) Successful in 9m54s
CI / verify (20.x) (push) Failing after 11s
CI / verify (24.x) (push) Successful in 10m11s
This commit is contained in:
Vendored
+141
-37
@@ -191,7 +191,7 @@
|
||||
renderError: '렌더링 실패',
|
||||
outline: '개요',
|
||||
cut: '잘라내기', copy: '복사', paste: '붙여넣기', selectAll: '전체 선택',
|
||||
regex: '정규식', shortcuts: '단축키', imageTooLarge: '이미지가 너무 큽니다({size}KB > {max}KB)',
|
||||
regex: '정규식', shortcuts: '단축키', imageTooLarge: '이미지가 너무 큽니다 ({size}KB > {max}KB)',
|
||||
},
|
||||
fr: {
|
||||
bold: 'Gras', italic: 'Italique', underline: 'Souligné', strikethrough: 'Barré',
|
||||
@@ -780,6 +780,18 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
// 语言切换即时刷新已渲染的 UI(状态栏统计标签、大纲标题),无需等待下一次输入
|
||||
if (typeof editor._updateWordCount === 'function') {
|
||||
try {
|
||||
editor._updateWordCount();
|
||||
}
|
||||
catch (_) { }
|
||||
}
|
||||
if (editor.el && typeof editor.el.querySelector === 'function') {
|
||||
const outlineTitle = editor.el.querySelector('.me-outline-title');
|
||||
if (outlineTitle)
|
||||
outlineTitle.textContent = instanceT('outline') || 'Outline';
|
||||
}
|
||||
return instanceLocale;
|
||||
};
|
||||
return {
|
||||
@@ -1773,6 +1785,9 @@
|
||||
};
|
||||
// ============ Preset plugins ============
|
||||
const escapeAttr = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
/** 实例级翻译:优先用 editor.t(实例 locale),否则退回全局 t。
|
||||
* 插件面板文案跟随实例语言而非全局语言。 */
|
||||
const instanceT = (editor) => (key, params) => (typeof editor.t === 'function' ? editor.t(key, params) : t(key, params));
|
||||
const autoSavePlugin = {
|
||||
name: 'autoSave', version: '0.1.1', description: 'Auto-save to localStorage', priority: 100,
|
||||
install(editor, options) {
|
||||
@@ -1949,6 +1964,7 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
install(editor) {
|
||||
if (!editor || !editor.textarea || typeof document === 'undefined')
|
||||
return;
|
||||
const tt = instanceT(editor);
|
||||
// Inject style once globally
|
||||
if (!document.getElementById('me-search-style')) {
|
||||
const style = document.createElement('style');
|
||||
@@ -2015,7 +2031,7 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'me-search';
|
||||
panel.dataset.replace = showReplace ? '1' : '0';
|
||||
panel.innerHTML = `<div class="me-search-row"><input type="text" class="me-search-find" placeholder="${t('searchPlaceholder') || 'Find'}" value="${escapeAttr(selected)}"/><button class="me-search-btn me-search-prev" title="${t('findPrev') || 'Previous'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></button><button class="me-search-btn me-search-next" title="${t('findNext') || 'Next'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button><span class="me-search-count"></span><button class="me-search-btn me-search-case me-active" title="${t('matchCase') || 'Match Case'}">Aa</button><button class="me-search-btn me-search-word" title="${t('wholeWord') || 'Whole Word'}">ab</button><button class="me-search-btn me-search-regex" title="${t('regex') || 'Regex'}">.*</button><button class="me-search-btn me-search-close" title="${t('close') || 'Close'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div><div class="me-search-row me-search-replace-row"><input type="text" class="me-search-replace" placeholder="${t('replacePlaceholder') || 'Replace'}"/><button class="me-search-btn me-search-replace-one">${t('replace') || 'Replace'}</button><button class="me-search-btn me-search-replace-all">${t('replaceAll') || 'All'}</button></div>`;
|
||||
panel.innerHTML = `<div class="me-search-row"><input type="text" class="me-search-find" placeholder="${tt('searchPlaceholder') || 'Find'}" value="${escapeAttr(selected)}"/><button class="me-search-btn me-search-prev" title="${tt('findPrev') || 'Previous'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></button><button class="me-search-btn me-search-next" title="${tt('findNext') || 'Next'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button><span class="me-search-count"></span><button class="me-search-btn me-search-case me-active" title="${tt('matchCase') || 'Match Case'}">Aa</button><button class="me-search-btn me-search-word" title="${tt('wholeWord') || 'Whole Word'}">ab</button><button class="me-search-btn me-search-regex" title="${tt('regex') || 'Regex'}">.*</button><button class="me-search-btn me-search-close" title="${tt('close') || 'Close'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div><div class="me-search-row me-search-replace-row"><input type="text" class="me-search-replace" placeholder="${tt('replacePlaceholder') || 'Replace'}"/><button class="me-search-btn me-search-replace-one">${tt('replace') || 'Replace'}</button><button class="me-search-btn me-search-replace-all">${tt('replaceAll') || 'All'}</button></div>`;
|
||||
editor.el.appendChild(panel);
|
||||
state._panel = panel;
|
||||
_updateReplaceVisible();
|
||||
@@ -2241,6 +2257,7 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
install(editor, options = {}) {
|
||||
if (!editor || !editor.textarea || typeof document === 'undefined')
|
||||
return;
|
||||
const tt = instanceT(editor);
|
||||
const maxSizeKB = options.maxSizeKB || 500;
|
||||
const _onPaste = (e) => {
|
||||
const items = e.clipboardData?.items;
|
||||
@@ -2255,7 +2272,7 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
if (maxSizeKB > 0 && sizeKB > maxSizeKB) {
|
||||
e.preventDefault();
|
||||
if (typeof editor.toast === 'function')
|
||||
editor.toast(t('imageTooLarge', { size: sizeKB.toFixed(0), max: maxSizeKB }) || `Image too large (${sizeKB.toFixed(0)}KB > ${maxSizeKB}KB)`, { type: 'warning' });
|
||||
editor.toast(tt('imageTooLarge', { size: sizeKB.toFixed(0), max: maxSizeKB }) || `Image too large (${sizeKB.toFixed(0)}KB > ${maxSizeKB}KB)`, { type: 'warning' });
|
||||
break;
|
||||
}
|
||||
e.preventDefault();
|
||||
@@ -2282,62 +2299,66 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
install(editor) {
|
||||
if (!editor || !editor.textarea || typeof document === 'undefined')
|
||||
return;
|
||||
const tt = instanceT(editor);
|
||||
if (!document.getElementById('me-shortcut-style')) {
|
||||
const s = document.createElement('style');
|
||||
s.id = 'me-shortcut-style';
|
||||
s.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 .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(s);
|
||||
}
|
||||
let _panel = null;
|
||||
const _close = () => { if (_panel) {
|
||||
_panel.remove();
|
||||
_panel = null;
|
||||
// 面板引用放入 holder:install 时无法预知未来打开的面板,
|
||||
// 卸载时经 holder 取实时引用,避免面板残留 DOM。
|
||||
const st = { panel: null };
|
||||
const _close = () => { if (st.panel) {
|
||||
st.panel.remove();
|
||||
st.panel = null;
|
||||
} };
|
||||
const _open = () => {
|
||||
if (_panel) {
|
||||
if (st.panel) {
|
||||
_close();
|
||||
return;
|
||||
}
|
||||
// i18n-aware shortcut labels
|
||||
// i18n-aware shortcut labels(跟随实例语言)
|
||||
const builtin = [
|
||||
['Ctrl+B', t('bold') || 'Bold'], ['Ctrl+I', t('italic') || 'Italic'],
|
||||
['Ctrl+U', t('underline') || 'Underline'], ['Ctrl+K', t('link') || 'Link'],
|
||||
['Ctrl+E', t('code') || 'Code'], ['Ctrl+1/2/3', t('h1') || 'Heading'],
|
||||
['Ctrl+Q', t('quote') || 'Quote'], ['Ctrl+Z', t('undo') || 'Undo'],
|
||||
['Ctrl+Y', t('redo') || 'Redo'], ['Ctrl+S', t('save') || 'Save'],
|
||||
['Ctrl+F', t('search') || 'Search'], ['Ctrl+H', t('replace') || 'Replace'],
|
||||
['Tab', t('indent') || 'Indent'], ['Shift+Tab', t('outdent') || 'Outdent'],
|
||||
['?', t('shortcuts') || 'Shortcuts'],
|
||||
['Ctrl+B', tt('bold') || 'Bold'], ['Ctrl+I', tt('italic') || 'Italic'],
|
||||
['Ctrl+U', tt('underline') || 'Underline'], ['Ctrl+K', tt('link') || 'Link'],
|
||||
['Ctrl+E', tt('code') || 'Code'], ['Ctrl+1/2/3', tt('h1') || 'Heading'],
|
||||
['Ctrl+Q', tt('quote') || 'Quote'], ['Ctrl+Z', tt('undo') || 'Undo'],
|
||||
['Ctrl+Y', tt('redo') || 'Redo'], ['Ctrl+S', tt('save') || 'Save'],
|
||||
['Ctrl+F', tt('search') || 'Search'], ['Ctrl+H', tt('replace') || 'Replace'],
|
||||
['Tab', tt('indent') || 'Indent'], ['Shift+Tab', tt('outdent') || 'Outdent'],
|
||||
['?', tt('shortcuts') || 'Shortcuts'],
|
||||
];
|
||||
let rows = '';
|
||||
builtin.forEach(([c, d]) => { rows += `<tr><td>${c}</td><td>${d}</td></tr>`; });
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'me-shortcut-overlay';
|
||||
overlay.innerHTML = `<div class="me-shortcut-panel"><h3>⌨️ ${t('shortcuts') || 'Shortcuts'}</h3><button class="me-shortcut-close">×</button><table>${rows}</table></div>`;
|
||||
overlay.innerHTML = `<div class="me-shortcut-panel"><h3>⌨️ ${tt('shortcuts') || 'Shortcuts'}</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'))
|
||||
_close(); });
|
||||
document.body.appendChild(overlay);
|
||||
_panel = overlay;
|
||||
st.panel = overlay;
|
||||
};
|
||||
const _onKeydown = (e) => {
|
||||
if (e.key === '?' && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
_open();
|
||||
}
|
||||
if (e.key === 'Escape' && _panel)
|
||||
if (e.key === 'Escape' && st.panel)
|
||||
_close();
|
||||
};
|
||||
editor.textarea.addEventListener('keydown', _onKeydown);
|
||||
setPluginState(editor, K_SHORTCUT, { _panel, _open, _close, _onKeydown });
|
||||
setPluginState(editor, K_SHORTCUT, { state: st, _open, _close, _onKeydown });
|
||||
},
|
||||
destroy(editor) {
|
||||
const state = pluginState(editor, K_SHORTCUT);
|
||||
if (state) {
|
||||
if (state._panel) {
|
||||
state._panel.remove();
|
||||
const exposed = pluginState(editor, K_SHORTCUT);
|
||||
if (exposed) {
|
||||
if (exposed.state && exposed.state.panel) {
|
||||
exposed.state.panel.remove();
|
||||
exposed.state.panel = null;
|
||||
}
|
||||
}
|
||||
const _onKeydown = state?._onKeydown;
|
||||
const _onKeydown = exposed?._onKeydown;
|
||||
if (_onKeydown && editor?.textarea)
|
||||
editor.textarea.removeEventListener('keydown', _onKeydown);
|
||||
deletePluginState(editor, K_SHORTCUT);
|
||||
@@ -3002,7 +3023,6 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
const paddingTop = parseFloat(taStyle.paddingTop) || 16;
|
||||
const paddingLeft = parseFloat(taStyle.paddingLeft) || 18;
|
||||
const fontSize = parseFloat(taStyle.fontSize) || 13.5;
|
||||
const charWidth = fontSize * 0.6;
|
||||
const textBefore = ta.value.substring(0, start);
|
||||
const lines = textBefore.split('\n');
|
||||
const currentLine = lines.length - 1;
|
||||
@@ -3015,7 +3035,18 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
let top = lineLocalTop - toolbarHeight - gap;
|
||||
if (top < gap)
|
||||
top = lineLocalTop + lineHeight + gap;
|
||||
const colX = taRect.left + paddingLeft + midCol * charWidth - ta.scrollLeft;
|
||||
// 逐字符估算列偏移:CJK 全角字符宽约 1em,其余按 0.6em,
|
||||
// 纯 ASCII 选区回退到原 0.6em 估算,中文选区不再整体偏左。
|
||||
const CJK_RE = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]/;
|
||||
const lineText = lines[lines.length - 1] || '';
|
||||
const measureX = (col) => {
|
||||
let w = 0;
|
||||
const n = Math.max(0, Math.min(col, lineText.length));
|
||||
for (let i = 0; i < n; i++)
|
||||
w += CJK_RE.test(lineText[i]) ? fontSize : fontSize * 0.6;
|
||||
return w;
|
||||
};
|
||||
const colX = taRect.left + paddingLeft + measureX(midCol) - ta.scrollLeft;
|
||||
let left = colX - paneRect.left - 80;
|
||||
left = Math.max(4, Math.min(left, paneRect.width - 200));
|
||||
this._floatingToolbar.style.top = top + 'px';
|
||||
@@ -3215,6 +3246,12 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
* @module outline
|
||||
* @version 0.4.0
|
||||
*/
|
||||
/** querySelector 用的 id 转义:优先原生 CSS.escape,环境缺失时退化为简易转义 */
|
||||
const cssEscape = (id) => {
|
||||
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function')
|
||||
return CSS.escape(id);
|
||||
return id.replace(/[^a-zA-Z0-9_\u00A0-\uFFFF-]/g, '\\$&');
|
||||
};
|
||||
function buildOutline() {
|
||||
if (!this.config.outline || !this.previewEl)
|
||||
return;
|
||||
@@ -3225,10 +3262,40 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
const headingRe = /<h([1-6])\s+id="([^"]+)"[^>]*>(.+?)<\/h\1>/gi;
|
||||
let m;
|
||||
while ((m = headingRe.exec(this.previewEl.innerHTML)) !== null) {
|
||||
headings.push({ level: parseInt(m[1], 10), id: m[2], text: m[3].replace(/<[^>]+>/g, '') });
|
||||
headings.push({ level: parseInt(m[1], 10), id: m[2], text: m[3].replace(/<[^>]+>/g, ''), srcLine: -1 });
|
||||
}
|
||||
if (!headings.length)
|
||||
return;
|
||||
// 计算每个渲染标题在源码中的行号(跳过围栏代码块内部),
|
||||
// 点击时按行号精确定位光标,重复标题文本也不会串位。
|
||||
const srcLines = [];
|
||||
const lines = this._value.split('\n');
|
||||
let inFence = false;
|
||||
let fenceChar = '';
|
||||
const isSetextText = (l) => /\S/.test(l) && !/^\s{0,3}([-*+]|\d+\.)\s/.test(l) && !/^\s{0,3}>/.test(l) && !/^\s{0,3}(#{1,6}\s|`{3,}|~{3,})/.test(l);
|
||||
for (let li = 0; li < lines.length; li++) {
|
||||
const l = lines[li];
|
||||
const fence = l.match(/^\s{0,3}(`{3,}|~{3,})/);
|
||||
if (fence) {
|
||||
if (!inFence) {
|
||||
inFence = true;
|
||||
fenceChar = fence[1][0];
|
||||
}
|
||||
else if (fence[1][0] === fenceChar)
|
||||
inFence = false;
|
||||
continue;
|
||||
}
|
||||
if (inFence)
|
||||
continue;
|
||||
if (/^ {0,3}(?:>\s*)*#{1,6}\s+\S/.test(l)) {
|
||||
srcLines.push(li);
|
||||
continue;
|
||||
}
|
||||
const next = lines[li + 1] || '';
|
||||
if (li + 1 < lines.length && /^ {0,3}(={3,}|-{3,})\s*$/.test(next) && isSetextText(l))
|
||||
srcLines.push(li);
|
||||
}
|
||||
headings.forEach((h, i) => { h.srcLine = i < srcLines.length ? srcLines[i] : -1; });
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'me-outline';
|
||||
panel.innerHTML = `<div class="me-outline-title">${this.t('outline') || '大纲'}</div>`;
|
||||
@@ -3247,7 +3314,7 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
continue;
|
||||
closeTo(item.level);
|
||||
h += '<ul>';
|
||||
h += `<li class="me-outline-l${item.level}"><a href="#${item.id}" data-line="${item.id}">${escapeHTML(item.text)}</a>`;
|
||||
h += `<li class="me-outline-l${item.level}"><a href="#${item.id}" data-line="${item.srcLine >= 0 ? item.srcLine + 1 : ''}">${escapeHTML(item.text)}</a>`;
|
||||
stack.push(item.level);
|
||||
}
|
||||
closeTo(-1);
|
||||
@@ -3261,10 +3328,19 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
return;
|
||||
e.preventDefault();
|
||||
const id = a.getAttribute('href').slice(1);
|
||||
const target = this.previewEl.querySelector('#' + CSS.escape(id));
|
||||
if (target) {
|
||||
const target = this.previewEl.querySelector('#' + cssEscape(id));
|
||||
if (target)
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
const idx = this._value.indexOf(target.textContent || '');
|
||||
// 光标定位:优先用构建时记录的源码行号(重复标题也正确),兜底 indexOf
|
||||
const ord = Array.prototype.indexOf.call(panel.querySelectorAll('a'), a);
|
||||
const lineIdx = ord > -1 && ord < headings.length ? headings[ord].srcLine : -1;
|
||||
if (lineIdx >= 0) {
|
||||
const lineText = lines[lineIdx] || '';
|
||||
const col = (lineText.match(/^ {0,3}(?:>\s*)*#{1,6}\s+/) || lineText.match(/^ {0,3}/) || [''])[0].length;
|
||||
this.setCursorPosition(lineIdx + 1, col);
|
||||
}
|
||||
else {
|
||||
const idx = this._value.indexOf(target?.textContent || '');
|
||||
if (idx !== -1) {
|
||||
this.textarea.focus();
|
||||
this.textarea.setSelectionRange(idx, idx);
|
||||
@@ -3282,6 +3358,10 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
function trackOutlineScroll() {
|
||||
if (!this.config.outline || !this.previewPane)
|
||||
return;
|
||||
// 防重复绑定:构造时绑定过、setOutline(true) 再次调用时跳过
|
||||
if (this._outlineScrollBound)
|
||||
return;
|
||||
this._outlineScrollBound = true;
|
||||
let ticking = false;
|
||||
const onScroll = () => {
|
||||
if (ticking)
|
||||
@@ -4094,6 +4174,10 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
canUndo() { return this._historyIndex > 0; }
|
||||
canRedo() { return this._historyIndex < this._history.length - 1; }
|
||||
_applyHistory() {
|
||||
// 撤销/重做前记录预览区滚动比例,重渲染后按比例恢复,不再跳顶
|
||||
const pv = this.previewPane;
|
||||
const pvMax = pv ? pv.scrollHeight - pv.clientHeight : 0;
|
||||
const pvRatio = pvMax > 0 ? pv.scrollTop / pvMax : null;
|
||||
this._value = this._history[this._historyIndex];
|
||||
this.textarea.value = this._value;
|
||||
this._render();
|
||||
@@ -4101,8 +4185,11 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
this._updateWordCount();
|
||||
if (this.gutter)
|
||||
this.gutter.scrollTop = this.textarea.scrollTop;
|
||||
if (this.previewPane && this.config.syncScroll)
|
||||
this.previewPane.scrollTop = 0;
|
||||
if (pv && pvRatio != null) {
|
||||
const max = pv.scrollHeight - pv.clientHeight;
|
||||
if (max > 0)
|
||||
pv.scrollTop = pvRatio * max;
|
||||
}
|
||||
this._emit('change', this._value);
|
||||
if (typeof this.config.onChange === 'function') {
|
||||
try {
|
||||
@@ -4269,6 +4356,23 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
setWordWrap(on) { this._wordWrap = !!on; if (this.textarea)
|
||||
this.textarea.style.whiteSpace = this._wordWrap ? 'pre-wrap' : 'pre'; return this; }
|
||||
isWordWrap() { return this._wordWrap; }
|
||||
/** 运行时开关大纲面板(公开 API,替代直接操作 config/_buildOutline 的私有用法) */
|
||||
setOutline(on) {
|
||||
this.config.outline = !!on;
|
||||
if (this._destroyed || !this.el)
|
||||
return this;
|
||||
if (this.config.outline) {
|
||||
this._buildOutline();
|
||||
this._trackOutlineScroll();
|
||||
}
|
||||
else {
|
||||
const panel = this.el.querySelector('.me-outline');
|
||||
if (panel)
|
||||
panel.remove();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
isOutline() { return !!this.config.outline; }
|
||||
_bindToolbarKeyboard() {
|
||||
if (!this.toolbarEl)
|
||||
return;
|
||||
@@ -5041,9 +5145,9 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
|
||||
/**
|
||||
* MetonaEditor — Type-safe, lightweight Markdown Editor
|
||||
* @module metona-editor
|
||||
* @version 0.4.1
|
||||
* @version 0.4.2
|
||||
*/
|
||||
const VERSION = '0.4.1';
|
||||
const VERSION = '0.4.2';
|
||||
const globalPlugins = [];
|
||||
// 全局插件在 afterCreate 安装:此时 DOM 已构建完成,依赖 textarea/el 的插件
|
||||
// (searchReplace / shortcutHelp / imagePaste)才能正常生效。
|
||||
|
||||
Reference in New Issue
Block a user