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) {
|
|
|
|
|
this.changed = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attach public API to window for <script> tag usage.
|
|
|
|
|
(window as any).HopDown = HopDown;
|
|
|
|
|
(window as any).inlineTag = inlineTag;
|
|
|
|
|
(window as any).defaultTags = defaultTags;
|
|
|
|
|
(window as any).defaultBlockTags = defaultBlockTags;
|
|
|
|
|
(window as any).defaultInlineTags = defaultInlineTags;
|
2026-04-28 18:17:32 -07:00
|
|
|
(window as any).defaultTheme = defaultTheme;
|
2026-04-28 09:59:30 -07:00
|
|
|
(window as any).Ribbit = Ribbit;
|
|
|
|
|
(window as any).RibbitEditor = RibbitEditor;
|
|
|
|
|
(window as any).RibbitPlugin = RibbitPlugin;
|
|
|
|
|
(window as any).camelCase = camelCase;
|
|
|
|
|
(window as any).decodeHtmlEntities = decodeHtmlEntities;
|
|
|
|
|
(window as any).encodeHtmlEntities = encodeHtmlEntities;
|