/** * Format painter: copy the inline formatting (marks) at the current selection, * then apply it to the next selection the user makes — like the paint-roller * tool in word processors. The plugin only holds the copied marks and an * "armed" flag; the host drives arm/apply through the exported helpers. */ import { EditorState, Plugin, PluginKey, Transaction } from 'prosemirror-state'; import { Mark } from 'prosemirror-model'; interface FormatPainterState { marks: readonly Mark[] | null; } export const formatPainterKey = new PluginKey('nileFormatPainter'); type PainterMeta = { type: 'arm'; marks: readonly Mark[] } | { type: 'disarm' }; export function formatPainterPlugin(): Plugin { return new Plugin({ key: formatPainterKey, state: { init: () => ({ marks: null }), apply(tr, value) { const meta = tr.getMeta(formatPainterKey) as PainterMeta | undefined; if (meta?.type === 'arm') return { marks: meta.marks }; if (meta?.type === 'disarm') return { marks: null }; return value; }, }, props: { attributes: state => (formatPainterKey.getState(state)?.marks ? { class: 'nile-wysiwyg--painting' } : {}) as Record, }, }); } /** True while the painter is armed (marks captured, waiting to apply). */ export function isFormatPainterArmed(state: EditorState): boolean { return !!formatPainterKey.getState(state)?.marks; } /** The inline marks to copy from the current selection/caret. */ function capturedMarks(state: EditorState): readonly Mark[] { const { $from, empty } = state.selection; if (empty) return state.storedMarks ?? $from.marks(); // Marks that hold across the selection start — the visible formatting. return $from.marksAcross(state.selection.$to) ?? $from.marks(); } /** * Arms the painter, capturing the current formatting. Returns a transaction to * dispatch, or null when there is nothing to copy. */ export function armFormatPainter(state: EditorState): Transaction | null { const marks = capturedMarks(state); return state.tr.setMeta(formatPainterKey, { type: 'arm', marks } as PainterMeta); } /** Disarms the painter without applying. */ export function disarmFormatPainter(state: EditorState): Transaction { return state.tr.setMeta(formatPainterKey, { type: 'disarm' } as PainterMeta); } /** * Applies the copied formatting to the current (non-empty) selection and * disarms. Returns null when not armed or the selection is empty. */ export function applyFormatPainter(state: EditorState): Transaction | null { const painter = formatPainterKey.getState(state); if (!painter?.marks) return null; const { from, to, empty } = state.selection; if (empty) return null; const tr = state.tr; tr.removeMark(from, to, null); for (const mark of painter.marks) tr.addMark(from, to, mark); tr.setMeta(formatPainterKey, { type: 'disarm' } as PainterMeta); return tr; }