prosemirror-view 1.36.0 → 1.37.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 1.37.1 (2024-12-11)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix an issue where `display: content` elements could break `posAtCoords`.
6
+
7
+ Work around an issue with Chrome reporting the selection incorrectly during composition.
8
+
9
+ ## 1.37.0 (2024-11-23)
10
+
11
+ ### Bug fixes
12
+
13
+ Fix an issue on Safari where the (incorrect) top-level selection was used, instead of the workaround that also works inside shadow roots.
14
+
15
+ ### New features
16
+
17
+ Mark views can now provide an `ignoreMutation` method. Add ViewMutationRecord to doc template
18
+
1
19
  ## 1.36.0 (2024-11-05)
2
20
 
3
21
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -389,10 +389,10 @@ function posFromCaret(view, node, offset, coords) {
389
389
  var outsideBlock = -1;
390
390
  for (var cur = node, sawBlock = false;;) {
391
391
  if (cur == view.dom) break;
392
- var desc = view.docView.nearestDesc(cur, true);
392
+ var desc = view.docView.nearestDesc(cur, true),
393
+ rect = void 0;
393
394
  if (!desc) return null;
394
- if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent || !desc.contentDOM)) {
395
- var rect = desc.dom.getBoundingClientRect();
395
+ if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent || !desc.contentDOM) && ((rect = desc.dom.getBoundingClientRect()).width || rect.height)) {
396
396
  if (desc.node.isBlock && desc.parent) {
397
397
  if (!sawBlock && rect.left > coords.left || rect.top > coords.top) outsideBlock = desc.posBefore;else if (!sawBlock && rect.right < coords.left || rect.bottom < coords.top) outsideBlock = desc.posAfter;
398
398
  sawBlock = true;
@@ -935,19 +935,20 @@ var ViewDesc = function () {
935
935
  }
936
936
  }, {
937
937
  key: "setSelection",
938
- value: function setSelection(anchor, head, root) {
938
+ value: function setSelection(anchor, head, view) {
939
939
  var force = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
940
940
  var from = Math.min(anchor, head),
941
941
  to = Math.max(anchor, head);
942
942
  for (var i = 0, offset = 0; i < this.children.length; i++) {
943
943
  var child = this.children[i],
944
944
  end = offset + child.size;
945
- if (from > offset && to < end) return child.setSelection(anchor - offset - child.border, head - offset - child.border, root, force);
945
+ if (from > offset && to < end) return child.setSelection(anchor - offset - child.border, head - offset - child.border, view, force);
946
946
  offset = end;
947
947
  }
948
948
  var anchorDOM = this.domFromPos(anchor, anchor ? -1 : 1);
949
949
  var headDOM = head == anchor ? anchorDOM : this.domFromPos(head, head ? -1 : 1);
950
- var domSel = root.getSelection();
950
+ var domSel = view.root.getSelection();
951
+ var selRange = view.domSelectionRange();
951
952
  var brKludge = false;
952
953
  if ((gecko || safari) && anchor == head) {
953
954
  var _anchorDOM = anchorDOM,
@@ -973,11 +974,11 @@ var ViewDesc = function () {
973
974
  brKludge = prev && (prev.nodeName == "BR" || prev.contentEditable == "false");
974
975
  }
975
976
  }
976
- if (gecko && domSel.focusNode && domSel.focusNode != headDOM.node && domSel.focusNode.nodeType == 1) {
977
- var _after2 = domSel.focusNode.childNodes[domSel.focusOffset];
977
+ if (gecko && selRange.focusNode && selRange.focusNode != headDOM.node && selRange.focusNode.nodeType == 1) {
978
+ var _after2 = selRange.focusNode.childNodes[selRange.focusOffset];
978
979
  if (_after2 && _after2.contentEditable == "false") force = true;
979
980
  }
980
- if (!(force || brKludge && safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, domSel.focusNode, domSel.focusOffset)) return;
981
+ if (!(force || brKludge && safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) return;
981
982
  var domSelExtended = false;
982
983
  if ((domSel.extend || anchor == head) && !brKludge) {
983
984
  domSel.collapse(anchorDOM.node, anchorDOM.offset);
@@ -1213,6 +1214,11 @@ var MarkViewDesc = function (_ViewDesc3) {
1213
1214
  copy.children = nodes;
1214
1215
  return copy;
1215
1216
  }
1217
+ }, {
1218
+ key: "ignoreMutation",
1219
+ value: function ignoreMutation(mutation) {
1220
+ return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : _get(_getPrototypeOf(MarkViewDesc.prototype), "ignoreMutation", this).call(this, mutation);
1221
+ }
1216
1222
  }, {
1217
1223
  key: "destroy",
1218
1224
  value: function destroy() {
@@ -1593,8 +1599,8 @@ var CustomNodeViewDesc = function (_NodeViewDesc2) {
1593
1599
  }
1594
1600
  }, {
1595
1601
  key: "setSelection",
1596
- value: function setSelection(anchor, head, root, force) {
1597
- this.spec.setSelection ? this.spec.setSelection(anchor, head, root) : _get(_getPrototypeOf(CustomNodeViewDesc.prototype), "setSelection", this).call(this, anchor, head, root, force);
1602
+ value: function setSelection(anchor, head, view, force) {
1603
+ this.spec.setSelection ? this.spec.setSelection(anchor, head, view.root) : _get(_getPrototypeOf(CustomNodeViewDesc.prototype), "setSelection", this).call(this, anchor, head, view, force);
1598
1604
  }
1599
1605
  }, {
1600
1606
  key: "destroy",
@@ -2179,7 +2185,7 @@ function selectionToDOM(view) {
2179
2185
  if (!sel.$from.parent.inlineContent) resetEditableFrom = temporarilyEditableNear(view, sel.from);
2180
2186
  if (!sel.empty && !sel.$from.parent.inlineContent) resetEditableTo = temporarilyEditableNear(view, sel.to);
2181
2187
  }
2182
- view.docView.setSelection(anchor, head, view.root, force);
2188
+ view.docView.setSelection(anchor, head, view, force);
2183
2189
  if (brokenSelectBetweenUneditable) {
2184
2190
  if (resetEditableFrom) resetEditable(resetEditableFrom);
2185
2191
  if (resetEditableTo) resetEditable(resetEditableTo);
@@ -2868,7 +2874,7 @@ var InputState = _createClass(function InputState() {
2868
2874
  this.lastIOSEnterFallbackTimeout = -1;
2869
2875
  this.lastFocus = 0;
2870
2876
  this.lastTouch = 0;
2871
- this.lastAndroidDelete = 0;
2877
+ this.lastChromeDelete = 0;
2872
2878
  this.composing = false;
2873
2879
  this.compositionNode = null;
2874
2880
  this.composingTimeout = -1;
@@ -4655,7 +4661,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4655
4661
  if (android && chrome) view.domObserver.suppressSelectionUpdates();
4656
4662
  return;
4657
4663
  }
4658
- if (chrome && android && change.endB == change.start) view.input.lastAndroidDelete = Date.now();
4664
+ if (chrome && change.endB == change.start) view.input.lastChromeDelete = Date.now();
4659
4665
  if (android && !inlineChange && $from.start() != $to.start() && $to.parentOffset == 0 && $from.depth == $to.depth && parse.sel && parse.sel.anchor == parse.sel.head && parse.sel.head == change.endA) {
4660
4666
  change.endB -= 2;
4661
4667
  $to = parse.doc.resolveNoCache(change.endB - parse.from);
@@ -4692,7 +4698,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4692
4698
  if (!tr) tr = view.state.tr.replace(chFrom, chTo, parse.doc.slice(change.start - parse.from, change.endB - parse.from));
4693
4699
  if (parse.sel) {
4694
4700
  var _sel3 = resolveSelection(view, tr.doc, parse.sel);
4695
- if (_sel3 && !(chrome && android && view.composing && _sel3.empty && (change.start != change.endB || view.input.lastAndroidDelete < Date.now() - 100) && (_sel3.head == chFrom || _sel3.head == tr.mapping.map(chTo) - 1) || ie && _sel3.empty && _sel3.head == chFrom)) tr.setSelection(_sel3);
4701
+ if (_sel3 && !(chrome && view.composing && _sel3.empty && (change.start != change.endB || view.input.lastChromeDelete < Date.now() - 100) && (_sel3.head == chFrom || _sel3.head == tr.mapping.map(chTo) - 1) || ie && _sel3.empty && _sel3.head == chFrom)) tr.setSelection(_sel3);
4696
4702
  }
4697
4703
  if (storedMarks) tr.ensureMarks(storedMarks);
4698
4704
  if (compositionID) tr.setMeta("composition", compositionID);
package/dist/index.d.cts CHANGED
@@ -238,6 +238,17 @@ declare global {
238
238
  }
239
239
  }
240
240
  /**
241
+ A ViewMutationRecord represents a DOM
242
+ [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
243
+ or a selection change happens within the view. When the change is
244
+ a selection change, the record will have a `type` property of
245
+ `"selection"` (which doesn't occur for native mutation records).
246
+ */
247
+ type ViewMutationRecord = MutationRecord | {
248
+ type: "selection";
249
+ target: DOMNode;
250
+ };
251
+ /**
241
252
  By default, document nodes are rendered using the result of the
242
253
  [`toDOM`](https://prosemirror.net/docs/ref/#model.NodeSpec.toDOM) method of their spec, and managed
243
254
  entirely by the editor. For some use cases, such as embedded
@@ -308,16 +319,11 @@ interface NodeView {
308
319
  */
309
320
  stopEvent?: (event: Event) => boolean;
310
321
  /**
311
- Called when a DOM
312
- [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
313
- or a selection change happens within the view. When the change is
314
- a selection change, the record will have a `type` property of
315
- `"selection"` (which doesn't occur for native mutation records).
316
- Return false if the editor should re-read the selection or
317
- re-parse the range around the mutation, true if it can safely be
318
- ignored.
322
+ Called when a [mutation](https://prosemirror.net/docs/ref/#view.ViewMutationRecord) happens within the
323
+ view. Return false if the editor should re-read the selection or re-parse
324
+ the range around the mutation, true if it can safely be ignored.
319
325
  */
320
- ignoreMutation?: (mutation: MutationRecord) => boolean;
326
+ ignoreMutation?: (mutation: ViewMutationRecord) => boolean;
321
327
  /**
322
328
  Called when the node view is removed from the editor or the whole
323
329
  editor is destroyed.
@@ -344,6 +350,12 @@ interface MarkView {
344
350
  */
345
351
  contentDOM?: HTMLElement | null;
346
352
  /**
353
+ Called when a [mutation](https://prosemirror.net/docs/ref/#view.ViewMutationRecord) happens within the
354
+ view. Return false if the editor should re-read the selection or re-parse
355
+ the range around the mutation, true if it can safely be ignored.
356
+ */
357
+ ignoreMutation?: (mutation: ViewMutationRecord) => boolean;
358
+ /**
347
359
  Called when the mark view is removed from the editor or the whole
348
360
  editor is destroyed.
349
361
  */
@@ -391,8 +403,8 @@ declare class ViewDesc {
391
403
  };
392
404
  emptyChildAt(side: number): boolean;
393
405
  domAfterPos(pos: number): DOMNode;
394
- setSelection(anchor: number, head: number, root: Document | ShadowRoot, force?: boolean): void;
395
- ignoreMutation(mutation: MutationRecord): boolean;
406
+ setSelection(anchor: number, head: number, view: EditorView, force?: boolean): void;
407
+ ignoreMutation(mutation: ViewMutationRecord): boolean;
396
408
  get contentLost(): boolean | null;
397
409
  markDirty(from: number, to: number): void;
398
410
  markParentsDirty(): void;
@@ -441,7 +453,7 @@ declare class TextViewDesc extends NodeViewDesc {
441
453
  offset: number;
442
454
  };
443
455
  localPosFromDOM(dom: DOMNode, offset: number, bias: number): number;
444
- ignoreMutation(mutation: MutationRecord): boolean;
456
+ ignoreMutation(mutation: ViewMutationRecord): boolean;
445
457
  slice(from: number, to: number, view: EditorView): TextViewDesc;
446
458
  markDirty(from: number, to: number): void;
447
459
  get domAtom(): boolean;
@@ -955,4 +967,4 @@ interface DirectEditorProps extends EditorProps {
955
967
  dispatchTransaction?: (tr: Transaction) => void;
956
968
  }
957
969
 
958
- export { type DOMEventMap, Decoration, type DecorationAttrs, DecorationSet, type DecorationSource, type DirectEditorProps, type EditorProps, EditorView, type MarkView, type MarkViewConstructor, type NodeView, type NodeViewConstructor };
970
+ export { type DOMEventMap, Decoration, type DecorationAttrs, DecorationSet, type DecorationSource, type DirectEditorProps, type EditorProps, EditorView, type MarkView, type MarkViewConstructor, type NodeView, type NodeViewConstructor, type ViewMutationRecord };
package/dist/index.d.ts CHANGED
@@ -238,6 +238,17 @@ declare global {
238
238
  }
239
239
  }
240
240
  /**
241
+ A ViewMutationRecord represents a DOM
242
+ [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
243
+ or a selection change happens within the view. When the change is
244
+ a selection change, the record will have a `type` property of
245
+ `"selection"` (which doesn't occur for native mutation records).
246
+ */
247
+ type ViewMutationRecord = MutationRecord | {
248
+ type: "selection";
249
+ target: DOMNode;
250
+ };
251
+ /**
241
252
  By default, document nodes are rendered using the result of the
242
253
  [`toDOM`](https://prosemirror.net/docs/ref/#model.NodeSpec.toDOM) method of their spec, and managed
243
254
  entirely by the editor. For some use cases, such as embedded
@@ -308,16 +319,11 @@ interface NodeView {
308
319
  */
309
320
  stopEvent?: (event: Event) => boolean;
310
321
  /**
311
- Called when a DOM
312
- [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
313
- or a selection change happens within the view. When the change is
314
- a selection change, the record will have a `type` property of
315
- `"selection"` (which doesn't occur for native mutation records).
316
- Return false if the editor should re-read the selection or
317
- re-parse the range around the mutation, true if it can safely be
318
- ignored.
322
+ Called when a [mutation](https://prosemirror.net/docs/ref/#view.ViewMutationRecord) happens within the
323
+ view. Return false if the editor should re-read the selection or re-parse
324
+ the range around the mutation, true if it can safely be ignored.
319
325
  */
320
- ignoreMutation?: (mutation: MutationRecord) => boolean;
326
+ ignoreMutation?: (mutation: ViewMutationRecord) => boolean;
321
327
  /**
322
328
  Called when the node view is removed from the editor or the whole
323
329
  editor is destroyed.
@@ -344,6 +350,12 @@ interface MarkView {
344
350
  */
345
351
  contentDOM?: HTMLElement | null;
346
352
  /**
353
+ Called when a [mutation](https://prosemirror.net/docs/ref/#view.ViewMutationRecord) happens within the
354
+ view. Return false if the editor should re-read the selection or re-parse
355
+ the range around the mutation, true if it can safely be ignored.
356
+ */
357
+ ignoreMutation?: (mutation: ViewMutationRecord) => boolean;
358
+ /**
347
359
  Called when the mark view is removed from the editor or the whole
348
360
  editor is destroyed.
349
361
  */
@@ -391,8 +403,8 @@ declare class ViewDesc {
391
403
  };
392
404
  emptyChildAt(side: number): boolean;
393
405
  domAfterPos(pos: number): DOMNode;
394
- setSelection(anchor: number, head: number, root: Document | ShadowRoot, force?: boolean): void;
395
- ignoreMutation(mutation: MutationRecord): boolean;
406
+ setSelection(anchor: number, head: number, view: EditorView, force?: boolean): void;
407
+ ignoreMutation(mutation: ViewMutationRecord): boolean;
396
408
  get contentLost(): boolean | null;
397
409
  markDirty(from: number, to: number): void;
398
410
  markParentsDirty(): void;
@@ -441,7 +453,7 @@ declare class TextViewDesc extends NodeViewDesc {
441
453
  offset: number;
442
454
  };
443
455
  localPosFromDOM(dom: DOMNode, offset: number, bias: number): number;
444
- ignoreMutation(mutation: MutationRecord): boolean;
456
+ ignoreMutation(mutation: ViewMutationRecord): boolean;
445
457
  slice(from: number, to: number, view: EditorView): TextViewDesc;
446
458
  markDirty(from: number, to: number): void;
447
459
  get domAtom(): boolean;
@@ -955,4 +967,4 @@ interface DirectEditorProps extends EditorProps {
955
967
  dispatchTransaction?: (tr: Transaction) => void;
956
968
  }
957
969
 
958
- export { type DOMEventMap, Decoration, type DecorationAttrs, DecorationSet, type DecorationSource, type DirectEditorProps, type EditorProps, EditorView, type MarkView, type MarkViewConstructor, type NodeView, type NodeViewConstructor };
970
+ export { type DOMEventMap, Decoration, type DecorationAttrs, DecorationSet, type DecorationSource, type DirectEditorProps, type EditorProps, EditorView, type MarkView, type MarkViewConstructor, type NodeView, type NodeViewConstructor, type ViewMutationRecord };
package/dist/index.js CHANGED
@@ -396,11 +396,12 @@ function posFromCaret(view, node, offset, coords) {
396
396
  for (let cur = node, sawBlock = false;;) {
397
397
  if (cur == view.dom)
398
398
  break;
399
- let desc = view.docView.nearestDesc(cur, true);
399
+ let desc = view.docView.nearestDesc(cur, true), rect;
400
400
  if (!desc)
401
401
  return null;
402
- if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent || !desc.contentDOM)) {
403
- let rect = desc.dom.getBoundingClientRect();
402
+ if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent || !desc.contentDOM) &&
403
+ // Ignore elements with zero-size bounding rectangles
404
+ ((rect = desc.dom.getBoundingClientRect()).width || rect.height)) {
404
405
  if (desc.node.isBlock && desc.parent) {
405
406
  // Only apply the horizontal test to the innermost block. Vertical for any parent.
406
407
  if (!sawBlock && rect.left > coords.left || rect.top > coords.top)
@@ -996,18 +997,19 @@ class ViewDesc {
996
997
  // custom things with the selection. Note that this falls apart when
997
998
  // a selection starts in such a node and ends in another, in which
998
999
  // case we just use whatever domFromPos produces as a best effort.
999
- setSelection(anchor, head, root, force = false) {
1000
+ setSelection(anchor, head, view, force = false) {
1000
1001
  // If the selection falls entirely in a child, give it to that child
1001
1002
  let from = Math.min(anchor, head), to = Math.max(anchor, head);
1002
1003
  for (let i = 0, offset = 0; i < this.children.length; i++) {
1003
1004
  let child = this.children[i], end = offset + child.size;
1004
1005
  if (from > offset && to < end)
1005
- return child.setSelection(anchor - offset - child.border, head - offset - child.border, root, force);
1006
+ return child.setSelection(anchor - offset - child.border, head - offset - child.border, view, force);
1006
1007
  offset = end;
1007
1008
  }
1008
1009
  let anchorDOM = this.domFromPos(anchor, anchor ? -1 : 1);
1009
1010
  let headDOM = head == anchor ? anchorDOM : this.domFromPos(head, head ? -1 : 1);
1010
- let domSel = root.getSelection();
1011
+ let domSel = view.root.getSelection();
1012
+ let selRange = view.domSelectionRange();
1011
1013
  let brKludge = false;
1012
1014
  // On Firefox, using Selection.collapse to put the cursor after a
1013
1015
  // BR node for some reason doesn't always work (#1073). On Safari,
@@ -1038,14 +1040,14 @@ class ViewDesc {
1038
1040
  }
1039
1041
  // Firefox can act strangely when the selection is in front of an
1040
1042
  // uneditable node. See #1163 and https://bugzilla.mozilla.org/show_bug.cgi?id=1709536
1041
- if (gecko && domSel.focusNode && domSel.focusNode != headDOM.node && domSel.focusNode.nodeType == 1) {
1042
- let after = domSel.focusNode.childNodes[domSel.focusOffset];
1043
+ if (gecko && selRange.focusNode && selRange.focusNode != headDOM.node && selRange.focusNode.nodeType == 1) {
1044
+ let after = selRange.focusNode.childNodes[selRange.focusOffset];
1043
1045
  if (after && after.contentEditable == "false")
1044
1046
  force = true;
1045
1047
  }
1046
1048
  if (!(force || brKludge && safari) &&
1047
- isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset) &&
1048
- isEquivalentPosition(headDOM.node, headDOM.offset, domSel.focusNode, domSel.focusOffset))
1049
+ isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) &&
1050
+ isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset))
1049
1051
  return;
1050
1052
  // Selection.extend can be used to create an 'inverted' selection
1051
1053
  // (one where the focus is before the anchor), but not all
@@ -1234,6 +1236,9 @@ class MarkViewDesc extends ViewDesc {
1234
1236
  copy.children = nodes;
1235
1237
  return copy;
1236
1238
  }
1239
+ ignoreMutation(mutation) {
1240
+ return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : super.ignoreMutation(mutation);
1241
+ }
1237
1242
  destroy() {
1238
1243
  if (this.spec.destroy)
1239
1244
  this.spec.destroy();
@@ -1577,9 +1582,9 @@ class CustomNodeViewDesc extends NodeViewDesc {
1577
1582
  deselectNode() {
1578
1583
  this.spec.deselectNode ? this.spec.deselectNode() : super.deselectNode();
1579
1584
  }
1580
- setSelection(anchor, head, root, force) {
1581
- this.spec.setSelection ? this.spec.setSelection(anchor, head, root)
1582
- : super.setSelection(anchor, head, root, force);
1585
+ setSelection(anchor, head, view, force) {
1586
+ this.spec.setSelection ? this.spec.setSelection(anchor, head, view.root)
1587
+ : super.setSelection(anchor, head, view, force);
1583
1588
  }
1584
1589
  destroy() {
1585
1590
  if (this.spec.destroy)
@@ -2239,7 +2244,7 @@ function selectionToDOM(view, force = false) {
2239
2244
  if (!sel.empty && !sel.$from.parent.inlineContent)
2240
2245
  resetEditableTo = temporarilyEditableNear(view, sel.to);
2241
2246
  }
2242
- view.docView.setSelection(anchor, head, view.root, force);
2247
+ view.docView.setSelection(anchor, head, view, force);
2243
2248
  if (brokenSelectBetweenUneditable) {
2244
2249
  if (resetEditableFrom)
2245
2250
  resetEditable(resetEditableFrom);
@@ -3038,7 +3043,7 @@ class InputState {
3038
3043
  this.lastIOSEnterFallbackTimeout = -1;
3039
3044
  this.lastFocus = 0;
3040
3045
  this.lastTouch = 0;
3041
- this.lastAndroidDelete = 0;
3046
+ this.lastChromeDelete = 0;
3042
3047
  this.composing = false;
3043
3048
  this.compositionNode = null;
3044
3049
  this.composingTimeout = -1;
@@ -5016,11 +5021,11 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
5016
5021
  view.domObserver.suppressSelectionUpdates(); // #820
5017
5022
  return;
5018
5023
  }
5019
- // Chrome Android will occasionally, during composition, delete the
5024
+ // Chrome will occasionally, during composition, delete the
5020
5025
  // entire composition and then immediately insert it again. This is
5021
5026
  // used to detect that situation.
5022
- if (chrome && android && change.endB == change.start)
5023
- view.input.lastAndroidDelete = Date.now();
5027
+ if (chrome && change.endB == change.start)
5028
+ view.input.lastChromeDelete = Date.now();
5024
5029
  // This tries to detect Android virtual keyboard
5025
5030
  // enter-and-pick-suggestion action. That sometimes (see issue
5026
5031
  // #1059) first fires a DOM mutation, before moving the selection to
@@ -5071,13 +5076,13 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
5071
5076
  tr = view.state.tr.replace(chFrom, chTo, parse.doc.slice(change.start - parse.from, change.endB - parse.from));
5072
5077
  if (parse.sel) {
5073
5078
  let sel = resolveSelection(view, tr.doc, parse.sel);
5074
- // Chrome Android will sometimes, during composition, report the
5079
+ // Chrome will sometimes, during composition, report the
5075
5080
  // selection in the wrong place. If it looks like that is
5076
5081
  // happening, don't update the selection.
5077
5082
  // Edge just doesn't move the cursor forward when you start typing
5078
5083
  // in an empty block or between br nodes.
5079
- if (sel && !(chrome && android && view.composing && sel.empty &&
5080
- (change.start != change.endB || view.input.lastAndroidDelete < Date.now() - 100) &&
5084
+ if (sel && !(chrome && view.composing && sel.empty &&
5085
+ (change.start != change.endB || view.input.lastChromeDelete < Date.now() - 100) &&
5081
5086
  (sel.head == chFrom || sel.head == tr.mapping.map(chTo) - 1) ||
5082
5087
  ie && sel.empty && sel.head == chFrom))
5083
5088
  tr.setSelection(sel);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.36.0",
3
+ "version": "1.37.1",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/README.md CHANGED
@@ -20,6 +20,8 @@ this module.
20
20
 
21
21
  @MarkView
22
22
 
23
+ @ViewMutationRecord
24
+
23
25
  @DOMEventMap
24
26
 
25
27
  ### Decorations
package/src/domchange.ts CHANGED
@@ -196,11 +196,11 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
196
196
  return
197
197
  }
198
198
 
199
- // Chrome Android will occasionally, during composition, delete the
199
+ // Chrome will occasionally, during composition, delete the
200
200
  // entire composition and then immediately insert it again. This is
201
201
  // used to detect that situation.
202
- if (browser.chrome && browser.android && change.endB == change.start)
203
- view.input.lastAndroidDelete = Date.now()
202
+ if (browser.chrome && change.endB == change.start)
203
+ view.input.lastChromeDelete = Date.now()
204
204
 
205
205
  // This tries to detect Android virtual keyboard
206
206
  // enter-and-pick-suggestion action. That sometimes (see issue
@@ -252,13 +252,13 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
252
252
  tr = view.state.tr.replace(chFrom, chTo, parse.doc.slice(change.start - parse.from, change.endB - parse.from))
253
253
  if (parse.sel) {
254
254
  let sel = resolveSelection(view, tr.doc, parse.sel)
255
- // Chrome Android will sometimes, during composition, report the
255
+ // Chrome will sometimes, during composition, report the
256
256
  // selection in the wrong place. If it looks like that is
257
257
  // happening, don't update the selection.
258
258
  // Edge just doesn't move the cursor forward when you start typing
259
259
  // in an empty block or between br nodes.
260
- if (sel && !(browser.chrome && browser.android && view.composing && sel.empty &&
261
- (change.start != change.endB || view.input.lastAndroidDelete < Date.now() - 100) &&
260
+ if (sel && !(browser.chrome && view.composing && sel.empty &&
261
+ (change.start != change.endB || view.input.lastChromeDelete < Date.now() - 100) &&
262
262
  (sel.head == chFrom || sel.head == tr.mapping.map(chTo) - 1) ||
263
263
  browser.ie && sel.empty && sel.head == chFrom))
264
264
  tr.setSelection(sel)
package/src/domcoords.ts CHANGED
@@ -225,10 +225,11 @@ function posFromCaret(view: EditorView, node: Node, offset: number, coords: {top
225
225
  let outsideBlock = -1
226
226
  for (let cur = node, sawBlock = false;;) {
227
227
  if (cur == view.dom) break
228
- let desc = view.docView.nearestDesc(cur, true)
228
+ let desc = view.docView.nearestDesc(cur, true), rect
229
229
  if (!desc) return null
230
- if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent || !desc.contentDOM)) {
231
- let rect = (desc.dom as HTMLElement).getBoundingClientRect()
230
+ if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent || !desc.contentDOM) &&
231
+ // Ignore elements with zero-size bounding rectangles
232
+ ((rect = (desc.dom as HTMLElement).getBoundingClientRect()).width || rect.height)) {
232
233
  if (desc.node.isBlock && desc.parent) {
233
234
  // Only apply the horizontal test to the innermost block. Vertical for any parent.
234
235
  if (!sawBlock && rect.left > coords.left || rect.top > coords.top) outsideBlock = desc.posBefore
@@ -151,8 +151,8 @@ export class DOMObserver {
151
151
  let desc = container && this.view.docView.nearestDesc(container)
152
152
  if (desc && desc.ignoreMutation({
153
153
  type: "selection",
154
- target: container!.nodeType == 3 ? container!.parentNode : container
155
- } as any)) {
154
+ target: container!.nodeType == 3 ? container!.parentNode! : container!
155
+ })) {
156
156
  this.setCurSelection()
157
157
  return true
158
158
  }
package/src/index.ts CHANGED
@@ -14,7 +14,7 @@ import {DOMSelection, DOMNode, DOMSelectionRange, deepActiveElement, clearReused
14
14
  import * as browser from "./browser"
15
15
 
16
16
  export {Decoration, DecorationSet, DecorationAttrs, DecorationSource} from "./decoration"
17
- export {NodeView, MarkView} from "./viewdesc"
17
+ export {NodeView, MarkView, ViewMutationRecord} from "./viewdesc"
18
18
 
19
19
  // Exported for testing
20
20
  import {serializeForClipboard, parseFromClipboard} from "./clipboard"
package/src/input.ts CHANGED
@@ -28,7 +28,7 @@ export class InputState {
28
28
  lastIOSEnterFallbackTimeout = -1
29
29
  lastFocus = 0
30
30
  lastTouch = 0
31
- lastAndroidDelete = 0
31
+ lastChromeDelete = 0
32
32
  composing = false
33
33
  compositionNode: Text | null = null
34
34
  composingTimeout = -1
package/src/selection.ts CHANGED
@@ -84,7 +84,7 @@ export function selectionToDOM(view: EditorView, force = false) {
84
84
  if (!sel.empty && !sel.$from.parent.inlineContent)
85
85
  resetEditableTo = temporarilyEditableNear(view, sel.to)
86
86
  }
87
- view.docView.setSelection(anchor, head, view.root, force)
87
+ view.docView.setSelection(anchor, head, view, force)
88
88
  if (brokenSelectBetweenUneditable) {
89
89
  if (resetEditableFrom) resetEditable(resetEditableFrom)
90
90
  if (resetEditableTo) resetEditable(resetEditableTo)
package/src/viewdesc.ts CHANGED
@@ -10,6 +10,13 @@ declare global {
10
10
  interface Node { pmViewDesc?: ViewDesc }
11
11
  }
12
12
 
13
+ /// A ViewMutationRecord represents a DOM
14
+ /// [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
15
+ /// or a selection change happens within the view. When the change is
16
+ /// a selection change, the record will have a `type` property of
17
+ /// `"selection"` (which doesn't occur for native mutation records).
18
+ export type ViewMutationRecord = MutationRecord | { type: "selection", target: DOMNode }
19
+
13
20
  /// By default, document nodes are rendered using the result of the
14
21
  /// [`toDOM`](#model.NodeSpec.toDOM) method of their spec, and managed
15
22
  /// entirely by the editor. For some use cases, such as embedded
@@ -70,15 +77,10 @@ export interface NodeView {
70
77
  /// which this returns true are not handled by the editor.
71
78
  stopEvent?: (event: Event) => boolean
72
79
 
73
- /// Called when a DOM
74
- /// [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
75
- /// or a selection change happens within the view. When the change is
76
- /// a selection change, the record will have a `type` property of
77
- /// `"selection"` (which doesn't occur for native mutation records).
78
- /// Return false if the editor should re-read the selection or
79
- /// re-parse the range around the mutation, true if it can safely be
80
- /// ignored.
81
- ignoreMutation?: (mutation: MutationRecord) => boolean
80
+ /// Called when a [mutation](#view.ViewMutationRecord) happens within the
81
+ /// view. Return false if the editor should re-read the selection or re-parse
82
+ /// the range around the mutation, true if it can safely be ignored.
83
+ ignoreMutation?: (mutation: ViewMutationRecord) => boolean
82
84
 
83
85
  /// Called when the node view is removed from the editor or the whole
84
86
  /// editor is destroyed.
@@ -99,6 +101,12 @@ export interface MarkView {
99
101
  /// The DOM node that should hold the mark's content. When this is not
100
102
  /// present, the `dom` property is used as the content DOM.
101
103
  contentDOM?: HTMLElement | null
104
+
105
+ /// Called when a [mutation](#view.ViewMutationRecord) happens within the
106
+ /// view. Return false if the editor should re-read the selection or re-parse
107
+ /// the range around the mutation, true if it can safely be ignored.
108
+ ignoreMutation?: (mutation: ViewMutationRecord) => boolean
109
+
102
110
 
103
111
  /// Called when the mark view is removed from the editor or the whole
104
112
  /// editor is destroyed.
@@ -387,19 +395,20 @@ export class ViewDesc {
387
395
  // custom things with the selection. Note that this falls apart when
388
396
  // a selection starts in such a node and ends in another, in which
389
397
  // case we just use whatever domFromPos produces as a best effort.
390
- setSelection(anchor: number, head: number, root: Document | ShadowRoot, force = false): void {
398
+ setSelection(anchor: number, head: number, view: EditorView, force = false): void {
391
399
  // If the selection falls entirely in a child, give it to that child
392
400
  let from = Math.min(anchor, head), to = Math.max(anchor, head)
393
401
  for (let i = 0, offset = 0; i < this.children.length; i++) {
394
402
  let child = this.children[i], end = offset + child.size
395
403
  if (from > offset && to < end)
396
- return child.setSelection(anchor - offset - child.border, head - offset - child.border, root, force)
404
+ return child.setSelection(anchor - offset - child.border, head - offset - child.border, view, force)
397
405
  offset = end
398
406
  }
399
407
 
400
408
  let anchorDOM = this.domFromPos(anchor, anchor ? -1 : 1)
401
409
  let headDOM = head == anchor ? anchorDOM : this.domFromPos(head, head ? -1 : 1)
402
- let domSel = (root as Document).getSelection()!
410
+ let domSel = (view.root as Document).getSelection()!
411
+ let selRange = view.domSelectionRange()
403
412
 
404
413
  let brKludge = false
405
414
  // On Firefox, using Selection.collapse to put the cursor after a
@@ -429,14 +438,14 @@ export class ViewDesc {
429
438
  }
430
439
  // Firefox can act strangely when the selection is in front of an
431
440
  // uneditable node. See #1163 and https://bugzilla.mozilla.org/show_bug.cgi?id=1709536
432
- if (browser.gecko && domSel.focusNode && domSel.focusNode != headDOM.node && domSel.focusNode.nodeType == 1) {
433
- let after = domSel.focusNode.childNodes[domSel.focusOffset]
441
+ if (browser.gecko && selRange.focusNode && selRange.focusNode != headDOM.node && selRange.focusNode.nodeType == 1) {
442
+ let after = selRange.focusNode.childNodes[selRange.focusOffset]
434
443
  if (after && (after as HTMLElement).contentEditable == "false") force = true
435
444
  }
436
445
 
437
446
  if (!(force || brKludge && browser.safari) &&
438
- isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode!, domSel.anchorOffset) &&
439
- isEquivalentPosition(headDOM.node, headDOM.offset, domSel.focusNode!, domSel.focusOffset))
447
+ isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode!, selRange.anchorOffset) &&
448
+ isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode!, selRange.focusOffset))
440
449
  return
441
450
 
442
451
  // Selection.extend can be used to create an 'inverted' selection
@@ -468,8 +477,8 @@ export class ViewDesc {
468
477
  }
469
478
  }
470
479
 
471
- ignoreMutation(mutation: MutationRecord): boolean {
472
- return !this.contentDOM && (mutation.type as any) != "selection"
480
+ ignoreMutation(mutation: ViewMutationRecord): boolean {
481
+ return !this.contentDOM && mutation.type != "selection"
473
482
  }
474
483
 
475
484
  get contentLost() {
@@ -548,8 +557,8 @@ class WidgetViewDesc extends ViewDesc {
548
557
  return stop ? stop(event) : false
549
558
  }
550
559
 
551
- ignoreMutation(mutation: MutationRecord) {
552
- return (mutation.type as any) != "selection" || this.widget.spec.ignoreSelection
560
+ ignoreMutation(mutation: ViewMutationRecord) {
561
+ return mutation.type != "selection" || this.widget.spec.ignoreSelection
553
562
  }
554
563
 
555
564
  destroy() {
@@ -578,7 +587,7 @@ class CompositionViewDesc extends ViewDesc {
578
587
  return {node: this.textDOM, offset: pos}
579
588
  }
580
589
 
581
- ignoreMutation(mut: MutationRecord) {
590
+ ignoreMutation(mut: ViewMutationRecord) {
582
591
  return mut.type === 'characterData' && mut.target.nodeValue == mut.oldValue
583
592
  }
584
593
  }
@@ -629,6 +638,10 @@ class MarkViewDesc extends ViewDesc {
629
638
  return copy
630
639
  }
631
640
 
641
+ ignoreMutation(mutation: ViewMutationRecord) {
642
+ return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : super.ignoreMutation(mutation)
643
+ }
644
+
632
645
  destroy() {
633
646
  if (this.spec.destroy) this.spec.destroy()
634
647
  super.destroy()
@@ -924,8 +937,8 @@ class TextViewDesc extends NodeViewDesc {
924
937
  return super.localPosFromDOM(dom, offset, bias)
925
938
  }
926
939
 
927
- ignoreMutation(mutation: MutationRecord) {
928
- return mutation.type != "characterData" && (mutation.type as any) != "selection"
940
+ ignoreMutation(mutation: ViewMutationRecord) {
941
+ return mutation.type != "characterData" && mutation.type != "selection"
929
942
  }
930
943
 
931
944
  slice(from: number, to: number, view: EditorView) {
@@ -987,9 +1000,9 @@ class CustomNodeViewDesc extends NodeViewDesc {
987
1000
  this.spec.deselectNode ? this.spec.deselectNode() : super.deselectNode()
988
1001
  }
989
1002
 
990
- setSelection(anchor: number, head: number, root: Document | ShadowRoot, force: boolean) {
991
- this.spec.setSelection ? this.spec.setSelection(anchor, head, root)
992
- : super.setSelection(anchor, head, root, force)
1003
+ setSelection(anchor: number, head: number, view: EditorView, force: boolean) {
1004
+ this.spec.setSelection ? this.spec.setSelection(anchor, head, view.root)
1005
+ : super.setSelection(anchor, head, view, force)
993
1006
  }
994
1007
 
995
1008
  destroy() {
@@ -1001,7 +1014,7 @@ class CustomNodeViewDesc extends NodeViewDesc {
1001
1014
  return this.spec.stopEvent ? this.spec.stopEvent(event) : false
1002
1015
  }
1003
1016
 
1004
- ignoreMutation(mutation: MutationRecord) {
1017
+ ignoreMutation(mutation: ViewMutationRecord) {
1005
1018
  return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : super.ignoreMutation(mutation)
1006
1019
  }
1007
1020
  }