@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,198 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { render, screen, act } from '@testing-library/react';
|
|
3
|
+
import { define, element, slot, data, defineDecorator, getGlobalRegistry } from '@barocss/dsl';
|
|
4
|
+
import { EditorView } from '../src';
|
|
5
|
+
import type { Decorator } from '@barocss/shared';
|
|
6
|
+
|
|
7
|
+
function mockEditor(getDocumentProxy: () => unknown = () => null) {
|
|
8
|
+
return {
|
|
9
|
+
getDocumentProxy,
|
|
10
|
+
on: () => {},
|
|
11
|
+
off: () => {},
|
|
12
|
+
dataStore: { getNode: () => null },
|
|
13
|
+
updateSelection: () => {},
|
|
14
|
+
executeCommand: () => false,
|
|
15
|
+
} as any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('EditorView with decorators', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
const registry = getGlobalRegistry();
|
|
21
|
+
if (!registry.has?.('doc')) {
|
|
22
|
+
define('doc', element('div', { className: 'document' }, [slot('content')]));
|
|
23
|
+
}
|
|
24
|
+
if (!registry.has?.('paragraph')) {
|
|
25
|
+
define('paragraph', element('p', { className: 'paragraph' }, [slot('content')]));
|
|
26
|
+
}
|
|
27
|
+
if (!registry.has?.('inline-text')) {
|
|
28
|
+
define('inline-text', element('span', { className: 'text' }, [data('text')]));
|
|
29
|
+
}
|
|
30
|
+
if (!registry.has?.('chip')) {
|
|
31
|
+
defineDecorator('chip', element('span', { className: 'chip', 'data-decorator': 'true' }, []));
|
|
32
|
+
}
|
|
33
|
+
if (!registry.has?.('comment')) {
|
|
34
|
+
defineDecorator('comment', element('div', { className: 'comment-block' }, []));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders content without decorators when none added', () => {
|
|
39
|
+
const docModel = {
|
|
40
|
+
sid: 'doc1',
|
|
41
|
+
stype: 'doc',
|
|
42
|
+
content: [{ sid: 'p1', stype: 'paragraph', content: [] }],
|
|
43
|
+
};
|
|
44
|
+
const editor = mockEditor(() => docModel);
|
|
45
|
+
render(<EditorView editor={editor} />);
|
|
46
|
+
const content = screen.getByTestId('editor-content');
|
|
47
|
+
expect(content).toBeTruthy();
|
|
48
|
+
expect(content.querySelector('[data-decorator-sid]')).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders content with block decorators when added via ref.addDecorator', async () => {
|
|
52
|
+
const docModel = {
|
|
53
|
+
sid: 'doc1',
|
|
54
|
+
stype: 'doc',
|
|
55
|
+
content: [{ sid: 'p1', stype: 'paragraph', content: [] }],
|
|
56
|
+
};
|
|
57
|
+
const editor = mockEditor(() => docModel);
|
|
58
|
+
const ref = { current: null as any };
|
|
59
|
+
render(<EditorView ref={ref} editor={editor} />);
|
|
60
|
+
await act(() => {
|
|
61
|
+
ref.current.addDecorator({
|
|
62
|
+
sid: 'dec1',
|
|
63
|
+
stype: 'chip',
|
|
64
|
+
category: 'block',
|
|
65
|
+
target: { sid: 'p1' },
|
|
66
|
+
position: 'after',
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
const content = screen.getByTestId('editor-content');
|
|
70
|
+
const decoratorEl = content.querySelector('[data-decorator-sid="dec1"]');
|
|
71
|
+
expect(decoratorEl).toBeTruthy();
|
|
72
|
+
expect(decoratorEl?.getAttribute('data-decorator-stype')).toBe('chip');
|
|
73
|
+
expect(decoratorEl?.getAttribute('data-decorator-category')).toBe('block');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('renders inline decorators in text when added via ref.addDecorator', async () => {
|
|
77
|
+
const docModel = {
|
|
78
|
+
sid: 'doc1',
|
|
79
|
+
stype: 'doc',
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
sid: 'p1',
|
|
83
|
+
stype: 'paragraph',
|
|
84
|
+
content: [{ sid: 't1', stype: 'inline-text', text: 'Hello world' }],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
const editor = mockEditor(() => docModel);
|
|
89
|
+
const ref = { current: null as any };
|
|
90
|
+
render(<EditorView ref={ref} editor={editor} />);
|
|
91
|
+
await act(() => {
|
|
92
|
+
ref.current.addDecorator({
|
|
93
|
+
sid: 'd1',
|
|
94
|
+
stype: 'chip',
|
|
95
|
+
category: 'inline',
|
|
96
|
+
target: { sid: 't1', startOffset: 0, endOffset: 5 },
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
const content = screen.getByTestId('editor-content');
|
|
100
|
+
const decoratorEl = content.querySelector('[data-decorator-sid="d1"]');
|
|
101
|
+
expect(decoratorEl).toBeTruthy();
|
|
102
|
+
expect(content.textContent).toContain('Hello world');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('ref.updateDecorator updates decorator and triggers re-render', async () => {
|
|
106
|
+
const docModel = {
|
|
107
|
+
sid: 'doc1',
|
|
108
|
+
stype: 'doc',
|
|
109
|
+
content: [{ sid: 'p1', stype: 'paragraph', content: [] }],
|
|
110
|
+
};
|
|
111
|
+
const editor = mockEditor(() => docModel);
|
|
112
|
+
const ref = { current: null as any };
|
|
113
|
+
render(<EditorView ref={ref} editor={editor} />);
|
|
114
|
+
await act(() => {
|
|
115
|
+
ref.current.addDecorator({
|
|
116
|
+
sid: 'upd-dec',
|
|
117
|
+
stype: 'chip',
|
|
118
|
+
category: 'block',
|
|
119
|
+
target: { sid: 'p1' },
|
|
120
|
+
position: 'after',
|
|
121
|
+
data: { label: 'before' },
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
expect(ref.current.getDecorators()[0].data?.label).toBe('before');
|
|
125
|
+
await act(() => {
|
|
126
|
+
ref.current.updateDecorator('upd-dec', { data: { label: 'after' } });
|
|
127
|
+
});
|
|
128
|
+
expect(ref.current.getDecorators()[0].data?.label).toBe('after');
|
|
129
|
+
const content = screen.getByTestId('editor-content');
|
|
130
|
+
expect(content.querySelector('[data-decorator-sid="upd-dec"]')).toBeTruthy();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('ref.addDecorator and ref.getDecorators use internal manager', async () => {
|
|
134
|
+
const docModel = {
|
|
135
|
+
sid: 'doc1',
|
|
136
|
+
stype: 'doc',
|
|
137
|
+
content: [{ sid: 'p1', stype: 'paragraph', content: [] }],
|
|
138
|
+
};
|
|
139
|
+
const editor = mockEditor(() => docModel);
|
|
140
|
+
const ref = { current: null as any };
|
|
141
|
+
render(<EditorView ref={ref} editor={editor} />);
|
|
142
|
+
expect(ref.current).toBeTruthy();
|
|
143
|
+
expect(ref.current.getDecorators()).toEqual([]);
|
|
144
|
+
await act(() => {
|
|
145
|
+
ref.current.addDecorator({
|
|
146
|
+
sid: 'internal-dec',
|
|
147
|
+
stype: 'chip',
|
|
148
|
+
category: 'block',
|
|
149
|
+
target: { sid: 'p1' },
|
|
150
|
+
position: 'after',
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
const list = ref.current.getDecorators();
|
|
154
|
+
expect(list).toHaveLength(1);
|
|
155
|
+
expect(list[0].sid).toBe('internal-dec');
|
|
156
|
+
const content = screen.getByTestId('editor-content');
|
|
157
|
+
const decoratorEl = content.querySelector('[data-decorator-sid="internal-dec"]');
|
|
158
|
+
expect(decoratorEl).toBeTruthy();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('renders decorator layer when decorator has layerTarget decorator', async () => {
|
|
162
|
+
const docModel = {
|
|
163
|
+
sid: 'doc1',
|
|
164
|
+
stype: 'doc',
|
|
165
|
+
content: [{ sid: 'p1', stype: 'paragraph', content: [] }],
|
|
166
|
+
};
|
|
167
|
+
const editor = mockEditor(() => docModel);
|
|
168
|
+
const ref = { current: null as any };
|
|
169
|
+
const { container } = render(<EditorView ref={ref} editor={editor} />);
|
|
170
|
+
await act(() => {
|
|
171
|
+
ref.current.addDecorator({
|
|
172
|
+
sid: 'overlay-dec',
|
|
173
|
+
stype: 'comment',
|
|
174
|
+
category: 'layer',
|
|
175
|
+
layerTarget: 'decorator',
|
|
176
|
+
target: { sid: 'p1' },
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
const decoratorLayer = container.querySelector('[data-bc-layer="decorator"]');
|
|
180
|
+
expect(decoratorLayer).toBeTruthy();
|
|
181
|
+
const decoratorEl = decoratorLayer?.querySelector('[data-decorator-sid="overlay-dec"]');
|
|
182
|
+
expect(decoratorEl).toBeTruthy();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('renders all four overlay layers (decorator, selection, context, custom)', () => {
|
|
186
|
+
const docModel = {
|
|
187
|
+
sid: 'doc1',
|
|
188
|
+
stype: 'doc',
|
|
189
|
+
content: [{ sid: 'p1', stype: 'paragraph', content: [] }],
|
|
190
|
+
};
|
|
191
|
+
const editor = mockEditor(() => docModel);
|
|
192
|
+
const { container } = render(<EditorView editor={editor} />);
|
|
193
|
+
expect(container.querySelector('[data-bc-layer="decorator"]')).toBeTruthy();
|
|
194
|
+
expect(container.querySelector('[data-bc-layer="selection"]')).toBeTruthy();
|
|
195
|
+
expect(container.querySelector('[data-bc-layer="context"]')).toBeTruthy();
|
|
196
|
+
expect(container.querySelector('[data-bc-layer="custom"]')).toBeTruthy();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { render, screen, act } from '@testing-library/react';
|
|
3
|
+
import { define, element, slot, data, defineDecorator, getGlobalRegistry } from '@barocss/dsl';
|
|
4
|
+
import { EditorView } from '../src';
|
|
5
|
+
import type { Decorator } from '@barocss/shared';
|
|
6
|
+
|
|
7
|
+
function mockEditor(getDocumentProxy: () => unknown = () => null) {
|
|
8
|
+
return {
|
|
9
|
+
getDocumentProxy,
|
|
10
|
+
on: () => {},
|
|
11
|
+
off: () => {},
|
|
12
|
+
dataStore: { getNode: () => null },
|
|
13
|
+
updateSelection: () => {},
|
|
14
|
+
executeCommand: () => false,
|
|
15
|
+
} as any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('EditorView layers', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
const registry = getGlobalRegistry();
|
|
21
|
+
if (!registry.has?.('doc')) {
|
|
22
|
+
define('doc', element('div', { className: 'document' }, [slot('content')]));
|
|
23
|
+
}
|
|
24
|
+
if (!registry.has?.('paragraph')) {
|
|
25
|
+
define('paragraph', element('p', { className: 'paragraph' }, [slot('content')]));
|
|
26
|
+
}
|
|
27
|
+
if (!registry.has?.('chip')) {
|
|
28
|
+
defineDecorator('chip', element('span', { className: 'chip', 'data-decorator': 'true' }, []));
|
|
29
|
+
}
|
|
30
|
+
if (!registry.has?.('comment')) {
|
|
31
|
+
defineDecorator('comment', element('div', { className: 'comment-block' }, []));
|
|
32
|
+
}
|
|
33
|
+
if (!registry.has?.('tooltip')) {
|
|
34
|
+
defineDecorator('tooltip', element('div', { className: 'tooltip-ui' }, []));
|
|
35
|
+
}
|
|
36
|
+
if (!registry.has?.('cursor')) {
|
|
37
|
+
defineDecorator('cursor', element('div', { className: 'cursor-ui' }, []));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const docModel = {
|
|
42
|
+
sid: 'doc1',
|
|
43
|
+
stype: 'doc',
|
|
44
|
+
content: [{ sid: 'p1', stype: 'paragraph', content: [] }],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe('layer DOM structure', () => {
|
|
48
|
+
it('renders content layer first with data-bc-layer="content"', () => {
|
|
49
|
+
const editor = mockEditor(() => docModel);
|
|
50
|
+
const { container } = render(<EditorView editor={editor} />);
|
|
51
|
+
const content = screen.getByTestId('editor-content');
|
|
52
|
+
expect(content.getAttribute('data-bc-layer')).toBe('content');
|
|
53
|
+
const layers = container.querySelectorAll('[data-bc-layer]');
|
|
54
|
+
expect(layers[0].getAttribute('data-bc-layer')).toBe('content');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders overlay layers in order: decorator, selection, context, custom', () => {
|
|
58
|
+
const editor = mockEditor(() => docModel);
|
|
59
|
+
const { container } = render(<EditorView editor={editor} />);
|
|
60
|
+
const layers = Array.from(container.querySelectorAll('[data-bc-layer]'));
|
|
61
|
+
const names = layers.map((el) => el.getAttribute('data-bc-layer'));
|
|
62
|
+
expect(names).toContain('content');
|
|
63
|
+
expect(names).toContain('decorator');
|
|
64
|
+
expect(names).toContain('selection');
|
|
65
|
+
expect(names).toContain('context');
|
|
66
|
+
expect(names).toContain('custom');
|
|
67
|
+
const contentIdx = names.indexOf('content');
|
|
68
|
+
const decoratorIdx = names.indexOf('decorator');
|
|
69
|
+
const selectionIdx = names.indexOf('selection');
|
|
70
|
+
const contextIdx = names.indexOf('context');
|
|
71
|
+
const customIdx = names.indexOf('custom');
|
|
72
|
+
expect(contentIdx).toBeLessThan(decoratorIdx);
|
|
73
|
+
expect(decoratorIdx).toBeLessThan(selectionIdx);
|
|
74
|
+
expect(selectionIdx).toBeLessThan(contextIdx);
|
|
75
|
+
expect(contextIdx).toBeLessThan(customIdx);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('applies default classNames to overlay layers', () => {
|
|
79
|
+
const editor = mockEditor(() => docModel);
|
|
80
|
+
const { container } = render(<EditorView editor={editor} />);
|
|
81
|
+
expect(container.querySelector('[data-bc-layer="decorator"]')?.className).toContain('barocss-editor-decorators');
|
|
82
|
+
expect(container.querySelector('[data-bc-layer="selection"]')?.className).toContain('barocss-editor-selection');
|
|
83
|
+
expect(container.querySelector('[data-bc-layer="context"]')?.className).toContain('barocss-editor-context');
|
|
84
|
+
expect(container.querySelector('[data-bc-layer="custom"]')?.className).toContain('barocss-editor-custom');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('overlay layers have position absolute and pointer-events none', () => {
|
|
88
|
+
const editor = mockEditor(() => docModel);
|
|
89
|
+
const { container } = render(<EditorView editor={editor} />);
|
|
90
|
+
const decoratorLayer = container.querySelector('[data-bc-layer="decorator"]') as HTMLElement;
|
|
91
|
+
expect(decoratorLayer?.style?.position).toBe('absolute');
|
|
92
|
+
expect(decoratorLayer?.style?.pointerEvents).toBe('none');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('decorator layer', () => {
|
|
97
|
+
it('renders only decorators with layerTarget decorator in decorator layer', async () => {
|
|
98
|
+
const editor = mockEditor(() => docModel);
|
|
99
|
+
const ref = { current: null as any };
|
|
100
|
+
const { container } = render(<EditorView ref={ref} editor={editor} />);
|
|
101
|
+
await act(() => {
|
|
102
|
+
ref.current.addDecorator({
|
|
103
|
+
sid: 'dec-layer-1',
|
|
104
|
+
stype: 'comment',
|
|
105
|
+
category: 'layer',
|
|
106
|
+
layerTarget: 'decorator',
|
|
107
|
+
target: { sid: 'p1' },
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
const decoratorLayer = container.querySelector('[data-bc-layer="decorator"]');
|
|
111
|
+
expect(decoratorLayer?.querySelector('[data-decorator-sid="dec-layer-1"]')).toBeTruthy();
|
|
112
|
+
expect(container.querySelector('[data-bc-layer="selection"]')?.querySelector('[data-decorator-sid="dec-layer-1"]')).toBeFalsy();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('renders multiple decorators with layerTarget decorator in decorator layer', async () => {
|
|
116
|
+
const editor = mockEditor(() => docModel);
|
|
117
|
+
const ref = { current: null as any };
|
|
118
|
+
const { container } = render(<EditorView ref={ref} editor={editor} />);
|
|
119
|
+
await act(() => {
|
|
120
|
+
ref.current.addDecorator({
|
|
121
|
+
sid: 'dec-a',
|
|
122
|
+
stype: 'comment',
|
|
123
|
+
category: 'layer',
|
|
124
|
+
layerTarget: 'decorator',
|
|
125
|
+
target: { sid: 'p1' },
|
|
126
|
+
});
|
|
127
|
+
ref.current.addDecorator({
|
|
128
|
+
sid: 'dec-b',
|
|
129
|
+
stype: 'chip',
|
|
130
|
+
category: 'layer',
|
|
131
|
+
layerTarget: 'decorator',
|
|
132
|
+
target: { sid: 'p1' },
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
const decoratorLayer = container.querySelector('[data-bc-layer="decorator"]');
|
|
136
|
+
expect(decoratorLayer?.querySelector('[data-decorator-sid="dec-a"]')).toBeTruthy();
|
|
137
|
+
expect(decoratorLayer?.querySelector('[data-decorator-sid="dec-b"]')).toBeTruthy();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('decorator layer is empty when no decorators have layerTarget decorator', () => {
|
|
141
|
+
const editor = mockEditor(() => docModel);
|
|
142
|
+
const { container } = render(<EditorView editor={editor} />);
|
|
143
|
+
const decoratorLayer = container.querySelector('[data-bc-layer="decorator"]');
|
|
144
|
+
expect(decoratorLayer?.querySelector('[data-decorator-sid]')).toBeFalsy();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('selection layer', () => {
|
|
149
|
+
it('renders only decorators with layerTarget selection in selection layer', async () => {
|
|
150
|
+
const editor = mockEditor(() => docModel);
|
|
151
|
+
const ref = { current: null as any };
|
|
152
|
+
const { container } = render(<EditorView ref={ref} editor={editor} />);
|
|
153
|
+
await act(() => {
|
|
154
|
+
ref.current.addDecorator({
|
|
155
|
+
sid: 'sel-1',
|
|
156
|
+
stype: 'cursor',
|
|
157
|
+
category: 'layer',
|
|
158
|
+
layerTarget: 'selection',
|
|
159
|
+
target: { sid: 'p1' },
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
const selectionLayer = container.querySelector('[data-bc-layer="selection"]');
|
|
163
|
+
expect(selectionLayer?.querySelector('[data-decorator-sid="sel-1"]')).toBeTruthy();
|
|
164
|
+
expect(container.querySelector('[data-bc-layer="decorator"]')?.querySelector('[data-decorator-sid="sel-1"]')).toBeFalsy();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('selection layer is empty when no decorators have layerTarget selection', () => {
|
|
168
|
+
const editor = mockEditor(() => docModel);
|
|
169
|
+
const { container } = render(<EditorView editor={editor} />);
|
|
170
|
+
const selectionLayer = container.querySelector('[data-bc-layer="selection"]');
|
|
171
|
+
expect(selectionLayer?.querySelector('[data-decorator-sid]')).toBeFalsy();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('context layer', () => {
|
|
176
|
+
it('renders only decorators with layerTarget context in context layer', async () => {
|
|
177
|
+
const editor = mockEditor(() => docModel);
|
|
178
|
+
const ref = { current: null as any };
|
|
179
|
+
const { container } = render(<EditorView ref={ref} editor={editor} />);
|
|
180
|
+
await act(() => {
|
|
181
|
+
ref.current.addDecorator({
|
|
182
|
+
sid: 'ctx-1',
|
|
183
|
+
stype: 'tooltip',
|
|
184
|
+
category: 'layer',
|
|
185
|
+
layerTarget: 'context',
|
|
186
|
+
target: { sid: 'p1' },
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
const contextLayer = container.querySelector('[data-bc-layer="context"]');
|
|
190
|
+
expect(contextLayer?.querySelector('[data-decorator-sid="ctx-1"]')).toBeTruthy();
|
|
191
|
+
expect(container.querySelector('[data-bc-layer="decorator"]')?.querySelector('[data-decorator-sid="ctx-1"]')).toBeFalsy();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('context layer is empty when no decorators have layerTarget context', () => {
|
|
195
|
+
const editor = mockEditor(() => docModel);
|
|
196
|
+
const { container } = render(<EditorView editor={editor} />);
|
|
197
|
+
const contextLayer = container.querySelector('[data-bc-layer="context"]');
|
|
198
|
+
expect(contextLayer?.querySelector('[data-decorator-sid]')).toBeFalsy();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('custom layer', () => {
|
|
203
|
+
it('renders only decorators with layerTarget custom in custom layer', async () => {
|
|
204
|
+
const editor = mockEditor(() => docModel);
|
|
205
|
+
const ref = { current: null as any };
|
|
206
|
+
const { container } = render(<EditorView ref={ref} editor={editor} />);
|
|
207
|
+
await act(() => {
|
|
208
|
+
ref.current.addDecorator({
|
|
209
|
+
sid: 'custom-dec-1',
|
|
210
|
+
stype: 'chip',
|
|
211
|
+
category: 'layer',
|
|
212
|
+
layerTarget: 'custom',
|
|
213
|
+
target: { sid: 'p1' },
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
const customLayer = container.querySelector('[data-bc-layer="custom"]');
|
|
217
|
+
expect(customLayer?.querySelector('[data-decorator-sid="custom-dec-1"]')).toBeTruthy();
|
|
218
|
+
expect(container.querySelector('[data-bc-layer="decorator"]')?.querySelector('[data-decorator-sid="custom-dec-1"]')).toBeFalsy();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('renders children inside custom layer after overlay decorators', () => {
|
|
222
|
+
const editor = mockEditor(() => docModel);
|
|
223
|
+
const { container } = render(
|
|
224
|
+
<EditorView editor={editor}>
|
|
225
|
+
<span data-testid="custom-child">Custom content</span>
|
|
226
|
+
</EditorView>
|
|
227
|
+
);
|
|
228
|
+
const customLayer = container.querySelector('[data-bc-layer="custom"]');
|
|
229
|
+
expect(customLayer).toBeTruthy();
|
|
230
|
+
expect(screen.getByTestId('custom-child').textContent).toBe('Custom content');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('custom layer is empty of decorators when none have layerTarget custom', () => {
|
|
234
|
+
const editor = mockEditor(() => docModel);
|
|
235
|
+
const { container } = render(<EditorView editor={editor} />);
|
|
236
|
+
const customLayer = container.querySelector('[data-bc-layer="custom"]');
|
|
237
|
+
expect(customLayer?.querySelector('[data-decorator-sid]')).toBeFalsy();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('layerTarget filtering', () => {
|
|
242
|
+
it('decorator with layerTarget content appears only in content layer', async () => {
|
|
243
|
+
const editor = mockEditor(() => docModel);
|
|
244
|
+
const ref = { current: null as any };
|
|
245
|
+
const { container } = render(<EditorView ref={ref} editor={editor} />);
|
|
246
|
+
await act(() => {
|
|
247
|
+
ref.current.addDecorator({
|
|
248
|
+
sid: 'content-only',
|
|
249
|
+
stype: 'chip',
|
|
250
|
+
category: 'block',
|
|
251
|
+
layerTarget: 'content',
|
|
252
|
+
target: { sid: 'p1' },
|
|
253
|
+
position: 'after',
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
const contentLayer = screen.getByTestId('editor-content');
|
|
257
|
+
expect(contentLayer.querySelector('[data-decorator-sid="content-only"]')).toBeTruthy();
|
|
258
|
+
expect(container.querySelector('[data-bc-layer="decorator"]')?.querySelector('[data-decorator-sid="content-only"]')).toBeFalsy();
|
|
259
|
+
expect(container.querySelector('[data-bc-layer="selection"]')?.querySelector('[data-decorator-sid="content-only"]')).toBeFalsy();
|
|
260
|
+
expect(container.querySelector('[data-bc-layer="context"]')?.querySelector('[data-decorator-sid="content-only"]')).toBeFalsy();
|
|
261
|
+
expect(container.querySelector('[data-bc-layer="custom"]')?.querySelector('[data-decorator-sid="content-only"]')).toBeFalsy();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('decorators with different layerTargets appear only in their layer', async () => {
|
|
265
|
+
const editor = mockEditor(() => docModel);
|
|
266
|
+
const ref = { current: null as any };
|
|
267
|
+
const { container } = render(<EditorView ref={ref} editor={editor} />);
|
|
268
|
+
await act(() => {
|
|
269
|
+
ref.current.addDecorator({
|
|
270
|
+
sid: 'd-decorator',
|
|
271
|
+
stype: 'comment',
|
|
272
|
+
category: 'layer',
|
|
273
|
+
layerTarget: 'decorator',
|
|
274
|
+
target: { sid: 'p1' },
|
|
275
|
+
});
|
|
276
|
+
ref.current.addDecorator({
|
|
277
|
+
sid: 'd-selection',
|
|
278
|
+
stype: 'cursor',
|
|
279
|
+
category: 'layer',
|
|
280
|
+
layerTarget: 'selection',
|
|
281
|
+
target: { sid: 'p1' },
|
|
282
|
+
});
|
|
283
|
+
ref.current.addDecorator({
|
|
284
|
+
sid: 'd-context',
|
|
285
|
+
stype: 'tooltip',
|
|
286
|
+
category: 'layer',
|
|
287
|
+
layerTarget: 'context',
|
|
288
|
+
target: { sid: 'p1' },
|
|
289
|
+
});
|
|
290
|
+
ref.current.addDecorator({
|
|
291
|
+
sid: 'd-custom',
|
|
292
|
+
stype: 'chip',
|
|
293
|
+
category: 'layer',
|
|
294
|
+
layerTarget: 'custom',
|
|
295
|
+
target: { sid: 'p1' },
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
expect(container.querySelector('[data-bc-layer="decorator"]')?.querySelector('[data-decorator-sid="d-decorator"]')).toBeTruthy();
|
|
299
|
+
expect(container.querySelector('[data-bc-layer="selection"]')?.querySelector('[data-decorator-sid="d-selection"]')).toBeTruthy();
|
|
300
|
+
expect(container.querySelector('[data-bc-layer="context"]')?.querySelector('[data-decorator-sid="d-context"]')).toBeTruthy();
|
|
301
|
+
expect(container.querySelector('[data-bc-layer="custom"]')?.querySelector('[data-decorator-sid="d-custom"]')).toBeTruthy();
|
|
302
|
+
expect(container.querySelector('[data-bc-layer="decorator"]')?.querySelector('[data-decorator-sid="d-selection"]')).toBeFalsy();
|
|
303
|
+
expect(container.querySelector('[data-bc-layer="selection"]')?.querySelector('[data-decorator-sid="d-context"]')).toBeFalsy();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('options.layers overrides', () => {
|
|
308
|
+
it('options.layers.decorator.className is applied to decorator layer', () => {
|
|
309
|
+
const editor = mockEditor(() => docModel);
|
|
310
|
+
const { container } = render(
|
|
311
|
+
<EditorView
|
|
312
|
+
editor={editor}
|
|
313
|
+
options={{ layers: { decorator: { className: 'my-decorator-layer' } } }}
|
|
314
|
+
/>
|
|
315
|
+
);
|
|
316
|
+
const el = container.querySelector('[data-bc-layer="decorator"]');
|
|
317
|
+
expect(el?.className).toContain('my-decorator-layer');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('options.layers.selection.style is applied to selection layer', () => {
|
|
321
|
+
const editor = mockEditor(() => docModel);
|
|
322
|
+
const { container } = render(
|
|
323
|
+
<EditorView
|
|
324
|
+
editor={editor}
|
|
325
|
+
options={{ layers: { selection: { style: { zIndex: 150 } } } }}
|
|
326
|
+
/>
|
|
327
|
+
);
|
|
328
|
+
const el = container.querySelector('[data-bc-layer="selection"]') as HTMLElement;
|
|
329
|
+
expect(el?.style?.zIndex).toBe('150');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('options.layers.context and custom can override className and style', () => {
|
|
333
|
+
const editor = mockEditor(() => docModel);
|
|
334
|
+
const { container } = render(
|
|
335
|
+
<EditorView
|
|
336
|
+
editor={editor}
|
|
337
|
+
options={{
|
|
338
|
+
layers: {
|
|
339
|
+
context: { className: 'my-ctx', style: { opacity: 0.9 } },
|
|
340
|
+
custom: { className: 'my-custom' },
|
|
341
|
+
},
|
|
342
|
+
}}
|
|
343
|
+
/>
|
|
344
|
+
);
|
|
345
|
+
const ctx = container.querySelector('[data-bc-layer="context"]') as HTMLElement;
|
|
346
|
+
const custom = container.querySelector('[data-bc-layer="custom"]');
|
|
347
|
+
expect(ctx?.className).toContain('my-ctx');
|
|
348
|
+
expect(ctx?.style?.opacity).toBe('0.9');
|
|
349
|
+
expect(custom?.className).toContain('my-custom');
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|