prosemirror-view 1.28.1 → 1.28.3

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,15 @@
1
+ ## 1.28.3 (2022-10-05)
2
+
3
+ ### Bug fixes
4
+
5
+ Work around the five-year-old Safari bug where it won't accurately report the selection inside shadow roots, which would break ProseMirror when put in shadow DOM.
6
+
7
+ ## 1.28.2 (2022-09-24)
8
+
9
+ ### Bug fixes
10
+
11
+ Avoid a crash in selection updates in a hidden editor on Safari.
12
+
1
13
  ## 1.28.1 (2022-09-21)
2
14
 
3
15
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -36,28 +36,6 @@ var prosemirrorModel = require('prosemirror-model');
36
36
 
37
37
  var prosemirrorTransform = require('prosemirror-transform');
38
38
 
39
- var nav = typeof navigator != "undefined" ? navigator : null;
40
- var doc = typeof document != "undefined" ? document : null;
41
- var agent = nav && nav.userAgent || "";
42
- var ie_edge = /Edge\/(\d+)/.exec(agent);
43
- var ie_upto10 = /MSIE \d/.exec(agent);
44
- var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(agent);
45
- var ie = !!(ie_upto10 || ie_11up || ie_edge);
46
- var ie_version = ie_upto10 ? document.documentMode : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0;
47
- var gecko = !ie && /gecko\/(\d+)/i.test(agent);
48
- gecko && +(/Firefox\/(\d+)/.exec(agent) || [0, 0])[1];
49
-
50
- var _chrome = !ie && /Chrome\/(\d+)/.exec(agent);
51
-
52
- var chrome = !!_chrome;
53
- var chrome_version = _chrome ? +_chrome[1] : 0;
54
- var safari = !ie && !!nav && /Apple Computer/.test(nav.vendor);
55
- var ios = safari && (/Mobile\/\w+/.test(agent) || !!nav && nav.maxTouchPoints > 2);
56
- var mac = ios || (nav ? /Mac/.test(nav.platform) : false);
57
- var android = /Android \d/.test(agent);
58
- var webkit = !!doc && "webkitFontSmoothing" in doc.documentElement.style;
59
- var webkit_version = webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] : 0;
60
-
61
39
  var domIndex = function domIndex(node) {
62
40
  for (var index = 0;; index++) {
63
41
  node = node.previousSibling;
@@ -130,9 +108,7 @@ function hasBlockDesc(dom) {
130
108
  }
131
109
 
132
110
  var selectionCollapsed = function selectionCollapsed(domSel) {
133
- var collapsed = domSel.isCollapsed;
134
- if (collapsed && chrome && domSel.rangeCount && !domSel.getRangeAt(0).collapsed) collapsed = false;
135
- return collapsed;
111
+ return domSel.focusNode && isEquivalentPosition(domSel.focusNode, domSel.focusOffset, domSel.anchorNode, domSel.anchorOffset);
136
112
  };
137
113
 
138
114
  function keyEvent(keyCode, key) {
@@ -143,6 +119,38 @@ function keyEvent(keyCode, key) {
143
119
  return event;
144
120
  }
145
121
 
122
+ function deepActiveElement(doc) {
123
+ var elt = doc.activeElement;
124
+
125
+ while (elt && elt.shadowRoot) {
126
+ elt = elt.shadowRoot.activeElement;
127
+ }
128
+
129
+ return elt;
130
+ }
131
+
132
+ var nav = typeof navigator != "undefined" ? navigator : null;
133
+ var doc = typeof document != "undefined" ? document : null;
134
+ var agent = nav && nav.userAgent || "";
135
+ var ie_edge = /Edge\/(\d+)/.exec(agent);
136
+ var ie_upto10 = /MSIE \d/.exec(agent);
137
+ var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(agent);
138
+ var ie = !!(ie_upto10 || ie_11up || ie_edge);
139
+ var ie_version = ie_upto10 ? document.documentMode : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0;
140
+ var gecko = !ie && /gecko\/(\d+)/i.test(agent);
141
+ gecko && +(/Firefox\/(\d+)/.exec(agent) || [0, 0])[1];
142
+
143
+ var _chrome = !ie && /Chrome\/(\d+)/.exec(agent);
144
+
145
+ var chrome = !!_chrome;
146
+ var chrome_version = _chrome ? +_chrome[1] : 0;
147
+ var safari = !ie && !!nav && /Apple Computer/.test(nav.vendor);
148
+ var ios = safari && (/Mobile\/\w+/.test(agent) || !!nav && nav.maxTouchPoints > 2);
149
+ var mac = ios || (nav ? /Mac/.test(nav.platform) : false);
150
+ var android = /Android \d/.test(agent);
151
+ var webkit = !!doc && "webkitFontSmoothing" in doc.documentElement.style;
152
+ var webkit_version = webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] : 0;
153
+
146
154
  function windowRect(doc) {
147
155
  return {
148
156
  left: 0,
@@ -654,15 +662,27 @@ function endOfTextblockHorizontal(view, state, dir) {
654
662
  var sel = view.domSelection();
655
663
  if (!maybeRTL.test($head.parent.textContent) || !sel.modify) return dir == "left" || dir == "backward" ? atStart : atEnd;
656
664
  return withFlushedState(view, state, function () {
657
- var oldRange = sel.getRangeAt(0),
658
- oldNode = sel.focusNode,
659
- oldOff = sel.focusOffset;
665
+ var _view$domSelectionRan = view.domSelectionRange(),
666
+ oldNode = _view$domSelectionRan.focusNode,
667
+ oldOff = _view$domSelectionRan.focusOffset,
668
+ anchorNode = _view$domSelectionRan.anchorNode,
669
+ anchorOffset = _view$domSelectionRan.anchorOffset;
670
+
660
671
  var oldBidiLevel = sel.caretBidiLevel;
661
672
  sel.modify("move", dir, "character");
662
673
  var parentDOM = $head.depth ? view.docView.domAfterPos($head.before()) : view.dom;
663
- var result = !parentDOM.contains(sel.focusNode.nodeType == 1 ? sel.focusNode : sel.focusNode.parentNode) || oldNode == sel.focusNode && oldOff == sel.focusOffset;
664
- sel.removeAllRanges();
665
- sel.addRange(oldRange);
674
+
675
+ var _view$domSelectionRan2 = view.domSelectionRange(),
676
+ newNode = _view$domSelectionRan2.focusNode,
677
+ newOff = _view$domSelectionRan2.focusOffset;
678
+
679
+ var result = newNode && !parentDOM.contains(newNode.nodeType == 1 ? newNode : newNode.parentNode) || oldNode == newNode && oldOff == newOff;
680
+
681
+ try {
682
+ sel.collapse(anchorNode, anchorOffset);
683
+ if (oldNode && (oldNode != anchorNode || oldOff != anchorOffset) && sel.extend) sel.extend(oldNode, oldOff);
684
+ } catch (_) {}
685
+
666
686
  if (oldBidiLevel != null) sel.caretBidiLevel = oldBidiLevel;
667
687
  return result;
668
688
  });
@@ -1104,9 +1124,7 @@ var ViewDesc = function () {
1104
1124
  try {
1105
1125
  if (anchor != head) domSel.extend(headDOM.node, headDOM.offset);
1106
1126
  domSelExtended = true;
1107
- } catch (err) {
1108
- if (!(err instanceof DOMException)) throw err;
1109
- }
1127
+ } catch (_) {}
1110
1128
  }
1111
1129
 
1112
1130
  if (!domSelExtended) {
@@ -1491,7 +1509,7 @@ var NodeViewDesc = function (_ViewDesc4) {
1491
1509
  from = _view$state$selection.from,
1492
1510
  to = _view$state$selection.to;
1493
1511
  if (!(view.state.selection instanceof prosemirrorState.TextSelection) || from < pos || to > pos + this.node.content.size) return null;
1494
- var sel = view.domSelection();
1512
+ var sel = view.domSelectionRange();
1495
1513
  var textNode = nearbyTextNode(sel.focusNode, sel.focusOffset);
1496
1514
  if (!textNode || !this.dom.contains(textNode.parentNode)) return null;
1497
1515
 
@@ -2401,7 +2419,7 @@ function replaceNodes(nodes, from, to, view, replacement) {
2401
2419
 
2402
2420
  function selectionFromDOM(view) {
2403
2421
  var origin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
2404
- var domSel = view.domSelection(),
2422
+ var domSel = view.domSelectionRange(),
2405
2423
  doc = view.state.doc;
2406
2424
  if (!domSel.focusNode) return null;
2407
2425
  var nearestDesc = view.docView.nearestDesc(domSel.focusNode),
@@ -2450,7 +2468,7 @@ function selectionToDOM(view) {
2450
2468
  if (!editorOwnsSelection(view)) return;
2451
2469
 
2452
2470
  if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && chrome) {
2453
- var domSel = view.domSelection(),
2471
+ var domSel = view.domSelectionRange(),
2454
2472
  curSel = view.domObserver.currentSelection;
2455
2473
 
2456
2474
  if (domSel.anchorNode && curSel.anchorNode && isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, curSel.anchorNode, curSel.anchorOffset)) {
@@ -2533,7 +2551,7 @@ function resetEditable(element) {
2533
2551
  function removeClassOnSelectionChange(view) {
2534
2552
  var doc = view.dom.ownerDocument;
2535
2553
  doc.removeEventListener("selectionchange", view.input.hideSelectionGuard);
2536
- var domSel = view.domSelection();
2554
+ var domSel = view.domSelectionRange();
2537
2555
  var node = domSel.anchorNode,
2538
2556
  offset = domSel.anchorOffset;
2539
2557
  doc.addEventListener("selectionchange", view.input.hideSelectionGuard = function () {
@@ -2595,7 +2613,7 @@ function hasFocusAndSelection(view) {
2595
2613
  }
2596
2614
 
2597
2615
  function hasSelection(view) {
2598
- var sel = view.domSelection();
2616
+ var sel = view.domSelectionRange();
2599
2617
  if (!sel.anchorNode) return false;
2600
2618
 
2601
2619
  try {
@@ -2607,7 +2625,7 @@ function hasSelection(view) {
2607
2625
 
2608
2626
  function anchorInRightPlace(view) {
2609
2627
  var anchorDOM = view.docView.domFromPos(view.state.selection.anchor, 0);
2610
- var domSel = view.domSelection();
2628
+ var domSel = view.domSelectionRange();
2611
2629
  return isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset);
2612
2630
  }
2613
2631
 
@@ -2671,7 +2689,7 @@ function isIgnorable(dom) {
2671
2689
  }
2672
2690
 
2673
2691
  function skipIgnoredNodesLeft(view) {
2674
- var sel = view.domSelection();
2692
+ var sel = view.domSelectionRange();
2675
2693
  var node = sel.focusNode,
2676
2694
  offset = sel.focusOffset;
2677
2695
  if (!node) return;
@@ -2717,11 +2735,11 @@ function skipIgnoredNodesLeft(view) {
2717
2735
  }
2718
2736
  }
2719
2737
 
2720
- if (force) setSelFocus(view, sel, node, offset);else if (moveNode) setSelFocus(view, sel, moveNode, moveOffset);
2738
+ if (force) setSelFocus(view, node, offset);else if (moveNode) setSelFocus(view, moveNode, moveOffset);
2721
2739
  }
2722
2740
 
2723
2741
  function skipIgnoredNodesRight(view) {
2724
- var sel = view.domSelection();
2742
+ var sel = view.domSelectionRange();
2725
2743
  var node = sel.focusNode,
2726
2744
  offset = sel.focusOffset;
2727
2745
  if (!node) return;
@@ -2760,7 +2778,7 @@ function skipIgnoredNodesRight(view) {
2760
2778
  }
2761
2779
  }
2762
2780
 
2763
- if (moveNode) setSelFocus(view, sel, moveNode, moveOffset);
2781
+ if (moveNode) setSelFocus(view, moveNode, moveOffset);
2764
2782
  }
2765
2783
 
2766
2784
  function isBlockNode(dom) {
@@ -2768,7 +2786,9 @@ function isBlockNode(dom) {
2768
2786
  return desc && desc.node && desc.node.isBlock;
2769
2787
  }
2770
2788
 
2771
- function setSelFocus(view, sel, node, offset) {
2789
+ function setSelFocus(view, node, offset) {
2790
+ var sel = view.domSelection();
2791
+
2772
2792
  if (selectionCollapsed(sel)) {
2773
2793
  var range = document.createRange();
2774
2794
  range.setEnd(node, offset);
@@ -2837,9 +2857,9 @@ function switchEditable(view, node, state) {
2837
2857
  function safariDownArrowBug(view) {
2838
2858
  if (!safari || view.state.selection.$head.parentOffset > 0) return false;
2839
2859
 
2840
- var _view$domSelection = view.domSelection(),
2841
- focusNode = _view$domSelection.focusNode,
2842
- focusOffset = _view$domSelection.focusOffset;
2860
+ var _view$domSelectionRan3 = view.domSelectionRange(),
2861
+ focusNode = _view$domSelectionRan3.focusNode,
2862
+ focusOffset = _view$domSelectionRan3.focusOffset;
2843
2863
 
2844
2864
  if (focusNode && focusNode.nodeType == 1 && focusOffset == 0 && focusNode.firstChild && focusNode.firstChild.contentEditable == "false") {
2845
2865
  var child = focusNode.firstChild;
@@ -3634,14 +3654,14 @@ editHandlers.compositionstart = editHandlers.compositionupdate = function (view)
3634
3654
  endComposition(view);
3635
3655
 
3636
3656
  if (gecko && state.selection.empty && $pos.parentOffset && !$pos.textOffset && $pos.nodeBefore.marks.length) {
3637
- var sel = view.domSelection();
3657
+ var sel = view.domSelectionRange();
3638
3658
 
3639
3659
  for (var node = sel.focusNode, offset = sel.focusOffset; node && node.nodeType == 1 && offset != 0;) {
3640
3660
  var before = offset < 0 ? node.lastChild : node.childNodes[offset - 1];
3641
3661
  if (!before) break;
3642
3662
 
3643
3663
  if (before.nodeType == 3) {
3644
- sel.collapse(before, before.nodeValue.length);
3664
+ view.domSelection().collapse(before, before.nodeValue.length);
3645
3665
  break;
3646
3666
  } else {
3647
3667
  node = before;
@@ -3893,7 +3913,7 @@ handlers.focus = function (view) {
3893
3913
  view.domObserver.start();
3894
3914
  view.focused = true;
3895
3915
  setTimeout(function () {
3896
- if (view.docView && view.hasFocus() && !view.domObserver.currentSelection.eq(view.domSelection())) selectionToDOM(view);
3916
+ if (view.docView && view.hasFocus() && !view.domObserver.currentSelection.eq(view.domSelectionRange())) selectionToDOM(view);
3897
3917
  }, 20);
3898
3918
  }
3899
3919
  };
@@ -4821,7 +4841,7 @@ var DOMObserver = function () {
4821
4841
  if (this.suppressingSelectionUpdates) return selectionToDOM(this.view);
4822
4842
 
4823
4843
  if (ie && ie_version <= 11 && !this.view.state.selection.empty) {
4824
- var sel = this.view.domSelection();
4844
+ var sel = this.view.domSelectionRange();
4825
4845
  if (sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset)) return this.flushSoon();
4826
4846
  }
4827
4847
 
@@ -4830,14 +4850,27 @@ var DOMObserver = function () {
4830
4850
  }, {
4831
4851
  key: "setCurSelection",
4832
4852
  value: function setCurSelection() {
4833
- this.currentSelection.set(this.view.domSelection());
4853
+ this.currentSelection.set(this.view.domSelectionRange());
4834
4854
  }
4835
4855
  }, {
4836
4856
  key: "ignoreSelectionChange",
4837
4857
  value: function ignoreSelectionChange(sel) {
4838
- if (sel.rangeCount == 0) return true;
4839
- var container = sel.getRangeAt(0).commonAncestorContainer;
4840
- var desc = this.view.docView.nearestDesc(container);
4858
+ if (!sel.focusNode) return true;
4859
+ var ancestors = new Set(),
4860
+ container;
4861
+
4862
+ for (var scan = sel.focusNode; scan; scan = parentNode(scan)) {
4863
+ ancestors.add(scan);
4864
+ }
4865
+
4866
+ for (var _scan = sel.anchorNode; _scan; _scan = parentNode(_scan)) {
4867
+ if (ancestors.has(_scan)) {
4868
+ container = _scan;
4869
+ break;
4870
+ }
4871
+ }
4872
+
4873
+ var desc = container && this.view.docView.nearestDesc(container);
4841
4874
 
4842
4875
  if (desc && desc.ignoreMutation({
4843
4876
  type: "selection",
@@ -4859,7 +4892,7 @@ var DOMObserver = function () {
4859
4892
  this.queue.length = 0;
4860
4893
  }
4861
4894
 
4862
- var sel = view.domSelection();
4895
+ var sel = view.domSelectionRange();
4863
4896
  var newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel);
4864
4897
  var from = -1,
4865
4898
  to = -1,
@@ -4979,6 +5012,40 @@ function checkCSS(view) {
4979
5012
  }
4980
5013
  }
4981
5014
 
5015
+ function safariShadowSelectionRange(view) {
5016
+ var found;
5017
+
5018
+ function read(event) {
5019
+ event.preventDefault();
5020
+ event.stopImmediatePropagation();
5021
+ found = event.getTargetRanges()[0];
5022
+ }
5023
+
5024
+ view.dom.addEventListener("beforeinput", read, true);
5025
+ document.execCommand("indent");
5026
+ view.dom.removeEventListener("beforeinput", read, true);
5027
+ var anchorNode = found.startContainer,
5028
+ anchorOffset = found.startOffset;
5029
+ var focusNode = found.endContainer,
5030
+ focusOffset = found.endOffset;
5031
+ var currentAnchor = view.domAtPos(view.state.selection.anchor);
5032
+
5033
+ if (isEquivalentPosition(currentAnchor.node, currentAnchor.offset, focusNode, focusOffset)) {
5034
+ var _ref3 = [focusNode, focusOffset, anchorNode, anchorOffset];
5035
+ anchorNode = _ref3[0];
5036
+ anchorOffset = _ref3[1];
5037
+ focusNode = _ref3[2];
5038
+ focusOffset = _ref3[3];
5039
+ }
5040
+
5041
+ return {
5042
+ anchorNode: anchorNode,
5043
+ anchorOffset: anchorOffset,
5044
+ focusNode: focusNode,
5045
+ focusOffset: focusOffset
5046
+ };
5047
+ }
5048
+
4982
5049
  function parseBetween(view, from_, to_) {
4983
5050
  var _view$docView$parseRa = view.docView.parseRange(from_, to_),
4984
5051
  parent = _view$docView$parseRa.node,
@@ -4987,7 +5054,7 @@ function parseBetween(view, from_, to_) {
4987
5054
  from = _view$docView$parseRa.from,
4988
5055
  to = _view$docView$parseRa.to;
4989
5056
 
4990
- var domSel = view.domSelection();
5057
+ var domSel = view.domSelectionRange();
4991
5058
  var find;
4992
5059
  var anchor = domSel.anchorNode;
4993
5060
 
@@ -5497,7 +5564,7 @@ var EditorView = function () {
5497
5564
  var forceSelUpdate = updateDoc && (ie || chrome) && !this.composing && !prev.selection.empty && !state.selection.empty && selectionContextChanged(prev.selection, state.selection);
5498
5565
 
5499
5566
  if (updateDoc) {
5500
- var chromeKludge = chrome ? this.trackWrites = this.domSelection().focusNode : null;
5567
+ var chromeKludge = chrome ? this.trackWrites = this.domSelectionRange().focusNode : null;
5501
5568
 
5502
5569
  if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) {
5503
5570
  this.docView.updateOuterDeco([]);
@@ -5508,7 +5575,7 @@ var EditorView = function () {
5508
5575
  if (chromeKludge && !this.trackWrites) forceSelUpdate = true;
5509
5576
  }
5510
5577
 
5511
- if (forceSelUpdate || !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelection()) && anchorInRightPlace(this))) {
5578
+ if (forceSelUpdate || !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) && anchorInRightPlace(this))) {
5512
5579
  selectionToDOM(this, forceSelUpdate);
5513
5580
  } else {
5514
5581
  syncNodeSelection(this, state.selection);
@@ -5533,7 +5600,7 @@ var EditorView = function () {
5533
5600
  value: function scrollToSelection() {
5534
5601
  var _this16 = this;
5535
5602
 
5536
- var startDOM = this.domSelection().focusNode;
5603
+ var startDOM = this.domSelectionRange().focusNode;
5537
5604
  if (this.someProp("handleScrollToSelection", function (f) {
5538
5605
  return f(_this16);
5539
5606
  })) ;else if (this.state.selection instanceof prosemirrorState.NodeSelection) {
@@ -5716,6 +5783,11 @@ var EditorView = function () {
5716
5783
  var dispatchTransaction = this._props.dispatchTransaction;
5717
5784
  if (dispatchTransaction) dispatchTransaction.call(this, tr);else this.updateState(this.state.apply(tr));
5718
5785
  }
5786
+ }, {
5787
+ key: "domSelectionRange",
5788
+ value: function domSelectionRange() {
5789
+ return safari && this.root.nodeType === 11 && deepActiveElement(this.dom.ownerDocument) == this.dom ? safariShadowSelectionRange(this) : this.domSelection();
5790
+ }
5719
5791
  }, {
5720
5792
  key: "domSelection",
5721
5793
  value: function domSelection() {
package/dist/index.js CHANGED
@@ -2,27 +2,6 @@ import { TextSelection, NodeSelection, Selection, AllSelection } from 'prosemirr
2
2
  import { DOMSerializer, Fragment, Mark, Slice, DOMParser } from 'prosemirror-model';
3
3
  import { dropPoint } from 'prosemirror-transform';
4
4
 
5
- const nav = typeof navigator != "undefined" ? navigator : null;
6
- const doc = typeof document != "undefined" ? document : null;
7
- const agent = (nav && nav.userAgent) || "";
8
- const ie_edge = /Edge\/(\d+)/.exec(agent);
9
- const ie_upto10 = /MSIE \d/.exec(agent);
10
- const ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(agent);
11
- const ie = !!(ie_upto10 || ie_11up || ie_edge);
12
- const ie_version = ie_upto10 ? document.documentMode : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0;
13
- const gecko = !ie && /gecko\/(\d+)/i.test(agent);
14
- gecko && +(/Firefox\/(\d+)/.exec(agent) || [0, 0])[1];
15
- const _chrome = !ie && /Chrome\/(\d+)/.exec(agent);
16
- const chrome = !!_chrome;
17
- const chrome_version = _chrome ? +_chrome[1] : 0;
18
- const safari = !ie && !!nav && /Apple Computer/.test(nav.vendor);
19
- // Is true for both iOS and iPadOS for convenience
20
- const ios = safari && (/Mobile\/\w+/.test(agent) || !!nav && nav.maxTouchPoints > 2);
21
- const mac = ios || (nav ? /Mac/.test(nav.platform) : false);
22
- const android = /Android \d/.test(agent);
23
- const webkit = !!doc && "webkitFontSmoothing" in doc.documentElement.style;
24
- const webkit_version = webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] : 0;
25
-
26
5
  const domIndex = function (node) {
27
6
  for (var index = 0;; index++) {
28
7
  node = node.previousSibling;
@@ -100,10 +79,7 @@ function hasBlockDesc(dom) {
100
79
  // Work around Chrome issue https://bugs.chromium.org/p/chromium/issues/detail?id=447523
101
80
  // (isCollapsed inappropriately returns true in shadow dom)
102
81
  const selectionCollapsed = function (domSel) {
103
- let collapsed = domSel.isCollapsed;
104
- if (collapsed && chrome && domSel.rangeCount && !domSel.getRangeAt(0).collapsed)
105
- collapsed = false;
106
- return collapsed;
82
+ return domSel.focusNode && isEquivalentPosition(domSel.focusNode, domSel.focusOffset, domSel.anchorNode, domSel.anchorOffset);
107
83
  };
108
84
  function keyEvent(keyCode, key) {
109
85
  let event = document.createEvent("Event");
@@ -112,6 +88,33 @@ function keyEvent(keyCode, key) {
112
88
  event.key = event.code = key;
113
89
  return event;
114
90
  }
91
+ function deepActiveElement(doc) {
92
+ let elt = doc.activeElement;
93
+ while (elt && elt.shadowRoot)
94
+ elt = elt.shadowRoot.activeElement;
95
+ return elt;
96
+ }
97
+
98
+ const nav = typeof navigator != "undefined" ? navigator : null;
99
+ const doc = typeof document != "undefined" ? document : null;
100
+ const agent = (nav && nav.userAgent) || "";
101
+ const ie_edge = /Edge\/(\d+)/.exec(agent);
102
+ const ie_upto10 = /MSIE \d/.exec(agent);
103
+ const ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(agent);
104
+ const ie = !!(ie_upto10 || ie_11up || ie_edge);
105
+ const ie_version = ie_upto10 ? document.documentMode : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0;
106
+ const gecko = !ie && /gecko\/(\d+)/i.test(agent);
107
+ gecko && +(/Firefox\/(\d+)/.exec(agent) || [0, 0])[1];
108
+ const _chrome = !ie && /Chrome\/(\d+)/.exec(agent);
109
+ const chrome = !!_chrome;
110
+ const chrome_version = _chrome ? +_chrome[1] : 0;
111
+ const safari = !ie && !!nav && /Apple Computer/.test(nav.vendor);
112
+ // Is true for both iOS and iPadOS for convenience
113
+ const ios = safari && (/Mobile\/\w+/.test(agent) || !!nav && nav.maxTouchPoints > 2);
114
+ const mac = ios || (nav ? /Mac/.test(nav.platform) : false);
115
+ const android = /Android \d/.test(agent);
116
+ const webkit = !!doc && "webkitFontSmoothing" in doc.documentElement.style;
117
+ const webkit_version = webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] : 0;
115
118
 
116
119
  function windowRect(doc) {
117
120
  return { left: 0, right: doc.documentElement.clientWidth,
@@ -580,16 +583,21 @@ function endOfTextblockHorizontal(view, state, dir) {
580
583
  // one character, and see if that moves the cursor out of the
581
584
  // textblock (or doesn't move it at all, when at the start/end of
582
585
  // the document).
583
- let oldRange = sel.getRangeAt(0), oldNode = sel.focusNode, oldOff = sel.focusOffset;
586
+ let { focusNode: oldNode, focusOffset: oldOff, anchorNode, anchorOffset } = view.domSelectionRange();
584
587
  let oldBidiLevel = sel.caretBidiLevel // Only for Firefox
585
588
  ;
586
589
  sel.modify("move", dir, "character");
587
590
  let parentDOM = $head.depth ? view.docView.domAfterPos($head.before()) : view.dom;
588
- let result = !parentDOM.contains(sel.focusNode.nodeType == 1 ? sel.focusNode : sel.focusNode.parentNode) ||
589
- (oldNode == sel.focusNode && oldOff == sel.focusOffset);
591
+ let { focusNode: newNode, focusOffset: newOff } = view.domSelectionRange();
592
+ let result = newNode && !parentDOM.contains(newNode.nodeType == 1 ? newNode : newNode.parentNode) ||
593
+ (oldNode == newNode && oldOff == newOff);
590
594
  // Restore the previous selection
591
- sel.removeAllRanges();
592
- sel.addRange(oldRange);
595
+ try {
596
+ sel.collapse(anchorNode, anchorOffset);
597
+ if (oldNode && (oldNode != anchorNode || oldOff != anchorOffset) && sel.extend)
598
+ sel.extend(oldNode, oldOff);
599
+ }
600
+ catch (_) { }
593
601
  if (oldBidiLevel != null)
594
602
  sel.caretBidiLevel = oldBidiLevel;
595
603
  return result;
@@ -961,14 +969,13 @@ class ViewDesc {
961
969
  domSel.extend(headDOM.node, headDOM.offset);
962
970
  domSelExtended = true;
963
971
  }
964
- catch (err) {
972
+ catch (_) {
965
973
  // In some cases with Chrome the selection is empty after calling
966
974
  // collapse, even when it should be valid. This appears to be a bug, but
967
975
  // it is difficult to isolate. If this happens fallback to the old path
968
976
  // without using extend.
969
- if (!(err instanceof DOMException))
970
- throw err;
971
- // declare global: DOMException
977
+ // Similarly, this could crash on Safari if the editor is hidden, and
978
+ // there was no selection.
972
979
  }
973
980
  }
974
981
  if (!domSelExtended) {
@@ -1289,7 +1296,7 @@ class NodeViewDesc extends ViewDesc {
1289
1296
  let { from, to } = view.state.selection;
1290
1297
  if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + this.node.content.size)
1291
1298
  return null;
1292
- let sel = view.domSelection();
1299
+ let sel = view.domSelectionRange();
1293
1300
  let textNode = nearbyTextNode(sel.focusNode, sel.focusOffset);
1294
1301
  if (!textNode || !this.dom.contains(textNode.parentNode))
1295
1302
  return null;
@@ -2025,7 +2032,7 @@ function replaceNodes(nodes, from, to, view, replacement) {
2025
2032
  }
2026
2033
 
2027
2034
  function selectionFromDOM(view, origin = null) {
2028
- let domSel = view.domSelection(), doc = view.state.doc;
2035
+ let domSel = view.domSelectionRange(), doc = view.state.doc;
2029
2036
  if (!domSel.focusNode)
2030
2037
  return null;
2031
2038
  let nearestDesc = view.docView.nearestDesc(domSel.focusNode), inWidget = nearestDesc && nearestDesc.size == 0;
@@ -2069,7 +2076,7 @@ function selectionToDOM(view, force = false) {
2069
2076
  // in Safari. And the drag selection delay is to workarond issues
2070
2077
  // which only present in Chrome.
2071
2078
  if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && chrome) {
2072
- let domSel = view.domSelection(), curSel = view.domObserver.currentSelection;
2079
+ let domSel = view.domSelectionRange(), curSel = view.domObserver.currentSelection;
2073
2080
  if (domSel.anchorNode && curSel.anchorNode &&
2074
2081
  isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, curSel.anchorNode, curSel.anchorOffset)) {
2075
2082
  view.input.mouseDown.delayedSelectionSync = true;
@@ -2144,7 +2151,7 @@ function resetEditable(element) {
2144
2151
  function removeClassOnSelectionChange(view) {
2145
2152
  let doc = view.dom.ownerDocument;
2146
2153
  doc.removeEventListener("selectionchange", view.input.hideSelectionGuard);
2147
- let domSel = view.domSelection();
2154
+ let domSel = view.domSelectionRange();
2148
2155
  let node = domSel.anchorNode, offset = domSel.anchorOffset;
2149
2156
  doc.addEventListener("selectionchange", view.input.hideSelectionGuard = () => {
2150
2157
  if (domSel.anchorNode != node || domSel.anchorOffset != offset) {
@@ -2208,7 +2215,7 @@ function hasFocusAndSelection(view) {
2208
2215
  return hasSelection(view);
2209
2216
  }
2210
2217
  function hasSelection(view) {
2211
- let sel = view.domSelection();
2218
+ let sel = view.domSelectionRange();
2212
2219
  if (!sel.anchorNode)
2213
2220
  return false;
2214
2221
  try {
@@ -2224,7 +2231,7 @@ function hasSelection(view) {
2224
2231
  }
2225
2232
  function anchorInRightPlace(view) {
2226
2233
  let anchorDOM = view.docView.domFromPos(view.state.selection.anchor, 0);
2227
- let domSel = view.domSelection();
2234
+ let domSel = view.domSelectionRange();
2228
2235
  return isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset);
2229
2236
  }
2230
2237
 
@@ -2291,7 +2298,7 @@ function isIgnorable(dom) {
2291
2298
  // Make sure the cursor isn't directly after one or more ignored
2292
2299
  // nodes, which will confuse the browser's cursor motion logic.
2293
2300
  function skipIgnoredNodesLeft(view) {
2294
- let sel = view.domSelection();
2301
+ let sel = view.domSelectionRange();
2295
2302
  let node = sel.focusNode, offset = sel.focusOffset;
2296
2303
  if (!node)
2297
2304
  return;
@@ -2343,14 +2350,14 @@ function skipIgnoredNodesLeft(view) {
2343
2350
  }
2344
2351
  }
2345
2352
  if (force)
2346
- setSelFocus(view, sel, node, offset);
2353
+ setSelFocus(view, node, offset);
2347
2354
  else if (moveNode)
2348
- setSelFocus(view, sel, moveNode, moveOffset);
2355
+ setSelFocus(view, moveNode, moveOffset);
2349
2356
  }
2350
2357
  // Make sure the cursor isn't directly before one or more ignored
2351
2358
  // nodes.
2352
2359
  function skipIgnoredNodesRight(view) {
2353
- let sel = view.domSelection();
2360
+ let sel = view.domSelectionRange();
2354
2361
  let node = sel.focusNode, offset = sel.focusOffset;
2355
2362
  if (!node)
2356
2363
  return;
@@ -2392,13 +2399,14 @@ function skipIgnoredNodesRight(view) {
2392
2399
  }
2393
2400
  }
2394
2401
  if (moveNode)
2395
- setSelFocus(view, sel, moveNode, moveOffset);
2402
+ setSelFocus(view, moveNode, moveOffset);
2396
2403
  }
2397
2404
  function isBlockNode(dom) {
2398
2405
  let desc = dom.pmViewDesc;
2399
2406
  return desc && desc.node && desc.node.isBlock;
2400
2407
  }
2401
- function setSelFocus(view, sel, node, offset) {
2408
+ function setSelFocus(view, node, offset) {
2409
+ let sel = view.domSelection();
2402
2410
  if (selectionCollapsed(sel)) {
2403
2411
  let range = document.createRange();
2404
2412
  range.setEnd(node, offset);
@@ -2474,7 +2482,7 @@ function switchEditable(view, node, state) {
2474
2482
  function safariDownArrowBug(view) {
2475
2483
  if (!safari || view.state.selection.$head.parentOffset > 0)
2476
2484
  return false;
2477
- let { focusNode, focusOffset } = view.domSelection();
2485
+ let { focusNode, focusOffset } = view.domSelectionRange();
2478
2486
  if (focusNode && focusNode.nodeType == 1 && focusOffset == 0 &&
2479
2487
  focusNode.firstChild && focusNode.firstChild.contentEditable == "false") {
2480
2488
  let child = focusNode.firstChild;
@@ -3200,13 +3208,13 @@ editHandlers.compositionstart = editHandlers.compositionupdate = view => {
3200
3208
  // the inserted text won't inherit the marks. So this moves it
3201
3209
  // inside if necessary.
3202
3210
  if (gecko && state.selection.empty && $pos.parentOffset && !$pos.textOffset && $pos.nodeBefore.marks.length) {
3203
- let sel = view.domSelection();
3211
+ let sel = view.domSelectionRange();
3204
3212
  for (let node = sel.focusNode, offset = sel.focusOffset; node && node.nodeType == 1 && offset != 0;) {
3205
3213
  let before = offset < 0 ? node.lastChild : node.childNodes[offset - 1];
3206
3214
  if (!before)
3207
3215
  break;
3208
3216
  if (before.nodeType == 3) {
3209
- sel.collapse(before, before.nodeValue.length);
3217
+ view.domSelection().collapse(before, before.nodeValue.length);
3210
3218
  break;
3211
3219
  }
3212
3220
  else {
@@ -3462,7 +3470,7 @@ handlers.focus = view => {
3462
3470
  view.domObserver.start();
3463
3471
  view.focused = true;
3464
3472
  setTimeout(() => {
3465
- if (view.docView && view.hasFocus() && !view.domObserver.currentSelection.eq(view.domSelection()))
3473
+ if (view.docView && view.hasFocus() && !view.domObserver.currentSelection.eq(view.domSelectionRange()))
3466
3474
  selectionToDOM(view);
3467
3475
  }, 20);
3468
3476
  }
@@ -4294,7 +4302,7 @@ class DOMObserver {
4294
4302
  // us a selection change event before the DOM changes are
4295
4303
  // reported.
4296
4304
  if (ie && ie_version <= 11 && !this.view.state.selection.empty) {
4297
- let sel = this.view.domSelection();
4305
+ let sel = this.view.domSelectionRange();
4298
4306
  // Selection.isCollapsed isn't reliable on IE
4299
4307
  if (sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset))
4300
4308
  return this.flushSoon();
@@ -4302,13 +4310,20 @@ class DOMObserver {
4302
4310
  this.flush();
4303
4311
  }
4304
4312
  setCurSelection() {
4305
- this.currentSelection.set(this.view.domSelection());
4313
+ this.currentSelection.set(this.view.domSelectionRange());
4306
4314
  }
4307
4315
  ignoreSelectionChange(sel) {
4308
- if (sel.rangeCount == 0)
4316
+ if (!sel.focusNode)
4309
4317
  return true;
4310
- let container = sel.getRangeAt(0).commonAncestorContainer;
4311
- let desc = this.view.docView.nearestDesc(container);
4318
+ let ancestors = new Set, container;
4319
+ for (let scan = sel.focusNode; scan; scan = parentNode(scan))
4320
+ ancestors.add(scan);
4321
+ for (let scan = sel.anchorNode; scan; scan = parentNode(scan))
4322
+ if (ancestors.has(scan)) {
4323
+ container = scan;
4324
+ break;
4325
+ }
4326
+ let desc = container && this.view.docView.nearestDesc(container);
4312
4327
  if (desc && desc.ignoreMutation({
4313
4328
  type: "selection",
4314
4329
  target: container.nodeType == 3 ? container.parentNode : container
@@ -4326,7 +4341,7 @@ class DOMObserver {
4326
4341
  mutations = this.queue.concat(mutations);
4327
4342
  this.queue.length = 0;
4328
4343
  }
4329
- let sel = view.domSelection();
4344
+ let sel = view.domSelectionRange();
4330
4345
  let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel);
4331
4346
  let from = -1, to = -1, typeOver = false, added = [];
4332
4347
  if (view.editable) {
@@ -4443,6 +4458,33 @@ function checkCSS(view) {
4443
4458
  cssCheckWarned = true;
4444
4459
  }
4445
4460
  }
4461
+ // Used to work around a Safari Selection/shadow DOM bug
4462
+ // Based on https://github.com/codemirror/dev/issues/414 fix
4463
+ function safariShadowSelectionRange(view) {
4464
+ let found;
4465
+ function read(event) {
4466
+ event.preventDefault();
4467
+ event.stopImmediatePropagation();
4468
+ found = event.getTargetRanges()[0];
4469
+ }
4470
+ // Because Safari (at least in 2018-2022) doesn't provide regular
4471
+ // access to the selection inside a shadowRoot, we have to perform a
4472
+ // ridiculous hack to get at it—using `execCommand` to trigger a
4473
+ // `beforeInput` event so that we can read the target range from the
4474
+ // event.
4475
+ view.dom.addEventListener("beforeinput", read, true);
4476
+ document.execCommand("indent");
4477
+ view.dom.removeEventListener("beforeinput", read, true);
4478
+ let anchorNode = found.startContainer, anchorOffset = found.startOffset;
4479
+ let focusNode = found.endContainer, focusOffset = found.endOffset;
4480
+ let currentAnchor = view.domAtPos(view.state.selection.anchor);
4481
+ // Since such a range doesn't distinguish between anchor and head,
4482
+ // use a heuristic that flips it around if its end matches the
4483
+ // current anchor.
4484
+ if (isEquivalentPosition(currentAnchor.node, currentAnchor.offset, focusNode, focusOffset))
4485
+ [anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset];
4486
+ return { anchorNode, anchorOffset, focusNode, focusOffset };
4487
+ }
4446
4488
 
4447
4489
  // Note that all referencing and parsing is done with the
4448
4490
  // start-of-operation selection and document, since that's the one
@@ -4451,7 +4493,7 @@ function checkCSS(view) {
4451
4493
  // readDOMChange.
4452
4494
  function parseBetween(view, from_, to_) {
4453
4495
  let { node: parent, fromOffset, toOffset, from, to } = view.docView.parseRange(from_, to_);
4454
- let domSel = view.domSelection();
4496
+ let domSel = view.domSelectionRange();
4455
4497
  let find;
4456
4498
  let anchor = domSel.anchorNode;
4457
4499
  if (anchor && view.dom.contains(anchor.nodeType == 1 ? anchor : anchor.parentNode)) {
@@ -4979,7 +5021,7 @@ class EditorView {
4979
5021
  // Chrome sometimes starts misreporting the selection, so this
4980
5022
  // tracks that and forces a selection reset when our update
4981
5023
  // did write to the node.
4982
- let chromeKludge = chrome ? (this.trackWrites = this.domSelection().focusNode) : null;
5024
+ let chromeKludge = chrome ? (this.trackWrites = this.domSelectionRange().focusNode) : null;
4983
5025
  if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) {
4984
5026
  this.docView.updateOuterDeco([]);
4985
5027
  this.docView.destroy();
@@ -4993,7 +5035,8 @@ class EditorView {
4993
5035
  // can cause a spurious DOM selection update, disrupting mouse
4994
5036
  // drag selection.
4995
5037
  if (forceSelUpdate ||
4996
- !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelection()) && anchorInRightPlace(this))) {
5038
+ !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) &&
5039
+ anchorInRightPlace(this))) {
4997
5040
  selectionToDOM(this, forceSelUpdate);
4998
5041
  }
4999
5042
  else {
@@ -5017,7 +5060,7 @@ class EditorView {
5017
5060
  @internal
5018
5061
  */
5019
5062
  scrollToSelection() {
5020
- let startDOM = this.domSelection().focusNode;
5063
+ let startDOM = this.domSelectionRange().focusNode;
5021
5064
  if (this.someProp("handleScrollToSelection", f => f(this))) ;
5022
5065
  else if (this.state.selection instanceof NodeSelection) {
5023
5066
  let target = this.docView.domAfterPos(this.state.selection.from);
@@ -5255,6 +5298,13 @@ class EditorView {
5255
5298
  /**
5256
5299
  @internal
5257
5300
  */
5301
+ domSelectionRange() {
5302
+ return safari && this.root.nodeType === 11 && deepActiveElement(this.dom.ownerDocument) == this.dom
5303
+ ? safariShadowSelectionRange(this) : this.domSelection();
5304
+ }
5305
+ /**
5306
+ @internal
5307
+ */
5258
5308
  domSelection() {
5259
5309
  return this.root.getSelection();
5260
5310
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.28.1",
3
+ "version": "1.28.3",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -1,7 +1,7 @@
1
1
  import {Selection, NodeSelection, TextSelection, AllSelection, EditorState} from "prosemirror-state"
2
2
  import {EditorView} from "./index"
3
3
  import * as browser from "./browser"
4
- import {domIndex, selectionCollapsed, DOMSelection} from "./dom"
4
+ import {domIndex, selectionCollapsed} from "./dom"
5
5
  import {selectionToDOM} from "./selection"
6
6
 
7
7
  function moveSelectionBlock(state: EditorState, dir: number) {
@@ -62,7 +62,7 @@ function isIgnorable(dom: Node) {
62
62
  // Make sure the cursor isn't directly after one or more ignored
63
63
  // nodes, which will confuse the browser's cursor motion logic.
64
64
  function skipIgnoredNodesLeft(view: EditorView) {
65
- let sel = view.domSelection()
65
+ let sel = view.domSelectionRange()
66
66
  let node = sel.focusNode!, offset = sel.focusOffset
67
67
  if (!node) return
68
68
  let moveNode, moveOffset: number | undefined, force = false
@@ -103,14 +103,14 @@ function skipIgnoredNodesLeft(view: EditorView) {
103
103
  }
104
104
  }
105
105
  }
106
- if (force) setSelFocus(view, sel, node, offset)
107
- else if (moveNode) setSelFocus(view, sel, moveNode, moveOffset!)
106
+ if (force) setSelFocus(view, node, offset)
107
+ else if (moveNode) setSelFocus(view, moveNode, moveOffset!)
108
108
  }
109
109
 
110
110
  // Make sure the cursor isn't directly before one or more ignored
111
111
  // nodes.
112
112
  function skipIgnoredNodesRight(view: EditorView) {
113
- let sel = view.domSelection()
113
+ let sel = view.domSelectionRange()
114
114
  let node = sel.focusNode!, offset = sel.focusOffset
115
115
  if (!node) return
116
116
  let len = nodeLen(node)
@@ -144,7 +144,7 @@ function skipIgnoredNodesRight(view: EditorView) {
144
144
  }
145
145
  }
146
146
  }
147
- if (moveNode) setSelFocus(view, sel, moveNode, moveOffset!)
147
+ if (moveNode) setSelFocus(view, moveNode, moveOffset!)
148
148
  }
149
149
 
150
150
  function isBlockNode(dom: Node) {
@@ -152,7 +152,8 @@ function isBlockNode(dom: Node) {
152
152
  return desc && desc.node && desc.node.isBlock
153
153
  }
154
154
 
155
- function setSelFocus(view: EditorView, sel: DOMSelection, node: Node, offset: number) {
155
+ function setSelFocus(view: EditorView, node: Node, offset: number) {
156
+ let sel = view.domSelection()
156
157
  if (selectionCollapsed(sel)) {
157
158
  let range = document.createRange()
158
159
  range.setEnd(node, offset)
@@ -222,7 +223,7 @@ function switchEditable(view: EditorView, node: HTMLElement, state: string) {
222
223
  // after it
223
224
  function safariDownArrowBug(view: EditorView) {
224
225
  if (!browser.safari || view.state.selection.$head.parentOffset > 0) return false
225
- let {focusNode, focusOffset} = view.domSelection()
226
+ let {focusNode, focusOffset} = view.domSelectionRange()
226
227
  if (focusNode && focusNode.nodeType == 1 && focusOffset == 0 &&
227
228
  focusNode.firstChild && (focusNode.firstChild as HTMLElement).contentEditable == "false") {
228
229
  let child = focusNode.firstChild as HTMLElement
package/src/dom.ts CHANGED
@@ -1,7 +1,9 @@
1
- import * as browser from "./browser"
2
-
3
1
  export type DOMNode = InstanceType<typeof window.Node>
4
2
  export type DOMSelection = InstanceType<typeof window.Selection>
3
+ export type DOMSelectionRange = {
4
+ focusNode: DOMNode | null, focusOffset: number,
5
+ anchorNode: DOMNode | null, anchorOffset: number
6
+ }
5
7
 
6
8
  export const domIndex = function(node: Node) {
7
9
  for (var index = 0;; index++) {
@@ -10,7 +12,7 @@ export const domIndex = function(node: Node) {
10
12
  }
11
13
  }
12
14
 
13
- export const parentNode = function(node: Node) {
15
+ export const parentNode = function(node: Node): Node | null {
14
16
  let parent = (node as HTMLSlotElement).assignedSlot || node.parentNode
15
17
  return parent && parent.nodeType == 11 ? (parent as ShadowRoot).host : parent
16
18
  }
@@ -80,11 +82,9 @@ function hasBlockDesc(dom: Node) {
80
82
 
81
83
  // Work around Chrome issue https://bugs.chromium.org/p/chromium/issues/detail?id=447523
82
84
  // (isCollapsed inappropriately returns true in shadow dom)
83
- export const selectionCollapsed = function(domSel: Selection) {
84
- let collapsed = domSel.isCollapsed
85
- if (collapsed && browser.chrome && domSel.rangeCount && !domSel.getRangeAt(0).collapsed)
86
- collapsed = false
87
- return collapsed
85
+ export const selectionCollapsed = function(domSel: DOMSelectionRange) {
86
+ return domSel.focusNode && isEquivalentPosition(domSel.focusNode, domSel.focusOffset,
87
+ domSel.anchorNode!, domSel.anchorOffset)
88
88
  }
89
89
 
90
90
  export function keyEvent(keyCode: number, key: string) {
@@ -94,3 +94,9 @@ export function keyEvent(keyCode: number, key: string) {
94
94
  ;(event as any).key = (event as any).code = key
95
95
  return event
96
96
  }
97
+
98
+ export function deepActiveElement(doc: Document) {
99
+ let elt = doc.activeElement
100
+ while (elt && elt.shadowRoot) elt = elt.shadowRoot.activeElement
101
+ return elt
102
+ }
package/src/domchange.ts CHANGED
@@ -15,7 +15,7 @@ import {EditorView} from "./index"
15
15
  function parseBetween(view: EditorView, from_: number, to_: number) {
16
16
  let {node: parent, fromOffset, toOffset, from, to} = view.docView.parseRange(from_, to_)
17
17
 
18
- let domSel = view.domSelection()
18
+ let domSel = view.domSelectionRange()
19
19
  let find: {node: DOMNode, offset: number, pos?: number}[] | undefined
20
20
  let anchor = domSel.anchorNode
21
21
  if (anchor && view.dom.contains(anchor.nodeType == 1 ? anchor : anchor.parentNode)) {
package/src/domcoords.ts CHANGED
@@ -452,15 +452,18 @@ function endOfTextblockHorizontal(view: EditorView, state: EditorState, dir: "le
452
452
  // one character, and see if that moves the cursor out of the
453
453
  // textblock (or doesn't move it at all, when at the start/end of
454
454
  // the document).
455
- let oldRange = sel.getRangeAt(0), oldNode = sel.focusNode, oldOff = sel.focusOffset
455
+ let {focusNode: oldNode, focusOffset: oldOff, anchorNode, anchorOffset} = view.domSelectionRange()
456
456
  let oldBidiLevel = (sel as any).caretBidiLevel // Only for Firefox
457
457
  ;(sel as any).modify("move", dir, "character")
458
458
  let parentDOM = $head.depth ? view.docView.domAfterPos($head.before()) : view.dom
459
- let result = !parentDOM.contains(sel.focusNode!.nodeType == 1 ? sel.focusNode : sel.focusNode!.parentNode) ||
460
- (oldNode == sel.focusNode && oldOff == sel.focusOffset)
459
+ let {focusNode: newNode, focusOffset: newOff} = view.domSelectionRange()
460
+ let result = newNode && !parentDOM.contains(newNode.nodeType == 1 ? newNode : newNode.parentNode) ||
461
+ (oldNode == newNode && oldOff == newOff)
461
462
  // Restore the previous selection
462
- sel.removeAllRanges()
463
- sel.addRange(oldRange)
463
+ try {
464
+ sel.collapse(anchorNode, anchorOffset)
465
+ if (oldNode && (oldNode != anchorNode || oldOff != anchorOffset) && sel.extend) sel.extend(oldNode, oldOff)
466
+ } catch (_) {}
464
467
  if (oldBidiLevel != null) (sel as any).caretBidiLevel = oldBidiLevel
465
468
  return result
466
469
  })
@@ -1,6 +1,6 @@
1
1
  import {Selection} from "prosemirror-state"
2
2
  import * as browser from "./browser"
3
- import {domIndex, isEquivalentPosition, selectionCollapsed, DOMSelection} from "./dom"
3
+ import {domIndex, isEquivalentPosition, selectionCollapsed, parentNode, DOMSelectionRange, DOMNode} from "./dom"
4
4
  import {hasFocusAndSelection, selectionToDOM, selectionFromDOM} from "./selection"
5
5
  import {EditorView} from "./index"
6
6
 
@@ -21,7 +21,7 @@ class SelectionState {
21
21
  focusNode: Node | null = null
22
22
  focusOffset: number = 0
23
23
 
24
- set(sel: DOMSelection) {
24
+ set(sel: DOMSelectionRange) {
25
25
  this.anchorNode = sel.anchorNode; this.anchorOffset = sel.anchorOffset
26
26
  this.focusNode = sel.focusNode; this.focusOffset = sel.focusOffset
27
27
  }
@@ -30,7 +30,7 @@ class SelectionState {
30
30
  this.anchorNode = this.focusNode = null
31
31
  }
32
32
 
33
- eq(sel: DOMSelection) {
33
+ eq(sel: DOMSelectionRange) {
34
34
  return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset &&
35
35
  sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset
36
36
  }
@@ -127,7 +127,7 @@ export class DOMObserver {
127
127
  // us a selection change event before the DOM changes are
128
128
  // reported.
129
129
  if (browser.ie && browser.ie_version <= 11 && !this.view.state.selection.empty) {
130
- let sel = this.view.domSelection()
130
+ let sel = this.view.domSelectionRange()
131
131
  // Selection.isCollapsed isn't reliable on IE
132
132
  if (sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode!, sel.anchorOffset))
133
133
  return this.flushSoon()
@@ -136,16 +136,21 @@ export class DOMObserver {
136
136
  }
137
137
 
138
138
  setCurSelection() {
139
- this.currentSelection.set(this.view.domSelection())
139
+ this.currentSelection.set(this.view.domSelectionRange())
140
140
  }
141
141
 
142
- ignoreSelectionChange(sel: DOMSelection) {
143
- if (sel.rangeCount == 0) return true
144
- let container = sel.getRangeAt(0).commonAncestorContainer
145
- let desc = this.view.docView.nearestDesc(container)
142
+ ignoreSelectionChange(sel: DOMSelectionRange) {
143
+ if (!sel.focusNode) return true
144
+ let ancestors: Set<Node> = new Set, container: DOMNode | undefined
145
+ for (let scan: DOMNode | null = sel.focusNode; scan; scan = parentNode(scan)) ancestors.add(scan)
146
+ for (let scan = sel.anchorNode; scan; scan = parentNode(scan)) if (ancestors.has(scan)) {
147
+ container = scan
148
+ break
149
+ }
150
+ let desc = container && this.view.docView.nearestDesc(container)
146
151
  if (desc && desc.ignoreMutation({
147
152
  type: "selection",
148
- target: container.nodeType == 3 ? container.parentNode : container
153
+ target: container!.nodeType == 3 ? container!.parentNode : container
149
154
  } as any)) {
150
155
  this.setCurSelection()
151
156
  return true
@@ -161,7 +166,7 @@ export class DOMObserver {
161
166
  this.queue.length = 0
162
167
  }
163
168
 
164
- let sel = view.domSelection()
169
+ let sel = view.domSelectionRange()
165
170
  let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel)
166
171
 
167
172
  let from = -1, to = -1, typeOver = false, added: Node[] = []
@@ -270,3 +275,34 @@ function checkCSS(view: EditorView) {
270
275
  cssCheckWarned = true
271
276
  }
272
277
  }
278
+
279
+ // Used to work around a Safari Selection/shadow DOM bug
280
+ // Based on https://github.com/codemirror/dev/issues/414 fix
281
+ export function safariShadowSelectionRange(view: EditorView): DOMSelectionRange {
282
+ let found: StaticRange | undefined
283
+ function read(event: InputEvent) {
284
+ event.preventDefault()
285
+ event.stopImmediatePropagation()
286
+ found = event.getTargetRanges()[0]
287
+ }
288
+
289
+ // Because Safari (at least in 2018-2022) doesn't provide regular
290
+ // access to the selection inside a shadowRoot, we have to perform a
291
+ // ridiculous hack to get at it—using `execCommand` to trigger a
292
+ // `beforeInput` event so that we can read the target range from the
293
+ // event.
294
+ view.dom.addEventListener("beforeinput", read, true)
295
+ document.execCommand("indent")
296
+ view.dom.removeEventListener("beforeinput", read, true)
297
+
298
+ let anchorNode = found!.startContainer, anchorOffset = found!.startOffset
299
+ let focusNode = found!.endContainer, focusOffset = found!.endOffset
300
+
301
+ let currentAnchor = view.domAtPos(view.state.selection.anchor)
302
+ // Since such a range doesn't distinguish between anchor and head,
303
+ // use a heuristic that flips it around if its end matches the
304
+ // current anchor.
305
+ if (isEquivalentPosition(currentAnchor.node, currentAnchor.offset, focusNode, focusOffset))
306
+ [anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset]
307
+ return {anchorNode, anchorOffset, focusNode, focusOffset}
308
+ }
package/src/index.ts CHANGED
@@ -7,9 +7,9 @@ import {docViewDesc, ViewDesc, NodeView, NodeViewDesc} from "./viewdesc"
7
7
  import {initInput, destroyInput, dispatchEvent, ensureListeners, clearComposition, InputState} from "./input"
8
8
  import {selectionToDOM, anchorInRightPlace, syncNodeSelection} from "./selection"
9
9
  import {Decoration, viewDecorations, DecorationSource} from "./decoration"
10
- import {DOMObserver} from "./domobserver"
10
+ import {DOMObserver, safariShadowSelectionRange} from "./domobserver"
11
11
  import {readDOMChange} from "./domchange"
12
- import {DOMSelection, DOMNode} from "./dom"
12
+ import {DOMSelection, DOMNode, DOMSelectionRange, deepActiveElement} from "./dom"
13
13
  import * as browser from "./browser"
14
14
 
15
15
  export {Decoration, DecorationSet, DecorationAttrs, DecorationSource} from "./decoration"
@@ -195,7 +195,7 @@ export class EditorView {
195
195
  // Chrome sometimes starts misreporting the selection, so this
196
196
  // tracks that and forces a selection reset when our update
197
197
  // did write to the node.
198
- let chromeKludge = browser.chrome ? (this.trackWrites = this.domSelection().focusNode) : null
198
+ let chromeKludge = browser.chrome ? (this.trackWrites = this.domSelectionRange().focusNode) : null
199
199
  if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) {
200
200
  this.docView.updateOuterDeco([])
201
201
  this.docView.destroy()
@@ -208,7 +208,8 @@ export class EditorView {
208
208
  // can cause a spurious DOM selection update, disrupting mouse
209
209
  // drag selection.
210
210
  if (forceSelUpdate ||
211
- !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelection()) && anchorInRightPlace(this))) {
211
+ !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) &&
212
+ anchorInRightPlace(this))) {
212
213
  selectionToDOM(this, forceSelUpdate)
213
214
  } else {
214
215
  syncNodeSelection(this, state.selection)
@@ -230,7 +231,7 @@ export class EditorView {
230
231
 
231
232
  /// @internal
232
233
  scrollToSelection() {
233
- let startDOM = this.domSelection().focusNode!
234
+ let startDOM = this.domSelectionRange().focusNode!
234
235
  if (this.someProp("handleScrollToSelection", f => f(this))) {
235
236
  // Handled
236
237
  } else if (this.state.selection instanceof NodeSelection) {
@@ -449,6 +450,12 @@ export class EditorView {
449
450
  else this.updateState(this.state.apply(tr))
450
451
  }
451
452
 
453
+ /// @internal
454
+ domSelectionRange(): DOMSelectionRange {
455
+ return browser.safari && this.root.nodeType === 11 && deepActiveElement(this.dom.ownerDocument) == this.dom
456
+ ? safariShadowSelectionRange(this) : this.domSelection()
457
+ }
458
+
452
459
  /// @internal
453
460
  domSelection(): DOMSelection {
454
461
  return (this.root as Document).getSelection()!
package/src/input.ts CHANGED
@@ -463,12 +463,12 @@ editHandlers.compositionstart = editHandlers.compositionupdate = view => {
463
463
  // the inserted text won't inherit the marks. So this moves it
464
464
  // inside if necessary.
465
465
  if (browser.gecko && state.selection.empty && $pos.parentOffset && !$pos.textOffset && $pos.nodeBefore!.marks.length) {
466
- let sel = view.domSelection()
466
+ let sel = view.domSelectionRange()
467
467
  for (let node = sel.focusNode, offset = sel.focusOffset; node && node.nodeType == 1 && offset != 0;) {
468
468
  let before = offset < 0 ? node.lastChild : node.childNodes[offset - 1]
469
469
  if (!before) break
470
470
  if (before.nodeType == 3) {
471
- sel.collapse(before, before.nodeValue!.length)
471
+ view.domSelection().collapse(before, before.nodeValue!.length)
472
472
  break
473
473
  } else {
474
474
  node = before
@@ -715,7 +715,7 @@ handlers.focus = view => {
715
715
  view.domObserver.start()
716
716
  view.focused = true
717
717
  setTimeout(() => {
718
- if (view.docView && view.hasFocus() && !view.domObserver.currentSelection.eq(view.domSelection()))
718
+ if (view.docView && view.hasFocus() && !view.domObserver.currentSelection.eq(view.domSelectionRange()))
719
719
  selectionToDOM(view)
720
720
  }, 20)
721
721
  }
package/src/selection.ts CHANGED
@@ -2,12 +2,12 @@ import {TextSelection, NodeSelection, Selection} from "prosemirror-state"
2
2
  import {ResolvedPos} from "prosemirror-model"
3
3
 
4
4
  import * as browser from "./browser"
5
- import {selectionCollapsed, isEquivalentPosition, domIndex, isOnEdge} from "./dom"
5
+ import {isEquivalentPosition, domIndex, isOnEdge, selectionCollapsed} from "./dom"
6
6
  import {EditorView} from "./index"
7
7
  import {NodeViewDesc} from "./viewdesc"
8
8
 
9
9
  export function selectionFromDOM(view: EditorView, origin: string | null = null) {
10
- let domSel = view.domSelection(), doc = view.state.doc
10
+ let domSel = view.domSelectionRange(), doc = view.state.doc
11
11
  if (!domSel.focusNode) return null
12
12
  let nearestDesc = view.docView.nearestDesc(domSel.focusNode), inWidget = nearestDesc && nearestDesc.size == 0
13
13
  let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1)
@@ -50,7 +50,7 @@ export function selectionToDOM(view: EditorView, force = false) {
50
50
  // in Safari. And the drag selection delay is to workarond issues
51
51
  // which only present in Chrome.
52
52
  if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && browser.chrome) {
53
- let domSel = view.domSelection(), curSel = view.domObserver.currentSelection
53
+ let domSel = view.domSelectionRange(), curSel = view.domObserver.currentSelection
54
54
  if (domSel.anchorNode && curSel.anchorNode &&
55
55
  isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset,
56
56
  curSel.anchorNode, curSel.anchorOffset)) {
@@ -121,7 +121,7 @@ function resetEditable(element: HTMLElement) {
121
121
  function removeClassOnSelectionChange(view: EditorView) {
122
122
  let doc = view.dom.ownerDocument
123
123
  doc.removeEventListener("selectionchange", view.input.hideSelectionGuard!)
124
- let domSel = view.domSelection()
124
+ let domSel = view.domSelectionRange()
125
125
  let node = domSel.anchorNode, offset = domSel.anchorOffset
126
126
  doc.addEventListener("selectionchange", view.input.hideSelectionGuard = () => {
127
127
  if (domSel.anchorNode != node || domSel.anchorOffset != offset) {
@@ -186,7 +186,7 @@ export function hasFocusAndSelection(view: EditorView) {
186
186
  }
187
187
 
188
188
  export function hasSelection(view: EditorView) {
189
- let sel = view.domSelection()
189
+ let sel = view.domSelectionRange()
190
190
  if (!sel.anchorNode) return false
191
191
  try {
192
192
  // Firefox will raise 'permission denied' errors when accessing
@@ -201,6 +201,6 @@ export function hasSelection(view: EditorView) {
201
201
 
202
202
  export function anchorInRightPlace(view: EditorView) {
203
203
  let anchorDOM = view.docView.domFromPos(view.state.selection.anchor, 0)
204
- let domSel = view.domSelection()
204
+ let domSel = view.domSelectionRange()
205
205
  return isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode!, domSel.anchorOffset)
206
206
  }
package/src/viewdesc.ts CHANGED
@@ -419,15 +419,16 @@ export class ViewDesc {
419
419
  if ((domSel.extend || anchor == head) && !brKludge) {
420
420
  domSel.collapse(anchorDOM.node, anchorDOM.offset)
421
421
  try {
422
- if (anchor != head) domSel.extend(headDOM.node, headDOM.offset)
422
+ if (anchor != head)
423
+ domSel.extend(headDOM.node, headDOM.offset)
423
424
  domSelExtended = true
424
- } catch (err) {
425
+ } catch (_) {
425
426
  // In some cases with Chrome the selection is empty after calling
426
427
  // collapse, even when it should be valid. This appears to be a bug, but
427
428
  // it is difficult to isolate. If this happens fallback to the old path
428
429
  // without using extend.
429
- if (!(err instanceof DOMException)) throw err
430
- // declare global: DOMException
430
+ // Similarly, this could crash on Safari if the editor is hidden, and
431
+ // there was no selection.
431
432
  }
432
433
  }
433
434
  if (!domSelExtended) {
@@ -757,7 +758,7 @@ export class NodeViewDesc extends ViewDesc {
757
758
  // are inside of this node
758
759
  let {from, to} = view.state.selection
759
760
  if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + this.node.content.size) return null
760
- let sel = view.domSelection()
761
+ let sel = view.domSelectionRange()
761
762
  let textNode = nearbyTextNode(sel.focusNode!, sel.focusOffset)
762
763
  if (!textNode || !this.dom.contains(textNode.parentNode)) return null
763
764