@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.
- package/LICENSE +23 -0
- package/README.md +89 -0
- package/dist/editor-view-react/src/EditorView.d.ts +14 -0
- package/dist/editor-view-react/src/EditorView.d.ts.map +1 -0
- package/dist/editor-view-react/src/EditorViewContentLayer.d.ts +9 -0
- package/dist/editor-view-react/src/EditorViewContentLayer.d.ts.map +1 -0
- package/dist/editor-view-react/src/EditorViewContext.d.ts +43 -0
- package/dist/editor-view-react/src/EditorViewContext.d.ts.map +1 -0
- package/dist/editor-view-react/src/EditorViewLayer.d.ts +8 -0
- package/dist/editor-view-react/src/EditorViewLayer.d.ts.map +1 -0
- package/dist/editor-view-react/src/EditorViewOverlayLayerContent.d.ts +14 -0
- package/dist/editor-view-react/src/EditorViewOverlayLayerContent.d.ts.map +1 -0
- package/dist/editor-view-react/src/dom-sync/classify-c1.d.ts +45 -0
- package/dist/editor-view-react/src/dom-sync/classify-c1.d.ts.map +1 -0
- package/dist/editor-view-react/src/dom-sync/edit-position.d.ts +6 -0
- package/dist/editor-view-react/src/dom-sync/edit-position.d.ts.map +1 -0
- package/dist/editor-view-react/src/index.d.ts +12 -0
- package/dist/editor-view-react/src/index.d.ts.map +1 -0
- package/dist/editor-view-react/src/input-handler.d.ts +51 -0
- package/dist/editor-view-react/src/input-handler.d.ts.map +1 -0
- package/dist/editor-view-react/src/mutation-observer-manager.d.ts +13 -0
- package/dist/editor-view-react/src/mutation-observer-manager.d.ts.map +1 -0
- package/dist/editor-view-react/src/selection-handler.d.ts +56 -0
- package/dist/editor-view-react/src/selection-handler.d.ts.map +1 -0
- package/dist/editor-view-react/src/types.d.ts +103 -0
- package/dist/editor-view-react/src/types.d.ts.map +1 -0
- package/dist/index.cjs +4 -0
- package/dist/index.js +11882 -0
- package/docs/SPEC_VERIFICATION.md +109 -0
- package/docs/editor-view-react-spec.md +359 -0
- package/docs/improvement-opportunities.md +66 -0
- package/docs/layers-spec.md +97 -0
- package/package.json +53 -0
- package/src/EditorView.tsx +312 -0
- package/src/EditorViewContentLayer.tsx +90 -0
- package/src/EditorViewContext.tsx +228 -0
- package/src/EditorViewLayer.tsx +35 -0
- package/src/EditorViewOverlayLayerContent.tsx +42 -0
- package/src/dom-sync/classify-c1.ts +91 -0
- package/src/dom-sync/edit-position.ts +27 -0
- package/src/index.ts +33 -0
- package/src/input-handler.ts +716 -0
- package/src/mutation-observer-manager.ts +65 -0
- package/src/selection-handler.ts +450 -0
- package/src/types.ts +123 -0
- package/test/EditorView-decorator.test.tsx +198 -0
- package/test/EditorView-layers.test.tsx +352 -0
- package/test/EditorView.test.tsx +218 -0
- package/test/dom-sync.test.ts +49 -0
- package/test/mutation-observer-manager.test.ts +48 -0
- package/test/selection-handler.test.ts +86 -0
- package/tsconfig.json +12 -0
- package/vite.config.ts +26 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { EditorViewLayerProps } from './types';
|
|
2
|
+
|
|
3
|
+
const LAYER_DEFAULTS: Record<string, { className: string; zIndex: number }> = {
|
|
4
|
+
decorator: { className: 'barocss-editor-decorators', zIndex: 10 },
|
|
5
|
+
selection: { className: 'barocss-editor-selection', zIndex: 100 },
|
|
6
|
+
context: { className: 'barocss-editor-context', zIndex: 200 },
|
|
7
|
+
custom: { className: 'barocss-editor-custom', zIndex: 1000 },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Overlay layer wrapper (decorator, selection, context, custom).
|
|
12
|
+
* Positioned absolute, pointer-events: none by default so content layer receives input.
|
|
13
|
+
*/
|
|
14
|
+
export function EditorViewLayer({ layer, className, style, children }: EditorViewLayerProps) {
|
|
15
|
+
const defaults = LAYER_DEFAULTS[layer] ?? { className: '', zIndex: 0 };
|
|
16
|
+
const baseStyle: React.CSSProperties = {
|
|
17
|
+
position: 'absolute',
|
|
18
|
+
top: 0,
|
|
19
|
+
left: 0,
|
|
20
|
+
right: 0,
|
|
21
|
+
bottom: 0,
|
|
22
|
+
pointerEvents: 'none',
|
|
23
|
+
zIndex: defaults.zIndex,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={className ?? defaults.className}
|
|
29
|
+
style={style ? { ...baseStyle, ...style } : baseStyle}
|
|
30
|
+
data-bc-layer={layer}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { getGlobalRegistry } from '@barocss/dsl';
|
|
3
|
+
import { ReactRenderer } from '@barocss/renderer-react';
|
|
4
|
+
import { useEditorViewContext } from './EditorViewContext';
|
|
5
|
+
import type { EditorViewLayerType } from './types';
|
|
6
|
+
|
|
7
|
+
export interface EditorViewOverlayLayerContentProps {
|
|
8
|
+
/** Layer to render (decorator, selection, context, custom). Only decorators with layerTarget === layer are rendered. */
|
|
9
|
+
layer: Exclude<EditorViewLayerType, 'content'>;
|
|
10
|
+
/** Registry for resolving decorator templates. If omitted, uses getGlobalRegistry(). */
|
|
11
|
+
registry?: import('@barocss/dsl').RendererRegistry;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Renders overlay layer content by filtering merged decorators by layerTarget and building them with ReactRenderer.buildOverlayDecorators.
|
|
16
|
+
* Uses getMergedDecorators(document) so remote, pattern, and generator decorators are included.
|
|
17
|
+
*/
|
|
18
|
+
export function EditorViewOverlayLayerContent({ layer, registry: registryProp }: EditorViewOverlayLayerContentProps) {
|
|
19
|
+
const { editor, getMergedDecorators, decoratorVersion } = useEditorViewContext();
|
|
20
|
+
const [documentSnapshot, setDocumentSnapshot] = useState<unknown>(() => editor.getDocumentProxy?.() ?? null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const onContent = () => setDocumentSnapshot(editor.getDocumentProxy?.() ?? null);
|
|
24
|
+
editor.on?.('editor:content.change', onContent);
|
|
25
|
+
setDocumentSnapshot(editor.getDocumentProxy?.() ?? null);
|
|
26
|
+
return () => editor.off?.('editor:content.change', onContent);
|
|
27
|
+
}, [editor]);
|
|
28
|
+
|
|
29
|
+
const renderer = useMemo(
|
|
30
|
+
() => new ReactRenderer(registryProp ?? getGlobalRegistry()),
|
|
31
|
+
[registryProp]
|
|
32
|
+
);
|
|
33
|
+
const decorators = useMemo(
|
|
34
|
+
() => getMergedDecorators(documentSnapshot),
|
|
35
|
+
[documentSnapshot, getMergedDecorators, decoratorVersion]
|
|
36
|
+
);
|
|
37
|
+
const filtered = useMemo(
|
|
38
|
+
() => decorators.filter((d) => (d.layerTarget ?? 'content') === layer),
|
|
39
|
+
[decorators, layer]
|
|
40
|
+
);
|
|
41
|
+
return <>{renderer.buildOverlayDecorators(filtered)}</>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C1 classification: pure text change within a single inline-text.
|
|
3
|
+
* Ported from editor-view-dom dom-change-classifier (C1 only).
|
|
4
|
+
*/
|
|
5
|
+
import type { Editor } from '@barocss/editor-core';
|
|
6
|
+
import { findClosestInlineTextNode, reconstructModelTextFromDOM } from './edit-position';
|
|
7
|
+
|
|
8
|
+
export interface ClassifiedChangeC1 {
|
|
9
|
+
case: 'C1';
|
|
10
|
+
nodeId: string;
|
|
11
|
+
prevText: string;
|
|
12
|
+
newText: string;
|
|
13
|
+
contentRange?: { startNodeId: string; startOffset: number; endNodeId: string; endOffset: number };
|
|
14
|
+
mutations: MutationRecord[];
|
|
15
|
+
metadata?: { usedInputHint?: boolean };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface InputHint {
|
|
19
|
+
contentRange: { startNodeId: string; startOffset: number; endNodeId: string; endOffset: number };
|
|
20
|
+
timestamp: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ClassifyOptions {
|
|
24
|
+
editor: Editor;
|
|
25
|
+
selection?: Selection;
|
|
26
|
+
modelSelection?: { startNodeId: string; startOffset: number; endNodeId: string; endOffset: number };
|
|
27
|
+
inputHint?: InputHint;
|
|
28
|
+
isComposing?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ClassifiedChange = ClassifiedChangeC1 | { case: 'UNKNOWN'; mutations: MutationRecord[] };
|
|
32
|
+
|
|
33
|
+
export function classifyDomChangeC1(
|
|
34
|
+
mutations: MutationRecord[],
|
|
35
|
+
options: ClassifyOptions
|
|
36
|
+
): ClassifiedChangeC1 | null {
|
|
37
|
+
if (mutations.length === 0) return null;
|
|
38
|
+
|
|
39
|
+
for (const mutation of mutations) {
|
|
40
|
+
const target = mutation.target;
|
|
41
|
+
const inlineTextNode = findClosestInlineTextNode(target);
|
|
42
|
+
if (!inlineTextNode) continue;
|
|
43
|
+
|
|
44
|
+
const nodeId = inlineTextNode.getAttribute('data-bc-sid');
|
|
45
|
+
if (!nodeId) continue;
|
|
46
|
+
|
|
47
|
+
const modelNode = options.editor.dataStore?.getNode?.(nodeId) as { stype?: string; text?: string } | undefined;
|
|
48
|
+
if (!modelNode || modelNode.stype !== 'inline-text') continue;
|
|
49
|
+
|
|
50
|
+
if (mutation.type === 'childList') {
|
|
51
|
+
const addedOrRemoved = [
|
|
52
|
+
...Array.from(mutation.addedNodes ?? []),
|
|
53
|
+
...Array.from(mutation.removedNodes ?? []),
|
|
54
|
+
];
|
|
55
|
+
const hasBlockLike = addedOrRemoved.some((n) => {
|
|
56
|
+
if (n.nodeType !== Node.ELEMENT_NODE) return false;
|
|
57
|
+
const tag = (n as Element).tagName.toLowerCase();
|
|
58
|
+
return ['p', 'div', 'li', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'].includes(tag);
|
|
59
|
+
});
|
|
60
|
+
if (hasBlockLike) continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const prevText = modelNode.text ?? '';
|
|
64
|
+
const newText = reconstructModelTextFromDOM(inlineTextNode);
|
|
65
|
+
|
|
66
|
+
let startOffset: number | undefined;
|
|
67
|
+
let endOffset: number | undefined;
|
|
68
|
+
let usedInputHint = false;
|
|
69
|
+
|
|
70
|
+
const hint = options.inputHint;
|
|
71
|
+
if (hint?.contentRange.startNodeId === nodeId && hint.contentRange.endNodeId === nodeId) {
|
|
72
|
+
startOffset = Math.max(0, Math.min(prevText.length, hint.contentRange.startOffset));
|
|
73
|
+
endOffset = Math.max(startOffset, Math.min(prevText.length, hint.contentRange.endOffset));
|
|
74
|
+
usedInputHint = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
case: 'C1',
|
|
79
|
+
nodeId,
|
|
80
|
+
prevText,
|
|
81
|
+
newText,
|
|
82
|
+
contentRange:
|
|
83
|
+
startOffset !== undefined && endOffset !== undefined
|
|
84
|
+
? { startNodeId: nodeId, startOffset, endNodeId: nodeId, endOffset }
|
|
85
|
+
: undefined,
|
|
86
|
+
mutations: [mutation],
|
|
87
|
+
metadata: usedInputHint ? { usedInputHint: true } : undefined,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM ↔ model position utilities for MutationObserver path.
|
|
3
|
+
* Ported from editor-view-dom edit-position-converter; uses @barocss/shared text-run-index.
|
|
4
|
+
*/
|
|
5
|
+
import { buildTextRunIndex } from '@barocss/shared';
|
|
6
|
+
|
|
7
|
+
export function findClosestInlineTextNode(node: Node): Element | null {
|
|
8
|
+
let current: Node | null = node;
|
|
9
|
+
while (current) {
|
|
10
|
+
if (current.nodeType === Node.ELEMENT_NODE) {
|
|
11
|
+
const el = current as Element;
|
|
12
|
+
if (el.getAttribute('data-bc-sid')) return el;
|
|
13
|
+
}
|
|
14
|
+
current = current.parentNode;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reconstruct full text of an inline-text container from DOM (all text nodes in order).
|
|
21
|
+
*/
|
|
22
|
+
export function reconstructModelTextFromDOM(inlineTextNode: Element): string {
|
|
23
|
+
const runs = buildTextRunIndex(inlineTextNode, undefined, {
|
|
24
|
+
normalizeWhitespace: false,
|
|
25
|
+
});
|
|
26
|
+
return runs.runs.map((r) => r.domTextNode.textContent ?? '').join('');
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @barocss/editor-view-react
|
|
3
|
+
* React view layer for Barocss Editor (Editor + ReactRenderer).
|
|
4
|
+
*/
|
|
5
|
+
export { EditorView } from './EditorView';
|
|
6
|
+
export { EditorViewContentLayer } from './EditorViewContentLayer';
|
|
7
|
+
export { EditorViewLayer } from './EditorViewLayer';
|
|
8
|
+
export {
|
|
9
|
+
EditorViewContextProvider,
|
|
10
|
+
useEditorViewContext,
|
|
11
|
+
useOptionalEditorViewContext,
|
|
12
|
+
type EditorViewViewState,
|
|
13
|
+
type EditorViewContextValue,
|
|
14
|
+
} from './EditorViewContext';
|
|
15
|
+
export { createMutationObserverManager } from './mutation-observer-manager';
|
|
16
|
+
export type { ReactMutationObserverManager } from './mutation-observer-manager';
|
|
17
|
+
export type {
|
|
18
|
+
EditorViewOptions,
|
|
19
|
+
EditorViewProps,
|
|
20
|
+
EditorViewRef,
|
|
21
|
+
EditorViewHandle,
|
|
22
|
+
DecoratorExportData,
|
|
23
|
+
LoadDecoratorsPatternFunctions,
|
|
24
|
+
DecoratorQueryOptions,
|
|
25
|
+
DecoratorTypeSchema,
|
|
26
|
+
ModelSelection,
|
|
27
|
+
EditorViewContentLayerOptions,
|
|
28
|
+
EditorViewContentLayerProps,
|
|
29
|
+
EditorViewLayerOptions,
|
|
30
|
+
EditorViewLayerProps,
|
|
31
|
+
EditorViewLayersConfig,
|
|
32
|
+
EditorViewLayerType,
|
|
33
|
+
} from './types';
|