/* * ribbit-editor.ts — WYSIWYG editing extension for Ribbit. */ import { HopDown } from './hopdown'; import { defaultTags, defaultBlockTags, defaultInlineTags, inlineTag } from './tags'; import { defaultTheme } from './default-theme'; import { Ribbit, RibbitPlugin, RibbitSettings, camelCase, decodeHtmlEntities, encodeHtmlEntities } from './ribbit'; /** * WYSIWYG markdown editor with VIEW, EDIT, and WYSIWYG modes. * * Extends Ribbit with contentEditable support and bidirectional * markdown↔HTML conversion on mode switches. * * Usage: * const editor = new RibbitEditor({ editorId: 'my-element' }); * editor.run(); * editor.wysiwyg(); // switch to WYSIWYG mode * editor.edit(); // switch to source editing mode * editor.view(); // switch to read-only view */ export class RibbitEditor extends Ribbit { run(): void { this.states = { VIEW: 'view', EDIT: 'edit', WYSIWYG: 'wysiwyg' }; this.#bindEvents(); this.plugins().forEach(plugin => { plugin.setEditable(); }); this.element.classList.add('loaded'); this.view(); } #bindEvents(): void { this.element.addEventListener('input', () => { if (this.state !== this.states.VIEW) { this.notifyChange(); } }); } htmlToMarkdown(html?: string): string { return this.converter.toMarkdown(html || this.element.innerHTML); } getMarkdown(): string { if (this.getState() === this.states.EDIT) { let html = this.element.innerHTML; html = html.replace(/<(?:div|br)>/ig, ''); html = html.replace(/<\/div>/ig, '\n'); this.cachedMarkdown = decodeHtmlEntities(html); } else if (this.getState() === this.states.WYSIWYG) { this.cachedMarkdown = this.htmlToMarkdown(this.element.innerHTML); } if (!this.cachedMarkdown) { this.cachedMarkdown = this.element.textContent || ''; } return this.cachedMarkdown; } wysiwyg(): void { if (this.getState() === this.states.WYSIWYG) return; this.changed = false; this.element.contentEditable = 'true'; this.element.innerHTML = this.getHTML(); Array.from(this.element.querySelectorAll('.macro')).forEach(el => { const macroEl = el as HTMLElement; if (macroEl.dataset.editable === 'false') { macroEl.contentEditable = 'false'; macroEl.style.opacity = '0.5'; } }); this.setState(this.states.WYSIWYG); } edit(): void { if (!this.theme.features?.sourceMode) { return; } if (this.state === this.states.EDIT) return; this.changed = false; this.element.contentEditable = 'true'; this.element.innerHTML = encodeHtmlEntities(this.getMarkdown()); this.setState(this.states.EDIT); } insertAtCursor(node: Node): void { const sel = window.getSelection()!; const range = sel.getRangeAt(0); range.deleteContents(); range.insertNode(node); range.setStartAfter(node); this.element.focus(); sel.removeAllRanges(); sel.addRange(range); } } // Attach public API to window for