Implement Toolbar

This commit is contained in:
gsb
2026-04-29 07:11:31 +00:00
parent 98719ec8cd
commit 1f523cbc0f
8 changed files with 694 additions and 122 deletions
+18 -87
View File
@@ -7,57 +7,24 @@ import { defaultTheme } from './default-theme';
import { ThemeManager } from './theme-manager';
import { RibbitEmitter, type RibbitEventMap } from './events';
import { type MacroDef } from './macros';
import type { RibbitTheme } from './types';
import { ToolbarManager } from './toolbar';
import type { RibbitTheme, ToolbarSlot } from './types';
export interface RibbitSettings {
api?: unknown;
editorId?: string;
plugins?: Array<{ new(settings: { name: string; wiki: Ribbit }): RibbitPlugin; name: string }>;
currentTheme?: string;
themes?: RibbitTheme[];
themesPath?: string;
macros?: MacroDef[];
toolbar?: ToolbarSlot[];
/** Set to false to prevent auto-rendering the toolbar. Default true. */
autoToolbar?: boolean;
on?: Partial<RibbitEventMap>;
}
/**
* Base class for editor plugins. Subclass and override toHTML/toMarkdown
* to add custom processing hooks.
*/
export class RibbitPlugin {
name: string;
wiki: Ribbit;
precedence: number;
constructor(settings: { name: string; wiki: Ribbit }) {
this.name = settings.name;
this.wiki = settings.wiki;
this.precedence = 50;
}
setEditable(): void {
}
toMarkdown(html: string): string {
return html;
}
toHTML(md: string): string {
return md;
}
}
/**
* Read-only markdown viewer. Renders markdown content into an HTML element.
*
* Usage:
* const viewer = new Ribbit({
* editorId: 'my-element',
* on: {
* ready: ({ mode, theme }) => console.log(`Ready in ${mode}`),
* },
* });
* viewer.run();
*/
export class Ribbit {
api: unknown;
@@ -67,11 +34,12 @@ export class Ribbit {
cachedMarkdown: string | null;
state: string | null;
changed: boolean;
enabledPlugins: Record<string, RibbitPlugin>;
theme: RibbitTheme;
themes: ThemeManager;
converter: HopDown;
themesPath: string;
toolbar: ToolbarManager;
protected autoToolbar: boolean;
private emitter: RibbitEmitter;
private macros: MacroDef[];
@@ -88,7 +56,6 @@ export class Ribbit {
this.cachedMarkdown = null;
this.state = null;
this.changed = false;
this.enabledPlugins = {};
this.themes = new ThemeManager(defaultTheme, this.themesPath, (theme, previous) => {
this.theme = theme;
@@ -117,13 +84,6 @@ export class Ribbit {
? new HopDown({ tags: this.theme.tags, macros: this.macros })
: new HopDown({ macros: this.macros });
(settings.plugins || []).forEach(plugin => {
this.enabledPlugins[plugin.name] = new plugin({
name: plugin.name,
wiki: this,
});
});
if (settings.on) {
for (const [event, handler] of Object.entries(settings.on)) {
if (handler) {
@@ -131,30 +91,29 @@ export class Ribbit {
}
}
}
this.toolbar = new ToolbarManager(
this,
this.theme.tags || {},
this.macros,
settings.toolbar,
);
this.autoToolbar = settings.autoToolbar !== false;
}
/**
* Register a callback for an event.
*
* editor.on('save', ({ markdown }) => {
* fetch('/api/save', { method: 'POST', body: markdown });
* });
*/
on<K extends keyof RibbitEventMap>(event: K, callback: RibbitEventMap[K]): void {
this.emitter.on(event, callback);
}
/**
* Remove a previously registered callback.
*
* editor.off('change', myHandler);
*/
off<K extends keyof RibbitEventMap>(event: K, callback: RibbitEventMap[K]): void {
this.emitter.off(event, callback);
}
run(): void {
this.element.classList.add('loaded');
if (this.autoToolbar) {
this.element.parentNode?.insertBefore(this.toolbar.render(), this.element);
}
this.view();
this.emitter.emit('ready', {
markdown: this.getMarkdown(),
@@ -164,10 +123,6 @@ export class Ribbit {
});
}
plugins(): RibbitPlugin[] {
return Object.values(this.enabledPlugins).sort((a, b) => a.precedence - b.precedence);
}
getState(): string | null {
return this.state;
}
@@ -203,12 +158,6 @@ export class Ribbit {
return this.cachedMarkdown;
}
/**
* Request a save. Fires the 'save' event with the current content.
* The consumer's callback handles persistence.
*
* editor.save(); // triggers on.save({ markdown, html })
*/
save(): void {
this.emitter.emit('save', {
markdown: this.getMarkdown(),
@@ -223,20 +172,12 @@ export class Ribbit {
this.element.contentEditable = 'false';
}
/**
* Invalidate cached markdown and HTML. Called when content changes.
* The next call to getMarkdown() or getHTML() will recompute.
*/
invalidateCache(): void {
this.changed = true;
this.cachedMarkdown = null;
this.cachedHTML = null;
}
/**
* Notify that content has changed. Called internally by the editor
* on input events. Fires the 'change' event with current content.
*/
notifyChange(): void {
this.emitter.emit('change', {
markdown: this.getMarkdown(),
@@ -245,10 +186,6 @@ export class Ribbit {
}
}
/**
* Convert a string to title case, splitting on whitespace.
* Returns an array of capitalized words.
*/
export function camelCase(words: string): string[] {
return words.trim().split(/\s+/g).map(word => {
const lc = word.toLowerCase();
@@ -256,18 +193,12 @@ export function camelCase(words: string): string[] {
});
}
/**
* Decode HTML entities in a string using a textarea element.
*/
export function decodeHtmlEntities(html: string): string {
const txt = document.createElement('textarea');
txt.innerHTML = html;
return txt.value;
}
/**
* Encode HTML-significant characters as numeric entities.
*/
export function encodeHtmlEntities(str: string): string {
return str.replace(/[\u00A0-\u9999<>&]/g, i => '&#' + i.charCodeAt(0) + ';');
}