@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,163 @@
|
|
|
1
|
+
import { DecorationSet } from "prosemirror-view";
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3
|
+
const none = [], noSpec = {};
|
|
4
|
+
const empty = DecorationSet.empty;
|
|
5
|
+
// An abstraction that allows the code dealing with decorations to
|
|
6
|
+
// treat multiple DecorationSet objects as if it were a single object
|
|
7
|
+
// with (a subset of) the same interface.
|
|
8
|
+
let DecorationGroup = class DecorationGroup {
|
|
9
|
+
members;
|
|
10
|
+
constructor(members){
|
|
11
|
+
this.members = members;
|
|
12
|
+
}
|
|
13
|
+
map(mapping, doc) {
|
|
14
|
+
const mappedDecos = this.members.map((member)=>member.map(mapping, doc, noSpec));
|
|
15
|
+
return DecorationGroup.from(mappedDecos);
|
|
16
|
+
}
|
|
17
|
+
forChild(offset, child) {
|
|
18
|
+
if (child.isLeaf) return DecorationSet.empty;
|
|
19
|
+
let found = [];
|
|
20
|
+
for(let i = 0; i < this.members.length; i++){
|
|
21
|
+
const result = this.members[i].forChild(offset, child);
|
|
22
|
+
if (result == empty) continue;
|
|
23
|
+
if (result instanceof DecorationGroup) found = found.concat(result.members);
|
|
24
|
+
else found.push(result);
|
|
25
|
+
}
|
|
26
|
+
return DecorationGroup.from(found);
|
|
27
|
+
}
|
|
28
|
+
eq(other) {
|
|
29
|
+
if (!(other instanceof DecorationGroup) || other.members.length != this.members.length) return false;
|
|
30
|
+
for(let i = 0; i < this.members.length; i++)if (!this.members[i].eq(other.members[i])) return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
locals(node) {
|
|
34
|
+
let result, sorted = true;
|
|
35
|
+
for(let i = 0; i < this.members.length; i++){
|
|
36
|
+
const locals = this.members[i].localsInner(node);
|
|
37
|
+
if (!locals.length) continue;
|
|
38
|
+
if (!result) {
|
|
39
|
+
result = locals;
|
|
40
|
+
} else {
|
|
41
|
+
if (sorted) {
|
|
42
|
+
result = result.slice();
|
|
43
|
+
sorted = false;
|
|
44
|
+
}
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
46
|
+
for(let j = 0; j < locals.length; j++)result.push(locals[j]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result ? removeOverlap(sorted ? result : result.sort(byPos)) : none;
|
|
50
|
+
}
|
|
51
|
+
// Create a group for the given array of decoration sets, or return
|
|
52
|
+
// a single set when possible.
|
|
53
|
+
static from(members) {
|
|
54
|
+
switch(members.length){
|
|
55
|
+
case 0:
|
|
56
|
+
return empty;
|
|
57
|
+
case 1:
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
59
|
+
return members[0];
|
|
60
|
+
default:
|
|
61
|
+
return new DecorationGroup(members.every((m)=>m instanceof DecorationSet) ? members : members.reduce((r, m)=>r.concat(m instanceof DecorationSet ? m : m.members), []));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
forEachSet(f) {
|
|
65
|
+
for(let i = 0; i < this.members.length; i++)// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
66
|
+
this.members[i].forEachSet(f);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
// Used to sort decorations so that ones with a low start position
|
|
70
|
+
// come first, and within a set with the same start position, those
|
|
71
|
+
// with an smaller end position come first.
|
|
72
|
+
function byPos(a, b) {
|
|
73
|
+
return a.from - b.from || a.to - b.to;
|
|
74
|
+
}
|
|
75
|
+
// Scan a sorted array of decorations for partially overlapping spans,
|
|
76
|
+
// and split those so that only fully overlapping spans are left (to
|
|
77
|
+
// make subsequent rendering easier). Will return the input array if
|
|
78
|
+
// no partially overlapping spans are found (the common case).
|
|
79
|
+
function removeOverlap(spans) {
|
|
80
|
+
let working = spans;
|
|
81
|
+
for(let i = 0; i < working.length - 1; i++){
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
83
|
+
const span = working[i];
|
|
84
|
+
if (span.from != span.to) for(let j = i + 1; j < working.length; j++){
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
86
|
+
const next = working[j];
|
|
87
|
+
if (next.from == span.from) {
|
|
88
|
+
if (next.to != span.to) {
|
|
89
|
+
if (working == spans) working = spans.slice();
|
|
90
|
+
// Followed by a partially overlapping larger span. Split that
|
|
91
|
+
// span.
|
|
92
|
+
working[j] = next.copy(next.from, span.to);
|
|
93
|
+
insertAhead(working, j + 1, next.copy(span.to, next.to));
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
} else {
|
|
97
|
+
if (next.from < span.to) {
|
|
98
|
+
if (working == spans) working = spans.slice();
|
|
99
|
+
// The end of this one overlaps with a subsequent span. Split
|
|
100
|
+
// this one.
|
|
101
|
+
working[i] = span.copy(span.from, next.from);
|
|
102
|
+
insertAhead(working, j, span.copy(next.from, span.to));
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return working;
|
|
109
|
+
}
|
|
110
|
+
function insertAhead(array, i, deco) {
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
112
|
+
while(i < array.length && byPos(deco, array[i]) > 0)i++;
|
|
113
|
+
array.splice(i, 0, deco);
|
|
114
|
+
}
|
|
115
|
+
const ViewDecorationsCache = new WeakMap();
|
|
116
|
+
/**
|
|
117
|
+
* Produces the DecorationSource for the current state, based
|
|
118
|
+
* on the decorations editor prop.
|
|
119
|
+
*
|
|
120
|
+
* The return value of this function is memoized; if it is to
|
|
121
|
+
* return an equivalent value to the last time it was called for
|
|
122
|
+
* a given EditorView, it will return exactly that previous value.
|
|
123
|
+
*
|
|
124
|
+
* This makes it safe to call in a React render function, even
|
|
125
|
+
* if its result is used in a dependencies array for a hook.
|
|
126
|
+
*/ export function viewDecorations(view, cursorWrapper) {
|
|
127
|
+
const found = [];
|
|
128
|
+
view.someProp("decorations", (f)=>{
|
|
129
|
+
const result = f(view.state);
|
|
130
|
+
if (result && result != empty) found.push(result);
|
|
131
|
+
});
|
|
132
|
+
// We don't have access to types for view.cursorWrapper here
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
134
|
+
if (cursorWrapper) {
|
|
135
|
+
found.push(// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
+
DecorationSet.create(view.state.doc, [
|
|
137
|
+
cursorWrapper
|
|
138
|
+
]));
|
|
139
|
+
}
|
|
140
|
+
const previous = ViewDecorationsCache.get(view);
|
|
141
|
+
if (!previous) {
|
|
142
|
+
const result = DecorationGroup.from(found);
|
|
143
|
+
ViewDecorationsCache.set(view, result);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
let numPrevious = 0;
|
|
147
|
+
let areSetsEqual = true;
|
|
148
|
+
previous.forEachSet((set)=>{
|
|
149
|
+
const next = found[numPrevious++];
|
|
150
|
+
if (next !== set) {
|
|
151
|
+
areSetsEqual = false;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
if (numPrevious !== found.length) {
|
|
155
|
+
areSetsEqual = false;
|
|
156
|
+
}
|
|
157
|
+
if (!areSetsEqual) {
|
|
158
|
+
const result = DecorationGroup.from(found);
|
|
159
|
+
ViewDecorationsCache.set(view, result);
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
return previous;
|
|
163
|
+
}
|
package/dist/esm/dom.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ export const domIndex = function(node) {
|
|
2
|
+
for(let index = 0;; index++){
|
|
3
|
+
node = node.previousSibling;
|
|
4
|
+
if (!node) return index;
|
|
5
|
+
}
|
|
6
|
+
};
|
|
7
|
+
export const parentNode = function(node) {
|
|
8
|
+
const parent = node.assignedSlot || node.parentNode;
|
|
9
|
+
return parent && parent.nodeType == 11 ? parent.host : parent;
|
|
10
|
+
};
|
|
11
|
+
let reusedRange = null;
|
|
12
|
+
// Note that this will always return the same range, because DOM range
|
|
13
|
+
// objects are every expensive, and keep slowing down subsequent DOM
|
|
14
|
+
// updates, for some reason.
|
|
15
|
+
export const textRange = function(node, from, to) {
|
|
16
|
+
const range = reusedRange || (reusedRange = document.createRange());
|
|
17
|
+
range.setEnd(node, to == null ? node.nodeValue.length : to);
|
|
18
|
+
range.setStart(node, from || 0);
|
|
19
|
+
return range;
|
|
20
|
+
};
|
|
21
|
+
// Scans forward and backward through DOM positions equivalent to the
|
|
22
|
+
// given one to see if the two are in the same place (i.e. after a
|
|
23
|
+
// text node vs at the end of that text node)
|
|
24
|
+
export const isEquivalentPosition = function(node, off, targetNode, targetOff) {
|
|
25
|
+
return targetNode && (scanFor(node, off, targetNode, targetOff, -1) || scanFor(node, off, targetNode, targetOff, 1));
|
|
26
|
+
};
|
|
27
|
+
const atomElements = /^(img|br|input|textarea|hr)$/i;
|
|
28
|
+
function scanFor(node, off, targetNode, targetOff, dir) {
|
|
29
|
+
for(;;){
|
|
30
|
+
if (node == targetNode && off == targetOff) return true;
|
|
31
|
+
if (off == (dir < 0 ? 0 : nodeSize(node))) {
|
|
32
|
+
const parent = node.parentNode;
|
|
33
|
+
if (!parent || parent.nodeType != 1 || hasBlockDesc(node) || atomElements.test(node.nodeName) || node.contentEditable == "false") return false;
|
|
34
|
+
off = domIndex(node) + (dir < 0 ? 0 : 1);
|
|
35
|
+
node = parent;
|
|
36
|
+
} else if (node.nodeType == 1) {
|
|
37
|
+
node = node.childNodes[off + (dir < 0 ? -1 : 0)];
|
|
38
|
+
if (node.contentEditable == "false") return false;
|
|
39
|
+
off = dir < 0 ? nodeSize(node) : 0;
|
|
40
|
+
} else {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function nodeSize(node) {
|
|
46
|
+
return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length;
|
|
47
|
+
}
|
|
48
|
+
export function isOnEdge(node, offset, parent) {
|
|
49
|
+
for(let atStart = offset == 0, atEnd = offset == nodeSize(node); atStart || atEnd;){
|
|
50
|
+
if (node == parent) return true;
|
|
51
|
+
const index = domIndex(node);
|
|
52
|
+
node = node.parentNode;
|
|
53
|
+
if (!node) return false;
|
|
54
|
+
atStart = atStart && index == 0;
|
|
55
|
+
atEnd = atEnd && index == nodeSize(node);
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
export function hasBlockDesc(dom) {
|
|
60
|
+
let desc;
|
|
61
|
+
for(let cur = dom; cur; cur = cur.parentNode)if (desc = cur.pmViewDesc) break;
|
|
62
|
+
return desc && desc.node && desc.node.isBlock && (desc.dom == dom || desc.contentDOM == dom);
|
|
63
|
+
}
|
|
64
|
+
// Work around Chrome issue https://bugs.chromium.org/p/chromium/issues/detail?id=447523
|
|
65
|
+
// (isCollapsed inappropriately returns true in shadow dom)
|
|
66
|
+
export const selectionCollapsed = function(domSel) {
|
|
67
|
+
return domSel.focusNode && isEquivalentPosition(domSel.focusNode, domSel.focusOffset, domSel.anchorNode, domSel.anchorOffset);
|
|
68
|
+
};
|
|
69
|
+
export function keyEvent(keyCode, key) {
|
|
70
|
+
const event = document.createEvent("Event");
|
|
71
|
+
event.initEvent("keydown", true, true);
|
|
72
|
+
event.keyCode = keyCode;
|
|
73
|
+
event.key = event.code = key;
|
|
74
|
+
return event;
|
|
75
|
+
}
|
|
76
|
+
export function deepActiveElement(doc) {
|
|
77
|
+
let elt = doc.activeElement;
|
|
78
|
+
while(elt && elt.shadowRoot)elt = elt.shadowRoot.activeElement;
|
|
79
|
+
return elt;
|
|
80
|
+
}
|
|
81
|
+
export function caretFromPoint(doc, x, y) {
|
|
82
|
+
if (doc.caretPositionFromPoint) {
|
|
83
|
+
try {
|
|
84
|
+
// Firefox throws for this call in hard-to-predict circumstances (#994)
|
|
85
|
+
const pos = doc.caretPositionFromPoint(x, y);
|
|
86
|
+
// Clip the offset, because Chrome will return a text offset
|
|
87
|
+
// into <input> nodes, which can't be treated as a regular DOM
|
|
88
|
+
// offset
|
|
89
|
+
if (pos) return {
|
|
90
|
+
node: pos.offsetNode,
|
|
91
|
+
offset: Math.min(nodeSize(pos.offsetNode), pos.offset)
|
|
92
|
+
};
|
|
93
|
+
} catch (_) {
|
|
94
|
+
// pass
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (doc.caretRangeFromPoint) {
|
|
98
|
+
const range = doc.caretRangeFromPoint(x, y);
|
|
99
|
+
if (range) return {
|
|
100
|
+
node: range.startContainer,
|
|
101
|
+
offset: Math.min(nodeSize(range.startContainer), range.startOffset)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
@@ -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,54 @@
|
|
|
1
|
+
/* Copyright (c) The New York Times Company */ import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { componentEventListeners } from "../plugins/componentEventListeners.js";
|
|
3
|
+
/**
|
|
4
|
+
* Produces a plugin that can be used with ProseMirror to handle DOM
|
|
5
|
+
* events at the EditorView.dom element.
|
|
6
|
+
*
|
|
7
|
+
* - `reactEventsPlugin` is a ProseMirror plugin for handling DOM events
|
|
8
|
+
* at the EditorView.dom element. It should be passed to `useEditorView`,
|
|
9
|
+
* along with any other plugins.
|
|
10
|
+
*
|
|
11
|
+
* - `registerEventListener` and `unregisterEventListener` should be
|
|
12
|
+
* passed to `EditorContext.Provider`.
|
|
13
|
+
*
|
|
14
|
+
* @privateRemarks
|
|
15
|
+
*
|
|
16
|
+
* This hook uses a combination of mutable and immutable updates to give
|
|
17
|
+
* us precise control over when we re-create the ProseMirror plugin.
|
|
18
|
+
*
|
|
19
|
+
* The plugin has a mutable reference to the set of handlers for each
|
|
20
|
+
* event type, but the set of event types is static. This means that we
|
|
21
|
+
* need to produce a new ProseMirror plugin whenever a new event type is
|
|
22
|
+
* registered. We avoid producing a new ProseMirrer plugin in any other
|
|
23
|
+
* scenario to avoid the performance overhead of reconfiguring the plugins
|
|
24
|
+
* in the EditorView.
|
|
25
|
+
*
|
|
26
|
+
* To accomplish this, we shallowly clone the registry whenever a new event
|
|
27
|
+
* type is registered.
|
|
28
|
+
*/ export function useComponentEventListeners() {
|
|
29
|
+
const [registry, setRegistry] = useState(new Map());
|
|
30
|
+
const registerEventListener = useCallback((eventType, handler)=>{
|
|
31
|
+
const handlers = registry.get(eventType) ?? [];
|
|
32
|
+
handlers.unshift(handler);
|
|
33
|
+
if (!registry.has(eventType)) {
|
|
34
|
+
registry.set(eventType, handlers);
|
|
35
|
+
setRegistry(new Map(registry));
|
|
36
|
+
}
|
|
37
|
+
}, [
|
|
38
|
+
registry
|
|
39
|
+
]);
|
|
40
|
+
const unregisterEventListener = useCallback((eventType, handler)=>{
|
|
41
|
+
const handlers = registry.get(eventType);
|
|
42
|
+
handlers?.splice(handlers.indexOf(handler), 1);
|
|
43
|
+
}, [
|
|
44
|
+
registry
|
|
45
|
+
]);
|
|
46
|
+
const componentEventListenersPlugin = useMemo(()=>componentEventListeners(registry), [
|
|
47
|
+
registry
|
|
48
|
+
]);
|
|
49
|
+
return {
|
|
50
|
+
registerEventListener,
|
|
51
|
+
unregisterEventListener,
|
|
52
|
+
componentEventListenersPlugin
|
|
53
|
+
};
|
|
54
|
+
}
|