@handlewithcare/react-prosemirror 2.5.3 → 2.6.0-tiptap.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 (66) hide show
  1. package/README.md +2 -3
  2. package/dist/cjs/components/__tests__/ProseMirror.composition.test.js +398 -0
  3. package/dist/cjs/components/__tests__/ProseMirror.domchange.test.js +270 -0
  4. package/dist/cjs/components/__tests__/ProseMirror.draw-decoration.test.js +1010 -0
  5. package/dist/cjs/components/__tests__/ProseMirror.draw.test.js +337 -0
  6. package/dist/cjs/components/__tests__/ProseMirror.node-view.test.js +315 -0
  7. package/dist/cjs/components/__tests__/ProseMirror.selection.test.js +444 -0
  8. package/dist/cjs/components/__tests__/ProseMirror.test.js +382 -0
  9. package/dist/cjs/contexts/__tests__/DeferredLayoutEffects.test.js +141 -0
  10. package/dist/cjs/hooks/__tests__/useEditorViewLayoutEffect.test.js +108 -0
  11. package/dist/cjs/hooks/useClientOnly.js +19 -0
  12. package/dist/cjs/plugins/__tests__/reactKeys.test.js +81 -0
  13. package/dist/cjs/selection/SelectionDOMObserver.js +171 -0
  14. package/dist/cjs/selection/hasFocusAndSelection.js +35 -0
  15. package/dist/cjs/selection/selectionFromDOM.js +77 -0
  16. package/dist/cjs/selection/selectionToDOM.js +226 -0
  17. package/dist/cjs/ssr.js +85 -0
  18. package/dist/cjs/tiptap/TiptapEditorContent.js +93 -0
  19. package/dist/cjs/tiptap/TiptapEditorView.js +84 -0
  20. package/dist/cjs/tiptap/hooks/useTiptapEditorEffect.js +27 -0
  21. package/dist/cjs/tiptap/hooks/useTiptapEditorEventCallback.js +26 -0
  22. package/dist/cjs/tiptap/index.js +32 -0
  23. package/dist/cjs/tiptap/tiptapNodeView.js +181 -0
  24. package/dist/esm/components/__tests__/ProseMirror.composition.test.js +395 -0
  25. package/dist/esm/components/__tests__/ProseMirror.domchange.test.js +266 -0
  26. package/dist/esm/components/__tests__/ProseMirror.draw-decoration.test.js +967 -0
  27. package/dist/esm/components/__tests__/ProseMirror.draw.test.js +294 -0
  28. package/dist/esm/components/__tests__/ProseMirror.node-view.test.js +272 -0
  29. package/dist/esm/components/__tests__/ProseMirror.selection.test.js +440 -0
  30. package/dist/esm/components/__tests__/ProseMirror.test.js +339 -0
  31. package/dist/esm/contexts/__tests__/DeferredLayoutEffects.test.js +98 -0
  32. package/dist/esm/hooks/__tests__/useEditorViewLayoutEffect.test.js +99 -0
  33. package/dist/esm/hooks/useClientOnly.js +9 -0
  34. package/dist/esm/hooks/useEditorEffect.js +4 -0
  35. package/dist/esm/hooks/useEditorEventCallback.js +3 -5
  36. package/dist/esm/plugins/__tests__/reactKeys.test.js +77 -0
  37. package/dist/esm/selection/SelectionDOMObserver.js +161 -0
  38. package/dist/esm/selection/hasFocusAndSelection.js +17 -0
  39. package/dist/esm/selection/selectionFromDOM.js +59 -0
  40. package/dist/esm/selection/selectionToDOM.js +196 -0
  41. package/dist/esm/ssr.js +82 -0
  42. package/dist/esm/tiptap/TiptapEditorContent.js +42 -0
  43. package/dist/esm/tiptap/TiptapEditorView.js +35 -0
  44. package/dist/esm/tiptap/hooks/useTiptapEditorEffect.js +34 -0
  45. package/dist/esm/tiptap/hooks/useTiptapEditorEventCallback.js +26 -0
  46. package/dist/esm/tiptap/index.js +5 -0
  47. package/dist/esm/tiptap/tiptapNodeView.js +149 -0
  48. package/dist/tsconfig.tsbuildinfo +1 -1
  49. package/dist/types/constants.d.ts +1 -1
  50. package/dist/types/hooks/__tests__/useEditorViewLayoutEffect.test.d.ts +1 -0
  51. package/dist/types/hooks/useClientOnly.d.ts +1 -0
  52. package/dist/types/hooks/useEditorEffect.d.ts +4 -0
  53. package/dist/types/hooks/useEditorEventCallback.d.ts +3 -5
  54. package/dist/types/props.d.ts +26 -26
  55. package/dist/types/selection/SelectionDOMObserver.d.ts +33 -0
  56. package/dist/types/selection/hasFocusAndSelection.d.ts +3 -0
  57. package/dist/types/selection/selectionFromDOM.d.ts +4 -0
  58. package/dist/types/selection/selectionToDOM.d.ts +9 -0
  59. package/dist/types/ssr.d.ts +19 -0
  60. package/dist/types/tiptap/TiptapEditorContent.d.ts +7 -0
  61. package/dist/types/tiptap/TiptapEditorView.d.ts +13 -0
  62. package/dist/types/tiptap/hooks/useTiptapEditorEffect.d.ts +21 -0
  63. package/dist/types/tiptap/hooks/useTiptapEditorEventCallback.d.ts +13 -0
  64. package/dist/types/tiptap/index.d.ts +5 -0
  65. package/dist/types/tiptap/tiptapNodeView.d.ts +48 -0
  66. package/package.json +8 -1
@@ -0,0 +1,339 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ function _extends() {
2
+ _extends = Object.assign || function(target) {
3
+ for(var i = 1; i < arguments.length; i++){
4
+ var source = arguments[i];
5
+ for(var key in source){
6
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
7
+ target[key] = source[key];
8
+ }
9
+ }
10
+ }
11
+ return target;
12
+ };
13
+ return _extends.apply(this, arguments);
14
+ }
15
+ import { render, screen } from "@testing-library/react";
16
+ import { Schema } from "prosemirror-model";
17
+ import { EditorState, Plugin } from "prosemirror-state";
18
+ import { doc, em, hr, li, p, schema, strong, ul } from "prosemirror-test-builder";
19
+ import React, { forwardRef, useEffect, useState } from "react";
20
+ import { useEditorEffect } from "../../hooks/useEditorEffect.js";
21
+ import { useStopEvent } from "../../hooks/useStopEvent.js";
22
+ import { reactKeys } from "../../plugins/reactKeys.js";
23
+ import { tempEditor } from "../../testing/editorViewTestHelpers.js";
24
+ import { ProseMirror } from "../ProseMirror.js";
25
+ import { ProseMirrorDoc } from "../ProseMirrorDoc.js";
26
+ describe("ProseMirror", ()=>{
27
+ it("renders a contenteditable", async ()=>{
28
+ const schema = new Schema({
29
+ nodes: {
30
+ text: {},
31
+ doc: {
32
+ content: "text*"
33
+ }
34
+ }
35
+ });
36
+ const editorState = EditorState.create({
37
+ schema
38
+ });
39
+ function TestEditor() {
40
+ return /*#__PURE__*/ React.createElement(ProseMirror, {
41
+ defaultState: editorState
42
+ }, /*#__PURE__*/ React.createElement(ProseMirrorDoc, {
43
+ "data-testid": "editor"
44
+ }));
45
+ }
46
+ render(/*#__PURE__*/ React.createElement(TestEditor, null));
47
+ const editor = screen.getByTestId("editor");
48
+ editor.focus();
49
+ await browser.keys("H");
50
+ await browser.keys("e");
51
+ await browser.keys("l");
52
+ await browser.keys("l");
53
+ await browser.keys("o");
54
+ await browser.keys(",");
55
+ await browser.keys(" ");
56
+ await browser.keys("w");
57
+ await browser.keys("o");
58
+ await browser.keys("r");
59
+ await browser.keys("l");
60
+ await browser.keys("d");
61
+ await browser.keys("!");
62
+ expect(editor.textContent).toBe("Hello, world!");
63
+ });
64
+ it("supports lifted editor state", async ()=>{
65
+ const schema = new Schema({
66
+ nodes: {
67
+ text: {},
68
+ doc: {
69
+ content: "text*"
70
+ }
71
+ }
72
+ });
73
+ let outerEditorState = EditorState.create({
74
+ schema
75
+ });
76
+ function TestEditor() {
77
+ const [editorState, setEditorState] = useState(outerEditorState);
78
+ useEffect(()=>{
79
+ outerEditorState = editorState;
80
+ }, [
81
+ editorState
82
+ ]);
83
+ return /*#__PURE__*/ React.createElement(ProseMirror, {
84
+ state: editorState,
85
+ dispatchTransaction: (tr)=>setEditorState(editorState.apply(tr))
86
+ }, /*#__PURE__*/ React.createElement(ProseMirrorDoc, {
87
+ "data-testid": "editor"
88
+ }));
89
+ }
90
+ render(/*#__PURE__*/ React.createElement(TestEditor, null));
91
+ const editor = screen.getByTestId("editor");
92
+ editor.focus();
93
+ await browser.keys("H");
94
+ await browser.keys("e");
95
+ await browser.keys("l");
96
+ await browser.keys("l");
97
+ await browser.keys("o");
98
+ await browser.keys(",");
99
+ await browser.keys(" ");
100
+ await browser.keys("w");
101
+ await browser.keys("o");
102
+ await browser.keys("r");
103
+ await browser.keys("l");
104
+ await browser.keys("d");
105
+ await browser.keys("!");
106
+ expect(outerEditorState.doc.textContent).toBe("Hello, world!");
107
+ });
108
+ it("supports React NodeViews", async ()=>{
109
+ const schema = new Schema({
110
+ nodes: {
111
+ text: {},
112
+ paragraph: {
113
+ content: "text*",
114
+ toDOM () {
115
+ return [
116
+ "p",
117
+ 0
118
+ ];
119
+ }
120
+ },
121
+ doc: {
122
+ content: "paragraph+"
123
+ }
124
+ }
125
+ });
126
+ const editorState = EditorState.create({
127
+ schema
128
+ });
129
+ const Paragraph = /*#__PURE__*/ forwardRef(function Paragraph(param, ref) {
130
+ let { children } = param;
131
+ return /*#__PURE__*/ React.createElement("p", {
132
+ ref: ref,
133
+ "data-testid": "paragraph"
134
+ }, children);
135
+ });
136
+ const reactNodeViews = {
137
+ paragraph: Paragraph
138
+ };
139
+ function TestEditor() {
140
+ return /*#__PURE__*/ React.createElement(ProseMirror, {
141
+ defaultState: editorState,
142
+ nodeViews: reactNodeViews
143
+ }, /*#__PURE__*/ React.createElement(ProseMirrorDoc, {
144
+ "data-testid": "editor"
145
+ }));
146
+ }
147
+ render(/*#__PURE__*/ React.createElement(TestEditor, null));
148
+ const editor = screen.getByTestId("editor");
149
+ editor.focus();
150
+ await browser.keys("H");
151
+ await browser.keys("e");
152
+ await browser.keys("l");
153
+ await browser.keys("l");
154
+ await browser.keys("o");
155
+ await browser.keys(",");
156
+ await browser.keys(" ");
157
+ await browser.keys("w");
158
+ await browser.keys("o");
159
+ await browser.keys("r");
160
+ await browser.keys("l");
161
+ await browser.keys("d");
162
+ await browser.keys("!");
163
+ expect(editor.textContent).toBe("Hello, world!");
164
+ // Ensure that ProseMirror really rendered our Paragraph
165
+ // component, not just any old <p> tag
166
+ expect(screen.getAllByTestId("paragraph").length).toBeGreaterThanOrEqual(1);
167
+ });
168
+ it("reflects the current state in .props", async ()=>{
169
+ const { view } = tempEditor({
170
+ doc: doc(p())
171
+ });
172
+ expect(view.state).toBe(view.props.state);
173
+ });
174
+ it("calls handleScrollToSelection when appropriate", async ()=>{
175
+ let scrolled = 0;
176
+ const { view } = tempEditor({
177
+ doc: doc(p()),
178
+ handleScrollToSelection: ()=>{
179
+ scrolled++;
180
+ return false;
181
+ }
182
+ });
183
+ view.dispatch(view.state.tr.scrollIntoView());
184
+ expect(scrolled).toBe(1);
185
+ });
186
+ it("can be queried for the DOM position at a doc position", async ()=>{
187
+ const { view } = tempEditor({
188
+ doc: doc(ul(li(p(strong("foo")))))
189
+ });
190
+ const inText = view.domAtPos(4);
191
+ expect(inText.offset).toBe(1);
192
+ expect(inText.node.nodeValue).toBe("foo");
193
+ const beforeLI = view.domAtPos(1);
194
+ expect(beforeLI.offset).toBe(0);
195
+ expect(beforeLI.node.nodeName).toBe("UL");
196
+ const afterP = view.domAtPos(7);
197
+ expect(afterP.offset).toBe(1);
198
+ expect(afterP.node.nodeName).toBe("LI");
199
+ });
200
+ it("can bias DOM position queries to enter nodes", async ()=>{
201
+ const { view } = tempEditor({
202
+ doc: doc(p(em(strong("a"), "b"), "c"))
203
+ });
204
+ function get(pos, bias) {
205
+ const r = view.domAtPos(pos, bias);
206
+ return (r.node.nodeType == 1 ? r.node.nodeName : r.node.nodeValue) + "@" + r.offset;
207
+ }
208
+ expect(get(1, 0)).toBe("P@0");
209
+ expect(get(1, -1)).toBe("P@0");
210
+ expect(get(1, 1)).toBe("a@0");
211
+ expect(get(2, -1)).toBe("a@1");
212
+ expect(get(2, 0)).toBe("EM@1");
213
+ expect(get(2, 1)).toBe("b@0");
214
+ expect(get(3, -1)).toBe("b@1");
215
+ expect(get(3, 0)).toBe("P@1");
216
+ expect(get(3, 1)).toBe("c@0");
217
+ expect(get(4, -1)).toBe("c@1");
218
+ expect(get(4, 0)).toBe("P@2");
219
+ expect(get(4, 1)).toBe("P@2");
220
+ });
221
+ it("can be queried for a node's DOM representation", async ()=>{
222
+ const { view } = tempEditor({
223
+ doc: doc(p("foo"), hr())
224
+ });
225
+ expect(view.nodeDOM(0).nodeName).toBe("P");
226
+ expect(view.nodeDOM(5).nodeName).toBe("HR");
227
+ expect(view.nodeDOM(3)).toBeNull();
228
+ });
229
+ it("can map DOM positions to doc positions", async ()=>{
230
+ const { view } = tempEditor({
231
+ doc: doc(p("foo"), hr())
232
+ });
233
+ expect(view.posAtDOM(view.dom.firstChild.firstChild, 2)).toBe(3);
234
+ expect(view.posAtDOM(view.dom, 1)).toBe(5);
235
+ expect(view.posAtDOM(view.dom, 2)).toBe(6);
236
+ expect(view.posAtDOM(view.dom.lastChild, 0, -1)).toBe(5);
237
+ expect(view.posAtDOM(view.dom.lastChild, 0, 1)).toBe(6);
238
+ });
239
+ it("binds this to itself in dispatchTransaction prop", async ()=>{
240
+ let thisBinding;
241
+ const { view } = tempEditor({
242
+ doc: doc(p("foo"), hr()),
243
+ dispatchTransaction () {
244
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
245
+ thisBinding = this;
246
+ }
247
+ });
248
+ view.dispatch(view.state.tr.insertText("x"));
249
+ expect(view).toBe(thisBinding);
250
+ });
251
+ it("replaces the EditorView when ProseMirror would redraw", async ()=>{
252
+ const viewPlugin = ()=>new Plugin({
253
+ props: {
254
+ nodeViews: {
255
+ horizontal_rule () {
256
+ const dom = document.createElement("hr");
257
+ return {
258
+ dom
259
+ };
260
+ }
261
+ }
262
+ }
263
+ });
264
+ const startDoc = doc(p());
265
+ const firstState = EditorState.create({
266
+ doc: startDoc,
267
+ schema,
268
+ plugins: [
269
+ viewPlugin(),
270
+ reactKeys()
271
+ ]
272
+ });
273
+ let firstView = null;
274
+ let secondView = null;
275
+ function Test() {
276
+ useEditorEffect((v)=>{
277
+ if (firstView === null) {
278
+ firstView = v;
279
+ } else {
280
+ secondView = v;
281
+ }
282
+ });
283
+ return null;
284
+ }
285
+ const Paragraph = /*#__PURE__*/ forwardRef(function Paragraph(param, ref) {
286
+ let { nodeProps , children , ...props } = param;
287
+ return /*#__PURE__*/ React.createElement("p", _extends({
288
+ ref: ref,
289
+ "data-testid": "node-view"
290
+ }, props), children);
291
+ });
292
+ const { rerender } = render(/*#__PURE__*/ React.createElement(ProseMirror, {
293
+ state: firstState,
294
+ nodeViews: {
295
+ paragraph: Paragraph
296
+ }
297
+ }, /*#__PURE__*/ React.createElement(Test, null), /*#__PURE__*/ React.createElement(ProseMirrorDoc, null)));
298
+ expect(()=>screen.getByTestId("node-view")).not.toThrow();
299
+ const secondState = EditorState.create({
300
+ doc: startDoc,
301
+ schema,
302
+ plugins: [
303
+ viewPlugin(),
304
+ reactKeys()
305
+ ]
306
+ });
307
+ rerender(/*#__PURE__*/ React.createElement(ProseMirror, {
308
+ state: secondState,
309
+ nodeViews: {
310
+ paragraph: Paragraph
311
+ }
312
+ }, /*#__PURE__*/ React.createElement(Test, null), /*#__PURE__*/ React.createElement(ProseMirrorDoc, null)));
313
+ expect(()=>screen.getByTestId("node-view")).not.toThrow();
314
+ expect(firstView).not.toBeNull();
315
+ expect(secondView).not.toBeNull();
316
+ expect(firstView === secondView).toBeFalsy();
317
+ });
318
+ it("supports focusing interactive controls", async ()=>{
319
+ tempEditor({
320
+ doc: doc(hr()),
321
+ nodeViews: {
322
+ horizontal_rule: /*#__PURE__*/ forwardRef(function Button(param, ref) {
323
+ let { nodeProps , ...props } = param;
324
+ useStopEvent(()=>{
325
+ return true;
326
+ });
327
+ return /*#__PURE__*/ React.createElement("button", _extends({
328
+ id: "button",
329
+ ref: ref,
330
+ type: "button"
331
+ }, props), "Click me");
332
+ })
333
+ }
334
+ });
335
+ const button = screen.getByText("Click me");
336
+ await $("#button").click();
337
+ expect(document.activeElement === button).toBeTruthy();
338
+ });
339
+ });
@@ -0,0 +1,98 @@
1
+ import { act, render, screen } from "@testing-library/react";
2
+ import React, { useLayoutEffect, useState } from "react";
3
+ import { LayoutGroup } from "../../components/LayoutGroup.js";
4
+ import { useLayoutGroupEffect } from "../../hooks/useLayoutGroupEffect.js";
5
+ describe("DeferredLayoutEffects", ()=>{
6
+ jest.useFakeTimers("modern");
7
+ it("registers multiple effects and runs them", ()=>{
8
+ function Parent() {
9
+ return /*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(Child, null));
10
+ }
11
+ function Child() {
12
+ const [double, setDouble] = useState(1);
13
+ useLayoutEffect(()=>{
14
+ if (double === 2) {
15
+ setTimeout(()=>{
16
+ setDouble((d)=>d * 2.5);
17
+ }, 500);
18
+ }
19
+ if (double === 20) {
20
+ setDouble((d)=>d * 2.5);
21
+ }
22
+ }, [
23
+ double
24
+ ]);
25
+ useLayoutGroupEffect(()=>{
26
+ const timeout = setTimeout(()=>{
27
+ setDouble((d)=>d * 2);
28
+ }, 1000);
29
+ return ()=>{
30
+ clearTimeout(timeout);
31
+ };
32
+ }, [
33
+ double
34
+ ]);
35
+ return /*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement("div", {
36
+ "data-testid": "double"
37
+ }, double));
38
+ }
39
+ // The component mounts ...
40
+ // ... the initial value should be 1
41
+ // ... there should be one timeout scheduled by the deferred effect
42
+ render(/*#__PURE__*/ React.createElement(Parent, null));
43
+ expect(screen.getByTestId("double").innerHTML).toBe("1");
44
+ // This block assert that deferred effects run.
45
+ // --------------------------------------------
46
+ // 1000 milliseconds go by ...
47
+ // ... the timeout set by the deferred effect should run
48
+ // ... the timeout should double the new value to 2
49
+ // ... the immediate effect should set a timeout
50
+ // ... the deferred effect should set a timeout
51
+ act(()=>{
52
+ jest.advanceTimersByTime(1000);
53
+ });
54
+ expect(screen.getByTestId("double").innerHTML).toBe("2");
55
+ // The next three blocks assert that cleanup of deferred effects run.
56
+ // ------------------------------------------------------------------
57
+ // 500 milliseconds go by ...
58
+ // ... the timeout set by the immediate effect should run
59
+ // ... the timeout should set the value to 5
60
+ // ... the old deferred effect should cancel its timeout
61
+ // ... the new deferred effect should set a new timeout
62
+ act(()=>{
63
+ jest.advanceTimersByTime(500);
64
+ });
65
+ expect(screen.getByTestId("double").innerHTML).toBe("5");
66
+ // ... 500 more milliseconds go by ...
67
+ // ... the canceled timeout should not run
68
+ // ... the rescheduled timoeut should not yet run
69
+ act(()=>{
70
+ jest.advanceTimersByTime(500);
71
+ });
72
+ expect(screen.getByTestId("double").innerHTML).toBe("5");
73
+ // ... 500 more milliseconds go by ...
74
+ // ... the rescheduled timeout should run
75
+ // ... the timeout should double the value to 10
76
+ // ... the deferred effect should set a new timeout
77
+ act(()=>{
78
+ jest.advanceTimersByTime(500);
79
+ });
80
+ expect(screen.getByTestId("double").innerHTML).toBe("10");
81
+ // The next block asserts that cancelation of deferred effects works.
82
+ // ------------------------------------------------------------------
83
+ // 1000 milliseconds go by ...
84
+ // ... the timeout set by the deferred effect should run
85
+ // ... the timeout should double the value to 20
86
+ // ... the immediate effect should then set the value to 50
87
+ // ... the deferred effect from the first render should not run
88
+ // ... the deferred effect from the second render should run
89
+ // ... the deferred effect that does run should set a new timeout
90
+ act(()=>{
91
+ jest.advanceTimersByTime(1000);
92
+ });
93
+ // For this assertion, we need to clear a timer from the React scheduler.
94
+ jest.advanceTimersByTime(1);
95
+ expect(screen.getByTestId("double").innerHTML).toBe("50");
96
+ expect(jest.getTimerCount()).toBe(1);
97
+ });
98
+ });
@@ -0,0 +1,99 @@
1
+ /* eslint-disable @typescript-eslint/no-empty-function */ import { render } from "@testing-library/react";
2
+ import React from "react";
3
+ import { LayoutGroup } from "../../components/LayoutGroup.js";
4
+ import { EditorContext } from "../../contexts/EditorContext.js";
5
+ import { EditorStateContext } from "../../contexts/EditorStateContext.js";
6
+ import { useEditorEffect } from "../useEditorEffect.js";
7
+ function TestComponent(param) {
8
+ let { effect , dependencies =[] } = param;
9
+ // eslint-disable-next-line react-hooks/exhaustive-deps
10
+ useEditorEffect(effect, dependencies);
11
+ return null;
12
+ }
13
+ describe("useEditorViewLayoutEffect", ()=>{
14
+ it("should run the effect", ()=>{
15
+ const effect = jest.fn();
16
+ const editorView = {};
17
+ const editorState = {};
18
+ const registerEventListener = ()=>{};
19
+ const unregisterEventListener = ()=>{};
20
+ render(/*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(EditorContext.Provider, {
21
+ value: {
22
+ view: editorView,
23
+ registerEventListener,
24
+ unregisterEventListener
25
+ }
26
+ }, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, {
27
+ value: editorState
28
+ }, /*#__PURE__*/ React.createElement(TestComponent, {
29
+ effect: effect
30
+ })))));
31
+ expect(effect).toHaveBeenCalled();
32
+ expect(effect).toHaveBeenCalledWith(editorView);
33
+ });
34
+ it("should not re-run the effect if no dependencies change", ()=>{
35
+ const effect = jest.fn();
36
+ const editorView = {};
37
+ const editorState = {};
38
+ const registerEventListener = ()=>{};
39
+ const unregisterEventListener = ()=>{};
40
+ const contextValue = {
41
+ view: editorView,
42
+ registerEventListener,
43
+ unregisterEventListener
44
+ };
45
+ const { rerender } = render(/*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(EditorContext.Provider, {
46
+ value: contextValue
47
+ }, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, {
48
+ value: editorState
49
+ }, /*#__PURE__*/ React.createElement(TestComponent, {
50
+ effect: effect,
51
+ dependencies: []
52
+ })), " ")));
53
+ rerender(/*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(EditorContext.Provider, {
54
+ value: contextValue
55
+ }, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, {
56
+ value: editorState
57
+ }, /*#__PURE__*/ React.createElement(TestComponent, {
58
+ effect: effect,
59
+ dependencies: []
60
+ })))));
61
+ expect(effect).toHaveBeenCalledTimes(1);
62
+ });
63
+ it("should re-run the effect if dependencies change", ()=>{
64
+ const effect = jest.fn();
65
+ const editorView = {};
66
+ const editorState = {};
67
+ const registerEventListener = ()=>{};
68
+ const unregisterEventListener = ()=>{};
69
+ const { rerender } = render(/*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(EditorContext.Provider, {
70
+ value: {
71
+ view: editorView,
72
+ registerEventListener,
73
+ unregisterEventListener
74
+ }
75
+ }, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, {
76
+ value: editorState
77
+ }, /*#__PURE__*/ React.createElement(TestComponent, {
78
+ effect: effect,
79
+ dependencies: [
80
+ "one"
81
+ ]
82
+ })))));
83
+ rerender(/*#__PURE__*/ React.createElement(LayoutGroup, null, /*#__PURE__*/ React.createElement(EditorContext.Provider, {
84
+ value: {
85
+ view: editorView,
86
+ registerEventListener,
87
+ unregisterEventListener
88
+ }
89
+ }, /*#__PURE__*/ React.createElement(EditorStateContext.Provider, {
90
+ value: editorState
91
+ }, /*#__PURE__*/ React.createElement(TestComponent, {
92
+ effect: effect,
93
+ dependencies: [
94
+ "two"
95
+ ]
96
+ })))));
97
+ expect(effect).toHaveBeenCalledTimes(2);
98
+ });
99
+ });
@@ -0,0 +1,9 @@
1
+ import { useSyncExternalStore } from "react";
2
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
3
+ function unsubscribe() {}
4
+ function subscribe() {
5
+ return unsubscribe;
6
+ }
7
+ export function useClientOnly() {
8
+ return useSyncExternalStore(subscribe, ()=>true, ()=>false);
9
+ }
@@ -15,6 +15,10 @@ import { useLayoutGroupEffect } from "./useLayoutGroupEffect.js";
15
15
  * synchronously after all DOM mutations, but they do so
16
16
  * _after_ the EditorView has been updated, even when the
17
17
  * EditorView lives in an ancestor component.
18
+ *
19
+ * This hook can only be used in a component that is mounted
20
+ * as a child of the TiptapEditorView component, including
21
+ * React node view components.
18
22
  */ export function useEditorEffect(effect, dependencies) {
19
23
  const { view, flushSyncRef } = useContext(EditorContext);
20
24
  // The rules of hooks want `effect` to be included in the
@@ -15,11 +15,9 @@ function assertIsReactEditorView(view) {
15
15
  * The callback will be called with the EditorView instance
16
16
  * as its first argument.
17
17
  *
18
- * This hook is dependent on both the
19
- * `EditorViewContext.Provider` and the
20
- * `DeferredLayoutEffectProvider`. It can only be used in a
21
- * component that is mounted as a child of both of these
22
- * providers.
18
+ * This hook can only be used in a component that is mounted
19
+ * as a child of the TiptapEditorView component, including
20
+ * React node view components.
23
21
  */ export function useEditorEventCallback(callback) {
24
22
  const ref = useRef(callback);
25
23
  const { view } = useContext(EditorContext);
@@ -0,0 +1,77 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Schema } from "prosemirror-model";
2
+ import { EditorState } from "prosemirror-state";
3
+ import { reactKeys, reactKeysPluginKey } from "../reactKeys.js";
4
+ const schema = new Schema({
5
+ nodes: {
6
+ doc: {
7
+ content: "block+"
8
+ },
9
+ paragraph: {
10
+ group: "block",
11
+ content: "inline*"
12
+ },
13
+ list: {
14
+ group: "block",
15
+ content: "list_item+"
16
+ },
17
+ list_item: {
18
+ content: "inline*"
19
+ },
20
+ text: {
21
+ group: "inline"
22
+ }
23
+ }
24
+ });
25
+ describe("reactNodeViewPlugin", ()=>{
26
+ it("should create a unique key for each node", ()=>{
27
+ const editorState = EditorState.create({
28
+ doc: schema.topNodeType.create(null, [
29
+ schema.nodes.paragraph.create(),
30
+ schema.nodes.paragraph.create(),
31
+ schema.nodes.paragraph.create()
32
+ ]),
33
+ plugins: [
34
+ reactKeys()
35
+ ]
36
+ });
37
+ const pluginState = reactKeysPluginKey.getState(editorState);
38
+ expect(pluginState.posToKey.size).toBe(3);
39
+ });
40
+ it("should maintain key stability when possible", ()=>{
41
+ const initialEditorState = EditorState.create({
42
+ doc: schema.topNodeType.create(null, [
43
+ schema.nodes.paragraph.create({}, schema.text("Hello")),
44
+ schema.nodes.paragraph.create(),
45
+ schema.nodes.paragraph.create()
46
+ ]),
47
+ plugins: [
48
+ reactKeys()
49
+ ]
50
+ });
51
+ const initialPluginState = reactKeysPluginKey.getState(initialEditorState);
52
+ const nextEditorState = initialEditorState.apply(initialEditorState.tr.insertText(", world!", 6));
53
+ const nextPluginState = reactKeysPluginKey.getState(nextEditorState);
54
+ expect(Array.from(initialPluginState.keyToPos.keys())).toEqual(Array.from(nextPluginState.keyToPos.keys()));
55
+ });
56
+ it("should create unique keys for new nodes", ()=>{
57
+ const initialEditorState = EditorState.create({
58
+ doc: schema.topNodeType.create(null, [
59
+ schema.nodes.paragraph.create(),
60
+ schema.nodes.paragraph.create(),
61
+ schema.nodes.paragraph.create()
62
+ ]),
63
+ plugins: [
64
+ reactKeys()
65
+ ]
66
+ });
67
+ const initialPluginState = reactKeysPluginKey.getState(initialEditorState);
68
+ const nextEditorState = initialEditorState.apply(initialEditorState.tr.insert(0, schema.nodes.list.createAndFill()));
69
+ const nextPluginState = reactKeysPluginKey.getState(nextEditorState);
70
+ // Adds new keys for new nodes
71
+ expect(nextPluginState.keyToPos.size).toBe(5);
72
+ // Maintains keys for previous nodes that are still there
73
+ Array.from(initialPluginState.keyToPos.keys()).forEach((key)=>{
74
+ expect(Array.from(nextPluginState.keyToPos.keys())).toContain(key);
75
+ });
76
+ });
77
+ });