/* * events.ts — typed event emitter for the ribbit editor. */ import type { RibbitTheme, PeerInfo, Revision } from './types'; export interface ContentPayload { markdown: string; html: string; } export interface ModeChangePayload { current: string; previous: string | null; } export interface ThemeChangePayload { current: RibbitTheme; previous: RibbitTheme; } export interface ReadyPayload { markdown: string; html: string; mode: string; theme: RibbitTheme; } export interface RibbitEventMap { /* * Content was modified. Fires on every edit. * * editor.on('change', ({ markdown }) => { * localStorage.setItem('draft', markdown); * }); */ change: (payload: ContentPayload) => void; /* * Save requested via editor.save(), toolbar button, or Ctrl+S. * * editor.on('save', ({ markdown, html }) => { * fetch('/api/save', { method: 'POST', body: markdown }); * }); */ save: (payload: ContentPayload) => void; /* * Editor mode switched between view, edit, and wysiwyg. * * editor.on('modeChange', ({ current, previous }) => { * toolbar.toggle(current !== 'view'); * main.classList.toggle('editing', current !== 'view'); * }); */ modeChange: (payload: ModeChangePayload) => void; /* * Theme switched via editor.themes.set(). * * editor.on('themeChange', ({ current, previous }) => { * analytics.track('theme_switch', { from: previous.name, to: current.name }); * }); */ themeChange: (payload: ThemeChangePayload) => void; /* * Editor initialized and first render complete. * * editor.on('ready', ({ mode, theme }) => { * console.log(`Editor ready in ${mode} mode with ${theme.name} theme`); * }); */ ready: (payload: ReadyPayload) => void; /* * Remote users connected, disconnected, or moved their cursors. * * editor.on('peerChange', ({ peers }) => { * updateUserList(peers); * }); */ peerChange: (payload: { peers: PeerInfo[] }) => void; /* * Document lock acquired or released. * * editor.on('lockChange', ({ holder }) => { * if (holder) showBanner(`Locked by ${holder.displayName}`); * else hideBanner(); * }); */ lockChange: (payload: { holder: PeerInfo | null }) => void; /* * Remote changes received while in source mode. * * editor.on('remoteActivity', ({ count }) => { * statusBar.textContent = `${count} remote changes`; * }); */ remoteActivity: (payload: { count: number }) => void; /* * A revision was created. * * editor.on('revisionCreated', ({ revision }) => { * console.log(`Revision ${revision.id} saved`); * }); */ revisionCreated: (payload: { revision: Revision }) => void; } type EventName = keyof RibbitEventMap; /** * Typed event emitter for ribbit editor lifecycle and collaboration events. * * @example * const emitter = new RibbitEmitter(); * emitter.on('change', ({ markdown }) => console.log(markdown)); * emitter.emit('change', { markdown: '# Hello', html: '

Hello

' }); */ export class RibbitEmitter { private listeners: Map>; constructor() { this.listeners = new Map(); } /** * Register a callback for an event. * * @example * emitter.on('save', ({ markdown }) => saveDraft(markdown)); */ on(event: K, callback: RibbitEventMap[K]): void { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } this.listeners.get(event)!.add(callback); } /** * Remove a previously registered callback. * * @example * emitter.off('save', savedCallback); */ off(event: K, callback: RibbitEventMap[K]): void { this.listeners.get(event)?.delete(callback); } /** * Emit an event, calling all registered callbacks with the payload. * * @example * emitter.emit('change', { markdown: '# Title', html: '

Title

' }); */ emit(event: K, ...args: Parameters): void { for (const callback of this.listeners.get(event) || []) { callback(...args); } } }