@barocss/editor-view-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +89 -0
  3. package/dist/editor-view-react/src/EditorView.d.ts +14 -0
  4. package/dist/editor-view-react/src/EditorView.d.ts.map +1 -0
  5. package/dist/editor-view-react/src/EditorViewContentLayer.d.ts +9 -0
  6. package/dist/editor-view-react/src/EditorViewContentLayer.d.ts.map +1 -0
  7. package/dist/editor-view-react/src/EditorViewContext.d.ts +43 -0
  8. package/dist/editor-view-react/src/EditorViewContext.d.ts.map +1 -0
  9. package/dist/editor-view-react/src/EditorViewLayer.d.ts +8 -0
  10. package/dist/editor-view-react/src/EditorViewLayer.d.ts.map +1 -0
  11. package/dist/editor-view-react/src/EditorViewOverlayLayerContent.d.ts +14 -0
  12. package/dist/editor-view-react/src/EditorViewOverlayLayerContent.d.ts.map +1 -0
  13. package/dist/editor-view-react/src/dom-sync/classify-c1.d.ts +45 -0
  14. package/dist/editor-view-react/src/dom-sync/classify-c1.d.ts.map +1 -0
  15. package/dist/editor-view-react/src/dom-sync/edit-position.d.ts +6 -0
  16. package/dist/editor-view-react/src/dom-sync/edit-position.d.ts.map +1 -0
  17. package/dist/editor-view-react/src/index.d.ts +12 -0
  18. package/dist/editor-view-react/src/index.d.ts.map +1 -0
  19. package/dist/editor-view-react/src/input-handler.d.ts +51 -0
  20. package/dist/editor-view-react/src/input-handler.d.ts.map +1 -0
  21. package/dist/editor-view-react/src/mutation-observer-manager.d.ts +13 -0
  22. package/dist/editor-view-react/src/mutation-observer-manager.d.ts.map +1 -0
  23. package/dist/editor-view-react/src/selection-handler.d.ts +56 -0
  24. package/dist/editor-view-react/src/selection-handler.d.ts.map +1 -0
  25. package/dist/editor-view-react/src/types.d.ts +103 -0
  26. package/dist/editor-view-react/src/types.d.ts.map +1 -0
  27. package/dist/index.cjs +4 -0
  28. package/dist/index.js +11882 -0
  29. package/docs/SPEC_VERIFICATION.md +109 -0
  30. package/docs/editor-view-react-spec.md +359 -0
  31. package/docs/improvement-opportunities.md +66 -0
  32. package/docs/layers-spec.md +97 -0
  33. package/package.json +53 -0
  34. package/src/EditorView.tsx +312 -0
  35. package/src/EditorViewContentLayer.tsx +90 -0
  36. package/src/EditorViewContext.tsx +228 -0
  37. package/src/EditorViewLayer.tsx +35 -0
  38. package/src/EditorViewOverlayLayerContent.tsx +42 -0
  39. package/src/dom-sync/classify-c1.ts +91 -0
  40. package/src/dom-sync/edit-position.ts +27 -0
  41. package/src/index.ts +33 -0
  42. package/src/input-handler.ts +716 -0
  43. package/src/mutation-observer-manager.ts +65 -0
  44. package/src/selection-handler.ts +450 -0
  45. package/src/types.ts +123 -0
  46. package/test/EditorView-decorator.test.tsx +198 -0
  47. package/test/EditorView-layers.test.tsx +352 -0
  48. package/test/EditorView.test.tsx +218 -0
  49. package/test/dom-sync.test.ts +49 -0
  50. package/test/mutation-observer-manager.test.ts +48 -0
  51. package/test/selection-handler.test.ts +86 -0
  52. package/tsconfig.json +12 -0
  53. package/vite.config.ts +26 -0
  54. package/vitest.config.ts +10 -0
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@barocss/editor-view-react",
3
+ "version": "0.1.0",
4
+ "description": "React view layer for Barocss Editor (renderer-react + Editor)",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "module": "./src/index.ts",
8
+ "types": "./src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.ts",
12
+ "import": "./src/index.ts"
13
+ }
14
+ },
15
+ "peerDependencies": {
16
+ "react": ">=18.0.0",
17
+ "react-dom": ">=18.0.0"
18
+ },
19
+ "dependencies": {
20
+ "@barocss/dsl": "1.0.0",
21
+ "@barocss/editor-core": "1.0.0",
22
+ "@barocss/renderer-react": "0.1.0",
23
+ "@barocss/shared": "1.0.0",
24
+ "@barocss/text-analyzer": "0.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "@testing-library/react": "^16.0.0",
28
+ "@types/react": "^19.0.0",
29
+ "@types/react-dom": "^19.0.0",
30
+ "jsdom": "^25.0.0",
31
+ "react": "^19.0.0",
32
+ "react-dom": "^19.0.0",
33
+ "typescript": "^5.9.3",
34
+ "vite": "^5.4.21",
35
+ "vite-plugin-dts": "^3.9.1",
36
+ "vitest": "^3.0.0"
37
+ },
38
+ "keywords": [
39
+ "editor",
40
+ "view",
41
+ "react",
42
+ "wysiwyg"
43
+ ],
44
+ "author": "Barocss Team",
45
+ "license": "MIT",
46
+ "scripts": {
47
+ "build": "vite build",
48
+ "dev": "vite build --watch",
49
+ "type-check": "tsc --noEmit",
50
+ "test": "vitest",
51
+ "test:run": "vitest run"
52
+ }
53
+ }
@@ -0,0 +1,312 @@
1
+ import { forwardRef, useImperativeHandle, useRef } from 'react';
2
+ import type { RendererRegistry } from '@barocss/dsl';
3
+ import type {
4
+ EditorViewProps,
5
+ EditorViewOverlayLayerProps,
6
+ EditorViewRef,
7
+ DecoratorExportData,
8
+ LoadDecoratorsPatternFunctions,
9
+ ModelSelection,
10
+ } from './types';
11
+ import type { DecoratorQueryOptions } from '@barocss/shared';
12
+ import { EditorViewContentLayer } from './EditorViewContentLayer';
13
+ import { EditorViewLayer } from './EditorViewLayer';
14
+ import { EditorViewOverlayLayerContent } from './EditorViewOverlayLayerContent';
15
+ import { EditorViewContextProvider, useEditorViewContext } from './EditorViewContext';
16
+
17
+ interface OverlaySlotProps {
18
+ registry?: RendererRegistry;
19
+ className?: string;
20
+ style?: React.CSSProperties;
21
+ }
22
+
23
+ function DecoratorLayerSlot({ registry, className, style }: OverlaySlotProps) {
24
+ return (
25
+ <EditorViewLayer layer="decorator" className={className} style={style}>
26
+ <EditorViewOverlayLayerContent layer="decorator" registry={registry} />
27
+ </EditorViewLayer>
28
+ );
29
+ }
30
+ function SelectionLayerSlot({ registry, className, style }: OverlaySlotProps) {
31
+ return (
32
+ <EditorViewLayer layer="selection" className={className} style={style}>
33
+ <EditorViewOverlayLayerContent layer="selection" registry={registry} />
34
+ </EditorViewLayer>
35
+ );
36
+ }
37
+ function ContextLayerSlot({ registry, className, style }: OverlaySlotProps) {
38
+ return (
39
+ <EditorViewLayer layer="context" className={className} style={style}>
40
+ <EditorViewOverlayLayerContent layer="context" registry={registry} />
41
+ </EditorViewLayer>
42
+ );
43
+ }
44
+ function CustomLayerSlot({
45
+ registry,
46
+ className,
47
+ style,
48
+ children,
49
+ }: OverlaySlotProps & { children?: React.ReactNode }) {
50
+ return (
51
+ <EditorViewLayer layer="custom" className={className} style={style}>
52
+ <EditorViewOverlayLayerContent layer="custom" registry={registry} />
53
+ {children}
54
+ </EditorViewLayer>
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Inner root: lives inside EditorViewContextProvider, exposes ref API (addDecorator, removeDecorator, getDecorators).
60
+ */
61
+ const EditorViewRoot = forwardRef<EditorViewRef, { options: EditorViewProps['options']; children: EditorViewProps['children'] }>(
62
+ function EditorViewRoot({ options = {}, children }, ref) {
63
+ const {
64
+ editor,
65
+ selectionHandler,
66
+ contentEditableRef,
67
+ decoratorManagerRef,
68
+ decoratorSchemaRegistryRef,
69
+ remoteDecoratorManagerRef,
70
+ patternDecoratorConfigManagerRef,
71
+ decoratorGeneratorManagerRef,
72
+ getMergedDecorators,
73
+ bumpDecoratorVersion,
74
+ } = useEditorViewContext();
75
+ const apiRef = useRef<EditorViewRef | null>(null);
76
+
77
+ useImperativeHandle(
78
+ ref,
79
+ () => {
80
+ if (!apiRef.current) {
81
+ apiRef.current = {
82
+ addDecorator(decorator) {
83
+ if ('generate' in decorator) {
84
+ decoratorGeneratorManagerRef.current?.registerGenerator(
85
+ decorator as import('@barocss/shared').DecoratorGenerator,
86
+ bumpDecoratorVersion
87
+ );
88
+ bumpDecoratorVersion();
89
+ return;
90
+ }
91
+ decoratorManagerRef.current?.add(decorator);
92
+ },
93
+ removeDecorator(id) {
94
+ try {
95
+ decoratorManagerRef.current?.remove(id);
96
+ } catch {
97
+ // ignore if not found
98
+ }
99
+ },
100
+ updateDecorator(id, updates) {
101
+ decoratorManagerRef.current?.update(id, updates);
102
+ },
103
+ getDecorators(options?: DecoratorQueryOptions) {
104
+ const model = editor.getDocumentProxy?.() ?? null;
105
+ let list = getMergedDecorators(model);
106
+ if (options?.enabledOnly !== false) {
107
+ list = list.filter((d) => d.enabled !== false);
108
+ }
109
+ if (options?.category) list = list.filter((d) => d.category === options.category);
110
+ if (options?.type) list = list.filter((d) => d.stype === options.type);
111
+ if (options?.nodeId) {
112
+ list = list.filter((d) => {
113
+ const t = d.target;
114
+ if (!t) return false;
115
+ const sid = 'sid' in t ? t.sid : undefined;
116
+ const startSid = 'startSid' in t ? t.startSid : undefined;
117
+ const endSid = 'endSid' in t ? t.endSid : undefined;
118
+ return sid === options.nodeId || startSid === options.nodeId || endSid === options.nodeId;
119
+ });
120
+ }
121
+ const sortBy = options?.sortBy ?? 'id';
122
+ const order = options?.sortOrder ?? 'asc';
123
+ if (sortBy) {
124
+ const mult = order === 'desc' ? -1 : 1;
125
+ list = [...list].sort((a, b) => {
126
+ const av = sortBy === 'id' ? a.sid : sortBy === 'type' ? a.stype : a.category;
127
+ const bv = sortBy === 'id' ? b.sid : sortBy === 'type' ? b.stype : b.category;
128
+ return av.localeCompare(bv) * mult;
129
+ });
130
+ }
131
+ return list;
132
+ },
133
+ getDecorator(id) {
134
+ const merged = getMergedDecorators(editor.getDocumentProxy?.() ?? null);
135
+ const found = merged.find((d) => d.sid === id);
136
+ if (found) return found;
137
+ const local = decoratorManagerRef.current?.get(id);
138
+ if (local) return local;
139
+ return remoteDecoratorManagerRef.current?.get(id);
140
+ },
141
+ exportDecorators(): DecoratorExportData {
142
+ const targetDecorators = (decoratorManagerRef.current?.getAll() ?? [])
143
+ .filter((d) => d.decoratorType !== 'pattern')
144
+ .map((d) => {
145
+ const { decoratorType, ...rest } = d;
146
+ return rest;
147
+ }) as DecoratorExportData['targetDecorators'];
148
+ const patternConfigs = patternDecoratorConfigManagerRef.current?.getConfigs() ?? [];
149
+ const patternDecorators = patternConfigs
150
+ .filter((c) => c.pattern instanceof RegExp)
151
+ .map((c) => ({
152
+ sid: c.sid,
153
+ stype: c.stype,
154
+ category: c.category,
155
+ pattern: { source: (c.pattern as RegExp).source, flags: (c.pattern as RegExp).flags },
156
+ priority: c.priority,
157
+ enabled: c.enabled,
158
+ }));
159
+ return { version: '1.0.0', targetDecorators, patternDecorators };
160
+ },
161
+ loadDecorators(data: DecoratorExportData, patternFunctions?: LoadDecoratorsPatternFunctions) {
162
+ decoratorManagerRef.current?.clear();
163
+ remoteDecoratorManagerRef.current?.clear();
164
+ patternDecoratorConfigManagerRef.current?.clear();
165
+ decoratorGeneratorManagerRef.current?.clear();
166
+ for (const d of data.targetDecorators) {
167
+ decoratorManagerRef.current?.add({
168
+ ...d,
169
+ decoratorType: 'target',
170
+ } as import('@barocss/shared').Decorator);
171
+ }
172
+ for (const p of data.patternDecorators) {
173
+ const fns = patternFunctions?.[p.sid];
174
+ if (!fns) {
175
+ console.warn(`[EditorView] Pattern '${p.sid}' functions not provided; skipping.`);
176
+ continue;
177
+ }
178
+ const pattern = new RegExp(p.pattern.source, p.pattern.flags);
179
+ patternDecoratorConfigManagerRef.current?.addConfig({
180
+ sid: p.sid,
181
+ stype: p.stype,
182
+ category: p.category,
183
+ pattern,
184
+ extractData: fns.extractData,
185
+ createDecorator: fns.createDecorator,
186
+ priority: p.priority,
187
+ enabled: p.enabled,
188
+ });
189
+ }
190
+ bumpDecoratorVersion();
191
+ },
192
+ get contentEditableElement() {
193
+ return contentEditableRef.current ?? null;
194
+ },
195
+ convertModelSelectionToDOM(sel: ModelSelection | null | undefined) {
196
+ selectionHandler.convertModelSelectionToDOM(sel as Parameters<typeof selectionHandler.convertModelSelectionToDOM>[0]);
197
+ },
198
+ convertDOMSelectionToModel(selection: Selection): ModelSelection {
199
+ return selectionHandler.convertDOMSelectionToModel(selection) as ModelSelection;
200
+ },
201
+ convertStaticRangeToModel(staticRange: StaticRange): ModelSelection | null {
202
+ return selectionHandler.convertStaticRangeToModel(staticRange) as ModelSelection | null;
203
+ },
204
+ defineDecoratorType(type, category, schema) {
205
+ const reg = decoratorSchemaRegistryRef.current;
206
+ if (!reg) return;
207
+ if (category === 'layer') reg.registerLayerType(type, schema);
208
+ else if (category === 'inline') reg.registerInlineType(type, schema);
209
+ else reg.registerBlockType(type, schema);
210
+ },
211
+ get decoratorManager() {
212
+ return decoratorManagerRef.current ?? null;
213
+ },
214
+ get remoteDecoratorManager() {
215
+ return remoteDecoratorManagerRef.current ?? null;
216
+ },
217
+ get patternDecoratorConfigManager() {
218
+ return patternDecoratorConfigManagerRef.current ?? null;
219
+ },
220
+ get decoratorGeneratorManager() {
221
+ return decoratorGeneratorManagerRef.current ?? null;
222
+ },
223
+ };
224
+ }
225
+ return apiRef.current;
226
+ },
227
+ [
228
+ editor,
229
+ selectionHandler,
230
+ contentEditableRef,
231
+ decoratorManagerRef,
232
+ decoratorSchemaRegistryRef,
233
+ remoteDecoratorManagerRef,
234
+ patternDecoratorConfigManagerRef,
235
+ decoratorGeneratorManagerRef,
236
+ getMergedDecorators,
237
+ bumpDecoratorVersion,
238
+ ]
239
+ );
240
+
241
+ const { className: containerClassName = '', layers: layersConfig } = options;
242
+ const contentOptions = {
243
+ registry: options.registry,
244
+ className: 'barocss-editor-content',
245
+ editable: true,
246
+ ...layersConfig?.content,
247
+ };
248
+
249
+ return (
250
+ <div
251
+ className={containerClassName}
252
+ style={{ position: 'relative', overflow: 'hidden' }}
253
+ data-editor-view="true"
254
+ >
255
+ <EditorViewContentLayer options={contentOptions} />
256
+ <DecoratorLayerSlot
257
+ registry={options.registry}
258
+ className={layersConfig?.decorator?.className}
259
+ style={layersConfig?.decorator?.style}
260
+ />
261
+ <SelectionLayerSlot
262
+ registry={options.registry}
263
+ className={layersConfig?.selection?.className}
264
+ style={layersConfig?.selection?.style}
265
+ />
266
+ <ContextLayerSlot
267
+ registry={options.registry}
268
+ className={layersConfig?.context?.className}
269
+ style={layersConfig?.context?.style}
270
+ />
271
+ <CustomLayerSlot
272
+ registry={options.registry}
273
+ className={layersConfig?.custom?.className}
274
+ style={layersConfig?.custom?.style}
275
+ >
276
+ {children}
277
+ </CustomLayerSlot>
278
+ </div>
279
+ );
280
+ }
281
+ );
282
+
283
+ const EditorViewBase = forwardRef<EditorViewRef, EditorViewProps>(function EditorView(
284
+ { editor, options = {}, children },
285
+ ref
286
+ ) {
287
+ return (
288
+ <EditorViewContextProvider editor={editor}>
289
+ <EditorViewRoot ref={ref} options={options} children={children} />
290
+ </EditorViewContextProvider>
291
+ );
292
+ });
293
+
294
+ function createOverlayLayer(layer: 'decorator' | 'selection' | 'context' | 'custom') {
295
+ return function OverlayLayer({ className, style, children }: EditorViewOverlayLayerProps) {
296
+ return (
297
+ <EditorViewLayer layer={layer} className={className} style={style}>
298
+ {children}
299
+ </EditorViewLayer>
300
+ );
301
+ };
302
+ }
303
+
304
+ /** EditorView with ref (addDecorator, removeDecorator, getDecorators) and static layer components. */
305
+ export const EditorView = Object.assign(EditorViewBase, {
306
+ ContentLayer: EditorViewContentLayer,
307
+ DecoratorLayer: createOverlayLayer('decorator'),
308
+ SelectionLayer: createOverlayLayer('selection'),
309
+ ContextLayer: createOverlayLayer('context'),
310
+ CustomLayer: createOverlayLayer('custom'),
311
+ Layer: EditorViewLayer,
312
+ });
@@ -0,0 +1,90 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { getGlobalRegistry } from '@barocss/dsl';
3
+ import { ReactRenderer } from '@barocss/renderer-react';
4
+ import { useEditorViewContext } from './EditorViewContext';
5
+ import type { EditorViewContentLayerProps } from './types';
6
+
7
+ /**
8
+ * EditorViewContentLayer: renders the editor document with ReactRenderer in a contenteditable div.
9
+ * Subscribes to editor:content.change and editor:selection.model.
10
+ * Must be used inside EditorView (EditorViewContextProvider); editor is taken from context only.
11
+ */
12
+ export function EditorViewContentLayer({ options = {} }: EditorViewContentLayerProps) {
13
+ const {
14
+ editor,
15
+ selectionHandler,
16
+ viewStateRef,
17
+ setContentEditableElement,
18
+ getMergedDecorators,
19
+ decoratorVersion,
20
+ } = useEditorViewContext();
21
+ const { className = '', editable = true, registry } = options;
22
+
23
+ const [documentSnapshot, setDocumentSnapshot] = useState<unknown>(() => editor.getDocumentProxy?.() ?? null);
24
+ const contentRef = useRef<HTMLDivElement | null>(null);
25
+
26
+ useEffect(() => {
27
+ const onContentChange = (e: { content?: unknown }) => {
28
+ if (viewStateRef?.current?.skipNextRenderFromMO) {
29
+ viewStateRef.current.skipNextRenderFromMO = false;
30
+ return;
31
+ }
32
+ const next = e?.content ?? editor.getDocumentProxy?.() ?? null;
33
+ setDocumentSnapshot(next);
34
+ };
35
+ editor.on?.('editor:content.change', onContentChange);
36
+ setDocumentSnapshot(editor.getDocumentProxy?.() ?? null);
37
+ return () => {
38
+ editor.off?.('editor:content.change', onContentChange);
39
+ };
40
+ }, [editor, viewStateRef]);
41
+
42
+ useEffect(() => {
43
+ const el = contentRef.current;
44
+ setContentEditableElement(el);
45
+ return () => setContentEditableElement(null);
46
+ }, [setContentEditableElement]);
47
+
48
+ useEffect(() => {
49
+ const onModelSelection = (sel: unknown) => {
50
+ if (viewStateRef?.current?.skipApplyModelSelectionToDOM) return;
51
+ requestAnimationFrame(() => {
52
+ requestAnimationFrame(() => {
53
+ selectionHandler.convertModelSelectionToDOM(sel as Parameters<typeof selectionHandler.convertModelSelectionToDOM>[0]);
54
+ });
55
+ });
56
+ };
57
+ editor.on?.('editor:selection.model', onModelSelection);
58
+ return () => editor.off?.('editor:selection.model', onModelSelection);
59
+ }, [editor, selectionHandler, viewStateRef]);
60
+
61
+ const renderer = useMemo(
62
+ () => new ReactRenderer(registry ?? getGlobalRegistry()),
63
+ [registry]
64
+ );
65
+
66
+ const decorators = useMemo(
67
+ () => getMergedDecorators(documentSnapshot),
68
+ [documentSnapshot, getMergedDecorators, decoratorVersion]
69
+ );
70
+
71
+ const content = useMemo(() => {
72
+ if (documentSnapshot == null) return null;
73
+ const model = documentSnapshot as { stype?: string };
74
+ if (!model.stype) return null;
75
+ return renderer.build(model, decorators);
76
+ }, [documentSnapshot, renderer, decorators]);
77
+
78
+ return (
79
+ <div
80
+ ref={contentRef}
81
+ className={className}
82
+ contentEditable={editable}
83
+ suppressContentEditableWarning
84
+ data-bc-layer="content"
85
+ data-testid="editor-content"
86
+ >
87
+ {content}
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,228 @@
1
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
2
+ import type { Editor } from '@barocss/editor-core';
3
+ import type { Decorator } from '@barocss/shared';
4
+ import {
5
+ DecoratorManager,
6
+ RemoteDecoratorManager,
7
+ PatternDecoratorConfigManager,
8
+ DecoratorGeneratorManager,
9
+ DecoratorSchemaRegistry,
10
+ runPatternFromModel,
11
+ } from '@barocss/shared';
12
+ import type { ReactSelectionHandler } from './selection-handler';
13
+ import type { ReactInputHandler } from './input-handler';
14
+ import type { ReactMutationObserverManager } from './mutation-observer-manager';
15
+ import { ReactSelectionHandler as ReactSelectionHandlerClass } from './selection-handler';
16
+ import { ReactInputHandler as ReactInputHandlerClass } from './input-handler';
17
+ import { createMutationObserverManager } from './mutation-observer-manager';
18
+
19
+ export interface EditorViewViewState {
20
+ isModelDrivenChange: boolean;
21
+ isRendering: boolean;
22
+ isComposing: boolean;
23
+ /** When true, next editor:content.change (from model commit during MO C1) must not trigger refresh (data-only update). */
24
+ skipNextRenderFromMO: boolean;
25
+ /** When true, editor:selection.model must not call convertModelSelectionToDOM (selection came from DOM input; leave DOM selection as-is). */
26
+ skipApplyModelSelectionToDOM: boolean;
27
+ }
28
+
29
+ export interface EditorViewContextValue {
30
+ editor: Editor;
31
+ viewStateRef: React.MutableRefObject<EditorViewViewState>;
32
+ selectionHandler: ReactSelectionHandler;
33
+ inputHandler: ReactInputHandler;
34
+ mutationObserverManager: ReactMutationObserverManager;
35
+ setContentEditableElement: (el: HTMLElement | null) => void;
36
+ /** Ref to content-editable root (set by ContentLayer on mount). */
37
+ contentEditableRef: React.RefObject<HTMLElement | null>;
38
+ decoratorManagerRef: React.RefObject<DecoratorManager | null>;
39
+ decoratorSchemaRegistryRef: React.RefObject<DecoratorSchemaRegistry | null>;
40
+ remoteDecoratorManagerRef: React.RefObject<RemoteDecoratorManager | null>;
41
+ patternDecoratorConfigManagerRef: React.RefObject<PatternDecoratorConfigManager | null>;
42
+ decoratorGeneratorManagerRef: React.RefObject<DecoratorGeneratorManager | null>;
43
+ getMergedDecorators: (model: unknown) => Decorator[];
44
+ bumpDecoratorVersion: () => void;
45
+ decoratorVersion: number;
46
+ }
47
+
48
+ const EditorViewContext = createContext<EditorViewContextValue | null>(null);
49
+
50
+ export function useEditorViewContext(): EditorViewContextValue {
51
+ const value = useContext(EditorViewContext);
52
+ if (!value) {
53
+ throw new Error('useEditorViewContext must be used within EditorViewContext.Provider');
54
+ }
55
+ return value;
56
+ }
57
+
58
+ export function useOptionalEditorViewContext(): EditorViewContextValue | null {
59
+ return useContext(EditorViewContext);
60
+ }
61
+
62
+ export function EditorViewContextProvider({ editor, children }: { editor: Editor; children: ReactNode }) {
63
+ const viewStateRef = useRef<EditorViewViewState>({
64
+ isModelDrivenChange: false,
65
+ isRendering: false,
66
+ isComposing: false,
67
+ skipNextRenderFromMO: false,
68
+ skipApplyModelSelectionToDOM: false,
69
+ });
70
+
71
+ const contentEditableRef = useRef<HTMLElement | null>(null);
72
+ const getContentEditableElement = useCallback(() => contentEditableRef.current, []);
73
+
74
+ const decoratorSchemaRegistryRef = useRef<DecoratorSchemaRegistry | null>(null);
75
+ if (decoratorSchemaRegistryRef.current === null) {
76
+ decoratorSchemaRegistryRef.current = new DecoratorSchemaRegistry();
77
+ }
78
+ const decoratorManagerRef = useRef<DecoratorManager | null>(null);
79
+ if (decoratorManagerRef.current === null) {
80
+ decoratorManagerRef.current = new DecoratorManager(decoratorSchemaRegistryRef.current);
81
+ }
82
+ const remoteDecoratorManagerRef = useRef<RemoteDecoratorManager | null>(null);
83
+ if (remoteDecoratorManagerRef.current === null) {
84
+ remoteDecoratorManagerRef.current = new RemoteDecoratorManager();
85
+ }
86
+ const patternDecoratorConfigManagerRef = useRef<PatternDecoratorConfigManager | null>(null);
87
+ if (patternDecoratorConfigManagerRef.current === null) {
88
+ patternDecoratorConfigManagerRef.current = new PatternDecoratorConfigManager();
89
+ }
90
+ const decoratorGeneratorManagerRef = useRef<DecoratorGeneratorManager | null>(null);
91
+ if (decoratorGeneratorManagerRef.current === null) {
92
+ decoratorGeneratorManagerRef.current = new DecoratorGeneratorManager();
93
+ }
94
+
95
+ const [decoratorVersion, setDecoratorVersion] = useState(0);
96
+ const bumpDecoratorVersion = useCallback(() => setDecoratorVersion((v) => v + 1), []);
97
+
98
+ useEffect(() => {
99
+ const manager = decoratorManagerRef.current;
100
+ if (!manager) return;
101
+ manager.on('decorator:added', bumpDecoratorVersion);
102
+ manager.on('decorator:updated', bumpDecoratorVersion);
103
+ manager.on('decorator:removed', bumpDecoratorVersion);
104
+ return () => {
105
+ manager.off('decorator:added', bumpDecoratorVersion);
106
+ manager.off('decorator:updated', bumpDecoratorVersion);
107
+ manager.off('decorator:removed', bumpDecoratorVersion);
108
+ };
109
+ }, [bumpDecoratorVersion]);
110
+
111
+ useEffect(() => {
112
+ const remote = remoteDecoratorManagerRef.current;
113
+ if (!remote) return;
114
+ remote.on('change', bumpDecoratorVersion);
115
+ return () => remote.off('change', bumpDecoratorVersion);
116
+ }, [bumpDecoratorVersion]);
117
+
118
+ const getMergedDecorators = useCallback((model: unknown): Decorator[] => {
119
+ const local = decoratorManagerRef.current?.getAll() ?? [];
120
+ const remote = remoteDecoratorManagerRef.current?.getAll() ?? [];
121
+ const patternConfigs = patternDecoratorConfigManagerRef.current?.getConfigs(true) ?? [];
122
+ const patternDecorators = runPatternFromModel(model as Record<string, unknown>, patternConfigs);
123
+ const genManager = decoratorGeneratorManagerRef.current;
124
+ let generatorDecorators: Decorator[] = [];
125
+ if (genManager && model && typeof model === 'object') {
126
+ const doc = model as Record<string, unknown>;
127
+ const traverse = (node: Record<string, unknown>): void => {
128
+ const text = typeof node.text === 'string' ? node.text : null;
129
+ generatorDecorators.push(
130
+ ...genManager.generateDecorators(node as Parameters<DecoratorGeneratorManager['generateDecorators']>[0], text, {
131
+ documentModel: doc,
132
+ })
133
+ );
134
+ const children = (node.children ?? node.content) as Record<string, unknown>[] | undefined;
135
+ if (Array.isArray(children)) {
136
+ for (const child of children) {
137
+ if (child && typeof child === 'object') traverse(child);
138
+ }
139
+ }
140
+ };
141
+ traverse(doc);
142
+ }
143
+ return [...local, ...remote, ...patternDecorators, ...generatorDecorators];
144
+ }, []);
145
+
146
+ const selectionHandler = useMemo(
147
+ () => new ReactSelectionHandlerClass(editor, getContentEditableElement),
148
+ [editor, getContentEditableElement]
149
+ );
150
+
151
+ const inputHandler = useMemo(
152
+ () => new ReactInputHandlerClass(editor, selectionHandler, viewStateRef),
153
+ [editor, selectionHandler]
154
+ );
155
+
156
+ const mutationObserverManager = useMemo(
157
+ () =>
158
+ createMutationObserverManager((mutations) => {
159
+ void inputHandler.handleDomMutations(mutations);
160
+ }),
161
+ [inputHandler]
162
+ );
163
+
164
+ const setContentEditableElement = useCallback(
165
+ (el: HTMLElement | null) => {
166
+ if (contentEditableRef.current === el) return;
167
+ if (contentEditableRef.current) {
168
+ mutationObserverManager.disconnect();
169
+ }
170
+ (contentEditableRef as React.MutableRefObject<HTMLElement | null>).current = el;
171
+ if (el) {
172
+ mutationObserverManager.setup(el);
173
+ }
174
+ },
175
+ [mutationObserverManager]
176
+ );
177
+
178
+ useEffect(() => {
179
+ const onSelectionChange = () => selectionHandler.handleSelectionChange();
180
+ document.addEventListener('selectionchange', onSelectionChange);
181
+ return () => document.removeEventListener('selectionchange', onSelectionChange);
182
+ }, [selectionHandler]);
183
+
184
+ const value = useMemo<EditorViewContextValue>(
185
+ () => ({
186
+ editor,
187
+ viewStateRef,
188
+ selectionHandler,
189
+ inputHandler,
190
+ mutationObserverManager,
191
+ setContentEditableElement,
192
+ contentEditableRef,
193
+ decoratorManagerRef,
194
+ decoratorSchemaRegistryRef,
195
+ remoteDecoratorManagerRef,
196
+ patternDecoratorConfigManagerRef,
197
+ decoratorGeneratorManagerRef,
198
+ getMergedDecorators,
199
+ bumpDecoratorVersion,
200
+ decoratorVersion,
201
+ }),
202
+ [
203
+ editor,
204
+ viewStateRef,
205
+ selectionHandler,
206
+ inputHandler,
207
+ mutationObserverManager,
208
+ setContentEditableElement,
209
+ contentEditableRef,
210
+ decoratorManagerRef,
211
+ decoratorSchemaRegistryRef,
212
+ remoteDecoratorManagerRef,
213
+ patternDecoratorConfigManagerRef,
214
+ decoratorGeneratorManagerRef,
215
+ getMergedDecorators,
216
+ bumpDecoratorVersion,
217
+ decoratorVersion,
218
+ ]
219
+ );
220
+
221
+ return (
222
+ <EditorViewContext.Provider value={value}>
223
+ {children}
224
+ </EditorViewContext.Provider>
225
+ );
226
+ }
227
+
228
+ export { EditorViewContext };