2026-04-28 09:59:30 -07:00
|
|
|
/*
|
|
|
|
|
* ribbit-editor.ts — WYSIWYG editing extension for Ribbit.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { HopDown } from './hopdown';
|
|
|
|
|
import { defaultTags, defaultBlockTags, defaultInlineTags, inlineTag } from './tags';
|
2026-04-28 18:17:32 -07:00
|
|
|
import { defaultTheme } from './default-theme';
|
2026-04-28 09:59:30 -07:00
|
|
|
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) {
|
2026-04-28 18:35:06 -07:00
|
|
|
this.notifyChange();
|
2026-04-28 09:59:30 -07:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
htmlToMarkdown(html?: string): string {
|
2026-04-28 18:17:32 -07:00
|
|
|
return this.converter.toMarkdown(html || this.element.innerHTML);
|
2026-04-28 09:59:30 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-04-28 18:17:32 -07:00
|
|
|
if (!this.theme.features?.sourceMode) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-28 09:59:30 -07:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 18:39:16 -07:00
|
|
|
// Public API — accessed as ribbit.Editor, ribbit.HopDown, etc.
|
|
|
|
|
export { RibbitEditor as Editor };
|
|
|
|
|
export { Ribbit as Viewer };
|
|
|
|
|
export { RibbitPlugin as Plugin };
|
|
|
|
|
export { HopDown };
|
|
|
|
|
export { inlineTag };
|
|
|
|
|
export { defaultTags, defaultBlockTags, defaultInlineTags };
|
|
|
|
|
export { defaultTheme };
|
|
|
|
|
export { camelCase, decodeHtmlEntities, encodeHtmlEntities };
|