ribbit/src/ts/events.ts

166 lines
4.4 KiB
TypeScript

/*
* 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: '<h1>Hello</h1>' });
*/
export class RibbitEmitter {
private listeners: Map<string, Set<Function>>;
constructor() {
this.listeners = new Map();
}
/**
* Register a callback for an event.
*
* @example
* emitter.on('save', ({ markdown }) => saveDraft(markdown));
*/
on<K extends EventName>(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<K extends EventName>(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: '<h1>Title</h1>' });
*/
emit<K extends EventName>(event: K, ...args: Parameters<RibbitEventMap[K]>): void {
for (const callback of this.listeners.get(event) || []) {
callback(...args);
}
}
}