fix: 移除 setInterval 轮询,改用纯事件驱动 selectionChange — 修复 CI 卡死问题
CI / test (20.x) (push) Canceled after 0s
CI / test (22.x) (push) Canceled after 0s
CI / test (24.x) (push) Canceled after 0s
CI / test (18.x) (push) Canceled after 1s

This commit is contained in:
2026-07-25 15:30:43 +08:00
parent 60588745d3
commit b48468f599
10 changed files with 98 additions and 237 deletions
+21 -58
View File
@@ -2695,7 +2695,6 @@ class MarkdownEditor {
this._syncing = false;
this._zenMouseHandler = null;
this._floatingToolbar = null;
this._selectionTimer = null;
this._floatingEnabled = this.config.floatingToolbar !== false;
this._renderFn = (typeof this.config.render === 'function') ? this.config.render : parseMarkdown;
this._highlightFn = (typeof this.config.highlight === 'function') ? this.config.highlight : null;
@@ -3701,9 +3700,21 @@ class MarkdownEditor {
if (!this._floatingEnabled)
return;
const ta = this.textarea;
let _lastSelStart = ta.selectionStart;
let _lastSelEnd = ta.selectionEnd;
const emitSelectionChange = () => {
const s = ta.selectionStart;
const e = ta.selectionEnd;
if (s !== _lastSelStart || e !== _lastSelEnd) {
_lastSelStart = s;
_lastSelEnd = e;
this._emit('selectionChange', { start: s, end: e, text: this._value.slice(s, e) });
}
};
const show = () => {
if (this._destroyed || this.config.readOnly || !this._floatingEnabled)
return;
emitSelectionChange();
const start = ta.selectionStart;
const end = ta.selectionEnd;
if (start === end) {
@@ -3712,7 +3723,6 @@ class MarkdownEditor {
}
if (!this._floatingToolbar || !this._floatingToolbar.parentNode)
this._buildFloatingToolbar();
// Position the toolbar near the selection (toolbar is absolute inside editorPane)
const taRect = ta.getBoundingClientRect();
const paneRect = this.editorPane.getBoundingClientRect();
const taStyle = getComputedStyle(ta);
@@ -3720,24 +3730,19 @@ class MarkdownEditor {
const paddingTop = parseFloat(taStyle.paddingTop) || 16;
const paddingLeft = parseFloat(taStyle.paddingLeft) || 18;
const fontSize = parseFloat(taStyle.fontSize) || 13.5;
// Monospace: char width ≈ fontSize * 0.6
const charWidth = fontSize * 0.6;
const textBefore = ta.value.substring(0, start);
const lines = textBefore.split('\n');
const currentLine = lines.length - 1;
const currentCol = textBefore.length - textBefore.lastIndexOf('\n') - 1;
// Middle column of selection
const midCol = currentCol + Math.floor((end - start) / 2 * ((ta.selectionDirection === 'backward') ? -1 : 1));
// Vertical: line position in textarea content, then convert to pane-local
const lineViewportTop = taRect.top + paddingTop + currentLine * lineHeight - ta.scrollTop;
const lineLocalTop = lineViewportTop - paneRect.top;
const toolbarHeight = 36;
const gap = 4;
let top = lineLocalTop - toolbarHeight - gap;
if (top < gap) {
if (top < gap)
top = lineLocalTop + lineHeight + gap;
}
// Horizontal: column position in textarea content, convert to pane-local
const colX = taRect.left + paddingLeft + midCol * charWidth - ta.scrollLeft;
let left = colX - paneRect.left - 80;
left = Math.max(4, Math.min(left, paneRect.width - 200));
@@ -3746,29 +3751,9 @@ class MarkdownEditor {
this._floatingToolbar.classList.add('me-visible');
};
const hide = () => { this._hideFloatingToolbar(); };
let lastSelStart = ta.selectionStart;
let lastSelEnd = ta.selectionEnd;
this._selectionTimer = setInterval(() => {
if (this._destroyed || !document.body.contains(ta) || !this._floatingEnabled)
return;
const s = ta.selectionStart;
const e = ta.selectionEnd;
if (s !== lastSelStart || e !== lastSelEnd) {
lastSelStart = s;
lastSelEnd = e;
const text = this._value.slice(s, e);
this._emit('selectionChange', { start: s, end: e, text });
}
}, 500);
this._cleanups.push(() => { if (this._selectionTimer) {
clearInterval(this._selectionTimer);
this._selectionTimer = null;
} });
ta.addEventListener('mouseup', () => { if (this._floatingEnabled)
setTimeout(show, 0); });
ta.addEventListener('mouseup', () => setTimeout(show, 0));
ta.addEventListener('keyup', () => {
if (!this._floatingEnabled)
return;
emitSelectionChange();
if (ta.selectionStart !== ta.selectionEnd)
setTimeout(show, 0);
else
@@ -3776,33 +3761,15 @@ class MarkdownEditor {
this._emit('cursorMove', this.getCursorPosition());
});
ta.addEventListener('blur', () => setTimeout(hide, 300));
ta.addEventListener('click', () => { if (ta.selectionStart === ta.selectionEnd)
setTimeout(hide, 0); });
ta.addEventListener('click', () => {
emitSelectionChange();
if (ta.selectionStart === ta.selectionEnd)
hide();
});
}
toggleFloatingToolbar() {
this._floatingEnabled = !this._floatingEnabled;
if (this._floatingEnabled) {
// Re-enable: restart the interval
if (!this._selectionTimer) {
const ta = this.textarea;
this._selectionTimer = setInterval(() => {
if (this._destroyed || !this._floatingEnabled)
return;
ta.selectionStart;
ta.selectionEnd;
}, 500);
this._cleanups.push(() => { if (this._selectionTimer) {
clearInterval(this._selectionTimer);
this._selectionTimer = null;
} });
}
}
else {
// Disable: clear interval and hide toolbar
if (this._selectionTimer) {
clearInterval(this._selectionTimer);
this._selectionTimer = null;
}
if (!this._floatingEnabled) {
if (this._floatingToolbar) {
if (this._floatingToolbar.parentNode)
this._floatingToolbar.parentNode.removeChild(this._floatingToolbar);
@@ -4334,10 +4301,6 @@ class MarkdownEditor {
clearTimeout(this._historyTimer);
if (this._outlineTimer)
clearTimeout(this._outlineTimer);
if (this._selectionTimer) {
clearInterval(this._selectionTimer);
this._selectionTimer = null;
}
this._cleanups.forEach((fn) => { try {
fn();
}
+1 -1
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -291,7 +291,6 @@ declare class MarkdownEditor {
_themeCtx: any;
_i18nCtx: InstanceI18n;
_floatingToolbar: HTMLElement | null;
_selectionTimer: ReturnType<typeof setInterval> | null;
_floatingEnabled: boolean;
constructor(container: string | HTMLElement, options?: EditorOptions);
_buildDOM(): void;
+21 -58
View File
@@ -2691,7 +2691,6 @@ class MarkdownEditor {
this._syncing = false;
this._zenMouseHandler = null;
this._floatingToolbar = null;
this._selectionTimer = null;
this._floatingEnabled = this.config.floatingToolbar !== false;
this._renderFn = (typeof this.config.render === 'function') ? this.config.render : parseMarkdown;
this._highlightFn = (typeof this.config.highlight === 'function') ? this.config.highlight : null;
@@ -3697,9 +3696,21 @@ class MarkdownEditor {
if (!this._floatingEnabled)
return;
const ta = this.textarea;
let _lastSelStart = ta.selectionStart;
let _lastSelEnd = ta.selectionEnd;
const emitSelectionChange = () => {
const s = ta.selectionStart;
const e = ta.selectionEnd;
if (s !== _lastSelStart || e !== _lastSelEnd) {
_lastSelStart = s;
_lastSelEnd = e;
this._emit('selectionChange', { start: s, end: e, text: this._value.slice(s, e) });
}
};
const show = () => {
if (this._destroyed || this.config.readOnly || !this._floatingEnabled)
return;
emitSelectionChange();
const start = ta.selectionStart;
const end = ta.selectionEnd;
if (start === end) {
@@ -3708,7 +3719,6 @@ class MarkdownEditor {
}
if (!this._floatingToolbar || !this._floatingToolbar.parentNode)
this._buildFloatingToolbar();
// Position the toolbar near the selection (toolbar is absolute inside editorPane)
const taRect = ta.getBoundingClientRect();
const paneRect = this.editorPane.getBoundingClientRect();
const taStyle = getComputedStyle(ta);
@@ -3716,24 +3726,19 @@ class MarkdownEditor {
const paddingTop = parseFloat(taStyle.paddingTop) || 16;
const paddingLeft = parseFloat(taStyle.paddingLeft) || 18;
const fontSize = parseFloat(taStyle.fontSize) || 13.5;
// Monospace: char width ≈ fontSize * 0.6
const charWidth = fontSize * 0.6;
const textBefore = ta.value.substring(0, start);
const lines = textBefore.split('\n');
const currentLine = lines.length - 1;
const currentCol = textBefore.length - textBefore.lastIndexOf('\n') - 1;
// Middle column of selection
const midCol = currentCol + Math.floor((end - start) / 2 * ((ta.selectionDirection === 'backward') ? -1 : 1));
// Vertical: line position in textarea content, then convert to pane-local
const lineViewportTop = taRect.top + paddingTop + currentLine * lineHeight - ta.scrollTop;
const lineLocalTop = lineViewportTop - paneRect.top;
const toolbarHeight = 36;
const gap = 4;
let top = lineLocalTop - toolbarHeight - gap;
if (top < gap) {
if (top < gap)
top = lineLocalTop + lineHeight + gap;
}
// Horizontal: column position in textarea content, convert to pane-local
const colX = taRect.left + paddingLeft + midCol * charWidth - ta.scrollLeft;
let left = colX - paneRect.left - 80;
left = Math.max(4, Math.min(left, paneRect.width - 200));
@@ -3742,29 +3747,9 @@ class MarkdownEditor {
this._floatingToolbar.classList.add('me-visible');
};
const hide = () => { this._hideFloatingToolbar(); };
let lastSelStart = ta.selectionStart;
let lastSelEnd = ta.selectionEnd;
this._selectionTimer = setInterval(() => {
if (this._destroyed || !document.body.contains(ta) || !this._floatingEnabled)
return;
const s = ta.selectionStart;
const e = ta.selectionEnd;
if (s !== lastSelStart || e !== lastSelEnd) {
lastSelStart = s;
lastSelEnd = e;
const text = this._value.slice(s, e);
this._emit('selectionChange', { start: s, end: e, text });
}
}, 500);
this._cleanups.push(() => { if (this._selectionTimer) {
clearInterval(this._selectionTimer);
this._selectionTimer = null;
} });
ta.addEventListener('mouseup', () => { if (this._floatingEnabled)
setTimeout(show, 0); });
ta.addEventListener('mouseup', () => setTimeout(show, 0));
ta.addEventListener('keyup', () => {
if (!this._floatingEnabled)
return;
emitSelectionChange();
if (ta.selectionStart !== ta.selectionEnd)
setTimeout(show, 0);
else
@@ -3772,33 +3757,15 @@ class MarkdownEditor {
this._emit('cursorMove', this.getCursorPosition());
});
ta.addEventListener('blur', () => setTimeout(hide, 300));
ta.addEventListener('click', () => { if (ta.selectionStart === ta.selectionEnd)
setTimeout(hide, 0); });
ta.addEventListener('click', () => {
emitSelectionChange();
if (ta.selectionStart === ta.selectionEnd)
hide();
});
}
toggleFloatingToolbar() {
this._floatingEnabled = !this._floatingEnabled;
if (this._floatingEnabled) {
// Re-enable: restart the interval
if (!this._selectionTimer) {
const ta = this.textarea;
this._selectionTimer = setInterval(() => {
if (this._destroyed || !this._floatingEnabled)
return;
ta.selectionStart;
ta.selectionEnd;
}, 500);
this._cleanups.push(() => { if (this._selectionTimer) {
clearInterval(this._selectionTimer);
this._selectionTimer = null;
} });
}
}
else {
// Disable: clear interval and hide toolbar
if (this._selectionTimer) {
clearInterval(this._selectionTimer);
this._selectionTimer = null;
}
if (!this._floatingEnabled) {
if (this._floatingToolbar) {
if (this._floatingToolbar.parentNode)
this._floatingToolbar.parentNode.removeChild(this._floatingToolbar);
@@ -4330,10 +4297,6 @@ class MarkdownEditor {
clearTimeout(this._historyTimer);
if (this._outlineTimer)
clearTimeout(this._outlineTimer);
if (this._selectionTimer) {
clearInterval(this._selectionTimer);
this._selectionTimer = null;
}
this._cleanups.forEach((fn) => { try {
fn();
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+21 -58
View File
@@ -2697,7 +2697,6 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
this._syncing = false;
this._zenMouseHandler = null;
this._floatingToolbar = null;
this._selectionTimer = null;
this._floatingEnabled = this.config.floatingToolbar !== false;
this._renderFn = (typeof this.config.render === 'function') ? this.config.render : parseMarkdown;
this._highlightFn = (typeof this.config.highlight === 'function') ? this.config.highlight : null;
@@ -3703,9 +3702,21 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
if (!this._floatingEnabled)
return;
const ta = this.textarea;
let _lastSelStart = ta.selectionStart;
let _lastSelEnd = ta.selectionEnd;
const emitSelectionChange = () => {
const s = ta.selectionStart;
const e = ta.selectionEnd;
if (s !== _lastSelStart || e !== _lastSelEnd) {
_lastSelStart = s;
_lastSelEnd = e;
this._emit('selectionChange', { start: s, end: e, text: this._value.slice(s, e) });
}
};
const show = () => {
if (this._destroyed || this.config.readOnly || !this._floatingEnabled)
return;
emitSelectionChange();
const start = ta.selectionStart;
const end = ta.selectionEnd;
if (start === end) {
@@ -3714,7 +3725,6 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
}
if (!this._floatingToolbar || !this._floatingToolbar.parentNode)
this._buildFloatingToolbar();
// Position the toolbar near the selection (toolbar is absolute inside editorPane)
const taRect = ta.getBoundingClientRect();
const paneRect = this.editorPane.getBoundingClientRect();
const taStyle = getComputedStyle(ta);
@@ -3722,24 +3732,19 @@ 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;
// Monospace: char width ≈ fontSize * 0.6
const charWidth = fontSize * 0.6;
const textBefore = ta.value.substring(0, start);
const lines = textBefore.split('\n');
const currentLine = lines.length - 1;
const currentCol = textBefore.length - textBefore.lastIndexOf('\n') - 1;
// Middle column of selection
const midCol = currentCol + Math.floor((end - start) / 2 * ((ta.selectionDirection === 'backward') ? -1 : 1));
// Vertical: line position in textarea content, then convert to pane-local
const lineViewportTop = taRect.top + paddingTop + currentLine * lineHeight - ta.scrollTop;
const lineLocalTop = lineViewportTop - paneRect.top;
const toolbarHeight = 36;
const gap = 4;
let top = lineLocalTop - toolbarHeight - gap;
if (top < gap) {
if (top < gap)
top = lineLocalTop + lineHeight + gap;
}
// Horizontal: column position in textarea content, convert to pane-local
const colX = taRect.left + paddingLeft + midCol * charWidth - ta.scrollLeft;
let left = colX - paneRect.left - 80;
left = Math.max(4, Math.min(left, paneRect.width - 200));
@@ -3748,29 +3753,9 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
this._floatingToolbar.classList.add('me-visible');
};
const hide = () => { this._hideFloatingToolbar(); };
let lastSelStart = ta.selectionStart;
let lastSelEnd = ta.selectionEnd;
this._selectionTimer = setInterval(() => {
if (this._destroyed || !document.body.contains(ta) || !this._floatingEnabled)
return;
const s = ta.selectionStart;
const e = ta.selectionEnd;
if (s !== lastSelStart || e !== lastSelEnd) {
lastSelStart = s;
lastSelEnd = e;
const text = this._value.slice(s, e);
this._emit('selectionChange', { start: s, end: e, text });
}
}, 500);
this._cleanups.push(() => { if (this._selectionTimer) {
clearInterval(this._selectionTimer);
this._selectionTimer = null;
} });
ta.addEventListener('mouseup', () => { if (this._floatingEnabled)
setTimeout(show, 0); });
ta.addEventListener('mouseup', () => setTimeout(show, 0));
ta.addEventListener('keyup', () => {
if (!this._floatingEnabled)
return;
emitSelectionChange();
if (ta.selectionStart !== ta.selectionEnd)
setTimeout(show, 0);
else
@@ -3778,33 +3763,15 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
this._emit('cursorMove', this.getCursorPosition());
});
ta.addEventListener('blur', () => setTimeout(hide, 300));
ta.addEventListener('click', () => { if (ta.selectionStart === ta.selectionEnd)
setTimeout(hide, 0); });
ta.addEventListener('click', () => {
emitSelectionChange();
if (ta.selectionStart === ta.selectionEnd)
hide();
});
}
toggleFloatingToolbar() {
this._floatingEnabled = !this._floatingEnabled;
if (this._floatingEnabled) {
// Re-enable: restart the interval
if (!this._selectionTimer) {
const ta = this.textarea;
this._selectionTimer = setInterval(() => {
if (this._destroyed || !this._floatingEnabled)
return;
ta.selectionStart;
ta.selectionEnd;
}, 500);
this._cleanups.push(() => { if (this._selectionTimer) {
clearInterval(this._selectionTimer);
this._selectionTimer = null;
} });
}
}
else {
// Disable: clear interval and hide toolbar
if (this._selectionTimer) {
clearInterval(this._selectionTimer);
this._selectionTimer = null;
}
if (!this._floatingEnabled) {
if (this._floatingToolbar) {
if (this._floatingToolbar.parentNode)
this._floatingToolbar.parentNode.removeChild(this._floatingToolbar);
@@ -4336,10 +4303,6 @@ dl{margin:.8em 0}dt{font-weight:650;margin:.6em 0 .2em}dd{margin:0 0 .3em 1.6em;
clearTimeout(this._historyTimer);
if (this._outlineTimer)
clearTimeout(this._outlineTimer);
if (this._selectionTimer) {
clearInterval(this._selectionTimer);
this._selectionTimer = null;
}
this._cleanups.forEach((fn) => { try {
fn();
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+21 -41
View File
@@ -83,7 +83,6 @@ export class MarkdownEditor {
_themeCtx: any;
_i18nCtx!: InstanceI18n;
_floatingToolbar!: HTMLElement | null;
_selectionTimer!: ReturnType<typeof setInterval> | null;
_floatingEnabled!: boolean;
constructor(container: string | HTMLElement, options: EditorOptions = {}) {
@@ -108,7 +107,7 @@ export class MarkdownEditor {
this._shortcuts = []; this._contextMenuItems = []; this._customActions = {};
this._outlineTimer = null; this._zenMode = false; this._wordWrap = true; this._syncing = false;
this._zenMouseHandler = null;
this._floatingToolbar = null; this._selectionTimer = null;
this._floatingToolbar = null;
this._floatingEnabled = this.config.floatingToolbar !== false;
this._renderFn = (typeof this.config.render === 'function') ? this.config.render : parseMarkdown;
@@ -721,14 +720,25 @@ export class MarkdownEditor {
if (!this._floatingEnabled) return;
const ta = this.textarea;
let _lastSelStart = ta.selectionStart;
let _lastSelEnd = ta.selectionEnd;
const emitSelectionChange = () => {
const s = ta.selectionStart; const e = ta.selectionEnd;
if (s !== _lastSelStart || e !== _lastSelEnd) {
_lastSelStart = s; _lastSelEnd = e;
this._emit('selectionChange', { start: s, end: e, text: this._value.slice(s, e) });
}
};
const show = () => {
if (this._destroyed || this.config.readOnly || !this._floatingEnabled) return;
emitSelectionChange();
const start = ta.selectionStart; const end = ta.selectionEnd;
if (start === end) { this._hideFloatingToolbar(); return; }
if (!this._floatingToolbar || !this._floatingToolbar.parentNode) this._buildFloatingToolbar();
// Position the toolbar near the selection (toolbar is absolute inside editorPane)
const taRect = ta.getBoundingClientRect();
const paneRect = this.editorPane.getBoundingClientRect();
const taStyle = getComputedStyle(ta);
@@ -736,27 +746,21 @@ export class MarkdownEditor {
const paddingTop = parseFloat(taStyle.paddingTop) || 16;
const paddingLeft = parseFloat(taStyle.paddingLeft) || 18;
const fontSize = parseFloat(taStyle.fontSize) || 13.5;
// Monospace: char width ≈ fontSize * 0.6
const charWidth = fontSize * 0.6;
const textBefore = ta.value.substring(0, start);
const lines = textBefore.split('\n');
const currentLine = lines.length - 1;
const currentCol = textBefore.length - textBefore.lastIndexOf('\n') - 1;
// Middle column of selection
const midCol = currentCol + Math.floor((end - start) / 2 * ((ta.selectionDirection === 'backward') ? -1 : 1));
// Vertical: line position in textarea content, then convert to pane-local
const lineViewportTop = taRect.top + paddingTop + currentLine * lineHeight - ta.scrollTop;
const lineLocalTop = lineViewportTop - paneRect.top;
const toolbarHeight = 36;
const gap = 4;
let top = lineLocalTop - toolbarHeight - gap;
if (top < gap) {
top = lineLocalTop + lineHeight + gap;
}
if (top < gap) top = lineLocalTop + lineHeight + gap;
// Horizontal: column position in textarea content, convert to pane-local
const colX = taRect.left + paddingLeft + midCol * charWidth - ta.scrollLeft;
let left = colX - paneRect.left - 80;
left = Math.max(4, Math.min(left, paneRect.width - 200));
@@ -768,46 +772,23 @@ export class MarkdownEditor {
const hide = () => { this._hideFloatingToolbar(); };
let lastSelStart = ta.selectionStart;
let lastSelEnd = ta.selectionEnd;
this._selectionTimer = setInterval(() => {
if (this._destroyed || !document.body.contains(ta) || !this._floatingEnabled) return;
const s = ta.selectionStart; const e = ta.selectionEnd;
if (s !== lastSelStart || e !== lastSelEnd) {
lastSelStart = s; lastSelEnd = e;
const text = this._value.slice(s, e);
this._emit('selectionChange', { start: s, end: e, text });
}
}, 500) as any;
this._cleanups.push(() => { if (this._selectionTimer) { clearInterval(this._selectionTimer); this._selectionTimer = null; } });
ta.addEventListener('mouseup', () => { if (this._floatingEnabled) setTimeout(show, 0); });
ta.addEventListener('mouseup', () => setTimeout(show, 0));
ta.addEventListener('keyup', () => {
if (!this._floatingEnabled) return;
emitSelectionChange();
if (ta.selectionStart !== ta.selectionEnd) setTimeout(show, 0);
else setTimeout(hide, 0);
this._emit('cursorMove', this.getCursorPosition());
});
ta.addEventListener('blur', () => setTimeout(hide, 300));
ta.addEventListener('click', () => { if (ta.selectionStart === ta.selectionEnd) setTimeout(hide, 0); });
ta.addEventListener('click', () => {
emitSelectionChange();
if (ta.selectionStart === ta.selectionEnd) hide();
});
}
toggleFloatingToolbar(): this {
this._floatingEnabled = !this._floatingEnabled;
if (this._floatingEnabled) {
// Re-enable: restart the interval
if (!this._selectionTimer) {
const ta = this.textarea;
this._selectionTimer = setInterval(() => {
if (this._destroyed || !this._floatingEnabled) return;
const s = ta.selectionStart; const e = ta.selectionEnd;
if (s !== e) { /* will show via keyup/mouseup */ }
}, 500) as any;
this._cleanups.push(() => { if (this._selectionTimer) { clearInterval(this._selectionTimer); this._selectionTimer = null; } });
}
} else {
// Disable: clear interval and hide toolbar
if (this._selectionTimer) { clearInterval(this._selectionTimer); this._selectionTimer = null; }
if (!this._floatingEnabled) {
if (this._floatingToolbar) {
if (this._floatingToolbar.parentNode) this._floatingToolbar.parentNode.removeChild(this._floatingToolbar);
this._floatingToolbar = null;
@@ -1161,7 +1142,6 @@ export class MarkdownEditor {
if (this._renderRaf) cancelAnimationFrame(this._renderRaf);
if (this._historyTimer) clearTimeout(this._historyTimer);
if (this._outlineTimer) clearTimeout(this._outlineTimer);
if (this._selectionTimer) { clearInterval(this._selectionTimer); this._selectionTimer = null; }
this._cleanups.forEach((fn) => { try { fn(); } catch (_) {} }); this._cleanups = [];
this._shortcuts = []; this._contextMenuItems = []; this._customActions = {};
if (this._floatingToolbar && this._floatingToolbar.parentNode) this._floatingToolbar.parentNode.removeChild(this._floatingToolbar);
+10 -17
View File
@@ -2179,13 +2179,10 @@ describe('MarkdownEditor - v0.2.3 浮动工具栏', () => {
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: 'hello world' });
// 浮动工具栏应被注入 CSS
const style = document.getElementById('me-float-style');
expect(style).not.toBeNull();
// _floatingToolbar 在首次 selection 时才创建 DOM
expect(ed._floatingToolbar).toBeNull();
// _selectionTimer 已启动
expect(ed._selectionTimer).not.toBeNull();
expect(ed._floatingEnabled).toBe(true);
ed.destroy();
});
@@ -2194,8 +2191,7 @@ describe('MarkdownEditor - v0.2.3 浮动工具栏', () => {
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: 'hello', floatingToolbar: false });
// 浮动工具栏应不启动(_selectionTimer 为 null
expect(ed._selectionTimer).toBeNull();
expect(ed._floatingEnabled).toBe(false);
expect(ed._floatingToolbar).toBeNull();
ed.destroy();
});
@@ -2224,26 +2220,23 @@ describe('MarkdownEditor - v0.2.3 浮动工具栏', () => {
expect(ed._floatingToolbar).not.toBeNull();
ed.destroy();
expect(ed._floatingToolbar).toBeNull();
expect(ed._selectionTimer).toBeNull();
});
test('selectionChange 事件触发', (done) => {
test('selectionChange 事件触发', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: 'hello world' });
const handler = jest.fn();
ed.on('selectionChange', handler);
// Fire keyup after setting selection — selectionChange is emitted synchronously in the handler
ed.textarea.setSelectionRange(0, 5);
// selectionChange 通过 interval 检测,等 300ms
setTimeout(() => {
expect(handler).toHaveBeenCalled();
expect(handler.mock.calls[0][0]).toHaveProperty('start', 0);
expect(handler.mock.calls[0][0]).toHaveProperty('end', 5);
expect(handler.mock.calls[0][0]).toHaveProperty('text', 'hello');
ed.destroy();
done();
}, 800);
ed.textarea.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight', bubbles: true }));
expect(handler).toHaveBeenCalled();
expect(handler.mock.calls[0][0]).toHaveProperty('start', 0);
expect(handler.mock.calls[0][0]).toHaveProperty('end', 5);
expect(handler.mock.calls[0][0]).toHaveProperty('text', 'hello');
ed.destroy();
});
test('cursorMove 事件在按键后触发', () => {