@handlewithcare/react-prosemirror 2.0.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.txt +12 -0
- package/README.md +705 -0
- package/dist/cjs/browser.js +53 -0
- package/dist/cjs/components/ChildNodeViews.js +376 -0
- package/dist/cjs/components/CursorWrapper.js +91 -0
- package/dist/cjs/components/CustomNodeView.js +79 -0
- package/dist/cjs/components/DocNodeView.js +104 -0
- package/dist/cjs/components/LayoutGroup.js +111 -0
- package/dist/cjs/components/MarkView.js +115 -0
- package/dist/cjs/components/NativeWidgetView.js +109 -0
- package/dist/cjs/components/NodeView.js +196 -0
- package/dist/cjs/components/NodeViewComponentProps.js +4 -0
- package/dist/cjs/components/OutputSpec.js +88 -0
- package/dist/cjs/components/ProseMirror.js +103 -0
- package/dist/cjs/components/ProseMirrorDoc.js +92 -0
- package/dist/cjs/components/SeparatorHackView.js +100 -0
- package/dist/cjs/components/TextNodeView.js +112 -0
- package/dist/cjs/components/TrailingHackView.js +90 -0
- package/dist/cjs/components/WidgetView.js +95 -0
- package/dist/cjs/components/WidgetViewComponentProps.js +4 -0
- package/dist/cjs/components/__tests__/ProseMirror.composition.test.js +398 -0
- package/dist/cjs/components/__tests__/ProseMirror.domchange.test.js +270 -0
- package/dist/cjs/components/__tests__/ProseMirror.draw-decoration.test.js +1010 -0
- package/dist/cjs/components/__tests__/ProseMirror.draw.test.js +337 -0
- package/dist/cjs/components/__tests__/ProseMirror.node-view.test.js +315 -0
- package/dist/cjs/components/__tests__/ProseMirror.selection.test.js +444 -0
- package/dist/cjs/components/__tests__/ProseMirror.test.js +382 -0
- package/dist/cjs/contexts/ChildDescriptorsContext.js +19 -0
- package/dist/cjs/contexts/EditorContext.js +12 -0
- package/dist/cjs/contexts/EditorStateContext.js +12 -0
- package/dist/cjs/contexts/LayoutGroupContext.js +12 -0
- package/dist/cjs/contexts/NodeViewContext.js +12 -0
- package/dist/cjs/contexts/SelectNodeContext.js +12 -0
- package/dist/cjs/contexts/StopEventContext.js +12 -0
- package/dist/cjs/contexts/__tests__/DeferredLayoutEffects.test.js +141 -0
- package/dist/cjs/decorations/ReactWidgetType.js +58 -0
- package/dist/cjs/decorations/computeDocDeco.js +44 -0
- package/dist/cjs/decorations/internalTypes.js +4 -0
- package/dist/cjs/decorations/iterDeco.js +79 -0
- package/dist/cjs/decorations/viewDecorations.js +163 -0
- package/dist/cjs/dom.js +142 -0
- package/dist/cjs/hooks/__tests__/useEditorViewLayoutEffect.test.js +108 -0
- package/dist/cjs/hooks/useClientOnly.js +18 -0
- package/dist/cjs/hooks/useComponentEventListeners.js +39 -0
- package/dist/cjs/hooks/useEditor.js +287 -0
- package/dist/cjs/hooks/useEditorEffect.js +35 -0
- package/dist/cjs/hooks/useEditorEventCallback.js +33 -0
- package/dist/cjs/hooks/useEditorEventListener.js +34 -0
- package/dist/cjs/hooks/useEditorState.js +16 -0
- package/dist/cjs/hooks/useForceUpdate.js +15 -0
- package/dist/cjs/hooks/useLayoutGroupEffect.js +19 -0
- package/dist/cjs/hooks/useNodeViewDescriptor.js +115 -0
- package/dist/cjs/hooks/useReactKeys.js +17 -0
- package/dist/cjs/hooks/useSelectNode.js +28 -0
- package/dist/cjs/hooks/useStopEvent.js +24 -0
- package/dist/cjs/index.js +53 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/plugins/__tests__/reactKeys.test.js +81 -0
- package/dist/cjs/plugins/beforeInputPlugin.js +143 -0
- package/dist/cjs/plugins/componentEventListeners.js +35 -0
- package/dist/cjs/plugins/componentEventListenersPlugin.js +35 -0
- package/dist/cjs/plugins/reactKeys.js +96 -0
- package/dist/cjs/props.js +269 -0
- package/dist/cjs/selection/SelectionDOMObserver.js +174 -0
- package/dist/cjs/selection/hasFocusAndSelection.js +35 -0
- package/dist/cjs/selection/selectionFromDOM.js +77 -0
- package/dist/cjs/selection/selectionToDOM.js +226 -0
- package/dist/cjs/ssr.js +85 -0
- package/dist/cjs/testing/editorViewTestHelpers.js +111 -0
- package/dist/cjs/testing/setupProseMirrorView.js +94 -0
- package/dist/cjs/viewdesc.js +664 -0
- package/dist/esm/browser.js +43 -0
- package/dist/esm/components/ChildNodeViews.js +318 -0
- package/dist/esm/components/CursorWrapper.js +40 -0
- package/dist/esm/components/CustomNodeView.js +28 -0
- package/dist/esm/components/DocNodeView.js +53 -0
- package/dist/esm/components/LayoutGroup.js +66 -0
- package/dist/esm/components/MarkView.js +64 -0
- package/dist/esm/components/NativeWidgetView.js +58 -0
- package/dist/esm/components/NodeView.js +145 -0
- package/dist/esm/components/NodeViewComponentProps.js +1 -0
- package/dist/esm/components/OutputSpec.js +38 -0
- package/dist/esm/components/ProseMirror.js +52 -0
- package/dist/esm/components/ProseMirrorDoc.js +34 -0
- package/dist/esm/components/SeparatorHackView.js +49 -0
- package/dist/esm/components/TextNodeView.js +102 -0
- package/dist/esm/components/TrailingHackView.js +39 -0
- package/dist/esm/components/WidgetView.js +44 -0
- package/dist/esm/components/WidgetViewComponentProps.js +1 -0
- package/dist/esm/components/__tests__/ProseMirror.composition.test.js +395 -0
- package/dist/esm/components/__tests__/ProseMirror.domchange.test.js +266 -0
- package/dist/esm/components/__tests__/ProseMirror.draw-decoration.test.js +967 -0
- package/dist/esm/components/__tests__/ProseMirror.draw.test.js +294 -0
- package/dist/esm/components/__tests__/ProseMirror.node-view.test.js +272 -0
- package/dist/esm/components/__tests__/ProseMirror.selection.test.js +440 -0
- package/dist/esm/components/__tests__/ProseMirror.test.js +339 -0
- package/dist/esm/contexts/ChildDescriptorsContext.js +9 -0
- package/dist/esm/contexts/EditorContext.js +7 -0
- package/dist/esm/contexts/EditorStateContext.js +2 -0
- package/dist/esm/contexts/LayoutGroupContext.js +2 -0
- package/dist/esm/contexts/NodeViewContext.js +2 -0
- package/dist/esm/contexts/SelectNodeContext.js +2 -0
- package/dist/esm/contexts/StopEventContext.js +2 -0
- package/dist/esm/contexts/__tests__/DeferredLayoutEffects.test.js +98 -0
- package/dist/esm/decorations/ReactWidgetType.js +40 -0
- package/dist/esm/decorations/computeDocDeco.js +44 -0
- package/dist/esm/decorations/internalTypes.js +1 -0
- package/dist/esm/decorations/iterDeco.js +73 -0
- package/dist/esm/decorations/viewDecorations.js +163 -0
- package/dist/esm/dom.js +105 -0
- package/dist/esm/hooks/__tests__/useEditorViewLayoutEffect.test.js +99 -0
- package/dist/esm/hooks/useClientOnly.js +8 -0
- package/dist/esm/hooks/useComponentEventListeners.js +54 -0
- package/dist/esm/hooks/useEditor.js +278 -0
- package/dist/esm/hooks/useEditorEffect.js +38 -0
- package/dist/esm/hooks/useEditorEventCallback.js +35 -0
- package/dist/esm/hooks/useEditorEventListener.js +28 -0
- package/dist/esm/hooks/useEditorState.js +8 -0
- package/dist/esm/hooks/useForceUpdate.js +8 -0
- package/dist/esm/hooks/useLayoutGroupEffect.js +9 -0
- package/dist/esm/hooks/useNodeViewDescriptor.js +105 -0
- package/dist/esm/hooks/useReactKeys.js +7 -0
- package/dist/esm/hooks/useSelectNode.js +18 -0
- package/dist/esm/hooks/useStopEvent.js +14 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/plugins/__tests__/reactKeys.test.js +77 -0
- package/dist/esm/plugins/beforeInputPlugin.js +133 -0
- package/dist/esm/plugins/componentEventListeners.js +25 -0
- package/dist/esm/plugins/componentEventListenersPlugin.js +25 -0
- package/dist/esm/plugins/reactKeys.js +81 -0
- package/dist/esm/props.js +251 -0
- package/dist/esm/selection/SelectionDOMObserver.js +164 -0
- package/dist/esm/selection/hasFocusAndSelection.js +17 -0
- package/dist/esm/selection/selectionFromDOM.js +59 -0
- package/dist/esm/selection/selectionToDOM.js +196 -0
- package/dist/esm/ssr.js +82 -0
- package/dist/esm/testing/editorViewTestHelpers.js +88 -0
- package/dist/esm/testing/setupProseMirrorView.js +76 -0
- package/dist/esm/viewdesc.js +654 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/browser.d.ts +15 -0
- package/dist/types/components/ChildNodeViews.d.ts +9 -0
- package/dist/types/components/CursorWrapper.d.ts +5 -0
- package/dist/types/components/CustomNodeView.d.ts +21 -0
- package/dist/types/components/DocNodeView.d.ts +20 -0
- package/dist/types/components/LayoutGroup.d.ts +12 -0
- package/dist/types/components/MarkView.d.ts +9 -0
- package/dist/types/components/NativeWidgetView.d.ts +8 -0
- package/dist/types/components/NodeView.d.ts +11 -0
- package/dist/types/components/NodeViewComponentProps.d.ts +12 -0
- package/dist/types/components/OutputSpec.d.ts +8 -0
- package/dist/types/components/ProseMirror.d.ts +15 -0
- package/dist/types/components/ProseMirrorDoc.d.ts +10 -0
- package/dist/types/components/SeparatorHackView.d.ts +6 -0
- package/dist/types/components/TextNodeView.d.ts +23 -0
- package/dist/types/components/TrailingHackView.d.ts +6 -0
- package/dist/types/components/WidgetView.d.ts +8 -0
- package/dist/types/components/WidgetViewComponentProps.d.ts +6 -0
- package/dist/types/components/__tests__/ProseMirror.composition.test.d.ts +1 -0
- package/dist/types/components/__tests__/ProseMirror.domchange.test.d.ts +1 -0
- package/dist/types/components/__tests__/ProseMirror.draw-decoration.test.d.ts +1 -0
- package/dist/types/components/__tests__/ProseMirror.draw.test.d.ts +1 -0
- package/dist/types/components/__tests__/ProseMirror.node-view.test.d.ts +1 -0
- package/dist/types/components/__tests__/ProseMirror.selection.test.d.ts +1 -0
- package/dist/types/components/__tests__/ProseMirror.test.d.ts +1 -0
- package/dist/types/contexts/ChildDescriptorsContext.d.ts +6 -0
- package/dist/types/contexts/EditorContext.d.ts +14 -0
- package/dist/types/contexts/EditorStateContext.d.ts +2 -0
- package/dist/types/contexts/LayoutGroupContext.d.ts +5 -0
- package/dist/types/contexts/NodeViewContext.d.ts +6 -0
- package/dist/types/contexts/SelectNodeContext.d.ts +3 -0
- package/dist/types/contexts/StopEventContext.d.ts +3 -0
- package/dist/types/contexts/__tests__/DeferredLayoutEffects.test.d.ts +1 -0
- package/dist/types/decorations/ReactWidgetType.d.ts +39 -0
- package/dist/types/decorations/computeDocDeco.d.ts +13 -0
- package/dist/types/decorations/internalTypes.d.ts +16 -0
- package/dist/types/decorations/iterDeco.d.ts +3 -0
- package/dist/types/decorations/viewDecorations.d.ts +13 -0
- package/dist/types/dom.d.ts +22 -0
- package/dist/types/hooks/__tests__/useEditorViewLayoutEffect.test.d.ts +1 -0
- package/dist/types/hooks/useClientOnly.d.ts +1 -0
- package/dist/types/hooks/useComponentEventListeners.d.ts +33 -0
- package/dist/types/hooks/useEditor.d.ts +66 -0
- package/dist/types/hooks/useEditorEffect.d.ts +17 -0
- package/dist/types/hooks/useEditorEventCallback.d.ts +15 -0
- package/dist/types/hooks/useEditorEventListener.d.ts +8 -0
- package/dist/types/hooks/useEditorState.d.ts +5 -0
- package/dist/types/hooks/useForceUpdate.d.ts +5 -0
- package/dist/types/hooks/useLayoutGroupEffect.d.ts +3 -0
- package/dist/types/hooks/useNodeViewDescriptor.d.ts +11 -0
- package/dist/types/hooks/useReactKeys.d.ts +5 -0
- package/dist/types/hooks/useSelectNode.d.ts +1 -0
- package/dist/types/hooks/useStopEvent.d.ts +2 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/plugins/__tests__/reactKeys.test.d.ts +1 -0
- package/dist/types/plugins/beforeInputPlugin.d.ts +3 -0
- package/dist/types/plugins/componentEventListeners.d.ts +4 -0
- package/dist/types/plugins/componentEventListenersPlugin.d.ts +4 -0
- package/dist/types/plugins/reactKeys.d.ts +19 -0
- package/dist/types/props.d.ts +1174 -0
- package/dist/types/selection/SelectionDOMObserver.d.ts +34 -0
- package/dist/types/selection/hasFocusAndSelection.d.ts +3 -0
- package/dist/types/selection/selectionFromDOM.d.ts +4 -0
- package/dist/types/selection/selectionToDOM.d.ts +9 -0
- package/dist/types/ssr.d.ts +19 -0
- package/dist/types/testing/editorViewTestHelpers.d.ts +23 -0
- package/dist/types/testing/setupProseMirrorView.d.ts +2 -0
- package/dist/types/viewdesc.d.ts +131 -0
- package/package.json +113 -0
|
@@ -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,7 @@
|
|
|
1
|
+
/* Copyright (c) The New York Times Company */ import { createContext } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Provides the EditorView, as well as the current
|
|
4
|
+
* EditorState. Should not be consumed directly; instead
|
|
5
|
+
* see `useEditorState`, `useEditorViewEvent`, and
|
|
6
|
+
* `useEditorViewLayoutEffect`.
|
|
7
|
+
*/ export const EditorContext = createContext(null);
|
|
@@ -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,40 @@
|
|
|
1
|
+
import { Decoration } from "prosemirror-view";
|
|
2
|
+
function compareObjs(a, b) {
|
|
3
|
+
if (a == b) return true;
|
|
4
|
+
for(const p in a)if (a[p] !== b[p]) return false;
|
|
5
|
+
for(const p in b)if (!(p in a)) return false;
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
const noSpec = {
|
|
9
|
+
side: 0
|
|
10
|
+
};
|
|
11
|
+
export class ReactWidgetType {
|
|
12
|
+
Component;
|
|
13
|
+
side;
|
|
14
|
+
spec;
|
|
15
|
+
constructor(Component, spec){
|
|
16
|
+
this.Component = Component;
|
|
17
|
+
this.spec = spec ?? noSpec;
|
|
18
|
+
this.side = this.spec.side ?? 0;
|
|
19
|
+
}
|
|
20
|
+
map(mapping, span, offset, oldOffset) {
|
|
21
|
+
const { pos, deleted } = mapping.mapResult(span.from + oldOffset, this.side < 0 ? -1 : 1);
|
|
22
|
+
// @ts-expect-error The Decoration constructor is private/internal, but
|
|
23
|
+
// we need to use it for our custom widget implementation here.
|
|
24
|
+
return deleted ? null : new Decoration(pos - offset, pos - offset, this);
|
|
25
|
+
}
|
|
26
|
+
valid() {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
eq(other) {
|
|
30
|
+
return this == other || other instanceof ReactWidgetType && (this.spec.key && this.spec.key == other.spec.key || this.Component == other.Component && compareObjs(this.spec, other.spec));
|
|
31
|
+
}
|
|
32
|
+
destroy() {
|
|
33
|
+
// Can be implemented with React effect hooks
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function widget(pos, component, spec) {
|
|
37
|
+
// @ts-expect-error The Decoration constructor is private/internal, but
|
|
38
|
+
// we need to use it for our custom widget implementation here.
|
|
39
|
+
return new Decoration(pos, pos, new ReactWidgetType(component, spec));
|
|
40
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Decoration } from "prosemirror-view";
|
|
2
|
+
const DocDecorationsCache = new WeakMap();
|
|
3
|
+
/**
|
|
4
|
+
* Produces the outer decorations for the doc node, based
|
|
5
|
+
* on the attributes editor prop.
|
|
6
|
+
*
|
|
7
|
+
* The return value of this function is memoized; if it is to
|
|
8
|
+
* return an equivalent value to the last time it was called for
|
|
9
|
+
* a given EditorView, it will return exactly that previous value.
|
|
10
|
+
*
|
|
11
|
+
* This makes it safe to call in a React render function, even
|
|
12
|
+
* if its result is used in a dependencies array for a hook.
|
|
13
|
+
*/ export function computeDocDeco(view) {
|
|
14
|
+
const attrs = Object.create(null);
|
|
15
|
+
attrs.class = "ProseMirror";
|
|
16
|
+
attrs.contenteditable = String(view.editable);
|
|
17
|
+
view.someProp("attributes", (value)=>{
|
|
18
|
+
if (typeof value == "function") value = value(view.state);
|
|
19
|
+
if (value) for(const attr in value){
|
|
20
|
+
if (attr == "class") attrs.class += " " + value[attr];
|
|
21
|
+
else if (attr == "style") attrs.style = (attrs.style ? attrs.style + ";" : "") + value[attr];
|
|
22
|
+
else if (!attrs[attr] && attr != "contenteditable" && attr != "nodeName") attrs[attr] = String(value[attr]);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
if (!attrs.translate) attrs.translate = "no";
|
|
26
|
+
const next = [
|
|
27
|
+
Decoration.node(0, view.state.doc.content.size, attrs)
|
|
28
|
+
];
|
|
29
|
+
const previous = DocDecorationsCache.get(view);
|
|
30
|
+
if (!previous) {
|
|
31
|
+
DocDecorationsCache.set(view, next);
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
34
|
+
if (previous[0].to !== view.state.doc.content.size) {
|
|
35
|
+
DocDecorationsCache.set(view, next);
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
// @ts-expect-error Internal property (Decoration.type)
|
|
39
|
+
if (!previous[0].type.eq(next[0].type)) {
|
|
40
|
+
DocDecorationsCache.set(view, next);
|
|
41
|
+
return next;
|
|
42
|
+
}
|
|
43
|
+
return previous;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { ReactWidgetType } from "./ReactWidgetType.js";
|
|
2
|
+
function compareSide(a, b) {
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
return a.type.side - b.type.side;
|
|
5
|
+
}
|
|
6
|
+
// This function abstracts iterating over the nodes and decorations in
|
|
7
|
+
// a fragment. Calls `onNode` for each node, with its local and child
|
|
8
|
+
// decorations. Splits text nodes when there is a decoration starting
|
|
9
|
+
// or ending inside of them. Calls `onWidget` for each widget.
|
|
10
|
+
export function iterDeco(parent, deco, // Callbacks have been slightly modified to pass
|
|
11
|
+
// the offset, so that we can pass the position as
|
|
12
|
+
// a prop to components
|
|
13
|
+
onWidget, onNode) {
|
|
14
|
+
const locals = deco.locals(parent);
|
|
15
|
+
let offset = 0;
|
|
16
|
+
// Simple, cheap variant for when there are no local decorations
|
|
17
|
+
if (locals.length == 0) {
|
|
18
|
+
for(let i = 0; i < parent.childCount; i++){
|
|
19
|
+
const child = parent.child(i);
|
|
20
|
+
onNode(child, locals, deco.forChild(offset, child), offset, i);
|
|
21
|
+
offset += child.nodeSize;
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let decoIndex = 0;
|
|
26
|
+
const active = [];
|
|
27
|
+
let restNode = null;
|
|
28
|
+
for(let parentIndex = 0;;){
|
|
29
|
+
if (decoIndex < locals.length && locals[decoIndex].to == offset) {
|
|
30
|
+
const widget = locals[decoIndex++];
|
|
31
|
+
let widgets;
|
|
32
|
+
while(decoIndex < locals.length && locals[decoIndex].to == offset)(widgets || (widgets = [
|
|
33
|
+
widget
|
|
34
|
+
])).push(locals[decoIndex++]);
|
|
35
|
+
if (widgets) {
|
|
36
|
+
widgets.sort(compareSide);
|
|
37
|
+
for(let i = 0; i < widgets.length; i++)onWidget(widgets[i], // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
!(widgets[i].type instanceof ReactWidgetType), offset, parentIndex + i, !!restNode);
|
|
39
|
+
} else {
|
|
40
|
+
onWidget(widget, // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
!(widget.type instanceof ReactWidgetType), offset, parentIndex, !!restNode);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let child, index;
|
|
45
|
+
if (restNode) {
|
|
46
|
+
index = -1;
|
|
47
|
+
child = restNode;
|
|
48
|
+
restNode = null;
|
|
49
|
+
} else if (parentIndex < parent.childCount) {
|
|
50
|
+
index = parentIndex;
|
|
51
|
+
child = parent.child(parentIndex++);
|
|
52
|
+
} else {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
for(let i = 0; i < active.length; i++)if (active[i].to <= offset) active.splice(i--, 1);
|
|
56
|
+
while(decoIndex < locals.length && locals[decoIndex].from <= offset && locals[decoIndex].to > offset)active.push(locals[decoIndex++]);
|
|
57
|
+
let end = offset + child.nodeSize;
|
|
58
|
+
if (child.isText) {
|
|
59
|
+
let cutAt = end;
|
|
60
|
+
if (decoIndex < locals.length && locals[decoIndex].from < cutAt) cutAt = locals[decoIndex].from;
|
|
61
|
+
for(let i = 0; i < active.length; i++)if (active[i].to < cutAt) cutAt = active[i].to;
|
|
62
|
+
if (cutAt < end) {
|
|
63
|
+
restNode = child.cut(cutAt - offset);
|
|
64
|
+
child = child.cut(0, cutAt - offset);
|
|
65
|
+
end = cutAt;
|
|
66
|
+
index = -1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const outerDeco = child.isInline && !child.isLeaf ? active.filter((d)=>!d.inline) : active.slice();
|
|
70
|
+
onNode(child, outerDeco, deco.forChild(offset, child), offset, index);
|
|
71
|
+
offset = end;
|
|
72
|
+
}
|
|
73
|
+
}
|