prosemirror-view 1.34.0 → 1.34.2

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.2 (2024-09-03)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix pasting in Chrome with the require-trusted-types-for CSP enabled.
6
+
7
+ 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.
8
+
9
+ ## 1.34.1 (2024-08-26)
10
+
11
+ ### Bug fixes
12
+
13
+ Fix null dereferences caused by the behavior of Chrome's newly supported `caretPositionFromPoint` method.
14
+
15
+ Fix an issue where on Android, with GBoard, enter presses in code blocks while a spell correction was selected didn't insert a new line.
16
+
1
17
  ## 1.34.0 (2024-08-23)
2
18
 
3
19
  ### New features
package/dist/index.cjs CHANGED
@@ -135,7 +135,7 @@ function caretFromPoint(doc, x, y) {
135
135
  var pos = doc.caretPositionFromPoint(x, y);
136
136
  if (pos) return {
137
137
  node: pos.offsetNode,
138
- offset: pos.offset
138
+ offset: Math.min(nodeSize(pos.offsetNode), pos.offset)
139
139
  };
140
140
  } catch (_) {}
141
141
  }
@@ -143,7 +143,7 @@ function caretFromPoint(doc, x, y) {
143
143
  var range = doc.caretRangeFromPoint(x, y);
144
144
  if (range) return {
145
145
  node: range.startContainer,
146
- offset: range.startOffset
146
+ offset: Math.min(nodeSize(range.startContainer), range.startOffset)
147
147
  };
148
148
  }
149
149
  }
@@ -2766,6 +2766,15 @@ var _detachedDoc = null;
2766
2766
  function detachedDoc() {
2767
2767
  return _detachedDoc || (_detachedDoc = document.implementation.createHTMLDocument("title"));
2768
2768
  }
2769
+ function maybeWrapTrusted(html) {
2770
+ var trustedTypes = window.trustedTypes;
2771
+ if (!trustedTypes) return html;
2772
+ return trustedTypes.createPolicy("detachedDocument", {
2773
+ createHTML: function createHTML(s) {
2774
+ return s;
2775
+ }
2776
+ }).createHTML(html);
2777
+ }
2769
2778
  function readHTML(html) {
2770
2779
  var metas = /^(\s*<meta [^>]*>)*/.exec(html);
2771
2780
  if (metas) html = html.slice(metas[0].length);
@@ -2777,7 +2786,7 @@ function readHTML(html) {
2777
2786
  }).join("") + html + wrap.map(function (n) {
2778
2787
  return "</" + n + ">";
2779
2788
  }).reverse().join("");
2780
- elt.innerHTML = html;
2789
+ elt.innerHTML = maybeWrapTrusted(html);
2781
2790
  if (wrap) for (var i = 0; i < wrap.length; i++) elt = elt.querySelector(wrap[i]) || elt;
2782
2791
  return elt;
2783
2792
  }
@@ -2901,7 +2910,7 @@ editHandlers.keydown = function (view, _event) {
2901
2910
  view.input.lastKeyCode = event.keyCode;
2902
2911
  view.input.lastKeyCodeTime = Date.now();
2903
2912
  if (android && chrome && event.keyCode == 13) return;
2904
- if (event.keyCode != 229) view.domObserver.forceFlush();
2913
+ if (view.domObserver.selectionChanged(view.domSelectionRange())) view.domObserver.flush();else if (event.keyCode != 229) view.domObserver.forceFlush();
2905
2914
  if (ios && event.keyCode == 13 && !event.ctrlKey && !event.altKey && !event.metaKey) {
2906
2915
  var now = Date.now();
2907
2916
  view.input.lastIOSEnter = now;
@@ -4266,6 +4275,11 @@ var DOMObserver = function () {
4266
4275
  }
4267
4276
  return this.queue;
4268
4277
  }
4278
+ }, {
4279
+ key: "selectionChanged",
4280
+ value: function selectionChanged(sel) {
4281
+ return !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(this.view) && !this.ignoreSelectionChange(sel);
4282
+ }
4269
4283
  }, {
4270
4284
  key: "flush",
4271
4285
  value: function flush() {
@@ -4273,8 +4287,8 @@ var DOMObserver = function () {
4273
4287
  if (!view.docView || this.flushingSoon > -1) return;
4274
4288
  var mutations = this.pendingRecords();
4275
4289
  if (mutations.length) this.queue = [];
4276
- var sel = view.domSelectionRange();
4277
- var newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel);
4290
+ var sel = view.domSelectionRange(),
4291
+ newSel = this.selectionChanged(sel);
4278
4292
  var from = -1,
4279
4293
  to = -1,
4280
4294
  typeOver = false,
@@ -4560,6 +4574,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4560
4574
  }
4561
4575
  view.input.lastKeyCode = null;
4562
4576
  var change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide);
4577
+ if (change) view.input.domChangeCount++;
4563
4578
  if ((ios && view.input.lastIOSEnter > Date.now() - 225 || android) && addedNodes.some(function (n) {
4564
4579
  return n.nodeType == 1 && !isInline.test(n.nodeName);
4565
4580
  }) && (!change || change.endA >= change.endB) && view.someProp("handleKeyDown", function (f) {
@@ -4587,7 +4602,6 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4587
4602
  return;
4588
4603
  }
4589
4604
  }
4590
- view.input.domChangeCount++;
4591
4605
  if (view.state.selection.from < view.state.selection.to && change.start == change.endB && view.state.selection instanceof prosemirrorState.TextSelection) {
4592
4606
  if (change.start > view.state.selection.from && change.start <= view.state.selection.from + 2 && view.state.selection.from >= parse.from) {
4593
4607
  change.start = view.state.selection.from;
package/dist/index.js CHANGED
@@ -139,15 +139,18 @@ function caretFromPoint(doc, x, y) {
139
139
  if (doc.caretPositionFromPoint) {
140
140
  try { // Firefox throws for this call in hard-to-predict circumstances (#994)
141
141
  let pos = doc.caretPositionFromPoint(x, y);
142
+ // Clip the offset, because Chrome will return a text offset
143
+ // into <input> nodes, which can't be treated as a regular DOM
144
+ // offset
142
145
  if (pos)
143
- return { node: pos.offsetNode, offset: pos.offset };
146
+ return { node: pos.offsetNode, offset: Math.min(nodeSize(pos.offsetNode), pos.offset) };
144
147
  }
145
148
  catch (_) { }
146
149
  }
147
150
  if (doc.caretRangeFromPoint) {
148
151
  let range = doc.caretRangeFromPoint(x, y);
149
152
  if (range)
150
- return { node: range.startContainer, offset: range.startOffset };
153
+ return { node: range.startContainer, offset: Math.min(nodeSize(range.startContainer), range.startOffset) };
151
154
  }
152
155
  }
153
156
 
@@ -2934,6 +2937,15 @@ let _detachedDoc = null;
2934
2937
  function detachedDoc() {
2935
2938
  return _detachedDoc || (_detachedDoc = document.implementation.createHTMLDocument("title"));
2936
2939
  }
2940
+ function maybeWrapTrusted(html) {
2941
+ let trustedTypes = window.trustedTypes;
2942
+ if (!trustedTypes)
2943
+ return html;
2944
+ // With the require-trusted-types-for CSP, Chrome will block
2945
+ // innerHTML, even on a detached document. This wraps the string in
2946
+ // a way that makes the browser allow us to use its parser again.
2947
+ return trustedTypes.createPolicy("detachedDocument", { createHTML: (s) => s }).createHTML(html);
2948
+ }
2937
2949
  function readHTML(html) {
2938
2950
  let metas = /^(\s*<meta [^>]*>)*/.exec(html);
2939
2951
  if (metas)
@@ -2942,7 +2954,7 @@ function readHTML(html) {
2942
2954
  let firstTag = /<([a-z][^>\s]+)/i.exec(html), wrap;
2943
2955
  if (wrap = firstTag && wrapMap[firstTag[1].toLowerCase()])
2944
2956
  html = wrap.map(n => "<" + n + ">").join("") + html + wrap.map(n => "</" + n + ">").reverse().join("");
2945
- elt.innerHTML = html;
2957
+ elt.innerHTML = maybeWrapTrusted(html);
2946
2958
  if (wrap)
2947
2959
  for (let i = 0; i < wrap.length; i++)
2948
2960
  elt = elt.querySelector(wrap[i]) || elt;
@@ -3083,7 +3095,9 @@ editHandlers.keydown = (view, _event) => {
3083
3095
  // and handling them eagerly tends to corrupt the input.
3084
3096
  if (android && chrome && event.keyCode == 13)
3085
3097
  return;
3086
- if (event.keyCode != 229)
3098
+ if (view.domObserver.selectionChanged(view.domSelectionRange()))
3099
+ view.domObserver.flush();
3100
+ else if (event.keyCode != 229)
3087
3101
  view.domObserver.forceFlush();
3088
3102
  // On iOS, if we preventDefault enter key presses, the virtual
3089
3103
  // keyboard gets confused. So the hack here is to set a flag that
@@ -4606,6 +4620,9 @@ class DOMObserver {
4606
4620
  this.queue.push(mut);
4607
4621
  return this.queue;
4608
4622
  }
4623
+ selectionChanged(sel) {
4624
+ return !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(this.view) && !this.ignoreSelectionChange(sel);
4625
+ }
4609
4626
  flush() {
4610
4627
  let { view } = this;
4611
4628
  if (!view.docView || this.flushingSoon > -1)
@@ -4613,8 +4630,7 @@ class DOMObserver {
4613
4630
  let mutations = this.pendingRecords();
4614
4631
  if (mutations.length)
4615
4632
  this.queue = [];
4616
- let sel = view.domSelectionRange();
4617
- let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel);
4633
+ let sel = view.domSelectionRange(), newSel = this.selectionChanged(sel);
4618
4634
  let from = -1, to = -1, typeOver = false, added = [];
4619
4635
  if (view.editable) {
4620
4636
  for (let i = 0; i < mutations.length; i++) {
@@ -4902,6 +4918,8 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4902
4918
  }
4903
4919
  view.input.lastKeyCode = null;
4904
4920
  let change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide);
4921
+ if (change)
4922
+ view.input.domChangeCount++;
4905
4923
  if ((ios && view.input.lastIOSEnter > Date.now() - 225 || android) &&
4906
4924
  addedNodes.some(n => n.nodeType == 1 && !isInline.test(n.nodeName)) &&
4907
4925
  (!change || change.endA >= change.endB) &&
@@ -4927,7 +4945,6 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4927
4945
  return;
4928
4946
  }
4929
4947
  }
4930
- view.input.domChangeCount++;
4931
4948
  // Handle the case where overwriting a selection by typing matches
4932
4949
  // the start or end of the selected content, creating a change
4933
4950
  // that's smaller than what was actually overwritten.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.34.0",
3
+ "version": "1.34.2",
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
  }
package/src/dom.ts CHANGED
@@ -141,11 +141,14 @@ export function caretFromPoint(doc: Document, x: number, y: number): {node: Node
141
141
  if ((doc as any).caretPositionFromPoint) {
142
142
  try { // Firefox throws for this call in hard-to-predict circumstances (#994)
143
143
  let pos = (doc as any).caretPositionFromPoint(x, y)
144
- if (pos) return {node: pos.offsetNode, offset: pos.offset}
144
+ // Clip the offset, because Chrome will return a text offset
145
+ // into <input> nodes, which can't be treated as a regular DOM
146
+ // offset
147
+ if (pos) return {node: pos.offsetNode, offset: Math.min(nodeSize(pos.offsetNode), pos.offset)}
145
148
  } catch (_) {}
146
149
  }
147
150
  if (doc.caretRangeFromPoint) {
148
151
  let range = doc.caretRangeFromPoint(x, y)
149
- if (range) return {node: range.startContainer, offset: range.startOffset}
152
+ if (range) return {node: range.startContainer, offset: Math.min(nodeSize(range.startContainer), range.startOffset)}
150
153
  }
151
154
  }
package/src/domchange.ts CHANGED
@@ -120,6 +120,7 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
120
120
  view.input.lastKeyCode = null
121
121
 
122
122
  let change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide)
123
+ if (change) view.input.domChangeCount++
123
124
  if ((browser.ios && view.input.lastIOSEnter > Date.now() - 225 || browser.android) &&
124
125
  addedNodes.some(n => n.nodeType == 1 && !isInline.test(n.nodeName)) &&
125
126
  (!change || change.endA >= change.endB) &&
@@ -144,7 +145,6 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
144
145
  }
145
146
  }
146
147
 
147
- view.input.domChangeCount++
148
148
  // Handle the case where overwriting a selection by typing matches
149
149
  // the start or end of the selected content, creating a change
150
150
  // that's smaller than what was actually overwritten.
@@ -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