prosemirror-view 1.40.0 → 1.41.0

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,25 @@
1
+ ## 1.41.0 (2025-09-06)
2
+
3
+ ### Bug fixes
4
+
5
+ Make sure `transformPasted` is properly called when pasting into a code block.
6
+
7
+ Fix an issue where the editor didn't properly set the DOM selection in a shadow DOM on Safari in some situations.
8
+
9
+ Work around a Safari issue that broke composition right after changing the set of active marks.
10
+
11
+ ### New features
12
+
13
+ Pass an argument to `transformPasted` that tells the handler whether the paste is plain text. Call transformPasted when pasting into a code block
14
+
15
+ ## 1.40.1 (2025-07-18)
16
+
17
+ ### Bug fixes
18
+
19
+ Fix an issue where, for some Android virtual keyboards, joining textblocks that contain an image with Backspace would run the handler for Enter instead.
20
+
21
+ Fix an issue causing `posAtCoords` to get confused by table cells sticking out of their parent row elements.
22
+
1
23
  ## 1.40.0 (2025-06-02)
2
24
 
3
25
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -403,7 +403,7 @@ function posFromCaret(view, node, offset, coords) {
403
403
  rect = void 0;
404
404
  if (!desc) return null;
405
405
  if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent || !desc.contentDOM) && ((rect = desc.dom.getBoundingClientRect()).width || rect.height)) {
406
- if (desc.node.isBlock && desc.parent) {
406
+ if (desc.node.isBlock && desc.parent && !/^T(R|BODY|HEAD|FOOT)$/.test(desc.dom.nodeName)) {
407
407
  if (!sawBlock && rect.left > coords.left || rect.top > coords.top) outsideBlock = desc.posBefore;else if (!sawBlock && rect.right < coords.left || rect.bottom < coords.top) outsideBlock = desc.posAfter;
408
408
  sawBlock = true;
409
409
  }
@@ -998,7 +998,7 @@ var ViewDesc = function () {
998
998
  }
999
999
  if (!(force || brKludge && safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) return;
1000
1000
  var domSelExtended = false;
1001
- if ((domSel.extend || anchor == head) && !brKludge) {
1001
+ if ((domSel.extend || anchor == head) && !(brKludge && gecko)) {
1002
1002
  domSel.collapse(anchorDOM.node, anchorDOM.offset);
1003
1003
  try {
1004
1004
  if (anchor != head) domSel.extend(headDOM.node, headDOM.offset);
@@ -2271,15 +2271,11 @@ function removeClassOnSelectionChange(view) {
2271
2271
  });
2272
2272
  }
2273
2273
  function selectCursorWrapper(view) {
2274
- var domSel = view.domSelection(),
2275
- range = document.createRange();
2274
+ var domSel = view.domSelection();
2276
2275
  if (!domSel) return;
2277
2276
  var node = view.cursorWrapper.dom,
2278
2277
  img = node.nodeName == "IMG";
2279
- if (img) range.setStart(node.parentNode, domIndex(node) + 1);else range.setStart(node, 0);
2280
- range.collapse(true);
2281
- domSel.removeAllRanges();
2282
- domSel.addRange(range);
2278
+ if (img) domSel.collapse(node.parentNode, domIndex(node) + 1);else domSel.collapse(node, 0);
2283
2279
  if (!img && !view.state.selection.visible && ie && ie_version <= 11) {
2284
2280
  node.disabled = true;
2285
2281
  node.disabled = false;
@@ -2677,12 +2673,18 @@ function parseFromClipboard(view, text, html, plainText, $context) {
2677
2673
  var inCode = $context.parent.type.spec.code;
2678
2674
  var dom, slice;
2679
2675
  if (!html && !text) return null;
2680
- var asText = text && (plainText || inCode || !html);
2676
+ var asText = !!text && (plainText || inCode || !html);
2681
2677
  if (asText) {
2682
2678
  view.someProp("transformPastedText", function (f) {
2683
2679
  text = f(text, inCode || plainText, view);
2684
2680
  });
2685
- if (inCode) return text ? new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0) : prosemirrorModel.Slice.empty;
2681
+ if (inCode) {
2682
+ slice = new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0);
2683
+ view.someProp("transformPasted", function (f) {
2684
+ slice = f(slice, view, true);
2685
+ });
2686
+ return slice;
2687
+ }
2686
2688
  var parsed = view.someProp("clipboardTextParser", function (f) {
2687
2689
  return f(text, $context, plainText, view);
2688
2690
  });
@@ -2739,7 +2741,7 @@ function parseFromClipboard(view, text, html, plainText, $context) {
2739
2741
  }
2740
2742
  }
2741
2743
  view.someProp("transformPasted", function (f) {
2742
- slice = f(slice, view);
2744
+ slice = f(slice, view, asText);
2743
2745
  });
2744
2746
  return slice;
2745
2747
  }
@@ -3492,7 +3494,7 @@ editHandlers.drop = function (view, _event) {
3492
3494
  var slice = dragging && dragging.slice;
3493
3495
  if (slice) {
3494
3496
  view.someProp("transformPasted", function (f) {
3495
- slice = f(slice, view);
3497
+ slice = f(slice, view, false);
3496
3498
  });
3497
3499
  } else {
3498
3500
  slice = parseFromClipboard(view, getText(event.dataTransfer), brokenClipboardAPI ? null : event.dataTransfer.getData("text/html"), false, $mouse);
@@ -4605,7 +4607,7 @@ function ruleFromNode(dom) {
4605
4607
  }
4606
4608
  return null;
4607
4609
  }
4608
- var isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;
4610
+ var isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|img|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;
4609
4611
  function readDOMChange(view, from, to, typeOver, addedNodes) {
4610
4612
  var compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0);
4611
4613
  view.input.compositionPendingChanges = 0;
package/dist/index.d.cts CHANGED
@@ -757,9 +757,10 @@ interface EditorProps<P = any> {
757
757
  clipboardTextParser?: (this: P, text: string, $context: ResolvedPos, plain: boolean, view: EditorView) => Slice;
758
758
  /**
759
759
  Can be used to transform pasted or dragged-and-dropped content
760
- before it is applied to the document.
760
+ before it is applied to the document. The `plain` flag will be
761
+ true when the text is pasted as plain text.
761
762
  */
762
- transformPasted?: (this: P, slice: Slice, view: EditorView) => Slice;
763
+ transformPasted?: (this: P, slice: Slice, view: EditorView, plain: boolean) => Slice;
763
764
  /**
764
765
  Can be used to transform copied or cut content before it is
765
766
  serialized to the clipboard.
package/dist/index.d.ts CHANGED
@@ -757,9 +757,10 @@ interface EditorProps<P = any> {
757
757
  clipboardTextParser?: (this: P, text: string, $context: ResolvedPos, plain: boolean, view: EditorView) => Slice;
758
758
  /**
759
759
  Can be used to transform pasted or dragged-and-dropped content
760
- before it is applied to the document.
760
+ before it is applied to the document. The `plain` flag will be
761
+ true when the text is pasted as plain text.
761
762
  */
762
- transformPasted?: (this: P, slice: Slice, view: EditorView) => Slice;
763
+ transformPasted?: (this: P, slice: Slice, view: EditorView, plain: boolean) => Slice;
763
764
  /**
764
765
  Can be used to transform copied or cut content before it is
765
766
  serialized to the clipboard.
package/dist/index.js CHANGED
@@ -414,7 +414,7 @@ function posFromCaret(view, node, offset, coords) {
414
414
  if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent || !desc.contentDOM) &&
415
415
  // Ignore elements with zero-size bounding rectangles
416
416
  ((rect = desc.dom.getBoundingClientRect()).width || rect.height)) {
417
- if (desc.node.isBlock && desc.parent) {
417
+ if (desc.node.isBlock && desc.parent && !/^T(R|BODY|HEAD|FOOT)$/.test(desc.dom.nodeName)) {
418
418
  // Only apply the horizontal test to the innermost block. Vertical for any parent.
419
419
  if (!sawBlock && rect.left > coords.left || rect.top > coords.top)
420
420
  outsideBlock = desc.posBefore;
@@ -1072,7 +1072,7 @@ class ViewDesc {
1072
1072
  // (one where the focus is before the anchor), but not all
1073
1073
  // browsers support it yet.
1074
1074
  let domSelExtended = false;
1075
- if ((domSel.extend || anchor == head) && !brKludge) {
1075
+ if ((domSel.extend || anchor == head) && !(brKludge && gecko)) {
1076
1076
  domSel.collapse(anchorDOM.node, anchorDOM.offset);
1077
1077
  try {
1078
1078
  if (anchor != head)
@@ -2333,17 +2333,14 @@ function removeClassOnSelectionChange(view) {
2333
2333
  });
2334
2334
  }
2335
2335
  function selectCursorWrapper(view) {
2336
- let domSel = view.domSelection(), range = document.createRange();
2336
+ let domSel = view.domSelection();
2337
2337
  if (!domSel)
2338
2338
  return;
2339
2339
  let node = view.cursorWrapper.dom, img = node.nodeName == "IMG";
2340
2340
  if (img)
2341
- range.setStart(node.parentNode, domIndex(node) + 1);
2341
+ domSel.collapse(node.parentNode, domIndex(node) + 1);
2342
2342
  else
2343
- range.setStart(node, 0);
2344
- range.collapse(true);
2345
- domSel.removeAllRanges();
2346
- domSel.addRange(range);
2343
+ domSel.collapse(node, 0);
2347
2344
  // Kludge to kill 'control selection' in IE11 when selecting an
2348
2345
  // invisible cursor wrapper, since that would result in those weird
2349
2346
  // resize handles and a selection that considers the absolutely
@@ -2821,11 +2818,14 @@ function parseFromClipboard(view, text, html, plainText, $context) {
2821
2818
  let dom, slice;
2822
2819
  if (!html && !text)
2823
2820
  return null;
2824
- let asText = text && (plainText || inCode || !html);
2821
+ let asText = !!text && (plainText || inCode || !html);
2825
2822
  if (asText) {
2826
2823
  view.someProp("transformPastedText", f => { text = f(text, inCode || plainText, view); });
2827
- if (inCode)
2828
- return text ? new Slice(Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0) : Slice.empty;
2824
+ if (inCode) {
2825
+ slice = new Slice(Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0);
2826
+ view.someProp("transformPasted", f => { slice = f(slice, view, true); });
2827
+ return slice;
2828
+ }
2829
2829
  let parsed = view.someProp("clipboardTextParser", f => f(text, $context, plainText, view));
2830
2830
  if (parsed) {
2831
2831
  slice = parsed;
@@ -2883,7 +2883,7 @@ function parseFromClipboard(view, text, html, plainText, $context) {
2883
2883
  slice = closeSlice(slice, openStart, openEnd);
2884
2884
  }
2885
2885
  }
2886
- view.someProp("transformPasted", f => { slice = f(slice, view); });
2886
+ view.someProp("transformPasted", f => { slice = f(slice, view, asText); });
2887
2887
  return slice;
2888
2888
  }
2889
2889
  const inlineParents = /^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/i;
@@ -3743,7 +3743,7 @@ editHandlers.drop = (view, _event) => {
3743
3743
  let $mouse = view.state.doc.resolve(eventPos.pos);
3744
3744
  let slice = dragging && dragging.slice;
3745
3745
  if (slice) {
3746
- view.someProp("transformPasted", f => { slice = f(slice, view); });
3746
+ view.someProp("transformPasted", f => { slice = f(slice, view, false); });
3747
3747
  }
3748
3748
  else {
3749
3749
  slice = parseFromClipboard(view, getText(event.dataTransfer), brokenClipboardAPI ? null : event.dataTransfer.getData("text/html"), false, $mouse);
@@ -4929,7 +4929,7 @@ function ruleFromNode(dom) {
4929
4929
  }
4930
4930
  return null;
4931
4931
  }
4932
- const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;
4932
+ const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|img|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;
4933
4933
  function readDOMChange(view, from, to, typeOver, addedNodes) {
4934
4934
  let compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0);
4935
4935
  view.input.compositionPendingChanges = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/clipboard.ts CHANGED
@@ -44,10 +44,14 @@ export function parseFromClipboard(view: EditorView, text: string, html: string
44
44
  let inCode = $context.parent.type.spec.code
45
45
  let dom: HTMLElement | undefined, slice: Slice | undefined
46
46
  if (!html && !text) return null
47
- let asText = text && (plainText || inCode || !html)
47
+ let asText = !!text && (plainText || inCode || !html)
48
48
  if (asText) {
49
49
  view.someProp("transformPastedText", f => { text = f(text, inCode || plainText, view) })
50
- if (inCode) return text ? new Slice(Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0) : Slice.empty
50
+ if (inCode) {
51
+ slice = new Slice(Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0)
52
+ view.someProp("transformPasted", f => { slice = f(slice!, view, true) })
53
+ return slice
54
+ }
51
55
  let parsed = view.someProp("clipboardTextParser", f => f(text, $context, plainText, view))
52
56
  if (parsed) {
53
57
  slice = parsed
@@ -101,7 +105,7 @@ export function parseFromClipboard(view: EditorView, text: string, html: string
101
105
  }
102
106
  }
103
107
 
104
- view.someProp("transformPasted", f => { slice = f(slice!, view) })
108
+ view.someProp("transformPasted", f => { slice = f(slice!, view, asText) })
105
109
  return slice
106
110
  }
107
111
 
package/src/domchange.ts CHANGED
@@ -76,7 +76,7 @@ function ruleFromNode(dom: DOMNode): Omit<TagParseRule, "tag"> | null {
76
76
  return null
77
77
  }
78
78
 
79
- const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i
79
+ const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|img|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i
80
80
 
81
81
  export function readDOMChange(view: EditorView, from: number, to: number, typeOver: boolean, addedNodes: readonly DOMNode[]) {
82
82
  let compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0)
package/src/domcoords.ts CHANGED
@@ -232,7 +232,7 @@ function posFromCaret(view: EditorView, node: Node, offset: number, coords: {top
232
232
  if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent || !desc.contentDOM) &&
233
233
  // Ignore elements with zero-size bounding rectangles
234
234
  ((rect = (desc.dom as HTMLElement).getBoundingClientRect()).width || rect.height)) {
235
- if (desc.node.isBlock && desc.parent) {
235
+ if (desc.node.isBlock && desc.parent && !/^T(R|BODY|HEAD|FOOT)$/.test(desc.dom!.nodeName)) {
236
236
  // Only apply the horizontal test to the innermost block. Vertical for any parent.
237
237
  if (!sawBlock && rect.left > coords.left || rect.top > coords.top) outsideBlock = desc.posBefore
238
238
  else if (!sawBlock && rect.right < coords.left || rect.bottom < coords.top) outsideBlock = desc.posAfter
package/src/index.ts CHANGED
@@ -714,8 +714,9 @@ export interface EditorProps<P = any> {
714
714
  clipboardTextParser?: (this: P, text: string, $context: ResolvedPos, plain: boolean, view: EditorView) => Slice
715
715
 
716
716
  /// Can be used to transform pasted or dragged-and-dropped content
717
- /// before it is applied to the document.
718
- transformPasted?: (this: P, slice: Slice, view: EditorView) => Slice
717
+ /// before it is applied to the document. The `plain` flag will be
718
+ /// true when the text is pasted as plain text.
719
+ transformPasted?: (this: P, slice: Slice, view: EditorView, plain: boolean) => Slice
719
720
 
720
721
  /// Can be used to transform copied or cut content before it is
721
722
  /// serialized to the clipboard.
package/src/input.ts CHANGED
@@ -718,7 +718,7 @@ editHandlers.drop = (view, _event) => {
718
718
  let $mouse = view.state.doc.resolve(eventPos.pos)
719
719
  let slice = dragging && dragging.slice
720
720
  if (slice) {
721
- view.someProp("transformPasted", f => { slice = f(slice!, view) })
721
+ view.someProp("transformPasted", f => { slice = f(slice!, view, false) })
722
722
  } else {
723
723
  slice = parseFromClipboard(view, getText(event.dataTransfer),
724
724
  brokenClipboardAPI ? null : event.dataTransfer.getData("text/html"), false, $mouse)
package/src/selection.ts CHANGED
@@ -147,14 +147,11 @@ function removeClassOnSelectionChange(view: EditorView) {
147
147
  }
148
148
 
149
149
  function selectCursorWrapper(view: EditorView) {
150
- let domSel = view.domSelection(), range = document.createRange()
150
+ let domSel = view.domSelection()
151
151
  if (!domSel) return
152
152
  let node = view.cursorWrapper!.dom, img = node.nodeName == "IMG"
153
- if (img) range.setStart(node.parentNode!, domIndex(node) + 1)
154
- else range.setStart(node, 0)
155
- range.collapse(true)
156
- domSel.removeAllRanges()
157
- domSel.addRange(range)
153
+ if (img) domSel.collapse(node.parentNode!, domIndex(node) + 1)
154
+ else domSel.collapse(node, 0)
158
155
  // Kludge to kill 'control selection' in IE11 when selecting an
159
156
  // invisible cursor wrapper, since that would result in those weird
160
157
  // resize handles and a selection that considers the absolutely
package/src/viewdesc.ts CHANGED
@@ -460,7 +460,7 @@ export class ViewDesc {
460
460
  // (one where the focus is before the anchor), but not all
461
461
  // browsers support it yet.
462
462
  let domSelExtended = false
463
- if ((domSel.extend || anchor == head) && !brKludge) {
463
+ if ((domSel.extend || anchor == head) && !(brKludge && browser.gecko)) {
464
464
  domSel.collapse(anchorDOM.node, anchorDOM.offset)
465
465
  try {
466
466
  if (anchor != head)