diff --git a/site/demo.html b/site/demo.html index d15bffe..e7713b8 100644 --- a/site/demo.html +++ b/site/demo.html @@ -229,15 +229,8 @@ bar.addEventListener('click', function(e){ else if (cmd === 'outline') { toggleOutline(btn); } else if (cmd === 'wrap') { editor.toggleWordWrap(); btn.classList.toggle('on', !editor.isWordWrap()); } else if (cmd === 'float') { - var hasFloat = editor._floatingToolbar && editor._floatingToolbar.parentNode; - if (hasFloat) { - if (editor._floatingToolbar.parentNode) editor._floatingToolbar.parentNode.removeChild(editor._floatingToolbar); - editor._floatingToolbar = null; - } else { - editor.config.floatingToolbar = true; - editor._initFloatingToolbar(); - } - btn.classList.toggle('on', !!editor._floatingToolbar); + editor.toggleFloatingToolbar(); + btn.classList.toggle('on', editor.isFloatingToolbar()); } else if (cmd === 'toast') { var types = ['success','error','warning','info']; diff --git a/src/core.ts b/src/core.ts index 90dea25..2fba20a 100644 --- a/src/core.ts +++ b/src/core.ts @@ -84,6 +84,7 @@ export class MarkdownEditor { _i18nCtx!: InstanceI18n; _floatingToolbar!: HTMLElement | null; _selectionTimer!: ReturnType | null; + _floatingEnabled!: boolean; constructor(container: string | HTMLElement, options: EditorOptions = {}) { if (!isBrowser()) { @@ -108,6 +109,7 @@ export class MarkdownEditor { this._outlineTimer = null; this._zenMode = false; this._wordWrap = true; 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; @@ -716,13 +718,15 @@ export class MarkdownEditor { document.head.appendChild(style); } + if (!this._floatingEnabled) return; + const ta = this.textarea; const show = () => { - if (this._destroyed || this.config.readOnly) return; + if (this._destroyed || this.config.readOnly || !this._floatingEnabled) return; const start = ta.selectionStart; const end = ta.selectionEnd; if (start === end) { this._hideFloatingToolbar(); return; } - if (!this._floatingToolbar) this._buildFloatingToolbar(); + if (!this._floatingToolbar || !this._floatingToolbar.parentNode) this._buildFloatingToolbar(); // Position the toolbar near the selection const rect = ta.getBoundingClientRect(); @@ -731,10 +735,9 @@ export class MarkdownEditor { const currentLine = lines.length - 1; const lineHeight = parseInt(getComputedStyle(ta).lineHeight, 10) || 22; - let top = rect.top + currentLine * lineHeight - 38; // above the selection + let top = rect.top + currentLine * lineHeight - 38; let left = rect.left + (ta.selectionDirection !== 'backward' ? (ta.selectionEnd - ta.selectionStart) / 2 : 0) * 8 + 10; - // Clamp to stay within the editor pane if (top < rect.top) top = rect.top + currentLine * lineHeight + lineHeight + 4; if (left < rect.left) left = rect.left + 4; const maxLeft = rect.right - 200; @@ -747,35 +750,55 @@ export class MarkdownEditor { const hide = () => { this._hideFloatingToolbar(); }; - // Detect selection changes via events + throttled polling let lastSelStart = ta.selectionStart; let lastSelEnd = ta.selectionEnd; - const check = () => { - if (this._destroyed || !document.body.contains(ta)) return; + 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 }); } - }; - // Use longer interval for polling (won't block event loop as much) - this._selectionTimer = setInterval(check, 500) as any; + }, 500) as any; this._cleanups.push(() => { if (this._selectionTimer) { clearInterval(this._selectionTimer); this._selectionTimer = null; } }); - ta.addEventListener('mouseup', () => setTimeout(show, 0)); + ta.addEventListener('mouseup', () => { if (this._floatingEnabled) setTimeout(show, 0); }); ta.addEventListener('keyup', () => { - const s = ta.selectionStart; const e = ta.selectionEnd; - if (s !== e) setTimeout(show, 0); + if (!this._floatingEnabled) return; + 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', () => setTimeout(() => { - if (ta.selectionStart === ta.selectionEnd) hide(); - }, 0)); + ta.addEventListener('click', () => { if (ta.selectionStart === ta.selectionEnd) setTimeout(hide, 0); }); } + 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._floatingToolbar) { + if (this._floatingToolbar.parentNode) this._floatingToolbar.parentNode.removeChild(this._floatingToolbar); + this._floatingToolbar = null; + } + } + return this; + } + isFloatingToolbar(): boolean { return this._floatingEnabled; } + _buildFloatingToolbar(): void { const bar = document.createElement('div'); bar.className = 'me-float-toolbar';