prosemirror-view 1.34.1 → 1.34.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,19 @@
1
+ ## 1.34.3 (2024-09-19)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix an issue that would, during some types of document updates, leave the document DOM in an improper form.
6
+
7
+ Handle the case where drag-selecting in Firefox creates a selection with multiple distinct ranges.
8
+
9
+ ## 1.34.2 (2024-09-03)
10
+
11
+ ### Bug fixes
12
+
13
+ Fix pasting in Chrome with the require-trusted-types-for CSP enabled.
14
+
15
+ Fix an issue where, for some kinds of IME on Windows, pressing Enter while in a composition would execute the resulting command on the wrong selection, erasing some typed content.
16
+
1
17
  ## 1.34.1 (2024-08-26)
2
18
 
3
19
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -1840,6 +1840,7 @@ var ViewTreeUpdater = function () {
1840
1840
  this.index++;
1841
1841
  return true;
1842
1842
  } else if (!locked && (updated = this.recreateWrapper(next, node, outerDeco, innerDeco, view, pos))) {
1843
+ this.destroyBetween(this.index, i);
1843
1844
  this.top.children[this.index] = updated;
1844
1845
  if (updated.contentDOM) {
1845
1846
  updated.dirty = CONTENT_DIRTY;
@@ -1858,7 +1859,7 @@ var ViewTreeUpdater = function () {
1858
1859
  }, {
1859
1860
  key: "recreateWrapper",
1860
1861
  value: function recreateWrapper(next, node, outerDeco, innerDeco, view, pos) {
1861
- if (next.dirty || node.isAtom || !next.children.length || !next.node.content.eq(node.content)) return null;
1862
+ if (next.dirty || node.isAtom || !next.children.length || !next.node.content.eq(node.content) || !sameOuterDeco(outerDeco, next.outerDeco) || !innerDeco.eq(next.innerDeco)) return null;
1862
1863
  var wrapper = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos);
1863
1864
  if (wrapper.contentDOM) {
1864
1865
  wrapper.children = next.children;
@@ -2105,10 +2106,10 @@ function selectionFromDOM(view) {
2105
2106
  var head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1);
2106
2107
  if (head < 0) return null;
2107
2108
  var $head = doc.resolve(head),
2108
- $anchor,
2109
+ anchor,
2109
2110
  selection;
2110
2111
  if (selectionCollapsed(domSel)) {
2111
- $anchor = $head;
2112
+ anchor = head;
2112
2113
  while (nearestDesc && !nearestDesc.node) nearestDesc = nearestDesc.parent;
2113
2114
  var nearestDescNode = nearestDesc.node;
2114
2115
  if (nearestDesc && nearestDescNode.isAtom && prosemirrorState.NodeSelection.isSelectable(nearestDescNode) && nearestDesc.parent && !(nearestDescNode.isInline && isOnEdge(domSel.focusNode, domSel.focusOffset, nearestDesc.dom))) {
@@ -2116,10 +2117,26 @@ function selectionFromDOM(view) {
2116
2117
  selection = new prosemirrorState.NodeSelection(head == pos ? $head : doc.resolve(pos));
2117
2118
  }
2118
2119
  } else {
2119
- var anchor = view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset, 1);
2120
+ if (domSel instanceof view.dom.ownerDocument.defaultView.Selection && domSel.rangeCount > 1) {
2121
+ var min = head,
2122
+ max = head;
2123
+ for (var i = 0; i < domSel.rangeCount; i++) {
2124
+ var range = domSel.getRangeAt(i);
2125
+ min = Math.min(min, view.docView.posFromDOM(range.startContainer, range.startOffset, 1));
2126
+ max = Math.max(max, view.docView.posFromDOM(range.endContainer, range.endOffset, -1));
2127
+ }
2128
+ if (min < 0) return null;
2129
+ var _ref3 = max == view.state.selection.anchor ? [max, min] : [min, max];
2130
+ var _ref4 = _slicedToArray(_ref3, 2);
2131
+ anchor = _ref4[0];
2132
+ head = _ref4[1];
2133
+ $head = doc.resolve(head);
2134
+ } else {
2135
+ anchor = view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset, 1);
2136
+ }
2120
2137
  if (anchor < 0) return null;
2121
- $anchor = doc.resolve(anchor);
2122
2138
  }
2139
+ var $anchor = doc.resolve(anchor);
2123
2140
  if (!selection) {
2124
2141
  var bias = origin == "pointer" || view.state.selection.head < $head.pos && !inWidget ? 1 : -1;
2125
2142
  selection = selectionBetween(view, $anchor, $head, bias);
@@ -2766,6 +2783,15 @@ var _detachedDoc = null;
2766
2783
  function detachedDoc() {
2767
2784
  return _detachedDoc || (_detachedDoc = document.implementation.createHTMLDocument("title"));
2768
2785
  }
2786
+ function maybeWrapTrusted(html) {
2787
+ var trustedTypes = window.trustedTypes;
2788
+ if (!trustedTypes) return html;
2789
+ return trustedTypes.createPolicy("detachedDocument", {
2790
+ createHTML: function createHTML(s) {
2791
+ return s;
2792
+ }
2793
+ }).createHTML(html);
2794
+ }
2769
2795
  function readHTML(html) {
2770
2796
  var metas = /^(\s*<meta [^>]*>)*/.exec(html);
2771
2797
  if (metas) html = html.slice(metas[0].length);
@@ -2777,7 +2803,7 @@ function readHTML(html) {
2777
2803
  }).join("") + html + wrap.map(function (n) {
2778
2804
  return "</" + n + ">";
2779
2805
  }).reverse().join("");
2780
- elt.innerHTML = html;
2806
+ elt.innerHTML = maybeWrapTrusted(html);
2781
2807
  if (wrap) for (var i = 0; i < wrap.length; i++) elt = elt.querySelector(wrap[i]) || elt;
2782
2808
  return elt;
2783
2809
  }
@@ -2901,7 +2927,7 @@ editHandlers.keydown = function (view, _event) {
2901
2927
  view.input.lastKeyCode = event.keyCode;
2902
2928
  view.input.lastKeyCodeTime = Date.now();
2903
2929
  if (android && chrome && event.keyCode == 13) return;
2904
- if (event.keyCode != 229) view.domObserver.forceFlush();
2930
+ if (view.domObserver.selectionChanged(view.domSelectionRange())) view.domObserver.flush();else if (event.keyCode != 229) view.domObserver.forceFlush();
2905
2931
  if (ios && event.keyCode == 13 && !event.ctrlKey && !event.altKey && !event.metaKey) {
2906
2932
  var now = Date.now();
2907
2933
  view.input.lastIOSEnter = now;
@@ -4266,6 +4292,11 @@ var DOMObserver = function () {
4266
4292
  }
4267
4293
  return this.queue;
4268
4294
  }
4295
+ }, {
4296
+ key: "selectionChanged",
4297
+ value: function selectionChanged(sel) {
4298
+ return !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(this.view) && !this.ignoreSelectionChange(sel);
4299
+ }
4269
4300
  }, {
4270
4301
  key: "flush",
4271
4302
  value: function flush() {
@@ -4273,8 +4304,8 @@ var DOMObserver = function () {
4273
4304
  if (!view.docView || this.flushingSoon > -1) return;
4274
4305
  var mutations = this.pendingRecords();
4275
4306
  if (mutations.length) this.queue = [];
4276
- var sel = view.domSelectionRange();
4277
- var newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel);
4307
+ var sel = view.domSelectionRange(),
4308
+ newSel = this.selectionChanged(sel);
4278
4309
  var from = -1,
4279
4310
  to = -1,
4280
4311
  typeOver = false,
@@ -4403,11 +4434,11 @@ function rangeToSelectionRange(view, range) {
4403
4434
  focusOffset = range.endOffset;
4404
4435
  var currentAnchor = view.domAtPos(view.state.selection.anchor);
4405
4436
  if (isEquivalentPosition(currentAnchor.node, currentAnchor.offset, focusNode, focusOffset)) {
4406
- var _ref3 = [focusNode, focusOffset, anchorNode, anchorOffset];
4407
- anchorNode = _ref3[0];
4408
- anchorOffset = _ref3[1];
4409
- focusNode = _ref3[2];
4410
- focusOffset = _ref3[3];
4437
+ var _ref5 = [focusNode, focusOffset, anchorNode, anchorOffset];
4438
+ anchorNode = _ref5[0];
4439
+ anchorOffset = _ref5[1];
4440
+ focusNode = _ref5[2];
4441
+ focusOffset = _ref5[3];
4411
4442
  }
4412
4443
  return {
4413
4444
  anchorNode: anchorNode,
package/dist/index.js CHANGED
@@ -1870,6 +1870,7 @@ class ViewTreeUpdater {
1870
1870
  return true;
1871
1871
  }
1872
1872
  else if (!locked && (updated = this.recreateWrapper(next, node, outerDeco, innerDeco, view, pos))) {
1873
+ this.destroyBetween(this.index, i);
1873
1874
  this.top.children[this.index] = updated;
1874
1875
  if (updated.contentDOM) {
1875
1876
  updated.dirty = CONTENT_DIRTY;
@@ -1889,7 +1890,8 @@ class ViewTreeUpdater {
1889
1890
  // identical content, move over its children.
1890
1891
  recreateWrapper(next, node, outerDeco, innerDeco, view, pos) {
1891
1892
  if (next.dirty || node.isAtom || !next.children.length ||
1892
- !next.node.content.eq(node.content))
1893
+ !next.node.content.eq(node.content) ||
1894
+ !sameOuterDeco(outerDeco, next.outerDeco) || !innerDeco.eq(next.innerDeco))
1893
1895
  return null;
1894
1896
  let wrapper = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos);
1895
1897
  if (wrapper.contentDOM) {
@@ -2160,9 +2162,9 @@ function selectionFromDOM(view, origin = null) {
2160
2162
  let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1);
2161
2163
  if (head < 0)
2162
2164
  return null;
2163
- let $head = doc.resolve(head), $anchor, selection;
2165
+ let $head = doc.resolve(head), anchor, selection;
2164
2166
  if (selectionCollapsed(domSel)) {
2165
- $anchor = $head;
2167
+ anchor = head;
2166
2168
  while (nearestDesc && !nearestDesc.node)
2167
2169
  nearestDesc = nearestDesc.parent;
2168
2170
  let nearestDescNode = nearestDesc.node;
@@ -2173,11 +2175,25 @@ function selectionFromDOM(view, origin = null) {
2173
2175
  }
2174
2176
  }
2175
2177
  else {
2176
- let anchor = view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset, 1);
2178
+ if (domSel instanceof view.dom.ownerDocument.defaultView.Selection && domSel.rangeCount > 1) {
2179
+ let min = head, max = head;
2180
+ for (let i = 0; i < domSel.rangeCount; i++) {
2181
+ let range = domSel.getRangeAt(i);
2182
+ min = Math.min(min, view.docView.posFromDOM(range.startContainer, range.startOffset, 1));
2183
+ max = Math.max(max, view.docView.posFromDOM(range.endContainer, range.endOffset, -1));
2184
+ }
2185
+ if (min < 0)
2186
+ return null;
2187
+ [anchor, head] = max == view.state.selection.anchor ? [max, min] : [min, max];
2188
+ $head = doc.resolve(head);
2189
+ }
2190
+ else {
2191
+ anchor = view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset, 1);
2192
+ }
2177
2193
  if (anchor < 0)
2178
2194
  return null;
2179
- $anchor = doc.resolve(anchor);
2180
2195
  }
2196
+ let $anchor = doc.resolve(anchor);
2181
2197
  if (!selection) {
2182
2198
  let bias = origin == "pointer" || (view.state.selection.head < $head.pos && !inWidget) ? 1 : -1;
2183
2199
  selection = selectionBetween(view, $anchor, $head, bias);
@@ -2937,6 +2953,15 @@ let _detachedDoc = null;
2937
2953
  function detachedDoc() {
2938
2954
  return _detachedDoc || (_detachedDoc = document.implementation.createHTMLDocument("title"));
2939
2955
  }
2956
+ function maybeWrapTrusted(html) {
2957
+ let trustedTypes = window.trustedTypes;
2958
+ if (!trustedTypes)
2959
+ return html;
2960
+ // With the require-trusted-types-for CSP, Chrome will block
2961
+ // innerHTML, even on a detached document. This wraps the string in
2962
+ // a way that makes the browser allow us to use its parser again.
2963
+ return trustedTypes.createPolicy("detachedDocument", { createHTML: (s) => s }).createHTML(html);
2964
+ }
2940
2965
  function readHTML(html) {
2941
2966
  let metas = /^(\s*<meta [^>]*>)*/.exec(html);
2942
2967
  if (metas)
@@ -2945,7 +2970,7 @@ function readHTML(html) {
2945
2970
  let firstTag = /<([a-z][^>\s]+)/i.exec(html), wrap;
2946
2971
  if (wrap = firstTag && wrapMap[firstTag[1].toLowerCase()])
2947
2972
  html = wrap.map(n => "<" + n + ">").join("") + html + wrap.map(n => "</" + n + ">").reverse().join("");
2948
- elt.innerHTML = html;
2973
+ elt.innerHTML = maybeWrapTrusted(html);
2949
2974
  if (wrap)
2950
2975
  for (let i = 0; i < wrap.length; i++)
2951
2976
  elt = elt.querySelector(wrap[i]) || elt;
@@ -3086,7 +3111,9 @@ editHandlers.keydown = (view, _event) => {
3086
3111
  // and handling them eagerly tends to corrupt the input.
3087
3112
  if (android && chrome && event.keyCode == 13)
3088
3113
  return;
3089
- if (event.keyCode != 229)
3114
+ if (view.domObserver.selectionChanged(view.domSelectionRange()))
3115
+ view.domObserver.flush();
3116
+ else if (event.keyCode != 229)
3090
3117
  view.domObserver.forceFlush();
3091
3118
  // On iOS, if we preventDefault enter key presses, the virtual
3092
3119
  // keyboard gets confused. So the hack here is to set a flag that
@@ -4609,6 +4636,9 @@ class DOMObserver {
4609
4636
  this.queue.push(mut);
4610
4637
  return this.queue;
4611
4638
  }
4639
+ selectionChanged(sel) {
4640
+ return !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(this.view) && !this.ignoreSelectionChange(sel);
4641
+ }
4612
4642
  flush() {
4613
4643
  let { view } = this;
4614
4644
  if (!view.docView || this.flushingSoon > -1)
@@ -4616,8 +4646,7 @@ class DOMObserver {
4616
4646
  let mutations = this.pendingRecords();
4617
4647
  if (mutations.length)
4618
4648
  this.queue = [];
4619
- let sel = view.domSelectionRange();
4620
- let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel);
4649
+ let sel = view.domSelectionRange(), newSel = this.selectionChanged(sel);
4621
4650
  let from = -1, to = -1, typeOver = false, added = [];
4622
4651
  if (view.editable) {
4623
4652
  for (let i = 0; i < mutations.length; i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.34.1",
3
+ "version": "1.34.3",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/clipboard.ts CHANGED
@@ -204,6 +204,15 @@ function detachedDoc() {
204
204
  return _detachedDoc || (_detachedDoc = document.implementation.createHTMLDocument("title"))
205
205
  }
206
206
 
207
+ function maybeWrapTrusted(html: string): string {
208
+ let trustedTypes = (window as any).trustedTypes
209
+ if (!trustedTypes) return html
210
+ // With the require-trusted-types-for CSP, Chrome will block
211
+ // innerHTML, even on a detached document. This wraps the string in
212
+ // a way that makes the browser allow us to use its parser again.
213
+ return trustedTypes.createPolicy("detachedDocument", {createHTML: (s: string) => s}).createHTML(html)
214
+ }
215
+
207
216
  function readHTML(html: string) {
208
217
  let metas = /^(\s*<meta [^>]*>)*/.exec(html)
209
218
  if (metas) html = html.slice(metas[0].length)
@@ -211,7 +220,7 @@ function readHTML(html: string) {
211
220
  let firstTag = /<([a-z][^>\s]+)/i.exec(html), wrap
212
221
  if (wrap = firstTag && wrapMap[firstTag[1].toLowerCase()])
213
222
  html = wrap.map(n => "<" + n + ">").join("") + html + wrap.map(n => "</" + n + ">").reverse().join("")
214
- elt.innerHTML = html
223
+ elt.innerHTML = maybeWrapTrusted(html)
215
224
  if (wrap) for (let i = 0; i < wrap.length; i++) elt = elt.querySelector(wrap[i]) || elt
216
225
  return elt
217
226
  }
@@ -163,14 +163,17 @@ export class DOMObserver {
163
163
  return this.queue
164
164
  }
165
165
 
166
+ selectionChanged(sel: DOMSelectionRange) {
167
+ return !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(this.view) && !this.ignoreSelectionChange(sel)
168
+ }
169
+
166
170
  flush() {
167
171
  let {view} = this
168
172
  if (!view.docView || this.flushingSoon > -1) return
169
173
  let mutations = this.pendingRecords()
170
174
  if (mutations.length) this.queue = []
171
175
 
172
- let sel = view.domSelectionRange()
173
- let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel)
176
+ let sel = view.domSelectionRange(), newSel = this.selectionChanged(sel)
174
177
 
175
178
  let from = -1, to = -1, typeOver = false, added: Node[] = []
176
179
  if (view.editable) {
package/src/input.ts CHANGED
@@ -112,7 +112,9 @@ editHandlers.keydown = (view: EditorView, _event: Event) => {
112
112
  // to be part of a confused sequence of composition events fired,
113
113
  // and handling them eagerly tends to corrupt the input.
114
114
  if (browser.android && browser.chrome && event.keyCode == 13) return
115
- if (event.keyCode != 229) view.domObserver.forceFlush()
115
+
116
+ if (view.domObserver.selectionChanged(view.domSelectionRange())) view.domObserver.flush()
117
+ else if (event.keyCode != 229) view.domObserver.forceFlush()
116
118
 
117
119
  // On iOS, if we preventDefault enter key presses, the virtual
118
120
  // keyboard gets confused. So the hack here is to set a flag that
package/src/selection.ts CHANGED
@@ -12,9 +12,9 @@ export function selectionFromDOM(view: EditorView, origin: string | null = 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)
14
14
  if (head < 0) return null
15
- let $head = doc.resolve(head), $anchor, selection
15
+ let $head = doc.resolve(head), anchor, selection
16
16
  if (selectionCollapsed(domSel)) {
17
- $anchor = $head
17
+ anchor = head
18
18
  while (nearestDesc && !nearestDesc.node) nearestDesc = nearestDesc.parent
19
19
  let nearestDescNode = (nearestDesc as NodeViewDesc).node
20
20
  if (nearestDesc && nearestDescNode.isAtom && NodeSelection.isSelectable(nearestDescNode) && nearestDesc.parent
@@ -23,10 +23,22 @@ export function selectionFromDOM(view: EditorView, origin: string | null = null)
23
23
  selection = new NodeSelection(head == pos ? $head : doc.resolve(pos))
24
24
  }
25
25
  } else {
26
- let anchor = view.docView.posFromDOM(domSel.anchorNode!, domSel.anchorOffset, 1)
26
+ if (domSel instanceof view.dom.ownerDocument.defaultView!.Selection && domSel.rangeCount > 1) {
27
+ let min = head, max = head
28
+ for (let i = 0; i < domSel.rangeCount; i++) {
29
+ let range = domSel.getRangeAt(i)
30
+ min = Math.min(min, view.docView.posFromDOM(range.startContainer, range.startOffset, 1))
31
+ max = Math.max(max, view.docView.posFromDOM(range.endContainer, range.endOffset, -1))
32
+ }
33
+ if (min < 0) return null
34
+ ;[anchor, head] = max == view.state.selection.anchor ? [max, min] : [min, max]
35
+ $head = doc.resolve(head)
36
+ } else {
37
+ anchor = view.docView.posFromDOM(domSel.anchorNode!, domSel.anchorOffset, 1)
38
+ }
27
39
  if (anchor < 0) return null
28
- $anchor = doc.resolve(anchor)
29
40
  }
41
+ let $anchor = doc.resolve(anchor)
30
42
 
31
43
  if (!selection) {
32
44
  let bias = origin == "pointer" || (view.state.selection.head < $head.pos && !inWidget) ? 1 : -1
package/src/viewdesc.ts CHANGED
@@ -1249,6 +1249,7 @@ class ViewTreeUpdater {
1249
1249
  this.index++
1250
1250
  return true
1251
1251
  } else if (!locked && (updated = this.recreateWrapper(next, node, outerDeco, innerDeco, view, pos))) {
1252
+ this.destroyBetween(this.index, i)
1252
1253
  this.top.children[this.index] = updated
1253
1254
  if (updated.contentDOM) {
1254
1255
  updated.dirty = CONTENT_DIRTY
@@ -1270,7 +1271,8 @@ class ViewTreeUpdater {
1270
1271
  recreateWrapper(next: NodeViewDesc, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource,
1271
1272
  view: EditorView, pos: number) {
1272
1273
  if (next.dirty || node.isAtom || !next.children.length ||
1273
- !next.node.content.eq(node.content)) return null
1274
+ !next.node.content.eq(node.content) ||
1275
+ !sameOuterDeco(outerDeco, next.outerDeco) || !innerDeco.eq(next.innerDeco)) return null
1274
1276
  let wrapper = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos)
1275
1277
  if (wrapper.contentDOM) {
1276
1278
  wrapper.children = next.children