@ckeditor/ckeditor5-engine 43.1.1 → 43.2.0-alpha.1

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/dist/index.js CHANGED
@@ -1621,20 +1621,19 @@ TextProxy$1.prototype.is = function(type) {
1621
1621
  *
1622
1622
  * @param styles Object holding normalized styles.
1623
1623
  */ getStyleNames(styles) {
1624
+ const styleNamesKeysSet = new Set();
1624
1625
  // Find all extractable styles that have a value.
1625
- const expandedStyleNames = Array.from(this._consumables.keys()).filter((name)=>{
1626
+ for (const name of this._consumables.keys()){
1626
1627
  const style = this.getNormalized(name, styles);
1627
- if (style && typeof style == 'object') {
1628
- return Object.keys(style).length;
1628
+ if (style && (typeof style != 'object' || Object.keys(style).length)) {
1629
+ styleNamesKeysSet.add(name);
1629
1630
  }
1630
- return style;
1631
- });
1631
+ }
1632
1632
  // For simple styles (for example `color`) we don't have a map of those styles
1633
1633
  // but they are 1 to 1 with normalized object keys.
1634
- const styleNamesKeysSet = new Set([
1635
- ...expandedStyleNames,
1636
- ...Object.keys(styles)
1637
- ]);
1634
+ for (const name of Object.keys(styles)){
1635
+ styleNamesKeysSet.add(name);
1636
+ }
1638
1637
  return Array.from(styleNamesKeysSet);
1639
1638
  }
1640
1639
  /**
@@ -7705,19 +7704,22 @@ const validNodesToInsert = [
7705
7704
  // in 'this._updateChildrenMappings()' so it will be processed separately.
7706
7705
  return;
7707
7706
  }
7708
- const domAttrKeys = Array.from(domElement.attributes).map((attr)=>attr.name);
7709
- const viewAttrKeys = viewElement.getAttributeKeys();
7710
- // Add or overwrite attributes.
7711
- for (const key of viewAttrKeys){
7712
- this.domConverter.setDomElementAttribute(domElement, key, viewElement.getAttribute(key), viewElement);
7713
- }
7714
- // Remove from DOM attributes which do not exists in the view.
7715
- for (const key of domAttrKeys){
7707
+ // Remove attributes from DOM elements if they do not exist in the view.
7708
+ //
7709
+ // Note: It is important to first remove DOM attributes and then set new ones, because some view attributes may be renamed
7710
+ // as they are set on DOM (due to unsafe attributes handling). If we set the view attribute first, and then remove
7711
+ // non-existing DOM attributes, then we would remove the attribute that we just set.
7712
+ for (const domAttr of domElement.attributes){
7713
+ const key = domAttr.name;
7716
7714
  // All other attributes not present in the DOM should be removed.
7717
7715
  if (!viewElement.hasAttribute(key)) {
7718
7716
  this.domConverter.removeDomElementAttribute(domElement, key);
7719
7717
  }
7720
7718
  }
7719
+ // Add or overwrite attributes.
7720
+ for (const key of viewElement.getAttributeKeys()){
7721
+ this.domConverter.setDomElementAttribute(domElement, key, viewElement.getAttribute(key), viewElement);
7722
+ }
7721
7723
  }
7722
7724
  /**
7723
7725
  * Checks if elements child list needs to be updated and possibly updates it.
@@ -8453,7 +8455,7 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
8453
8455
  if (viewElement) {
8454
8456
  this._domToViewMapping.delete(domElement);
8455
8457
  this._viewToDomMapping.delete(viewElement);
8456
- for (const child of Array.from(domElement.children)){
8458
+ for (const child of domElement.children){
8457
8459
  this.unbindDomElement(child);
8458
8460
  }
8459
8461
  }
@@ -9927,6 +9929,10 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
9927
9929
  * If set to `true` DOM events will be listened on the capturing phase.
9928
9930
  * Default value is `false`.
9929
9931
  */ useCapture = false;
9932
+ /**
9933
+ * If set to `true`, indicates that the function specified by listener will never call `preventDefault()`.
9934
+ * Default value is `false`.
9935
+ */ usePassive = false;
9930
9936
  /**
9931
9937
  * @inheritDoc
9932
9938
  */ observe(domElement) {
@@ -9939,7 +9945,8 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
9939
9945
  this.onDomEvent(domEvent);
9940
9946
  }
9941
9947
  }, {
9942
- useCapture: this.useCapture
9948
+ useCapture: this.useCapture,
9949
+ usePassive: this.usePassive
9943
9950
  });
9944
9951
  });
9945
9952
  }
@@ -10481,7 +10488,7 @@ function sameNodes(child1, child2) {
10481
10488
  priority: 'highest',
10482
10489
  useCapture: true
10483
10490
  });
10484
- this.listenTo(domDocument, 'selectionchange', (evt, domEvent)=>{
10491
+ this.listenTo(domDocument, 'selectionchange', ()=>{
10485
10492
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10486
10493
  // @if CK_DEBUG_TYPING // _debouncedLine();
10487
10494
  // @if CK_DEBUG_TYPING // const domSelection = domDocument.defaultView!.getSelection();
@@ -11909,7 +11916,14 @@ Node$1.prototype.is = function(type) {
11909
11916
  * {@link module:engine/model/node~Node#offsetSize offset sizes} of all nodes that are before this node in this node list.
11910
11917
  */ getNodeStartOffset(node) {
11911
11918
  const index = this.getNodeIndex(node);
11912
- return index === null ? null : this._nodes.slice(0, index).reduce((sum, node)=>sum + node.offsetSize, 0);
11919
+ if (index === null) {
11920
+ return null;
11921
+ }
11922
+ let sum = 0;
11923
+ for(let i = 0; i < index; i++){
11924
+ sum += this._nodes[i].offsetSize;
11925
+ }
11926
+ return sum;
11913
11927
  }
11914
11928
  /**
11915
11929
  * Converts index to offset in node list.
@@ -14520,26 +14534,23 @@ Position.prototype.is = function(type) {
14520
14534
  // If we are going to return just a one range, one of the ranges need to be the reference one.
14521
14535
  // Other ranges will be stuck to that range, if possible.
14522
14536
  const ref = ranges[0];
14523
- // 2. Sort all the ranges so it's easier to process them.
14537
+ // 2. Sort all the ranges, so it's easier to process them.
14524
14538
  ranges.sort((a, b)=>{
14525
14539
  return a.start.isAfter(b.start) ? 1 : -1;
14526
14540
  });
14527
14541
  // 3. Check at which index the reference range is now.
14528
14542
  const refIndex = ranges.indexOf(ref);
14529
14543
  // 4. At this moment we don't need the original range.
14530
- // We are going to modify the result and we need to return a new instance of Range.
14544
+ // We are going to modify the result, and we need to return a new instance of Range.
14531
14545
  // We have to create a copy of the reference range.
14532
14546
  const result = new this(ref.start, ref.end);
14533
14547
  // 5. Ranges should be checked and glued starting from the range that is closest to the reference range.
14534
14548
  // Since ranges are sorted, start with the range with index that is closest to reference range index.
14535
- if (refIndex > 0) {
14536
- // eslint-disable-next-line no-constant-condition
14537
- for(let i = refIndex - 1; true; i++){
14538
- if (ranges[i].end.isEqual(result.start)) {
14539
- result.start = Position._createAt(ranges[i].start);
14540
- } else {
14541
- break;
14542
- }
14549
+ for(let i = refIndex - 1; i >= 0; i--){
14550
+ if (ranges[i].end.isEqual(result.start)) {
14551
+ result.start = Position._createAt(ranges[i].start);
14552
+ } else {
14553
+ break;
14543
14554
  }
14544
14555
  }
14545
14556
  // 6. Ranges should be checked and glued starting from the range that is closest to the reference range.
@@ -26921,6 +26932,8 @@ const transformations = new Map();
26921
26932
  operationsA.splice(i, 1, ...newOpsA);
26922
26933
  operationsB.splice(indexB, 1, ...newOpsB);
26923
26934
  }
26935
+ handlePartialMarkerOperations(operationsA);
26936
+ handlePartialMarkerOperations(operationsB);
26924
26937
  if (options.padWithNoOps) {
26925
26938
  // If no-operations padding is enabled, count how many extra `a` and `b` operations were generated.
26926
26939
  const brokenOperationsACount = operationsA.length - data.originalOperationsACount;
@@ -27036,6 +27049,9 @@ const transformations = new Map();
27036
27049
  } else {
27037
27050
  const range = Range._createFromPositionAndShift(opB.sourcePosition, opB.howMany);
27038
27051
  if (opA.splitPosition.hasSameParentAs(opB.sourcePosition) && range.containsPosition(opA.splitPosition)) {
27052
+ // TODO: Potential bug -- we are saving offset value directly and it is not later updated during OT.
27053
+ // TODO: This may cause a bug it here was an non-undone operation that may have impacted this offset.
27054
+ // TODO: Similar error was with MarkerOperation relations, where full path was saved and never updated.
27039
27055
  const howMany = range.end.offset - opA.splitPosition.offset;
27040
27056
  const offset = opA.splitPosition.offset - range.start.offset;
27041
27057
  this._setRelation(opA, opB, {
@@ -27073,17 +27089,7 @@ const transformations = new Map();
27073
27089
  if (!markerRange) {
27074
27090
  return;
27075
27091
  }
27076
- if (opB instanceof MoveOperation) {
27077
- const movedRange = Range._createFromPositionAndShift(opB.sourcePosition, opB.howMany);
27078
- const affectedLeft = movedRange.containsPosition(markerRange.start) || movedRange.start.isEqual(markerRange.start);
27079
- const affectedRight = movedRange.containsPosition(markerRange.end) || movedRange.end.isEqual(markerRange.end);
27080
- if ((affectedLeft || affectedRight) && !movedRange.containsRange(markerRange)) {
27081
- this._setRelation(opA, opB, {
27082
- side: affectedLeft ? 'left' : 'right',
27083
- path: affectedLeft ? markerRange.start.path.slice() : markerRange.end.path.slice()
27084
- });
27085
- }
27086
- } else if (opB instanceof MergeOperation) {
27092
+ if (opB instanceof MergeOperation) {
27087
27093
  const wasInLeftElement = markerRange.start.isEqual(opB.targetPosition);
27088
27094
  const wasStartBeforeMergedElement = markerRange.start.isEqual(opB.deletionPosition);
27089
27095
  const wasEndBeforeMergedElement = markerRange.end.isEqual(opB.deletionPosition);
@@ -27195,6 +27201,56 @@ const transformations = new Map();
27195
27201
  operations.push(new NoOperation(0));
27196
27202
  }
27197
27203
  }
27204
+ /**
27205
+ * Transformed operations set may include marker operations which were broken into multiple marker operations during transformation.
27206
+ * It represents marker range being broken into multiple pieces as the transformation was processed. Each partial marker operation is
27207
+ * a piece of the original marker range.
27208
+ *
27209
+ * These partial marker operations ("marker range pieces") should be "glued" together if, after transformations, the ranges ended up
27210
+ * next to each other.
27211
+ *
27212
+ * If the ranges did not end up next to each other, then partial marker operations should be discarded, as the marker range cannot
27213
+ * be broken into two pieces.
27214
+ *
27215
+ * There is always one "reference" marker operation (the original operation) and there may be some partial marker operations. Partial
27216
+ * marker operations have base version set to `-1`. If the `operations` set includes partial marker operations, then they are always
27217
+ * after the original marker operation.
27218
+ *
27219
+ * See also `MarkerOperation` x `MoveOperation` transformation.
27220
+ * See also https://github.com/ckeditor/ckeditor5/pull/17071.
27221
+ */ function handlePartialMarkerOperations(operations) {
27222
+ const markerOps = new Map();
27223
+ for(let i = 0; i < operations.length; i++){
27224
+ const op = operations[i];
27225
+ if (!(op instanceof MarkerOperation)) {
27226
+ continue;
27227
+ }
27228
+ if (op.baseVersion !== -1) {
27229
+ markerOps.set(op.name, {
27230
+ op,
27231
+ ranges: op.newRange ? [
27232
+ op.newRange
27233
+ ] : []
27234
+ });
27235
+ } else {
27236
+ if (op.newRange) {
27237
+ // `markerOps.get( op.name )` must exist because original marker operation is always before partial marker operations.
27238
+ // If the original marker operation was changed to `NoOperation`, then the partial marker operations would be changed
27239
+ // to `NoOperation` as well, so this is not a case.
27240
+ markerOps.get(op.name).ranges.push(op.newRange);
27241
+ }
27242
+ operations.splice(i, 1);
27243
+ i--;
27244
+ }
27245
+ }
27246
+ for (const { op, ranges } of markerOps.values()){
27247
+ if (ranges.length) {
27248
+ op.newRange = Range._createFromRanges(ranges);
27249
+ } else {
27250
+ op.newRange = null;
27251
+ }
27252
+ }
27253
+ }
27198
27254
  // -----------------------
27199
27255
  setTransformation(AttributeOperation, AttributeOperation, (a, b, context)=>{
27200
27256
  // If operations in conflict, check if their ranges intersect and manage them properly.
@@ -27562,32 +27618,47 @@ setTransformation(MarkerOperation, MergeOperation, (a, b)=>{
27562
27618
  a
27563
27619
  ];
27564
27620
  });
27565
- setTransformation(MarkerOperation, MoveOperation, (a, b, context)=>{
27621
+ setTransformation(MarkerOperation, MoveOperation, (a, b)=>{
27622
+ const result = [
27623
+ a
27624
+ ];
27566
27625
  if (a.oldRange) {
27567
27626
  a.oldRange = Range._createFromRanges(a.oldRange._getTransformedByMoveOperation(b));
27568
27627
  }
27569
27628
  if (a.newRange) {
27570
- if (context.abRelation) {
27571
- const aNewRange = Range._createFromRanges(a.newRange._getTransformedByMoveOperation(b));
27572
- if (context.abRelation.side == 'left' && b.targetPosition.isEqual(a.newRange.start)) {
27573
- a.newRange.end = aNewRange.end;
27574
- a.newRange.start.path = context.abRelation.path;
27575
- return [
27576
- a
27577
- ];
27578
- } else if (context.abRelation.side == 'right' && b.targetPosition.isEqual(a.newRange.end)) {
27579
- a.newRange.start = aNewRange.start;
27580
- a.newRange.end.path = context.abRelation.path;
27581
- return [
27582
- a
27583
- ];
27584
- }
27629
+ // In many simple cases the marker range will be kept integral after the transformation. For example, if some nodes
27630
+ // were inserted before the range, or into the range, then the marker range is not broken into two.
27631
+ //
27632
+ // However, if some nodes are taken out of the range and moved somewhere else, or are moved into the range, then the marker
27633
+ // range is "broken" into two or three pieces, and these pieces must be transformed and updated separately.
27634
+ //
27635
+ // When the marker range is transformed by move operation, as a result we get an array with one (simple case) or multiple
27636
+ // ("broken range" case) ranges.
27637
+ const ranges = a.newRange._getTransformedByMoveOperation(b);
27638
+ a.newRange = ranges[0];
27639
+ // If there are multiple ranges, we will create separate marker operations for each piece of the original marker range.
27640
+ // Since they will be marker operations, they will be processed through the transformation process.
27641
+ //
27642
+ // However, we cannot create multiple ranges for the same marker (for the same marker name). A marker has only one range.
27643
+ // So, we cannot really have multiple marker operations for the same marker. We will keep the track of the separate marker
27644
+ // operations to see, if after all transformations, the marker pieces are next to each other or not. If so, we will glue
27645
+ // them together to the original marker operation (`a`). If not, we will discard them. These extra operations will never
27646
+ // be executed, as they will only exist temporarily during the transformation process.
27647
+ //
27648
+ // We will call these additional marker operations "partial marker operations" and we will mark them with negative base version.
27649
+ //
27650
+ // See also `handlePartialMarkerOperations()`.
27651
+ // See also https://github.com/ckeditor/ckeditor5/pull/17071.
27652
+ //
27653
+ for(let i = 1; i < ranges.length; i++){
27654
+ const op = a.clone();
27655
+ op.oldRange = null;
27656
+ op.newRange = ranges[i];
27657
+ op.baseVersion = -1;
27658
+ result.push(op);
27585
27659
  }
27586
- a.newRange = Range._createFromRanges(a.newRange._getTransformedByMoveOperation(b));
27587
27660
  }
27588
- return [
27589
- a
27590
- ];
27661
+ return result;
27591
27662
  });
27592
27663
  setTransformation(MarkerOperation, SplitOperation, (a, b, context)=>{
27593
27664
  if (a.oldRange) {
@@ -27600,6 +27671,8 @@ setTransformation(MarkerOperation, SplitOperation, (a, b, context)=>{
27600
27671
  a.newRange.start = Position._createAt(b.insertionPosition);
27601
27672
  } else if (a.newRange.start.isEqual(b.splitPosition) && !context.abRelation.wasInLeftElement) {
27602
27673
  a.newRange.start = Position._createAt(b.moveTargetPosition);
27674
+ } else {
27675
+ a.newRange.start = aNewRange.start;
27603
27676
  }
27604
27677
  if (a.newRange.end.isEqual(b.splitPosition) && context.abRelation.wasInRightElement) {
27605
27678
  a.newRange.end = Position._createAt(b.moveTargetPosition);
@@ -27712,6 +27785,7 @@ setTransformation(MergeOperation, MergeOperation, (a, b, context)=>{
27712
27785
  }
27713
27786
  }
27714
27787
  // The default case.
27788
+ // TODO: Possibly, there's a missing case for same `targetPosition` but different `sourcePosition`.
27715
27789
  //
27716
27790
  if (a.sourcePosition.hasSameParentAs(b.targetPosition)) {
27717
27791
  a.howMany += b.howMany;
@@ -27737,10 +27811,10 @@ setTransformation(MergeOperation, MoveOperation, (a, b, context)=>{
27737
27811
  // was to have it all deleted, together with its children. From user experience point of view, moving back the
27738
27812
  // removed nodes might be unexpected. This means that in this scenario we will block the merging.
27739
27813
  //
27740
- // The exception of this rule would be if the remove operation was later undone.
27814
+ // The exception to this rule would be if the remove operation was later undone.
27741
27815
  //
27742
27816
  const removedRange = Range._createFromPositionAndShift(b.sourcePosition, b.howMany);
27743
- if (b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove) {
27817
+ if (b.type == 'remove' && !context.bWasUndone) {
27744
27818
  if (a.deletionPosition.hasSameParentAs(b.sourcePosition) && removedRange.containsPosition(a.sourcePosition)) {
27745
27819
  return [
27746
27820
  new NoOperation(0)
@@ -27822,59 +27896,66 @@ setTransformation(MergeOperation, SplitOperation, (a, b, context)=>{
27822
27896
  // Case 1:
27823
27897
  //
27824
27898
  // Merge operation moves nodes to the place where split happens.
27825
- // This is a classic situation when there are two paragraphs, and there is a split (enter) after the first
27899
+ //
27900
+ // This is a classic situation when there are two paragraphs, and there is a split (enter) at the end of the first
27826
27901
  // paragraph and there is a merge (delete) at the beginning of the second paragraph:
27827
27902
  //
27828
27903
  // <p>Foo{}</p><p>[]Bar</p>.
27829
27904
  //
27830
- // Split is after `Foo`, while merge is from `Bar` to the end of `Foo`.
27905
+ // User A presses enter after `Foo`, while User B presses backspace before `Bar`. It is intuitive that after both operations, the
27906
+ // editor state should stay the same.
27831
27907
  //
27832
27908
  // State after split:
27833
- // <p>Foo</p><p></p><p>Bar</p>
27909
+ // <p>Foo</p><p></p><p>[]Bar</p>
27834
27910
  //
27835
- // Now, `Bar` should be merged to the new paragraph:
27911
+ // When this happens, `Bar` should be merged to the newly created paragraph, to maintain the editor state:
27836
27912
  // <p>Foo</p><p>Bar</p>
27837
27913
  //
27838
- // Instead of merging it to the original paragraph:
27914
+ // Another option is to merge into the original paragraph `Foo`, according to the `targetPosition`. This results in an incorrect state:
27839
27915
  // <p>FooBar</p><p></p>
27840
27916
  //
27841
- // This means that `targetPosition` needs to be transformed. This is the default case though.
27842
- // For example, if the split would be after `F`, `targetPosition` should also be transformed.
27917
+ // Also, consider an example where User A also writes something in the new paragraph:
27918
+ // <p>Foo</p><p>Xyz</p><p>[]Bar</p>
27919
+ //
27920
+ // In this case it is clear that merge should happen into `[ 1, 3 ]` not into `[ 0, 3 ]`. It first has to be transformed to `[ 1, 0 ]`,
27921
+ // and then transformed be insertion into `[ 1, 3 ]`.
27843
27922
  //
27844
- // There are three exceptions, though, when we want to keep `targetPosition` as it was.
27923
+ // So, usually we want to move `targetPosition` to the new paragraph when it is same as split position. This is how it is handled
27924
+ // in the default transformation (`_getTransformedBySplitOperation()`). We don't need a special case for this.
27845
27925
  //
27846
- // First exception is when the merge target position is inside an element (not at the end, as usual). This
27847
- // happens when the merge operation earlier was transformed by "the same" merge operation. If merge operation
27848
- // targets inside the element we want to keep the original target position (and not transform it) because
27849
- // we have additional context telling us that we want to merge to the original element. We can check if the
27850
- // merge operation points inside element by checking what is `SplitOperation#howMany`. Since merge target position
27851
- // is same as split position, if `howMany` is non-zero, it means that the merge target position is inside an element.
27926
+ // However, there are two exceptions, when we **do not** want to transform `targetPosition`, and we need a special case then.
27852
27927
  //
27853
- // Second exception is when the element to merge is in the graveyard and split operation uses it. In that case
27928
+ // These exceptions happen only if undo is involved. During OT, above presented case (`<p>Foo{}</p><p>[]Bar</p>`) is the only way
27929
+ // how `SplitOperation#splitPosition` and `MergeOperation#targetPosition` can be the same.
27930
+ //
27931
+ // First exception is when the element to merge is in the graveyard and split operation uses it. In that case
27854
27932
  // if target position would be transformed, the merge operation would target at the source position:
27855
27933
  //
27856
- // root: <p>Foo</p> graveyard: <p></p>
27934
+ // root: <p>Foo[]</p> graveyard: <p></p>
27857
27935
  //
27858
27936
  // SplitOperation: root [ 0, 3 ] using graveyard [ 0 ] (howMany = 0)
27859
27937
  // MergeOperation: graveyard [ 0, 0 ] -> root [ 0, 3 ] (howMany = 0)
27860
27938
  //
27861
- // Since split operation moves the graveyard node back to the root, the merge operation source position changes.
27862
- // We would like to merge from the empty <p> to the "Foo" <p>:
27863
- //
27864
- // root: <p>Foo</p><p></p> graveyard:
27939
+ // Since split operation moves the graveyard element back to the root (to path `[ 1 ]`), the merge operation `sourcePosition` changes.
27940
+ // After split we have: `<p>Foo</p><p></p>`, so `sourcePosition` is `[ 1, 0 ]`. But if `targetPosition` is transformed, then it
27941
+ // also becomes `[ 1, 0 ]`. In this case, we want to keep the `targetPosition` as it was.
27865
27942
  //
27866
- // MergeOperation#sourcePosition = root [ 1, 0 ]
27943
+ // Second exception is connected strictly with undo relations. If this `MergeOperation` was earlier transformed by
27944
+ // `MergeOperation` and we stored an information that earlier the target position was not affected, then here, when transforming by
27945
+ // `SplitOperation` we are not going to change it as well.
27867
27946
  //
27868
- // If `targetPosition` is transformed, it would become root [ 1, 0 ] as well. It has to be kept as it was.
27947
+ // For these two cases we will only transform `sourcePosition` and return early.
27869
27948
  //
27870
- // Third exception is connected with relations. If this happens during undo and we have explicit information
27871
- // that target position has not been affected by the operation which is undone by this split then this split should
27872
- // not move the target position either.
27949
+ // Note, that earlier there was also third special case here. `targetPosition` was not transformed, if it pointed into the middle of
27950
+ // target element, not into its end (as usual). This can also happen only with undo involved. However, it wasn't always a correct
27951
+ // solution, as in some cases we actually wanted to transform `targetPosition`. Also, this case usually happens together with the second
27952
+ // case described above. There is only one scenario that we have in our unit tests, where this third case happened without second case.
27953
+ // However, this scenario went fine no matter if we transformed `targetPosition` or not. That's because this happened in the middle
27954
+ // of transformation process and the operation was correctly transformed later on.
27873
27955
  //
27874
27956
  if (a.targetPosition.isEqual(b.splitPosition)) {
27875
- const mergeInside = b.howMany != 0;
27876
27957
  const mergeSplittingElement = b.graveyardPosition && a.deletionPosition.isEqual(b.graveyardPosition);
27877
- if (mergeInside || mergeSplittingElement || context.abRelation == 'mergeTargetNotMoved') {
27958
+ if (mergeSplittingElement || context.abRelation == 'mergeTargetNotMoved') {
27878
27959
  a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation(b);
27879
27960
  return [
27880
27961
  a
@@ -28218,12 +28299,14 @@ setTransformation(MoveOperation, MergeOperation, (a, b, context)=>{
28218
28299
  const results = [];
28219
28300
  let gyMoveSource = b.graveyardPosition.clone();
28220
28301
  let splitNodesMoveSource = b.targetPosition._getTransformedByMergeOperation(b);
28302
+ // `a.targetPosition` points to graveyard, so it was probably affected by `b` (which moved merged element to the graveyard).
28303
+ const aTarget = a.targetPosition.getTransformedByOperation(b);
28221
28304
  if (a.howMany > 1) {
28222
- results.push(new MoveOperation(a.sourcePosition, a.howMany - 1, a.targetPosition, 0));
28223
- gyMoveSource = gyMoveSource._getTransformedByMove(a.sourcePosition, a.targetPosition, a.howMany - 1);
28224
- splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove(a.sourcePosition, a.targetPosition, a.howMany - 1);
28305
+ results.push(new MoveOperation(a.sourcePosition, a.howMany - 1, aTarget, 0));
28306
+ gyMoveSource = gyMoveSource._getTransformedByMove(a.sourcePosition, aTarget, a.howMany - 1);
28307
+ splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove(a.sourcePosition, aTarget, a.howMany - 1);
28225
28308
  }
28226
- const gyMoveTarget = b.deletionPosition._getCombined(a.sourcePosition, a.targetPosition);
28309
+ const gyMoveTarget = b.deletionPosition._getCombined(a.sourcePosition, aTarget);
28227
28310
  const gyMove = new MoveOperation(gyMoveSource, 1, gyMoveTarget, 0);
28228
28311
  const splitNodesMoveTargetPath = gyMove.getMovedRangeStart().path.slice();
28229
28312
  splitNodesMoveTargetPath.push(0);