prosemirror-transform 1.10.5 → 1.12.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,19 @@
1
+ ## 1.12.0 (2026-03-30)
2
+
3
+ ### Bug fixes
4
+
5
+ Handle deletions that go from the beginning of one textblock to the beginning of another specially, deleting the selected textblocks entirely.
6
+
7
+ ### New features
8
+
9
+ `ReplaceStep.MAP_BIAS` can now be adjusted to change the way insertion steps are mapped over other insertions at the same place. The default behavior remains the same for the sake of backwards compatibility.
10
+
11
+ ## 1.11.0 (2026-01-21)
12
+
13
+ ### New features
14
+
15
+ The new `Tranform.changedRange` method gives you a document range that includes all changes made by the transform/transaction.
16
+
1
17
  ## 1.10.5 (2025-11-11)
2
18
 
3
19
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -610,8 +610,8 @@ var ReplaceStep = function (_Step5) {
610
610
  }, {
611
611
  key: "map",
612
612
  value: function map(mapping) {
613
- var from = mapping.mapResult(this.from, 1),
614
- to = mapping.mapResult(this.to, -1);
613
+ var to = mapping.mapResult(this.to, -1);
614
+ var from = this.from == this.to && ReplaceStep.MAP_BIAS < 0 ? to : mapping.mapResult(this.from, 1);
615
615
  if (from.deletedAcross && to.deletedAcross) return null;
616
616
  return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice, this.structure);
617
617
  }
@@ -650,6 +650,7 @@ var ReplaceStep = function (_Step5) {
650
650
  }]);
651
651
  return ReplaceStep;
652
652
  }(Step);
653
+ ReplaceStep.MAP_BIAS = 1;
653
654
  Step.jsonID("replace", ReplaceStep);
654
655
  var ReplaceAroundStep = function (_Step6) {
655
656
  _inherits(ReplaceAroundStep, _Step6);
@@ -1507,6 +1508,18 @@ function _replaceRangeWith(tr, from, to, node) {
1507
1508
  function _deleteRange(tr, from, to) {
1508
1509
  var $from = tr.doc.resolve(from),
1509
1510
  $to = tr.doc.resolve(to);
1511
+ if ($from.parent.isTextblock && $to.parent.isTextblock && $from.start() != $to.start() && $from.parentOffset == 0 && $to.parentOffset == 0) {
1512
+ var shared = $from.sharedDepth(to),
1513
+ isolated = false;
1514
+ for (var d = $from.depth; d > shared; d--) if ($from.node(d).type.spec.isolating) isolated = true;
1515
+ for (var _d4 = $to.depth; _d4 > shared; _d4--) if ($to.node(_d4).type.spec.isolating) isolated = true;
1516
+ if (!isolated) {
1517
+ for (var _d5 = $from.depth; _d5 > 0 && from == $from.start(_d5); _d5--) from = $from.before(_d5);
1518
+ for (var _d6 = $to.depth; _d6 > 0 && to == $to.start(_d6); _d6--) to = $to.before(_d6);
1519
+ $from = tr.doc.resolve(from);
1520
+ $to = tr.doc.resolve(to);
1521
+ }
1522
+ }
1510
1523
  var covered = coveredDepths($from, $to);
1511
1524
  for (var i = 0; i < covered.length; i++) {
1512
1525
  var depth = covered[i],
@@ -1514,8 +1527,8 @@ function _deleteRange(tr, from, to) {
1514
1527
  if (last && depth == 0 || $from.node(depth).type.contentMatch.validEnd) return tr["delete"]($from.start(depth), $to.end(depth));
1515
1528
  if (depth > 0 && (last || $from.node(depth - 1).canReplace($from.index(depth - 1), $to.indexAfter(depth - 1)))) return tr["delete"]($from.before(depth), $to.after(depth));
1516
1529
  }
1517
- for (var d = 1; d <= $from.depth && d <= $to.depth; d++) {
1518
- if (from - $from.start(d) == $from.depth - d && to > $from.end(d) && $to.end(d) - to != $to.depth - d && $from.start(d - 1) == $to.start(d - 1) && $from.node(d - 1).canReplace($from.index(d - 1), $to.index(d - 1))) return tr["delete"]($from.before(d), to);
1530
+ for (var _d7 = 1; _d7 <= $from.depth && _d7 <= $to.depth; _d7++) {
1531
+ if (from - $from.start(_d7) == $from.depth - _d7 && to > $from.end(_d7) && $to.end(_d7) - to != $to.depth - _d7 && $from.start(_d7 - 1) == $to.start(_d7 - 1) && $from.node(_d7 - 1).canReplace($from.index(_d7 - 1), $to.index(_d7 - 1))) return tr["delete"]($from.before(_d7), to);
1519
1532
  }
1520
1533
  tr["delete"](from, to);
1521
1534
  }
@@ -1691,6 +1704,27 @@ var Transform = function () {
1691
1704
  get: function get() {
1692
1705
  return this.steps.length > 0;
1693
1706
  }
1707
+ }, {
1708
+ key: "changedRange",
1709
+ value: function changedRange() {
1710
+ var from = 1e9,
1711
+ to = -1e9;
1712
+ for (var i = 0; i < this.mapping.maps.length; i++) {
1713
+ var map = this.mapping.maps[i];
1714
+ if (i) {
1715
+ from = map.map(from, 1);
1716
+ to = map.map(to, -1);
1717
+ }
1718
+ map.forEach(function (_f, _t, fromB, toB) {
1719
+ from = Math.min(from, fromB);
1720
+ to = Math.max(to, toB);
1721
+ });
1722
+ }
1723
+ return from == 1e9 ? null : {
1724
+ from: from,
1725
+ to: to
1726
+ };
1727
+ }
1694
1728
  }, {
1695
1729
  key: "addStep",
1696
1730
  value: function addStep(step, doc) {
package/dist/index.d.cts CHANGED
@@ -326,6 +326,16 @@ declare class Transform {
326
326
  */
327
327
  get docChanged(): boolean;
328
328
  /**
329
+ Return a single range, in post-transform document positions,
330
+ that covers all content changed by this transform. Returns null
331
+ if no replacements are made. Note that this will ignore changes
332
+ that add/remove marks without replacing the underlying content.
333
+ */
334
+ changedRange(): {
335
+ from: number;
336
+ to: number;
337
+ } | null;
338
+ /**
329
339
  Replace the part of the document between `from` and `to` with the
330
340
  given `slice`.
331
341
  */
@@ -694,6 +704,15 @@ declare class ReplaceStep extends Step {
694
704
  map(mapping: Mappable): ReplaceStep | null;
695
705
  merge(other: Step): ReplaceStep | null;
696
706
  toJSON(): any;
707
+ /**
708
+ By default, for backwards compatibility, an inserting step
709
+ mapped over an insertion at that same position fill move after
710
+ the inserted content. In a collaborative editing situation, that
711
+ can make redone insertions appear in unexpected places. You can
712
+ set this to -1 to make such mapping keep the step before the
713
+ insertion instead.
714
+ */
715
+ static MAP_BIAS: -1 | 1;
697
716
  }
698
717
  /**
699
718
  Replace a part of the document with a slice of content, but
package/dist/index.d.ts CHANGED
@@ -326,6 +326,16 @@ declare class Transform {
326
326
  */
327
327
  get docChanged(): boolean;
328
328
  /**
329
+ Return a single range, in post-transform document positions,
330
+ that covers all content changed by this transform. Returns null
331
+ if no replacements are made. Note that this will ignore changes
332
+ that add/remove marks without replacing the underlying content.
333
+ */
334
+ changedRange(): {
335
+ from: number;
336
+ to: number;
337
+ } | null;
338
+ /**
329
339
  Replace the part of the document between `from` and `to` with the
330
340
  given `slice`.
331
341
  */
@@ -694,6 +704,15 @@ declare class ReplaceStep extends Step {
694
704
  map(mapping: Mappable): ReplaceStep | null;
695
705
  merge(other: Step): ReplaceStep | null;
696
706
  toJSON(): any;
707
+ /**
708
+ By default, for backwards compatibility, an inserting step
709
+ mapped over an insertion at that same position fill move after
710
+ the inserted content. In a collaborative editing situation, that
711
+ can make redone insertions appear in unexpected places. You can
712
+ set this to -1 to make such mapping keep the step before the
713
+ insertion instead.
714
+ */
715
+ static MAP_BIAS: -1 | 1;
697
716
  }
698
717
  /**
699
718
  Replace a part of the document with a slice of content, but
package/dist/index.js CHANGED
@@ -711,7 +711,8 @@ class ReplaceStep extends Step {
711
711
  return new ReplaceStep(this.from, this.from + this.slice.size, doc.slice(this.from, this.to));
712
712
  }
713
713
  map(mapping) {
714
- let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1);
714
+ let to = mapping.mapResult(this.to, -1);
715
+ let from = this.from == this.to && ReplaceStep.MAP_BIAS < 0 ? to : mapping.mapResult(this.from, 1);
715
716
  if (from.deletedAcross && to.deletedAcross)
716
717
  return null;
717
718
  return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice, this.structure);
@@ -750,6 +751,15 @@ class ReplaceStep extends Step {
750
751
  return new ReplaceStep(json.from, json.to, Slice.fromJSON(schema, json.slice), !!json.structure);
751
752
  }
752
753
  }
754
+ /**
755
+ By default, for backwards compatibility, an inserting step
756
+ mapped over an insertion at that same position fill move after
757
+ the inserted content. In a collaborative editing situation, that
758
+ can make redone insertions appear in unexpected places. You can
759
+ set this to -1 to make such mapping keep the step before the
760
+ insertion instead.
761
+ */
762
+ ReplaceStep.MAP_BIAS = 1;
753
763
  Step.jsonID("replace", ReplaceStep);
754
764
  /**
755
765
  Replace a part of the document with a slice of content, but
@@ -1736,6 +1746,26 @@ function replaceRangeWith(tr, from, to, node) {
1736
1746
  }
1737
1747
  function deleteRange(tr, from, to) {
1738
1748
  let $from = tr.doc.resolve(from), $to = tr.doc.resolve(to);
1749
+ // When the deleted range spans from the start of one textblock to
1750
+ // the start of another one, move out of the start of both blocks.
1751
+ if ($from.parent.isTextblock && $to.parent.isTextblock && $from.start() != $to.start() &&
1752
+ $from.parentOffset == 0 && $to.parentOffset == 0) {
1753
+ let shared = $from.sharedDepth(to), isolated = false;
1754
+ for (let d = $from.depth; d > shared; d--)
1755
+ if ($from.node(d).type.spec.isolating)
1756
+ isolated = true;
1757
+ for (let d = $to.depth; d > shared; d--)
1758
+ if ($to.node(d).type.spec.isolating)
1759
+ isolated = true;
1760
+ if (!isolated) {
1761
+ for (let d = $from.depth; d > 0 && from == $from.start(d); d--)
1762
+ from = $from.before(d);
1763
+ for (let d = $to.depth; d > 0 && to == $to.start(d); d--)
1764
+ to = $to.before(d);
1765
+ $from = tr.doc.resolve(from);
1766
+ $to = tr.doc.resolve(to);
1767
+ }
1768
+ }
1739
1769
  let covered = coveredDepths($from, $to);
1740
1770
  for (let i = 0; i < covered.length; i++) {
1741
1771
  let depth = covered[i], last = i == covered.length - 1;
@@ -1946,6 +1976,27 @@ class Transform {
1946
1976
  return this.steps.length > 0;
1947
1977
  }
1948
1978
  /**
1979
+ Return a single range, in post-transform document positions,
1980
+ that covers all content changed by this transform. Returns null
1981
+ if no replacements are made. Note that this will ignore changes
1982
+ that add/remove marks without replacing the underlying content.
1983
+ */
1984
+ changedRange() {
1985
+ let from = 1e9, to = -1e9;
1986
+ for (let i = 0; i < this.mapping.maps.length; i++) {
1987
+ let map = this.mapping.maps[i];
1988
+ if (i) {
1989
+ from = map.map(from, 1);
1990
+ to = map.map(to, -1);
1991
+ }
1992
+ map.forEach((_f, _t, fromB, toB) => {
1993
+ from = Math.min(from, fromB);
1994
+ to = Math.max(to, toB);
1995
+ });
1996
+ }
1997
+ return from == 1e9 ? null : { from, to };
1998
+ }
1999
+ /**
1949
2000
  @internal
1950
2001
  */
1951
2002
  addStep(step, doc) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-transform",
3
- "version": "1.10.5",
3
+ "version": "1.12.0",
4
4
  "description": "ProseMirror document transformations",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/replace.ts CHANGED
@@ -425,6 +425,22 @@ export function replaceRangeWith(tr: Transform, from: number, to: number, node:
425
425
 
426
426
  export function deleteRange(tr: Transform, from: number, to: number) {
427
427
  let $from = tr.doc.resolve(from), $to = tr.doc.resolve(to)
428
+
429
+ // When the deleted range spans from the start of one textblock to
430
+ // the start of another one, move out of the start of both blocks.
431
+ if ($from.parent.isTextblock && $to.parent.isTextblock && $from.start() != $to.start() &&
432
+ $from.parentOffset == 0 && $to.parentOffset == 0) {
433
+ let shared = $from.sharedDepth(to), isolated = false
434
+ for (let d = $from.depth; d > shared; d--) if ($from.node(d).type.spec.isolating) isolated = true
435
+ for (let d = $to.depth; d > shared; d--) if ($to.node(d).type.spec.isolating) isolated = true
436
+ if (!isolated) {
437
+ for (let d = $from.depth; d > 0 && from == $from.start(d); d--) from = $from.before(d)
438
+ for (let d = $to.depth; d > 0 && to == $to.start(d); d--) to = $to.before(d)
439
+ $from = tr.doc.resolve(from)
440
+ $to = tr.doc.resolve(to)
441
+ }
442
+ }
443
+
428
444
  let covered = coveredDepths($from, $to)
429
445
  for (let i = 0; i < covered.length; i++) {
430
446
  let depth = covered[i], last = i == covered.length - 1
@@ -40,7 +40,8 @@ export class ReplaceStep extends Step {
40
40
  }
41
41
 
42
42
  map(mapping: Mappable) {
43
- let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1)
43
+ let to = mapping.mapResult(this.to, -1)
44
+ let from = this.from == this.to && ReplaceStep.MAP_BIAS < 0 ? to : mapping.mapResult(this.from, 1)
44
45
  if (from.deletedAcross && to.deletedAcross) return null
45
46
  return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice, this.structure)
46
47
  }
@@ -74,6 +75,14 @@ export class ReplaceStep extends Step {
74
75
  throw new RangeError("Invalid input for ReplaceStep.fromJSON")
75
76
  return new ReplaceStep(json.from, json.to, Slice.fromJSON(schema, json.slice), !!json.structure)
76
77
  }
78
+
79
+ /// By default, for backwards compatibility, an inserting step
80
+ /// mapped over an insertion at that same position fill move after
81
+ /// the inserted content. In a collaborative editing situation, that
82
+ /// can make redone insertions appear in unexpected places. You can
83
+ /// set this to -1 to make such mapping keep the step before the
84
+ /// insertion instead.
85
+ static MAP_BIAS: -1 | 1 = 1
77
86
  }
78
87
 
79
88
  Step.jsonID("replace", ReplaceStep)
package/src/transform.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import {Node, NodeType, Mark, MarkType, ContentMatch, Slice, Fragment, NodeRange, Attrs} from "prosemirror-model"
2
-
3
2
  import {Mapping} from "./map"
4
3
  import {Step} from "./step"
5
4
  import {addMark, removeMark, clearIncompatible} from "./mark"
@@ -66,6 +65,26 @@ export class Transform {
66
65
  return this.steps.length > 0
67
66
  }
68
67
 
68
+ /// Return a single range, in post-transform document positions,
69
+ /// that covers all content changed by this transform. Returns null
70
+ /// if no replacements are made. Note that this will ignore changes
71
+ /// that add/remove marks without replacing the underlying content.
72
+ changedRange() {
73
+ let from = 1e9, to = -1e9
74
+ for (let i = 0; i < this.mapping.maps.length; i++) {
75
+ let map = this.mapping.maps[i]
76
+ if (i) {
77
+ from = map.map(from, 1)
78
+ to = map.map(to, -1)
79
+ }
80
+ map.forEach((_f, _t, fromB, toB) => {
81
+ from = Math.min(from, fromB)
82
+ to = Math.max(to, toB)
83
+ })
84
+ }
85
+ return from == 1e9 ? null : {from, to}
86
+ }
87
+
69
88
  /// @internal
70
89
  addStep(step: Step, doc: Node) {
71
90
  this.docs.push(this.doc)
@@ -206,7 +225,7 @@ export class Transform {
206
225
  if (mark instanceof Mark) {
207
226
  if (mark.isInSet(node.marks)) this.step(new RemoveNodeMarkStep(pos, mark))
208
227
  } else {
209
- let set = node.marks, found, steps: Step[] = []
228
+ let set = node.marks, found: Mark | undefined, steps: Step[] = []
210
229
  while (found = mark.isInSet(set)) {
211
230
  steps.push(new RemoveNodeMarkStep(pos, found))
212
231
  set = found.removeFromSet(set)