2026-04-28 18:35:06 -07:00
|
|
|
/*
|
|
|
|
|
* events.ts — typed event emitter for the ribbit editor.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-04-29 09:02:38 -07:00
|
|
|
import type { RibbitTheme, PeerInfo, Revision } from './types';
|
2026-04-28 18:35:06 -07:00
|
|
|
|
|
|
|
|
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;
|
2026-04-29 09:02:38 -07:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* 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;
|
2026-04-28 18:35:06 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type EventName = keyof RibbitEventMap;
|
|
|
|
|
|
|
|
|
|
export class RibbitEmitter {
|
|
|
|
|
private listeners: Map<string, Set<Function>>;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
this.listeners = new Map();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Register a callback for an event.
|
|
|
|
|
*/
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
emit<K extends EventName>(event: K, ...args: Parameters<RibbitEventMap[K]>): void {
|
|
|
|
|
for (const callback of this.listeners.get(event) || []) {
|
|
|
|
|
callback(...args);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|