@handlewithcare/react-prosemirror 2.4.11 → 2.4.12
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/dist/cjs/components/Editor.js +28 -0
- package/dist/cjs/components/NodeViews.js +73 -0
- package/dist/cjs/{contexts/__tests__/DeferredLayoutEffects.test.js → components/__tests__/LayoutGroup.test.js} +2 -2
- package/dist/cjs/components/__tests__/ProseMirror.test.js +136 -263
- package/dist/cjs/contexts/NodeViewsContext.js +10 -0
- package/dist/cjs/hooks/__tests__/useEditorViewLayoutEffect.test.js +27 -28
- package/dist/cjs/hooks/__tests__/useNodeViews.test.js +159 -0
- package/dist/cjs/hooks/useEditor.js +2 -4
- package/dist/cjs/hooks/useEditorView.js +100 -0
- package/dist/cjs/hooks/useNodePos.js +69 -0
- package/dist/cjs/hooks/useNodeViews.js +100 -0
- package/dist/cjs/nodeViews/createReactNodeViewConstructor.js +244 -0
- package/dist/cjs/nodeViews/phrasingContentTags.js +57 -0
- package/dist/cjs/plugins/__tests__/react.test.js +139 -0
- package/dist/cjs/plugins/react.js +71 -0
- package/dist/esm/components/Editor.js +15 -0
- package/dist/esm/components/NodeViews.js +26 -0
- package/dist/esm/{contexts/__tests__/DeferredLayoutEffects.test.js → components/__tests__/LayoutGroup.test.js} +2 -2
- package/dist/esm/components/__tests__/ProseMirror.test.js +135 -267
- package/dist/esm/contexts/NodeViewsContext.js +9 -0
- package/dist/esm/hooks/__tests__/useEditorViewLayoutEffect.test.js +27 -28
- package/dist/esm/hooks/__tests__/useNodeViews.test.js +116 -0
- package/dist/esm/hooks/useEditor.js +2 -4
- package/dist/esm/hooks/useEditorView.js +99 -0
- package/dist/esm/hooks/useNodePos.js +16 -0
- package/dist/esm/hooks/useNodeViews.js +53 -0
- package/dist/esm/nodeViews/createReactNodeViewConstructor.js +214 -0
- package/dist/esm/nodeViews/phrasingContentTags.js +49 -0
- package/dist/esm/plugins/__tests__/react.test.js +135 -0
- package/dist/esm/plugins/react.js +64 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/components/Editor.d.ts +7 -0
- package/dist/types/components/NodeViews.d.ts +6 -0
- package/dist/types/components/__tests__/LayoutGroup.test.d.ts +1 -0
- package/dist/types/contexts/NodeViewsContext.d.ts +19 -0
- package/dist/types/hooks/__tests__/useNodeViews.test.d.ts +1 -0
- package/dist/types/hooks/useEditorView.d.ts +23 -0
- package/dist/types/hooks/useNodePos.d.ts +9 -0
- package/dist/types/hooks/useNodeViews.d.ts +5 -0
- package/dist/types/nodeViews/createReactNodeViewConstructor.d.ts +48 -0
- package/dist/types/nodeViews/phrasingContentTags.d.ts +1 -0
- package/dist/types/plugins/__tests__/react.test.d.ts +1 -0
- package/dist/types/plugins/react.d.ts +21 -0
- package/dist/types/props.d.ts +27 -27
- package/package.json +1 -1
- package/dist/cjs/components/__tests__/ProseMirror.composition.test.js +0 -398
- package/dist/cjs/components/__tests__/ProseMirror.domchange.test.js +0 -270
- package/dist/cjs/components/__tests__/ProseMirror.draw-decoration.test.js +0 -1010
- package/dist/cjs/components/__tests__/ProseMirror.draw.test.js +0 -337
- package/dist/cjs/components/__tests__/ProseMirror.node-view.test.js +0 -315
- package/dist/cjs/components/__tests__/ProseMirror.selection.test.js +0 -444
- package/dist/cjs/plugins/__tests__/reactKeys.test.js +0 -81
- package/dist/esm/components/__tests__/ProseMirror.composition.test.js +0 -395
- package/dist/esm/components/__tests__/ProseMirror.domchange.test.js +0 -266
- package/dist/esm/components/__tests__/ProseMirror.draw-decoration.test.js +0 -967
- package/dist/esm/components/__tests__/ProseMirror.draw.test.js +0 -294
- package/dist/esm/components/__tests__/ProseMirror.node-view.test.js +0 -272
- package/dist/esm/components/__tests__/ProseMirror.selection.test.js +0 -440
- package/dist/esm/plugins/__tests__/reactKeys.test.js +0 -77
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: all[name]
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
REACT_NODE_VIEW: ()=>REACT_NODE_VIEW,
|
|
13
|
+
findNodeKeyUp: ()=>findNodeKeyUp,
|
|
14
|
+
createReactNodeViewConstructor: ()=>createReactNodeViewConstructor
|
|
15
|
+
});
|
|
16
|
+
const _react = /*#__PURE__*/ _interopRequireWildcard(require("react"));
|
|
17
|
+
const _reactDom = require("react-dom");
|
|
18
|
+
const _nodeViewsContextJs = require("../contexts/NodeViewsContext.js");
|
|
19
|
+
const _useEditorEffectJs = require("../hooks/useEditorEffect.js");
|
|
20
|
+
const _useNodePosJs = require("../hooks/useNodePos.js");
|
|
21
|
+
const _reactJs = require("../plugins/react.js");
|
|
22
|
+
const _phrasingContentTagsJs = require("./phrasingContentTags.js");
|
|
23
|
+
function _getRequireWildcardCache(nodeInterop) {
|
|
24
|
+
if (typeof WeakMap !== "function") return null;
|
|
25
|
+
var cacheBabelInterop = new WeakMap();
|
|
26
|
+
var cacheNodeInterop = new WeakMap();
|
|
27
|
+
return (_getRequireWildcardCache = function(nodeInterop) {
|
|
28
|
+
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
|
29
|
+
})(nodeInterop);
|
|
30
|
+
}
|
|
31
|
+
function _interopRequireWildcard(obj, nodeInterop) {
|
|
32
|
+
if (!nodeInterop && obj && obj.__esModule) {
|
|
33
|
+
return obj;
|
|
34
|
+
}
|
|
35
|
+
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
|
|
36
|
+
return {
|
|
37
|
+
default: obj
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
var cache = _getRequireWildcardCache(nodeInterop);
|
|
41
|
+
if (cache && cache.has(obj)) {
|
|
42
|
+
return cache.get(obj);
|
|
43
|
+
}
|
|
44
|
+
var newObj = {};
|
|
45
|
+
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
|
|
46
|
+
for(var key in obj){
|
|
47
|
+
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
48
|
+
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
|
|
49
|
+
if (desc && (desc.get || desc.set)) {
|
|
50
|
+
Object.defineProperty(newObj, key, desc);
|
|
51
|
+
} else {
|
|
52
|
+
newObj[key] = obj[key];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
newObj.default = obj;
|
|
57
|
+
if (cache) {
|
|
58
|
+
cache.set(obj, newObj);
|
|
59
|
+
}
|
|
60
|
+
return newObj;
|
|
61
|
+
}
|
|
62
|
+
const REACT_NODE_VIEW = Symbol("react node view");
|
|
63
|
+
let didWarnReactPlugin = false;
|
|
64
|
+
function findNodeKeyUp(editorView, pos) {
|
|
65
|
+
const pluginState = _reactJs.reactPluginKey.getState(editorView.state);
|
|
66
|
+
if (!pluginState) return _reactJs.ROOT_NODE_KEY;
|
|
67
|
+
const $pos = editorView.state.doc.resolve(pos);
|
|
68
|
+
for(let d = $pos.depth; d > 0; d--){
|
|
69
|
+
const ancestorNodeTypeName = $pos.node(d).type.name;
|
|
70
|
+
const ancestorNodeView = editorView.props.nodeViews?.[ancestorNodeTypeName];
|
|
71
|
+
if (!ancestorNodeView?.[REACT_NODE_VIEW]) continue;
|
|
72
|
+
const ancestorPos = $pos.before(d);
|
|
73
|
+
const ancestorKey = pluginState.posToKey.get(ancestorPos);
|
|
74
|
+
if (ancestorKey) return ancestorKey;
|
|
75
|
+
}
|
|
76
|
+
return _reactJs.ROOT_NODE_KEY;
|
|
77
|
+
}
|
|
78
|
+
function createReactNodeViewConstructor(nodeViewConstructor, registerPortal) {
|
|
79
|
+
function nodeViewConstructorWrapper(node, editorView, getPos, decorations, innerDecorations) {
|
|
80
|
+
const nodeView = nodeViewConstructor(node, editorView, getPos, decorations, innerDecorations);
|
|
81
|
+
const { component: NodeView } = nodeView;
|
|
82
|
+
if (!NodeView) {
|
|
83
|
+
return nodeView;
|
|
84
|
+
}
|
|
85
|
+
const reactPluginState = _reactJs.reactPluginKey.getState(editorView.state);
|
|
86
|
+
if (!reactPluginState) {
|
|
87
|
+
if (!didWarnReactPlugin) {
|
|
88
|
+
console.error("The React ProseMirror plugin is required to use React node views. " + "Make sure to add it to the ProseMirror editor state.");
|
|
89
|
+
didWarnReactPlugin = true;
|
|
90
|
+
}
|
|
91
|
+
return nodeView;
|
|
92
|
+
}
|
|
93
|
+
const { dom , contentDOM } = nodeView;
|
|
94
|
+
// Use a span if the provided contentDOM is in the "phrasing" content
|
|
95
|
+
// category. Otherwise use a div. This is our best attempt at not
|
|
96
|
+
// breaking the intended content model, for now.
|
|
97
|
+
//
|
|
98
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Content_categories#phrasing_content
|
|
99
|
+
const ContentDOMWrapper = contentDOM && (_phrasingContentTagsJs.phrasingContentTags.includes(contentDOM.tagName.toLocaleLowerCase()) ? "span" : "div");
|
|
100
|
+
const nodeKey = reactPluginState.posToKey.get(getPos()) ?? (0, _reactJs.createNodeKey)();
|
|
101
|
+
/**
|
|
102
|
+
* Wrapper component to provide some imperative handles for updating
|
|
103
|
+
* and re-rendering its child. Takes and renders an arbitrary ElementType
|
|
104
|
+
* that expects NodeViewComponentProps as props.
|
|
105
|
+
*/ const NodeViewWrapper = /*#__PURE__*/ (0, _react.forwardRef)(function NodeViewWrapper(param, ref) {
|
|
106
|
+
let { initialState } = param;
|
|
107
|
+
const [node, setNode] = (0, _react.useState)(initialState.node);
|
|
108
|
+
const [decorations, setDecorations] = (0, _react.useState)(initialState.decorations);
|
|
109
|
+
const [isSelected, setIsSelected] = (0, _react.useState)(initialState.isSelected);
|
|
110
|
+
const nodeViews = (0, _react.useContext)(_nodeViewsContextJs.NodeViewsContext);
|
|
111
|
+
const childNodeViews = nodeViews[nodeKey];
|
|
112
|
+
const [childNodeViewPortals, setChildNodeViewPortals] = (0, _react.useState)(childNodeViews?.map((param)=>{
|
|
113
|
+
let { portal } = param;
|
|
114
|
+
return portal;
|
|
115
|
+
}));
|
|
116
|
+
// `getPos` is technically derived from the EditorView
|
|
117
|
+
// state, so it's not safe to call until after the EditorView
|
|
118
|
+
// has been updated
|
|
119
|
+
(0, _useEditorEffectJs.useEditorEffect)(()=>{
|
|
120
|
+
setChildNodeViewPortals(childNodeViews?.sort((a, b)=>a.getPos() - b.getPos()).map((param)=>{
|
|
121
|
+
let { portal } = param;
|
|
122
|
+
return portal;
|
|
123
|
+
}));
|
|
124
|
+
}, [
|
|
125
|
+
childNodeViews
|
|
126
|
+
]);
|
|
127
|
+
const [contentDOMWrapper, setContentDOMWrapper] = (0, _react.useState)(null);
|
|
128
|
+
const [contentDOMParent, setContentDOMParent] = (0, _react.useState)(null);
|
|
129
|
+
(0, _react.useImperativeHandle)(ref, ()=>({
|
|
130
|
+
node,
|
|
131
|
+
contentDOMWrapper: contentDOMWrapper,
|
|
132
|
+
contentDOMParent: contentDOMParent,
|
|
133
|
+
setNode,
|
|
134
|
+
setDecorations,
|
|
135
|
+
setIsSelected
|
|
136
|
+
}), [
|
|
137
|
+
node,
|
|
138
|
+
contentDOMWrapper,
|
|
139
|
+
contentDOMParent
|
|
140
|
+
]);
|
|
141
|
+
return /*#__PURE__*/ _react.default.createElement(_useNodePosJs.NodePosProvider, {
|
|
142
|
+
nodeKey: nodeKey
|
|
143
|
+
}, /*#__PURE__*/ _react.default.createElement(NodeView, {
|
|
144
|
+
node: node,
|
|
145
|
+
decorations: decorations,
|
|
146
|
+
isSelected: isSelected
|
|
147
|
+
}, childNodeViewPortals, ContentDOMWrapper && /*#__PURE__*/ _react.default.createElement(ContentDOMWrapper, {
|
|
148
|
+
style: {
|
|
149
|
+
display: "contents"
|
|
150
|
+
},
|
|
151
|
+
ref: (nextContentDOMWrapper)=>{
|
|
152
|
+
setContentDOMWrapper(nextContentDOMWrapper);
|
|
153
|
+
// we preserve a reference to the contentDOMWrapper'
|
|
154
|
+
// parent so that later we can reassemble the DOM hierarchy
|
|
155
|
+
// React expects when cleaning up the ContentDOMWrapper element
|
|
156
|
+
if (nextContentDOMWrapper?.parentNode) {
|
|
157
|
+
setContentDOMParent(nextContentDOMWrapper.parentNode);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
})));
|
|
161
|
+
});
|
|
162
|
+
NodeViewWrapper.displayName = `NodeView(${NodeView.displayName ?? NodeView.name})`;
|
|
163
|
+
let componentRef = null;
|
|
164
|
+
const element = /*#__PURE__*/ _react.default.createElement(NodeViewWrapper, {
|
|
165
|
+
initialState: {
|
|
166
|
+
node,
|
|
167
|
+
decorations,
|
|
168
|
+
isSelected: false
|
|
169
|
+
},
|
|
170
|
+
ref: (c)=>{
|
|
171
|
+
componentRef = c;
|
|
172
|
+
if (!componentRef || componentRef.node.isLeaf) return;
|
|
173
|
+
const contentDOMWrapper = componentRef.contentDOMWrapper;
|
|
174
|
+
if (!contentDOMWrapper || !(contentDOMWrapper instanceof HTMLElement)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// We always set contentDOM when !node.isLeaf, which is checked above
|
|
178
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
179
|
+
contentDOMWrapper.appendChild(contentDOM);
|
|
180
|
+
// Synchronize the ProseMirror selection to the DOM, because mounting the
|
|
181
|
+
// component changes the DOM outside of a ProseMirror update.
|
|
182
|
+
const { node } = componentRef;
|
|
183
|
+
const pos = getPos();
|
|
184
|
+
const end = pos + node.nodeSize;
|
|
185
|
+
const { from , to } = editorView.state.selection;
|
|
186
|
+
if (editorView.hasFocus() && pos < from && to < end) {
|
|
187
|
+
// This call seems like it should be a no-op, given the editor already has
|
|
188
|
+
// focus, but it causes ProseMirror to synchronize the DOM selection with
|
|
189
|
+
// its state again, placing the DOM selection in a reasonable place within
|
|
190
|
+
// the node.
|
|
191
|
+
editorView.focus();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
const portal = /*#__PURE__*/ (0, _reactDom.createPortal)(element, dom, nodeKey);
|
|
196
|
+
const unregisterPortal = registerPortal(editorView, getPos, portal);
|
|
197
|
+
return {
|
|
198
|
+
ignoreMutation (record) {
|
|
199
|
+
return !contentDOM?.contains(record.target);
|
|
200
|
+
},
|
|
201
|
+
...nodeView,
|
|
202
|
+
selectNode () {
|
|
203
|
+
componentRef?.setIsSelected(true);
|
|
204
|
+
nodeView.selectNode?.();
|
|
205
|
+
},
|
|
206
|
+
deselectNode () {
|
|
207
|
+
componentRef?.setIsSelected(false);
|
|
208
|
+
nodeView.deselectNode?.();
|
|
209
|
+
},
|
|
210
|
+
update (node, decorations, innerDecorations) {
|
|
211
|
+
// If this node view's parent has been removed from the registry, we
|
|
212
|
+
// need to rebuild it and its children with new registry keys
|
|
213
|
+
const positionRegistry = _reactJs.reactPluginKey.getState(editorView.state);
|
|
214
|
+
if (positionRegistry && nodeKey !== positionRegistry.posToKey.get(getPos())) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (nodeView.update?.(node, decorations, innerDecorations) === false) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
if (node.type === componentRef?.node.type) {
|
|
221
|
+
componentRef?.setNode(node);
|
|
222
|
+
componentRef?.setDecorations(decorations);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
},
|
|
227
|
+
destroy () {
|
|
228
|
+
// React expects the contentDOMParent to be a child of the
|
|
229
|
+
// DOM element where the portal was mounted, but in some situations
|
|
230
|
+
// contenteditable may have already detached the contentDOMParent
|
|
231
|
+
// from the DOM. Here we attempt to reassemble the DOM that React
|
|
232
|
+
// expects when cleaning up the portal.
|
|
233
|
+
if (componentRef?.contentDOMParent) {
|
|
234
|
+
this.dom.appendChild(componentRef.contentDOMParent);
|
|
235
|
+
}
|
|
236
|
+
unregisterPortal();
|
|
237
|
+
nodeView.destroy?.();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return Object.assign(nodeViewConstructorWrapper, {
|
|
242
|
+
[REACT_NODE_VIEW]: true
|
|
243
|
+
});
|
|
244
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "phrasingContentTags", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: ()=>phrasingContentTags
|
|
8
|
+
});
|
|
9
|
+
const phrasingContentTags = [
|
|
10
|
+
"abbr",
|
|
11
|
+
"audio",
|
|
12
|
+
"b",
|
|
13
|
+
"bdo",
|
|
14
|
+
"br",
|
|
15
|
+
"button",
|
|
16
|
+
"canvas",
|
|
17
|
+
"cite",
|
|
18
|
+
"code",
|
|
19
|
+
"data",
|
|
20
|
+
"datalist",
|
|
21
|
+
"dfn",
|
|
22
|
+
"em",
|
|
23
|
+
"embed",
|
|
24
|
+
"i",
|
|
25
|
+
"iframe",
|
|
26
|
+
"img",
|
|
27
|
+
"input",
|
|
28
|
+
"kbd",
|
|
29
|
+
"keygen",
|
|
30
|
+
"label",
|
|
31
|
+
"mark",
|
|
32
|
+
"math",
|
|
33
|
+
"meter",
|
|
34
|
+
"noscript",
|
|
35
|
+
"object",
|
|
36
|
+
"output",
|
|
37
|
+
"picture",
|
|
38
|
+
"progress",
|
|
39
|
+
"q",
|
|
40
|
+
"ruby",
|
|
41
|
+
"s",
|
|
42
|
+
"samp",
|
|
43
|
+
"script",
|
|
44
|
+
"select",
|
|
45
|
+
"small",
|
|
46
|
+
"span",
|
|
47
|
+
"strong",
|
|
48
|
+
"sub",
|
|
49
|
+
"sup",
|
|
50
|
+
"svg",
|
|
51
|
+
"textarea",
|
|
52
|
+
"time",
|
|
53
|
+
"u",
|
|
54
|
+
"var",
|
|
55
|
+
"video",
|
|
56
|
+
"wbr"
|
|
57
|
+
];
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */ "use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _prosemirrorModel = require("prosemirror-model");
|
|
6
|
+
const _prosemirrorState = require("prosemirror-state");
|
|
7
|
+
const _prosemirrorTransform = require("prosemirror-transform");
|
|
8
|
+
const _reactJs = require("../react.js");
|
|
9
|
+
const schema = new _prosemirrorModel.Schema({
|
|
10
|
+
nodes: {
|
|
11
|
+
doc: {
|
|
12
|
+
content: "block+"
|
|
13
|
+
},
|
|
14
|
+
paragraph: {
|
|
15
|
+
group: "block",
|
|
16
|
+
content: "inline*"
|
|
17
|
+
},
|
|
18
|
+
list: {
|
|
19
|
+
group: "block",
|
|
20
|
+
content: "list_item+"
|
|
21
|
+
},
|
|
22
|
+
list_item: {
|
|
23
|
+
content: "paragraph+"
|
|
24
|
+
},
|
|
25
|
+
text: {
|
|
26
|
+
group: "inline"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
describe("reactNodeViewPlugin", ()=>{
|
|
31
|
+
it("should create a unique key for each node", ()=>{
|
|
32
|
+
const editorState = _prosemirrorState.EditorState.create({
|
|
33
|
+
doc: schema.topNodeType.create(null, [
|
|
34
|
+
schema.nodes.paragraph.create(),
|
|
35
|
+
schema.nodes.paragraph.create(),
|
|
36
|
+
schema.nodes.paragraph.create()
|
|
37
|
+
]),
|
|
38
|
+
plugins: [
|
|
39
|
+
(0, _reactJs.react)()
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
const pluginState = _reactJs.reactPluginKey.getState(editorState);
|
|
43
|
+
expect(pluginState.posToKey.size).toBe(3);
|
|
44
|
+
});
|
|
45
|
+
it("should maintain key stability when possible", ()=>{
|
|
46
|
+
const initialEditorState = _prosemirrorState.EditorState.create({
|
|
47
|
+
doc: schema.topNodeType.create(null, [
|
|
48
|
+
schema.nodes.paragraph.create(),
|
|
49
|
+
schema.nodes.paragraph.create(),
|
|
50
|
+
schema.nodes.paragraph.create()
|
|
51
|
+
]),
|
|
52
|
+
plugins: [
|
|
53
|
+
(0, _reactJs.react)()
|
|
54
|
+
]
|
|
55
|
+
});
|
|
56
|
+
const initialPluginState = _reactJs.reactPluginKey.getState(initialEditorState);
|
|
57
|
+
const nextEditorState = initialEditorState.apply(initialEditorState.tr.insertText("Hello, world!", 1));
|
|
58
|
+
const nextPluginState = _reactJs.reactPluginKey.getState(nextEditorState);
|
|
59
|
+
expect(Array.from(nextPluginState.keyToPos.keys())).toEqual(Array.from(initialPluginState.keyToPos.keys()));
|
|
60
|
+
});
|
|
61
|
+
it("should create unique keys for new nodes", ()=>{
|
|
62
|
+
const initialEditorState = _prosemirrorState.EditorState.create({
|
|
63
|
+
doc: schema.topNodeType.create(null, [
|
|
64
|
+
schema.nodes.paragraph.create(),
|
|
65
|
+
schema.nodes.paragraph.create(),
|
|
66
|
+
schema.nodes.paragraph.create()
|
|
67
|
+
]),
|
|
68
|
+
plugins: [
|
|
69
|
+
(0, _reactJs.react)()
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
const initialPluginState = _reactJs.reactPluginKey.getState(initialEditorState);
|
|
73
|
+
const nextEditorState = initialEditorState.apply(initialEditorState.tr.insert(0, schema.nodes.list.createAndFill()));
|
|
74
|
+
const nextPluginState = _reactJs.reactPluginKey.getState(nextEditorState);
|
|
75
|
+
// Adds new keys for new nodes
|
|
76
|
+
expect(nextPluginState.keyToPos.size).toBe(6);
|
|
77
|
+
// Maintains keys for previous nodes that are still there
|
|
78
|
+
Array.from(initialPluginState.keyToPos.keys()).forEach((key)=>{
|
|
79
|
+
expect(Array.from(nextPluginState.keyToPos.keys())).toContain(key);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
it("should maintain key stability when splitting a node", ()=>{
|
|
83
|
+
const initialEditorState = _prosemirrorState.EditorState.create({
|
|
84
|
+
doc: schema.topNodeType.create(null, [
|
|
85
|
+
schema.nodes.list.create(null, [
|
|
86
|
+
schema.nodes.list_item.create(null, [
|
|
87
|
+
schema.nodes.paragraph.create(null, [
|
|
88
|
+
schema.text("first")
|
|
89
|
+
])
|
|
90
|
+
])
|
|
91
|
+
])
|
|
92
|
+
]),
|
|
93
|
+
plugins: [
|
|
94
|
+
(0, _reactJs.react)()
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
const initialPluginState = _reactJs.reactPluginKey.getState(initialEditorState);
|
|
98
|
+
const nextEditorState = initialEditorState.apply(initialEditorState.tr.insert(1, schema.nodes.list_item.create(null, [
|
|
99
|
+
schema.nodes.paragraph.create(null, [
|
|
100
|
+
schema.text("second")
|
|
101
|
+
])
|
|
102
|
+
])));
|
|
103
|
+
const nextPluginState = _reactJs.reactPluginKey.getState(nextEditorState);
|
|
104
|
+
// The new list item was inserted before the original one,
|
|
105
|
+
// pushing it further into the document. The original list
|
|
106
|
+
// item should keep its original key, and the new list item
|
|
107
|
+
// should be assigned a new one
|
|
108
|
+
expect(nextPluginState.posToKey.get(11)).toBe(initialPluginState.posToKey.get(1));
|
|
109
|
+
expect(nextPluginState.posToKey.get(1)).not.toBe(initialPluginState.posToKey.get(1));
|
|
110
|
+
});
|
|
111
|
+
it("should maintain key stability when wrapping a node", ()=>{
|
|
112
|
+
const initialEditorState = _prosemirrorState.EditorState.create({
|
|
113
|
+
doc: schema.topNodeType.create(null, [
|
|
114
|
+
schema.nodes.paragraph.create(null, [
|
|
115
|
+
schema.text("content")
|
|
116
|
+
])
|
|
117
|
+
]),
|
|
118
|
+
plugins: [
|
|
119
|
+
(0, _reactJs.react)()
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
const initialPluginState = _reactJs.reactPluginKey.getState(initialEditorState);
|
|
123
|
+
const start = 1;
|
|
124
|
+
const end = 9;
|
|
125
|
+
const tr = initialEditorState.tr.delete(start, end);
|
|
126
|
+
const $start = tr.doc.resolve(start);
|
|
127
|
+
const range = $start.blockRange();
|
|
128
|
+
const wrapping = range && (0, _prosemirrorTransform.findWrapping)(range, schema.nodes.list, null);
|
|
129
|
+
tr.wrap(range, wrapping);
|
|
130
|
+
const nextEditorState = initialEditorState.apply(tr);
|
|
131
|
+
const nextPluginState = _reactJs.reactPluginKey.getState(nextEditorState);
|
|
132
|
+
// The new list and list item nodes were wrapped around the
|
|
133
|
+
// paragraph, pushing it further into the document. The paragraph
|
|
134
|
+
// should keep its original key, and the new nodes
|
|
135
|
+
// should be assigned a new one
|
|
136
|
+
expect(nextPluginState.posToKey.get(2)).toBe(initialPluginState.posToKey.get(0));
|
|
137
|
+
expect(nextPluginState.posToKey.get(0)).not.toBe(initialPluginState.posToKey.get(0));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: all[name]
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
ROOT_NODE_KEY: ()=>ROOT_NODE_KEY,
|
|
13
|
+
createNodeKey: ()=>createNodeKey,
|
|
14
|
+
reactPluginKey: ()=>reactPluginKey,
|
|
15
|
+
react: ()=>react
|
|
16
|
+
});
|
|
17
|
+
const _prosemirrorState = require("prosemirror-state");
|
|
18
|
+
const ROOT_NODE_KEY = Symbol("@nytimes/react-prosemirror/root-node-key");
|
|
19
|
+
function createNodeKey() {
|
|
20
|
+
return Math.floor(Math.random() * 0xffffff).toString(16);
|
|
21
|
+
}
|
|
22
|
+
const reactPluginKey = new _prosemirrorState.PluginKey("@nytimes/react-prosemirror/react");
|
|
23
|
+
function react() {
|
|
24
|
+
return new _prosemirrorState.Plugin({
|
|
25
|
+
key: reactPluginKey,
|
|
26
|
+
state: {
|
|
27
|
+
init (_, state) {
|
|
28
|
+
const next = {
|
|
29
|
+
posToKey: new Map(),
|
|
30
|
+
keyToPos: new Map()
|
|
31
|
+
};
|
|
32
|
+
state.doc.descendants((node, pos)=>{
|
|
33
|
+
if (node.isText) return false;
|
|
34
|
+
const key = createNodeKey();
|
|
35
|
+
next.posToKey.set(pos, key);
|
|
36
|
+
next.keyToPos.set(key, pos);
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
return next;
|
|
40
|
+
},
|
|
41
|
+
/**
|
|
42
|
+
* Keeps node keys (mostly) stable across transactions.
|
|
43
|
+
*
|
|
44
|
+
* To accomplish this, we map each node position backwards
|
|
45
|
+
* through the transaction to identify its previous position,
|
|
46
|
+
* and thereby retrieve its previous key.
|
|
47
|
+
*/ apply (tr, value, _, newState) {
|
|
48
|
+
if (!tr.docChanged) return value;
|
|
49
|
+
const next = {
|
|
50
|
+
posToKey: new Map(),
|
|
51
|
+
keyToPos: new Map()
|
|
52
|
+
};
|
|
53
|
+
for (const [pos, key] of value.posToKey.entries()){
|
|
54
|
+
const { pos: newPos , deleted } = tr.mapping.mapResult(pos);
|
|
55
|
+
if (deleted) continue;
|
|
56
|
+
next.posToKey.set(newPos, key);
|
|
57
|
+
next.keyToPos.set(key, newPos);
|
|
58
|
+
}
|
|
59
|
+
newState.doc.descendants((node, pos)=>{
|
|
60
|
+
if (node.isText) return false;
|
|
61
|
+
if (next.posToKey.has(pos)) return true;
|
|
62
|
+
const key = createNodeKey();
|
|
63
|
+
next.posToKey.set(pos, key);
|
|
64
|
+
next.keyToPos.set(key, pos);
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
return next;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { EditorContext } from "../contexts/EditorContext.js";
|
|
3
|
+
import { useEditorView } from "../hooks/useEditorView.js";
|
|
4
|
+
import { useNodeViews } from "../hooks/useNodeViews.js";
|
|
5
|
+
export function Editor(param) {
|
|
6
|
+
let { mount , children , ...options } = param;
|
|
7
|
+
const { nodeViews , nodeViewsComponent } = useNodeViews(options.nodeViews);
|
|
8
|
+
const value = useEditorView(mount, {
|
|
9
|
+
...options,
|
|
10
|
+
nodeViews
|
|
11
|
+
});
|
|
12
|
+
return /*#__PURE__*/ React.createElement(EditorContext.Provider, {
|
|
13
|
+
value: value
|
|
14
|
+
}, children, nodeViewsComponent);
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { NodeViewsContext } from "../contexts/NodeViewsContext.js";
|
|
3
|
+
import { useEditorEffect } from "../hooks/useEditorEffect.js";
|
|
4
|
+
import { ROOT_NODE_KEY } from "../plugins/react.js";
|
|
5
|
+
export function NodeViews(param) {
|
|
6
|
+
let { portals } = param;
|
|
7
|
+
const rootRegisteredPortals = portals[ROOT_NODE_KEY];
|
|
8
|
+
const [rootPortals, setRootPortals] = useState(rootRegisteredPortals?.map((param)=>{
|
|
9
|
+
let { portal } = param;
|
|
10
|
+
return portal;
|
|
11
|
+
}) ?? []);
|
|
12
|
+
// `getPos` is technically derived from the EditorView
|
|
13
|
+
// state, so it's not safe to call until after the EditorView
|
|
14
|
+
// has been updated
|
|
15
|
+
useEditorEffect(()=>{
|
|
16
|
+
setRootPortals(rootRegisteredPortals?.sort((a, b)=>a.getPos() - b.getPos()).map((param)=>{
|
|
17
|
+
let { portal } = param;
|
|
18
|
+
return portal;
|
|
19
|
+
}) ?? []);
|
|
20
|
+
}, [
|
|
21
|
+
rootRegisteredPortals
|
|
22
|
+
]);
|
|
23
|
+
return /*#__PURE__*/ React.createElement(NodeViewsContext.Provider, {
|
|
24
|
+
value: portals
|
|
25
|
+
}, rootPortals);
|
|
26
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { act, render, screen } from "@testing-library/react";
|
|
2
2
|
import React, { useLayoutEffect, useState } from "react";
|
|
3
|
-
import { LayoutGroup } from "../../components/LayoutGroup.js";
|
|
4
3
|
import { useLayoutGroupEffect } from "../../hooks/useLayoutGroupEffect.js";
|
|
5
|
-
|
|
4
|
+
import { LayoutGroup } from "../LayoutGroup.js";
|
|
5
|
+
describe("LayoutGroup", ()=>{
|
|
6
6
|
jest.useFakeTimers("modern");
|
|
7
7
|
it("registers multiple effects and runs them", ()=>{
|
|
8
8
|
function Parent() {
|