@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,218 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen, act } from '@testing-library/react';
|
|
3
|
+
import { createRef } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
EditorView,
|
|
6
|
+
EditorViewLayer,
|
|
7
|
+
EditorViewContextProvider,
|
|
8
|
+
useEditorViewContext,
|
|
9
|
+
useOptionalEditorViewContext,
|
|
10
|
+
} from '../src';
|
|
11
|
+
|
|
12
|
+
function mockEditor() {
|
|
13
|
+
return {
|
|
14
|
+
getDocumentProxy: () => null,
|
|
15
|
+
on: () => {},
|
|
16
|
+
off: () => {},
|
|
17
|
+
dataStore: { getNode: () => null },
|
|
18
|
+
updateSelection: () => {},
|
|
19
|
+
executeCommand: () => false,
|
|
20
|
+
} as any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('EditorView', () => {
|
|
24
|
+
it('renders root div with data-editor-view="true" and position relative', () => {
|
|
25
|
+
const editor = mockEditor();
|
|
26
|
+
const { container } = render(<EditorView editor={editor} />);
|
|
27
|
+
const root = container.firstElementChild;
|
|
28
|
+
expect(root?.getAttribute('data-editor-view')).toBe('true');
|
|
29
|
+
expect((root as HTMLElement)?.style?.position).toBe('relative');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders content layer with data-bc-layer="content" and data-testid="editor-content"', () => {
|
|
33
|
+
const editor = mockEditor();
|
|
34
|
+
render(<EditorView editor={editor} />);
|
|
35
|
+
const content = screen.getByTestId('editor-content');
|
|
36
|
+
expect(content).toBeTruthy();
|
|
37
|
+
expect(content.getAttribute('data-bc-layer')).toBe('content');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('applies options.className to root container', () => {
|
|
41
|
+
const editor = mockEditor();
|
|
42
|
+
const { container } = render(
|
|
43
|
+
<EditorView editor={editor} options={{ className: 'my-editor-root' }} />
|
|
44
|
+
);
|
|
45
|
+
const root = container.firstElementChild;
|
|
46
|
+
expect(root?.className).toContain('my-editor-root');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('renders all overlay layers (decorator, selection, context, custom) by default', () => {
|
|
50
|
+
const editor = mockEditor();
|
|
51
|
+
const { container } = render(<EditorView editor={editor} />);
|
|
52
|
+
expect(container.querySelector('[data-bc-layer="decorator"]')).toBeTruthy();
|
|
53
|
+
expect(container.querySelector('[data-bc-layer="selection"]')).toBeTruthy();
|
|
54
|
+
expect(container.querySelector('[data-bc-layer="context"]')).toBeTruthy();
|
|
55
|
+
expect(container.querySelector('[data-bc-layer="custom"]')).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('applies options.layers.* className and style to overlay layers', () => {
|
|
59
|
+
const editor = mockEditor();
|
|
60
|
+
const { container } = render(
|
|
61
|
+
<EditorView
|
|
62
|
+
editor={editor}
|
|
63
|
+
options={{
|
|
64
|
+
layers: {
|
|
65
|
+
decorator: { className: 'my-decorator-layer' },
|
|
66
|
+
selection: { style: { zIndex: 999 } },
|
|
67
|
+
},
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
const decoratorLayer = container.querySelector('[data-bc-layer="decorator"]');
|
|
72
|
+
const selectionLayer = container.querySelector('[data-bc-layer="selection"]');
|
|
73
|
+
expect(decoratorLayer?.className).toContain('my-decorator-layer');
|
|
74
|
+
expect((selectionLayer as HTMLElement)?.style?.zIndex).toBe('999');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('renders children inside custom layer when layers.custom or children present', () => {
|
|
78
|
+
const editor = mockEditor();
|
|
79
|
+
const { container } = render(
|
|
80
|
+
<EditorView editor={editor} options={{ layers: { custom: {} } }}>
|
|
81
|
+
<span data-testid="custom-child">Custom</span>
|
|
82
|
+
</EditorView>
|
|
83
|
+
);
|
|
84
|
+
const customLayer = container.querySelector('[data-bc-layer="custom"]');
|
|
85
|
+
expect(customLayer).toBeTruthy();
|
|
86
|
+
expect(screen.getByTestId('custom-child').textContent).toBe('Custom');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('ref exposes getDecorator, contentEditableElement, exportDecorators, loadDecorators, convertModelSelectionToDOM, convertDOMSelectionToModel, convertStaticRangeToModel, defineDecoratorType', () => {
|
|
90
|
+
const editor = mockEditor();
|
|
91
|
+
const ref = createRef<any>();
|
|
92
|
+
render(<EditorView ref={ref} editor={editor} />);
|
|
93
|
+
const api = ref.current;
|
|
94
|
+
expect(api).toBeTruthy();
|
|
95
|
+
expect(typeof api.getDecorator).toBe('function');
|
|
96
|
+
expect(typeof api.exportDecorators).toBe('function');
|
|
97
|
+
expect(typeof api.loadDecorators).toBe('function');
|
|
98
|
+
expect(typeof api.convertModelSelectionToDOM).toBe('function');
|
|
99
|
+
expect(typeof api.convertDOMSelectionToModel).toBe('function');
|
|
100
|
+
expect(typeof api.convertStaticRangeToModel).toBe('function');
|
|
101
|
+
expect(typeof api.defineDecoratorType).toBe('function');
|
|
102
|
+
expect(api.getDecorator('nonexistent')).toBeUndefined();
|
|
103
|
+
const exported = api.exportDecorators();
|
|
104
|
+
expect(exported.version).toBe('1.0.0');
|
|
105
|
+
expect(Array.isArray(exported.targetDecorators)).toBe(true);
|
|
106
|
+
expect(Array.isArray(exported.patternDecorators)).toBe(true);
|
|
107
|
+
expect(api.contentEditableElement).toBe(screen.getByTestId('editor-content'));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('ref.defineDecoratorType registers type; addDecorator applies schema defaults', () => {
|
|
111
|
+
const editor = mockEditor();
|
|
112
|
+
const ref = createRef<any>();
|
|
113
|
+
render(<EditorView ref={ref} editor={editor} />);
|
|
114
|
+
ref.current.defineDecoratorType('highlight', 'layer', {
|
|
115
|
+
dataSchema: { color: { type: 'string', default: 'yellow' } },
|
|
116
|
+
});
|
|
117
|
+
act(() => {
|
|
118
|
+
ref.current.addDecorator({
|
|
119
|
+
sid: 'd1',
|
|
120
|
+
stype: 'highlight',
|
|
121
|
+
category: 'layer',
|
|
122
|
+
data: {},
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
const list = ref.current.getDecorators();
|
|
126
|
+
expect(list.length).toBeGreaterThanOrEqual(1);
|
|
127
|
+
const d = list.find((x: { sid: string }) => x.sid === 'd1');
|
|
128
|
+
expect(d).toBeTruthy();
|
|
129
|
+
expect((d as { data?: { color?: string } }).data?.color).toBe('yellow');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('ref.addDecorator accepts DecoratorGenerator and registers it', () => {
|
|
133
|
+
const editor = mockEditor();
|
|
134
|
+
const ref = createRef<any>();
|
|
135
|
+
render(<EditorView ref={ref} editor={editor} />);
|
|
136
|
+
const generator = {
|
|
137
|
+
sid: 'gen-1',
|
|
138
|
+
generate: () => [],
|
|
139
|
+
};
|
|
140
|
+
act(() => {
|
|
141
|
+
ref.current.addDecorator(generator);
|
|
142
|
+
});
|
|
143
|
+
expect(ref.current.decoratorGeneratorManager?.getGenerator('gen-1')).toBe(generator);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('EditorViewLayer', () => {
|
|
148
|
+
it('renders div with data-bc-layer and position absolute, pointer-events none', () => {
|
|
149
|
+
const { container } = render(<EditorViewLayer layer="selection" />);
|
|
150
|
+
const el = container.firstElementChild as HTMLElement;
|
|
151
|
+
expect(el?.getAttribute('data-bc-layer')).toBe('selection');
|
|
152
|
+
expect(el?.style?.position).toBe('absolute');
|
|
153
|
+
expect(el?.style?.pointerEvents).toBe('none');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('uses default className and zIndex per layer type', () => {
|
|
157
|
+
const { container } = render(<EditorViewLayer layer="decorator" />);
|
|
158
|
+
const el = container.firstElementChild as HTMLElement;
|
|
159
|
+
expect(el?.className).toContain('barocss-editor-decorators');
|
|
160
|
+
expect(el?.style?.zIndex).toBe('10');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('merges style prop with base style', () => {
|
|
164
|
+
const { container } = render(
|
|
165
|
+
<EditorViewLayer layer="custom" style={{ opacity: 0.5 }} />
|
|
166
|
+
);
|
|
167
|
+
const el = container.firstElementChild as HTMLElement;
|
|
168
|
+
expect(el?.style?.opacity).toBe('0.5');
|
|
169
|
+
expect(el?.style?.position).toBe('absolute');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('EditorViewContext', () => {
|
|
174
|
+
it('useEditorViewContext throws when used outside Provider', () => {
|
|
175
|
+
function Consumer() {
|
|
176
|
+
useEditorViewContext();
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
expect(() => render(<Consumer />)).toThrow(/must be used within EditorViewContext/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('useOptionalEditorViewContext returns null when outside Provider', () => {
|
|
183
|
+
function Consumer() {
|
|
184
|
+
const ctx = useOptionalEditorViewContext();
|
|
185
|
+
return <span data-testid="ctx">{ctx === null ? 'null' : 'value'}</span>;
|
|
186
|
+
}
|
|
187
|
+
render(<Consumer />);
|
|
188
|
+
expect(screen.getByTestId('ctx').textContent).toBe('null');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('EditorViewContextProvider provides editor, selectionHandler, inputHandler, mutationObserverManager, setContentEditableElement', () => {
|
|
192
|
+
const editor = mockEditor();
|
|
193
|
+
function Consumer() {
|
|
194
|
+
const ctx = useEditorViewContext();
|
|
195
|
+
return (
|
|
196
|
+
<span
|
|
197
|
+
data-testid="ctx"
|
|
198
|
+
data-has-editor={!!ctx.editor}
|
|
199
|
+
data-has-selection-handler={!!ctx.selectionHandler}
|
|
200
|
+
data-has-input-handler={!!ctx.inputHandler}
|
|
201
|
+
data-has-mutation-manager={!!ctx.mutationObserverManager}
|
|
202
|
+
data-has-set-content-editable={!!ctx.setContentEditableElement}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
render(
|
|
207
|
+
<EditorViewContextProvider editor={editor}>
|
|
208
|
+
<Consumer />
|
|
209
|
+
</EditorViewContextProvider>
|
|
210
|
+
);
|
|
211
|
+
const el = screen.getByTestId('ctx');
|
|
212
|
+
expect(el.getAttribute('data-has-editor')).toBe('true');
|
|
213
|
+
expect(el.getAttribute('data-has-selection-handler')).toBe('true');
|
|
214
|
+
expect(el.getAttribute('data-has-input-handler')).toBe('true');
|
|
215
|
+
expect(el.getAttribute('data-has-mutation-manager')).toBe('true');
|
|
216
|
+
expect(el.getAttribute('data-has-set-content-editable')).toBe('true');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { findClosestInlineTextNode, reconstructModelTextFromDOM } from '../src/dom-sync/edit-position';
|
|
3
|
+
|
|
4
|
+
describe('findClosestInlineTextNode', () => {
|
|
5
|
+
it('returns element with data-bc-sid when node is that element', () => {
|
|
6
|
+
const el = document.createElement('span');
|
|
7
|
+
el.setAttribute('data-bc-sid', 'n1');
|
|
8
|
+
expect(findClosestInlineTextNode(el)).toBe(el);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('returns closest ancestor with data-bc-sid when node is child', () => {
|
|
12
|
+
const wrap = document.createElement('span');
|
|
13
|
+
wrap.setAttribute('data-bc-sid', 'n1');
|
|
14
|
+
const text = document.createTextNode('hi');
|
|
15
|
+
wrap.appendChild(text);
|
|
16
|
+
expect(findClosestInlineTextNode(text)).toBe(wrap);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns null when no ancestor has data-bc-sid', () => {
|
|
20
|
+
const div = document.createElement('div');
|
|
21
|
+
const text = document.createTextNode('hi');
|
|
22
|
+
div.appendChild(text);
|
|
23
|
+
expect(findClosestInlineTextNode(text)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns null for null node', () => {
|
|
27
|
+
expect(findClosestInlineTextNode(null as any)).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('reconstructModelTextFromDOM', () => {
|
|
32
|
+
it('returns concatenated text from text nodes in order', () => {
|
|
33
|
+
const span = document.createElement('span');
|
|
34
|
+
span.setAttribute('data-bc-sid', 'n1');
|
|
35
|
+
const t1 = document.createTextNode('Hello');
|
|
36
|
+
const t2 = document.createTextNode(' ');
|
|
37
|
+
const t3 = document.createTextNode('World');
|
|
38
|
+
span.appendChild(t1);
|
|
39
|
+
span.appendChild(t2);
|
|
40
|
+
span.appendChild(t3);
|
|
41
|
+
expect(reconstructModelTextFromDOM(span)).toBe('Hello World');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns empty string when element has no text nodes', () => {
|
|
45
|
+
const span = document.createElement('span');
|
|
46
|
+
span.setAttribute('data-bc-sid', 'n1');
|
|
47
|
+
expect(reconstructModelTextFromDOM(span)).toBe('');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { createMutationObserverManager } from '../src/mutation-observer-manager';
|
|
3
|
+
|
|
4
|
+
describe('createMutationObserverManager', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('setup() observes the given element and invokes callback with batched mutations', async () => {
|
|
10
|
+
const onMutations = vi.fn();
|
|
11
|
+
const manager = createMutationObserverManager(onMutations);
|
|
12
|
+
|
|
13
|
+
const el = document.createElement('div');
|
|
14
|
+
document.body.appendChild(el);
|
|
15
|
+
|
|
16
|
+
manager.setup(el);
|
|
17
|
+
|
|
18
|
+
const text = document.createTextNode('hello');
|
|
19
|
+
el.appendChild(text);
|
|
20
|
+
text.textContent = 'world';
|
|
21
|
+
|
|
22
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
23
|
+
|
|
24
|
+
expect(onMutations).toHaveBeenCalledTimes(1);
|
|
25
|
+
const mutations = onMutations.mock.calls[0][0] as MutationRecord[];
|
|
26
|
+
expect(Array.isArray(mutations)).toBe(true);
|
|
27
|
+
expect(mutations.length).toBeGreaterThanOrEqual(1);
|
|
28
|
+
|
|
29
|
+
manager.disconnect();
|
|
30
|
+
document.body.removeChild(el);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('disconnect() stops observing and clears pending', async () => {
|
|
34
|
+
const onMutations = vi.fn();
|
|
35
|
+
const manager = createMutationObserverManager(onMutations);
|
|
36
|
+
|
|
37
|
+
const el = document.createElement('div');
|
|
38
|
+
document.body.appendChild(el);
|
|
39
|
+
manager.setup(el);
|
|
40
|
+
manager.disconnect();
|
|
41
|
+
|
|
42
|
+
el.appendChild(document.createTextNode('x'));
|
|
43
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
44
|
+
|
|
45
|
+
expect(onMutations).not.toHaveBeenCalled();
|
|
46
|
+
document.body.removeChild(el);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { ReactSelectionHandler } from '../src/selection-handler';
|
|
3
|
+
|
|
4
|
+
function createMockEditor(getNode: (id: string) => unknown) {
|
|
5
|
+
return {
|
|
6
|
+
dataStore: { getNode },
|
|
7
|
+
updateSelection: () => {},
|
|
8
|
+
} as any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('ReactSelectionHandler', () => {
|
|
12
|
+
it('instantiates with editor and getContentEditableElement', () => {
|
|
13
|
+
const getEl = () => document.createElement('div');
|
|
14
|
+
const editor = createMockEditor(() => null);
|
|
15
|
+
const handler = new ReactSelectionHandler(editor, getEl);
|
|
16
|
+
expect(handler).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('isSelectionInsideEditableText returns false when selection is empty', () => {
|
|
20
|
+
const getEl = () => document.createElement('div');
|
|
21
|
+
const editor = createMockEditor(() => ({ stype: 'inline-text' }));
|
|
22
|
+
const handler = new ReactSelectionHandler(editor, getEl);
|
|
23
|
+
const sel = window.getSelection();
|
|
24
|
+
sel?.removeAllRanges();
|
|
25
|
+
expect(handler.isSelectionInsideEditableText()).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('isSelectionInsideEditableText returns true when selection is inside inline-text node', () => {
|
|
29
|
+
const root = document.createElement('div');
|
|
30
|
+
root.setAttribute('contenteditable', 'true');
|
|
31
|
+
const span = document.createElement('span');
|
|
32
|
+
span.setAttribute('data-bc-sid', 't1');
|
|
33
|
+
const text = document.createTextNode('hello');
|
|
34
|
+
span.appendChild(text);
|
|
35
|
+
root.appendChild(span);
|
|
36
|
+
document.body.appendChild(root);
|
|
37
|
+
|
|
38
|
+
const getEl = () => root;
|
|
39
|
+
const editor = createMockEditor((id) => (id === 't1' ? { stype: 'inline-text' } : null));
|
|
40
|
+
const handler = new ReactSelectionHandler(editor, getEl);
|
|
41
|
+
|
|
42
|
+
const range = document.createRange();
|
|
43
|
+
range.setStart(text, 0);
|
|
44
|
+
range.setEnd(text, 2);
|
|
45
|
+
const sel = window.getSelection();
|
|
46
|
+
sel?.removeAllRanges();
|
|
47
|
+
sel?.addRange(range);
|
|
48
|
+
|
|
49
|
+
expect(handler.isSelectionInsideEditableText()).toBe(true);
|
|
50
|
+
|
|
51
|
+
document.body.removeChild(root);
|
|
52
|
+
sel?.removeAllRanges();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('setProgrammaticChange(true) causes handleSelectionChange to skip updateSelection', () => {
|
|
56
|
+
const root = document.createElement('div');
|
|
57
|
+
const span = document.createElement('span');
|
|
58
|
+
span.setAttribute('data-bc-sid', 't1');
|
|
59
|
+
span.appendChild(document.createTextNode('x'));
|
|
60
|
+
root.appendChild(span);
|
|
61
|
+
document.body.appendChild(root);
|
|
62
|
+
|
|
63
|
+
const updateSelection = vi.fn();
|
|
64
|
+
const editor = createMockEditor((id) => (id === 't1' ? { stype: 'inline-text' } : null));
|
|
65
|
+
editor.updateSelection = updateSelection;
|
|
66
|
+
|
|
67
|
+
const getEl = () => root;
|
|
68
|
+
const handler = new ReactSelectionHandler(editor, getEl);
|
|
69
|
+
|
|
70
|
+
const range = document.createRange();
|
|
71
|
+
range.setStart(span.firstChild!, 0);
|
|
72
|
+
range.setEnd(span.firstChild!, 1);
|
|
73
|
+
window.getSelection()?.removeAllRanges();
|
|
74
|
+
window.getSelection()?.addRange(range);
|
|
75
|
+
|
|
76
|
+
handler.setProgrammaticChange(true);
|
|
77
|
+
handler.handleSelectionChange();
|
|
78
|
+
expect(updateSelection).not.toHaveBeenCalled();
|
|
79
|
+
|
|
80
|
+
handler.setProgrammaticChange(false);
|
|
81
|
+
handler.handleSelectionChange();
|
|
82
|
+
expect(updateSelection).toHaveBeenCalled();
|
|
83
|
+
|
|
84
|
+
document.body.removeChild(root);
|
|
85
|
+
});
|
|
86
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"noUnusedLocals": false,
|
|
7
|
+
"noUnusedParameters": false,
|
|
8
|
+
"skipLibCheck": true
|
|
9
|
+
},
|
|
10
|
+
"include": ["src/**/*"],
|
|
11
|
+
"exclude": ["dist", "node_modules"]
|
|
12
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import dts from 'vite-plugin-dts';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [
|
|
6
|
+
dts({
|
|
7
|
+
insertTypesEntry: true,
|
|
8
|
+
}),
|
|
9
|
+
],
|
|
10
|
+
build: {
|
|
11
|
+
minify: true,
|
|
12
|
+
lib: {
|
|
13
|
+
entry: 'src/index.ts',
|
|
14
|
+
name: 'BarocssEditorViewReact',
|
|
15
|
+
fileName: 'index',
|
|
16
|
+
formats: ['es', 'cjs'],
|
|
17
|
+
},
|
|
18
|
+
rollupOptions: {
|
|
19
|
+
external: (id) =>
|
|
20
|
+
id === 'react' ||
|
|
21
|
+
id === 'react-dom' ||
|
|
22
|
+
id === 'react/jsx-runtime' ||
|
|
23
|
+
id.startsWith('react/'),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|