Implement Toolbar
This commit is contained in:
+18
-87
@@ -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) + ';');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user