@handlewithcare/react-prosemirror 3.1.0-tiptap.52 → 3.1.0-tiptap.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/cjs/ReactEditorView.js +6 -0
  2. package/dist/cjs/components/ChildNodeViews.js +16 -10
  3. package/dist/cjs/components/CursorWrapper.js +3 -7
  4. package/dist/cjs/components/ProseMirror.js +5 -3
  5. package/dist/cjs/components/TextNodeView.js +260 -47
  6. package/dist/cjs/components/TrailingHackView.js +70 -0
  7. package/dist/cjs/components/WidgetView.js +3 -1
  8. package/dist/cjs/contexts/ChildDescriptionsContext.js +3 -1
  9. package/dist/cjs/decorations/viewDecorations.js +1 -6
  10. package/dist/cjs/hooks/useEditor.js +2 -10
  11. package/dist/cjs/hooks/useMarkViewDescription.js +61 -3
  12. package/dist/cjs/hooks/useNodeViewDescription.js +64 -21
  13. package/dist/cjs/plugins/beforeInputPlugin.js +147 -45
  14. package/dist/cjs/plugins/reactKeys.js +21 -14
  15. package/dist/cjs/viewdesc.js +52 -4
  16. package/dist/esm/ReactEditorView.js +6 -0
  17. package/dist/esm/components/ChildNodeViews.js +17 -11
  18. package/dist/esm/components/CursorWrapper.js +3 -7
  19. package/dist/esm/components/ProseMirror.js +5 -3
  20. package/dist/esm/components/TextNodeView.js +209 -45
  21. package/dist/esm/components/TrailingHackView.js +71 -1
  22. package/dist/esm/components/WidgetView.js +3 -1
  23. package/dist/esm/contexts/ChildDescriptionsContext.js +3 -1
  24. package/dist/esm/decorations/viewDecorations.js +1 -6
  25. package/dist/esm/hooks/useEditor.js +2 -10
  26. package/dist/esm/hooks/useMarkViewDescription.js +62 -4
  27. package/dist/esm/hooks/useNodeViewDescription.js +65 -22
  28. package/dist/esm/plugins/beforeInputPlugin.js +147 -45
  29. package/dist/esm/plugins/reactKeys.js +21 -14
  30. package/dist/esm/viewdesc.js +51 -4
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/dist/types/ReactEditorView.d.ts +6 -1
  33. package/dist/types/components/TextNodeView.d.ts +20 -5
  34. package/dist/types/components/TrailingHackView.d.ts +1 -1
  35. package/dist/types/components/__tests__/ProseMirror.composition.test.d.ts +17 -1
  36. package/dist/types/contexts/ChildDescriptionsContext.d.ts +5 -3
  37. package/dist/types/decorations/viewDecorations.d.ts +2 -2
  38. package/dist/types/hooks/useEditor.d.ts +1 -2
  39. package/dist/types/hooks/useMarkViewDescription.d.ts +2 -1
  40. package/dist/types/hooks/useNodeViewDescription.d.ts +2 -1
  41. package/dist/types/plugins/beforeInputPlugin.d.ts +1 -2
  42. package/dist/types/plugins/reactKeys.d.ts +9 -8
  43. package/dist/types/viewdesc.d.ts +3 -2
  44. package/package.json +3 -1
@@ -2,7 +2,7 @@ import { useCallback, useContext, useMemo, useRef } from "react";
2
2
  import { ReactEditorView } from "../ReactEditorView.js";
3
3
  import { ChildDescriptionsContext } from "../contexts/ChildDescriptionsContext.js";
4
4
  import { EditorContext } from "../contexts/EditorContext.js";
5
- import { CompositionViewDesc, ReactNodeViewDesc, sortViewDescs } from "../viewdesc.js";
5
+ import { ReactNodeViewDesc, TextViewDesc, sortViewDescs } from "../viewdesc.js";
6
6
  import { useClientLayoutEffect } from "./useClientLayoutEffect.js";
7
7
  import { useEffectEvent } from "./useEffectEvent.js";
8
8
  export function useNodeViewDescription(getDOM, getContentDOM, constructor, props) {
@@ -131,32 +131,75 @@ export function useNodeViewDescription(getDOM, getContentDOM, constructor, props
131
131
  children.sort(sortViewDescs);
132
132
  for (const child of children){
133
133
  child.parent = viewDesc;
134
- // Because TextNodeViews can't locate the DOM nodes
135
- // for compositions, we need to override them here
136
- if (child instanceof CompositionViewDesc) {
137
- const compositionTopDOM = viewDesc?.contentDOM?.firstChild;
138
- if (!compositionTopDOM) throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
139
- let textDOM = compositionTopDOM;
140
- while(textDOM.firstChild){
141
- textDOM = textDOM.firstChild;
142
- }
143
- if (!textDOM || !(textDOM instanceof Text)) throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
144
- child.dom = compositionTopDOM;
145
- child.textDOM = textDOM;
146
- child.text = textDOM.data;
147
- child.textDOM.pmViewDesc = child;
148
- // It should not be possible to be in a composition because one could
149
- // not start between the renders that switch the view type.
150
- view.input.compositionNodes.push(child);
151
- }
152
134
  }
153
135
  });
136
+ const findCompositionDOM = useCallback((compositionViewDesc)=>{
137
+ if (!props.node.isTextblock) return;
138
+ const children = childrenRef.current;
139
+ // Because TextNodeViews can't locate the DOM nodes
140
+ // for compositions, we need to override them here
141
+ if (!viewDescRef.current?.contentDOM) return;
142
+ let compositionTopDOM = null;
143
+ for (const childNode of viewDescRef.current.contentDOM.childNodes){
144
+ if (children.every((child)=>child.dom !== childNode)) {
145
+ compositionTopDOM = childNode;
146
+ break;
147
+ }
148
+ }
149
+ if (!compositionTopDOM) {
150
+ // Otherwise the IME extended an existing tracked text node. Take it over.
151
+ const reactView = view;
152
+ const imeTextNode = reactView.input.compositionNode;
153
+ if (!imeTextNode || !viewDescRef.current.contentDOM.contains(imeTextNode.parentNode)) {
154
+ return;
155
+ }
156
+ const claimedDesc = imeTextNode.pmViewDesc;
157
+ if (!(claimedDesc instanceof TextViewDesc)) return;
158
+ if (claimedDesc.node.text === imeTextNode.nodeValue) return; // not extended
159
+ // Walk up to the direct child of contentDOM that contains the IME text node
160
+ // (could be the text node itself, could be wrapped in a mark span).
161
+ let topDOM = imeTextNode;
162
+ while(topDOM.parentNode !== viewDescRef.current.contentDOM){
163
+ const next = topDOM.parentNode;
164
+ if (!next) return;
165
+ topDOM = next;
166
+ }
167
+ // Detach the displaced TextViewDesc from the sibling list so sibling-size
168
+ // accounting (used by posBeforeChild) doesn't double-count this text node.
169
+ const displacedIdx = children.indexOf(claimedDesc);
170
+ if (displacedIdx >= 0) children.splice(displacedIdx, 1);
171
+ reactView.displacedNodes.push(claimedDesc);
172
+ compositionViewDesc.dom = topDOM;
173
+ compositionViewDesc.textDOM = imeTextNode;
174
+ compositionViewDesc.text = imeTextNode.data;
175
+ imeTextNode.pmViewDesc = compositionViewDesc;
176
+ compositionViewDesc._displacedDesc = claimedDesc;
177
+ reactView.input.compositionNodes.push(compositionViewDesc);
178
+ return;
179
+ }
180
+ let textDOM = compositionTopDOM;
181
+ while(textDOM.firstChild){
182
+ textDOM = textDOM.firstChild;
183
+ }
184
+ if (!textDOM || !(textDOM instanceof Text)) {
185
+ console.error(compositionTopDOM, textDOM);
186
+ throw new Error(`Started a composition but couldn't find the text node it belongs to.`);
187
+ }
188
+ compositionViewDesc.dom = compositionTopDOM;
189
+ compositionViewDesc.textDOM = textDOM;
190
+ compositionViewDesc.text = textDOM.data;
191
+ compositionViewDesc.textDOM.pmViewDesc = compositionViewDesc;
192
+ view.input.compositionNodes.push(compositionViewDesc);
193
+ }, [
194
+ props.node.isTextblock,
195
+ view
196
+ ]);
154
197
  const childContextValue = useMemo(()=>({
155
198
  parentRef: viewDescRef,
156
- siblingsRef: childrenRef
199
+ siblingsRef: childrenRef,
200
+ findCompositionDOM
157
201
  }), [
158
- childrenRef,
159
- viewDescRef
202
+ findCompositionDOM
160
203
  ]);
161
204
  return {
162
205
  childContextValue,
@@ -1,7 +1,10 @@
1
1
  import { Fragment, Slice } from "prosemirror-model";
2
2
  import { Plugin, TextSelection } from "prosemirror-state";
3
+ import { ReactEditorView } from "../ReactEditorView.js";
3
4
  import { CursorWrapper } from "../components/CursorWrapper.js";
4
5
  import { widget } from "../decorations/ReactWidgetType.js";
6
+ import { TextViewDesc, sortViewDescs } from "../viewdesc.js";
7
+ import { reactKeysPluginKey } from "./reactKeys.js";
5
8
  function insertText(view, eventData) {
6
9
  let options = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {};
7
10
  if (eventData === null) return false;
@@ -44,33 +47,63 @@ function handleGapCursorComposition(view) {
44
47
  tr.setSelection(TextSelection.near(tr.doc.resolve($from.pos + 1)));
45
48
  view.dispatch(tr);
46
49
  }
47
- export function beforeInputPlugin(setCursorWrapper) {
50
+ export function beforeInputPlugin() {
48
51
  let compositionMarks = null;
49
- let precompositionSnapshot = null;
50
52
  return new Plugin({
51
53
  props: {
52
54
  handleDOMEvents: {
53
55
  compositionstart (view) {
54
- compositionMarks = view.state.storedMarks ?? view.state.selection.$from.marks();
55
- view.dispatch(view.state.tr.deleteSelection());
56
- handleGapCursorComposition(view);
56
+ if (!(view instanceof ReactEditorView)) return false;
57
+ view.compositionStarting = true;
57
58
  const { state } = view;
58
- const $pos = state.selection.$from;
59
+ const { selection } = state;
60
+ const isEmptyTr = state.tr.delete(selection.from, selection.to);
61
+ const $from = isEmptyTr.doc.resolve(isEmptyTr.mapping.map(selection.from));
62
+ const isEmptyTextblock = $from.parent.isTextblock && $from.parent.childCount === 0;
63
+ compositionMarks = view.state.storedMarks;
64
+ // Render a CursorWrapper with empty marks if starting a composition in an
65
+ // empty textblock with no marks. This prevents the browser from adding a
66
+ // <br> to the text block when it becomes empty (either via canceling the
67
+ // composition with the escape key or deleting all composition text when
68
+ // the composition node is the only text node in the text block)
69
+ if (compositionMarks === null && isEmptyTextblock) {
70
+ compositionMarks = [];
71
+ }
72
+ const tr = view.state.tr.setStoredMarks(null);
73
+ view.dispatch(tr);
74
+ handleGapCursorComposition(view);
59
75
  if (compositionMarks) {
60
- setCursorWrapper(widget(state.selection.from, CursorWrapper, {
61
- key: "cursor-wrapper",
62
- marks: compositionMarks
76
+ view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
77
+ cursorWrapper: widget(state.selection.from, CursorWrapper, {
78
+ key: "cursor-wrapper",
79
+ marks: compositionMarks,
80
+ side: 0,
81
+ raw: true
82
+ })
63
83
  }));
84
+ // Pin the DOM cursor to PM's canonical position before the IME
85
+ // captures wherever the browser happened to leave it. Without this,
86
+ // a cursor at a mark boundary lands in either the left or right text
87
+ // node depending on the user's last navigation direction, and the
88
+ // IME composes into whichever one it found.
89
+ } else if (view.state.selection.empty) {
90
+ // @ts-expect-error internal method
91
+ view.domObserver.disconnectSelection();
92
+ try {
93
+ view.docView.setSelection(view.state.selection.anchor, view.state.selection.head, view, true // force — skip the isEquivalentPosition early-return
94
+ );
95
+ } finally{
96
+ // @ts-expect-error internal method
97
+ view.domObserver.setCurSelection();
98
+ // @ts-expect-error internal method
99
+ view.domObserver.connectSelection();
100
+ }
64
101
  }
65
- // Snapshot the siblings of the node that contains the
66
- // current cursor. We'll restore this later, so that React
67
- // doesn't panic about unknown DOM nodes.
68
- const { node: parent } = view.domAtPos($pos.pos);
69
- precompositionSnapshot = [];
70
- for (const node of parent.childNodes){
71
- precompositionSnapshot.push(node);
72
- }
73
- // @ts-expect-error Internal property - input
102
+ view.compositionStarting = false;
103
+ // We set composing to true after creating the cursor wrapper
104
+ // so that no existing text nodes try to protect themselves
105
+ // while we're creating the cursor wrapper, which may need
106
+ // to split a text node.
74
107
  view.input.composing = true;
75
108
  return true;
76
109
  },
@@ -78,40 +111,44 @@ export function beforeInputPlugin(setCursorWrapper) {
78
111
  return true;
79
112
  },
80
113
  compositionend (view, event) {
81
- // @ts-expect-error Internal property - input
114
+ if (!(view instanceof ReactEditorView)) return false;
115
+ if (!view.composing) return false;
82
116
  view.input.composing = false;
83
- const { state } = view;
84
- const { node: parent } = view.domAtPos(state.selection.from);
85
- if (precompositionSnapshot) {
86
- // Restore the snapshot of the parent node's children
87
- // from before the composition started. This gives us a
88
- // clean slate from which to dispatch our transaction
89
- // and trigger a React update.
90
- precompositionSnapshot.forEach((prevNode, i)=>{
91
- if (parent.childNodes.length <= i) {
92
- parent.appendChild(prevNode);
93
- return;
94
- }
95
- parent.replaceChild(prevNode, parent.childNodes.item(i));
96
- });
97
- if (parent.childNodes.length > precompositionSnapshot.length) {
98
- for(let i = precompositionSnapshot.length; i < parent.childNodes.length; i++){
99
- parent.removeChild(parent.childNodes.item(i));
100
- }
117
+ compositionMarks = null;
118
+ for (const displaced of view.displacedNodes){
119
+ // Put the displaced TextViewDesc back into its parent's child list.
120
+ const parent = displaced.parent;
121
+ if (parent && !parent.children.includes(displaced)) {
122
+ parent.children.push(displaced);
123
+ parent.children.sort(sortViewDescs);
124
+ }
125
+ // Restore pmViewDesc claim on the text node.
126
+ displaced.dom.pmViewDesc = displaced;
127
+ // Truncate the IME text node back to what the displaced PM node says it
128
+ // is. The composed content lives in PM state; the next React render will
129
+ // mount a sibling TextNodeView that inserts its own DOM (e.g.
130
+ // `<span class="word">k</span>`) right after this node.
131
+ const claimedText = displaced.node.text ?? "";
132
+ if (displaced.nodeDOM.nodeValue !== claimedText) {
133
+ displaced.nodeDOM.nodeValue = claimedText;
101
134
  }
102
135
  }
103
- if (event.data) {
104
- insertText(view, event.data, {
105
- marks: compositionMarks
106
- });
136
+ view.dispatch(view.state.tr.setMeta(reactKeysPluginKey, {
137
+ cursorWrapper: null
138
+ }));
139
+ if (view.input.compositionNode && isCompositionNodeOrphaned(view.input.compositionNode)) {
140
+ view.input.compositionNode.remove();
107
141
  }
108
- compositionMarks = null;
109
- precompositionSnapshot = null;
110
- setCursorWrapper(null);
142
+ view.input.compositionEndedAt = event.timeStamp;
143
+ view.input.compositionNode = null;
144
+ view.input.compositionNodes = [];
145
+ view.input.compositionID++;
111
146
  return true;
112
147
  },
113
148
  beforeinput (view, event) {
114
- event.preventDefault();
149
+ if (event.inputType !== "insertFromComposition") {
150
+ event.preventDefault();
151
+ }
115
152
  switch(event.inputType){
116
153
  case "insertParagraph":
117
154
  case "insertLineBreak":
@@ -156,6 +193,63 @@ export function beforeInputPlugin(setCursorWrapper) {
156
193
  insertText(view, event.data);
157
194
  break;
158
195
  }
196
+ case "insertCompositionText":
197
+ case "deleteCompositionText":
198
+ case "insertFromComposition":
199
+ {
200
+ if (!(view instanceof ReactEditorView)) break;
201
+ const { tr } = view.state;
202
+ // There's always a range on insertCompositionText beforeinput events
203
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
204
+ const range = event.getTargetRanges()[0];
205
+ const start = view.posAtDOM(range.startContainer, range.startOffset);
206
+ const end = view.posAtDOM(range.endContainer, range.endOffset, 1);
207
+ if (view.state.doc.textBetween(start, end, "**", "*") === event.data) {
208
+ return;
209
+ }
210
+ if (event.data) {
211
+ if (compositionMarks) tr.ensureMarks(compositionMarks);
212
+ tr.insertText(event.data, start, end);
213
+ } else {
214
+ tr.delete(start, end);
215
+ }
216
+ // When updating a composition within an existing text node,
217
+ // we need to avoid remounting it. If the composition is at
218
+ // the very beginning of the text node, the start position of
219
+ // that node will either be mapped forward (if inserting new
220
+ // content) or deleted (if replacing existing content).
221
+ //
222
+ // This will cause the reactKeys plugin to mint a new key for
223
+ // that node, which triggers a remount. So we check to see whether
224
+ // we're working on a composition at the very beginning of a text
225
+ // node, and if so, tell the react keys plugin not to change the
226
+ // key for that node.
227
+ //
228
+ // We need to check that the marks are the same — if they're not,
229
+ // then we're inserting text _before_ this text node, not at the
230
+ // start of it, so we actually _do_ want to map the exsting node
231
+ // forward.
232
+ const $start = view.state.doc.resolve(start);
233
+ const $end = view.state.doc.resolve(end);
234
+ const marks = compositionMarks ?? $start.marksAcross($end) ?? [];
235
+ if ($start.textOffset === 0 && $end.nodeAfter?.marks.every((m)=>m.isInSet(marks))) {
236
+ tr.setMeta(reactKeysPluginKey, {
237
+ overrides: {
238
+ [start]: start
239
+ }
240
+ });
241
+ }
242
+ view.dom.addEventListener("input", ()=>{
243
+ const sel = view.domSelectionRange();
244
+ if (sel.focusNode && sel.focusNode.nodeType === 3) {
245
+ view.input.compositionNode = sel.focusNode;
246
+ }
247
+ view.dispatch(tr);
248
+ }, {
249
+ once: true
250
+ });
251
+ break;
252
+ }
159
253
  case "deleteWordBackward":
160
254
  case "deleteHardLineBackward":
161
255
  case "deleteSoftLineBackward":
@@ -189,3 +283,11 @@ export function beforeInputPlugin(setCursorWrapper) {
189
283
  }
190
284
  });
191
285
  }
286
+ function isCompositionNodeOrphaned(tn) {
287
+ if (tn.pmViewDesc) return false;
288
+ for(let parent = tn.parentNode; parent; parent = parent.parentNode){
289
+ const desc = parent.pmViewDesc;
290
+ if (desc instanceof TextViewDesc && desc.nodeDOM === tn) return false;
291
+ }
292
+ return true;
293
+ }
@@ -1,4 +1,6 @@
1
1
  import { Plugin, PluginKey } from "prosemirror-state";
2
+ import { DecorationSet } from "prosemirror-view";
3
+ import { widget } from "../decorations/ReactWidgetType.js";
2
4
  export function createNodeKey() {
3
5
  const key = Math.floor(Math.random() * 0xffffffffffff).toString(16);
4
6
  return key;
@@ -11,14 +13,14 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
11
13
  * key for a given node can be accessed by that node's
12
14
  * current position in the document, and vice versa.
13
15
  */ export function reactKeys() {
14
- let composing = false;
15
16
  return new Plugin({
16
17
  key: reactKeysPluginKey,
17
18
  state: {
18
19
  init (_, state) {
19
20
  const next = {
20
21
  posToKey: new Map(),
21
- keyToPos: new Map()
22
+ keyToPos: new Map(),
23
+ cursorWrapper: null
22
24
  };
23
25
  state.doc.descendants((_, pos)=>{
24
26
  const key = createNodeKey();
@@ -36,14 +38,20 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
36
38
  * and assign its key to that new position, dropping it if the
37
39
  * node was deleted.
38
40
  */ apply (tr, value, _, newState) {
39
- if (!tr.docChanged || composing) {
40
- return value;
41
- }
42
- const overrides = tr.getMeta(reactKeysPluginKey)?.overrides;
41
+ const meta = tr.getMeta(reactKeysPluginKey);
42
+ const overrides = meta && "overrides" in meta ? meta.overrides : {};
43
+ const cursorWrapper = meta && "cursorWrapper" in meta ? meta.cursorWrapper : undefined;
43
44
  const next = {
44
45
  posToKey: new Map(),
45
- keyToPos: new Map()
46
+ keyToPos: new Map(),
47
+ cursorWrapper: cursorWrapper === undefined ? value.cursorWrapper ? widget(tr.mapping.map(value.cursorWrapper.from, -1), value.cursorWrapper.type.Component, value.cursorWrapper.spec) : null : cursorWrapper
46
48
  };
49
+ if (!tr.docChanged) {
50
+ return {
51
+ ...value,
52
+ cursorWrapper: next.cursorWrapper
53
+ };
54
+ }
47
55
  const posToKeyEntries = Array.from(value.posToKey.entries()).sort((param, param1)=>{
48
56
  let [a] = param, [b] = param1;
49
57
  return a - b;
@@ -69,13 +77,12 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr
69
77
  }
70
78
  },
71
79
  props: {
72
- handleDOMEvents: {
73
- compositionstart: ()=>{
74
- composing = true;
75
- },
76
- compositionend: ()=>{
77
- composing = false;
78
- }
80
+ decorations (state) {
81
+ const deco = reactKeysPluginKey.getState(state)?.cursorWrapper;
82
+ if (!deco) return DecorationSet.empty;
83
+ return DecorationSet.create(state.doc, [
84
+ deco
85
+ ]);
79
86
  }
80
87
  }
81
88
  });
@@ -16,7 +16,17 @@ import { domIndex, isEquivalentPosition } from "./dom.js";
16
16
  export function sortViewDescs(a, b) {
17
17
  if (a instanceof TrailingHackViewDesc) return 1;
18
18
  if (b instanceof TrailingHackViewDesc) return -1;
19
- return a.getPos() - b.getPos();
19
+ const posDiff = a.getPos() - b.getPos();
20
+ if (posDiff !== 0) return posDiff;
21
+ // When two descs share the same PM position (e.g. a zero-width widget
22
+ // and a text node that starts at the same position), fall back to DOM
23
+ // order so that the viewdesc children match the actual DOM layout.
24
+ // Without this, position computations like `posBeforeChild` can return
25
+ // the wrong PM position for the widget's container.
26
+ const cmp = a.dom.compareDocumentPosition(b.dom);
27
+ if (cmp & 4 /* DOCUMENT_POSITION_FOLLOWING */ ) return -1;
28
+ if (cmp & 2 /* DOCUMENT_POSITION_PRECEDING */ ) return 1;
29
+ return 0;
20
30
  }
21
31
  const NOT_DIRTY = 0, CHILD_DIRTY = 1, CONTENT_DIRTY = 2, NODE_DIRTY = 3;
22
32
  // Superclass for the various kinds of descriptions. Defines their
@@ -235,7 +245,9 @@ export class ViewDesc {
235
245
  prev = i ? this.children[i - 1] : null;
236
246
  if (!prev || prev.dom.parentNode == this.contentDOM) break;
237
247
  }
238
- if (prev && side && enter && !prev.border && !prev.domAtom) return prev.domFromPos(prev.size, side);
248
+ if (prev && side && enter && !prev.border && !prev.domAtom) {
249
+ return prev.domFromPos(prev.size, side);
250
+ }
239
251
  return {
240
252
  node: this.contentDOM,
241
253
  offset: prev ? domIndex(prev.dom) + 1 : 0
@@ -370,7 +382,9 @@ export class ViewDesc {
370
382
  const after = selRange.focusNode.childNodes[selRange.focusOffset];
371
383
  if (after && after.contentEditable == "false") force = true;
372
384
  }
373
- if (!(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) return;
385
+ if (view.composing || !(force || brKludge && browser.safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) {
386
+ return;
387
+ }
374
388
  // Selection.extend can be used to create an 'inverted' selection
375
389
  // (one where the focus is before the anchor), but not all
376
390
  // browsers support it yet.
@@ -645,7 +659,10 @@ export class TextViewDesc extends NodeViewDesc {
645
659
  skip: skip || true
646
660
  };
647
661
  }
648
- update(_node, _outerDeco, _innerDeco, _view) {
662
+ update(node, outerDeco, _innerDeco, _view) {
663
+ if (this.dirty == NODE_DIRTY || this.dirty != NOT_DIRTY && !this.inParent() || !node.sameMarkup(this.node)) return false;
664
+ this.updateOuterDeco(outerDeco);
665
+ this.node = node;
649
666
  this.dirty = NOT_DIRTY;
650
667
  return true;
651
668
  }
@@ -763,3 +780,33 @@ export function sameOuterDeco(a, b) {
763
780
  for(let i = 0; i < a.length; i++)if (!a[i].type.eq(b[i].type)) return false;
764
781
  return true;
765
782
  }
783
+ // Find a piece of text in an inline fragment, overlapping from-to.
784
+ // Ported from prosemirror-view's findTextInFragment.
785
+ export function findTextInFragment(frag, text, from, to) {
786
+ for(let i = 0, pos = 0; i < frag.childCount && pos <= to;){
787
+ const child = frag.child(i++);
788
+ const childStart = pos;
789
+ pos += child.nodeSize;
790
+ if (!child.isText) continue;
791
+ let str = child.text;
792
+ while(i < frag.childCount){
793
+ const next = frag.child(i++);
794
+ pos += next.nodeSize;
795
+ if (!next.isText) break;
796
+ str += next.text;
797
+ }
798
+ if (pos >= from) {
799
+ if (pos >= to && str.slice(to - text.length - childStart, to - childStart) === text) {
800
+ return to - text.length;
801
+ }
802
+ const found = childStart < to ? str.lastIndexOf(text, to - childStart - 1) : -1;
803
+ if (found >= 0 && found + text.length + childStart >= from) {
804
+ return childStart + found;
805
+ }
806
+ if (from === to && str.length >= to + text.length - childStart && str.slice(to - childStart, to - childStart + text.length) === text) {
807
+ return to;
808
+ }
809
+ }
810
+ }
811
+ return -1;
812
+ }