@ckeditor/ckeditor5-engine 45.2.0-alpha.7 → 45.2.1-alpha.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/dist/index.js CHANGED
@@ -9826,6 +9826,13 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
9826
9826
  if (startsWithFiller(domParent)) {
9827
9827
  offset += INLINE_FILLER_LENGTH;
9828
9828
  }
9829
+ // In case someone uses outdated view position, but DOM text node was already changed while typing.
9830
+ // See: https://github.com/ckeditor/ckeditor5/issues/18648.
9831
+ // Note that when checking Renderer#_isSelectionInInlineFiller() this might be other element
9832
+ // than a text node as it is triggered before applying view changes to the DOM.
9833
+ if (domParent.data && offset > domParent.data.length) {
9834
+ offset = domParent.data.length;
9835
+ }
9829
9836
  return {
9830
9837
  parent: domParent,
9831
9838
  offset
@@ -15626,32 +15633,92 @@ Range.prototype.is = function(type) {
15626
15633
  } else {
15627
15634
  viewOffset = viewParent.parent.getChildIndex(viewParent) + 1;
15628
15635
  viewParent = viewParent.parent;
15636
+ // Cache view position after stepping out of the view element to make sure that all visited view positions are cached.
15637
+ // Otherwise, cache invalidation may work incorrectly.
15638
+ if (useCache) {
15639
+ this._cache.save(viewParent, viewOffset, viewContainer, traversedModelOffset);
15640
+ }
15629
15641
  continue;
15630
15642
  }
15631
15643
  }
15632
- lastLength = this.getModelLength(viewNode);
15644
+ if (useCache) {
15645
+ lastLength = this._getModelLengthAndCache(viewNode, viewContainer, traversedModelOffset);
15646
+ } else {
15647
+ lastLength = this.getModelLength(viewNode);
15648
+ }
15633
15649
  traversedModelOffset += lastLength;
15634
15650
  viewOffset++;
15635
- if (useCache) {
15636
- // Note, that we cache the view position before and after a visited element here, so before we (possibly) "enter" it
15637
- // (see `else` below).
15638
- //
15639
- // Since `MapperCache#save` does not overwrite already cached model offsets, this way the cached position is set to
15640
- // a correct location, that is the closest to the mapped `viewContainer`.
15641
- //
15642
- // However, in some cases, we still need to "hoist" the cached position (see `MapperCache#_hoistViewPosition()`).
15643
- this._cache.save(viewParent, viewOffset, viewContainer, traversedModelOffset);
15651
+ }
15652
+ let viewPosition = new Position$1(viewParent, viewOffset);
15653
+ if (useCache) {
15654
+ // Make sure to hoist view position and save cache for all view positions along the way that have the same `modelOffset`.
15655
+ //
15656
+ // Consider example view:
15657
+ //
15658
+ // <p>Foo<strong><i>bar<em>xyz</em></i></strong>abc</p>
15659
+ //
15660
+ // Lets assume that we looked for model offset `9`, starting from `6` (as it was previously cached value).
15661
+ // In this case we will only traverse over `<em>xyz</em>` and cache view positions after "xyz" and after `</em>`.
15662
+ // After stepping over `<em>xyz</em>`, we will stop processing this view, as we will reach target model offset `9`.
15663
+ //
15664
+ // However, position before `</strong>` and before `abc` are also valid positions for model offset `9`.
15665
+ // Additionally, `Mapper` is supposed to return "hoisted" view positions, that is, we prefer positions that are closer to
15666
+ // the mapped `viewContainer`. If a position is nested inside attribute elements, it should be "moved up" if possible.
15667
+ //
15668
+ // As we hoist the view position, we need to make sure that all view positions valid for model offset `9` are cached.
15669
+ // This is necessary for cache invalidation to work correctly.
15670
+ //
15671
+ // To hoist a view position, we "go up" as long as the position is at the end of a non-mapped view element. We also cache
15672
+ // all necessary values. See example:
15673
+ //
15674
+ // <p>Foo<strong><i>bar<em>xyz</em>^</i></strong>abc</p>
15675
+ // <p>Foo<strong><i>bar<em>xyz</em></i>^</strong>abc</p>
15676
+ // <p>Foo<strong><i>bar<em>xyz</em></i></strong>^abc</p>
15677
+ //
15678
+ while(viewPosition.isAtEnd && viewPosition.parent !== viewContainer && viewPosition.parent.parent){
15679
+ const cacheViewParent = viewPosition.parent.parent;
15680
+ const cacheViewOffset = cacheViewParent.getChildIndex(viewPosition.parent) + 1;
15681
+ this._cache.save(cacheViewParent, cacheViewOffset, viewContainer, traversedModelOffset);
15682
+ viewPosition = new Position$1(cacheViewParent, cacheViewOffset);
15644
15683
  }
15645
15684
  }
15646
15685
  if (traversedModelOffset == targetModelOffset) {
15647
15686
  // If it equals we found the position.
15648
- return this._moveViewPositionToTextNode(new Position$1(viewParent, viewOffset));
15687
+ //
15688
+ // Try moving view position into a text node if possible, as the editor engine prefers positions inside view text nodes.
15689
+ //
15690
+ // <p>Foo<strong><i>bar<em>xyz</em></i></strong>[]abc</p> --> <p>Foo<strong><i>bar<em>xyz</em></i></strong>{}abc</p>
15691
+ //
15692
+ return this._moveViewPositionToTextNode(viewPosition);
15649
15693
  } else {
15650
15694
  // If it is higher we overstepped with the last traversed view node.
15651
15695
  // We need to "enter" it, and look for the view position / model offset inside the last visited view node.
15652
15696
  return this._findPositionStartingFrom(new Position$1(viewNode, 0), traversedModelOffset - lastLength, targetModelOffset, viewContainer, useCache);
15653
15697
  }
15654
15698
  }
15699
+ /**
15700
+ * Gets the length of the view element in the model and updates cache values after each view item it visits.
15701
+ *
15702
+ * See also {@link #getModelLength}.
15703
+ *
15704
+ * @param viewNode View node.
15705
+ * @param viewContainer Ancestor of `viewNode` that is a mapped view element.
15706
+ * @param modelOffset Model offset at which the `viewNode` starts.
15707
+ * @returns Length of the node in the tree model.
15708
+ */ _getModelLengthAndCache(viewNode, viewContainer, modelOffset) {
15709
+ let len = 0;
15710
+ if (this._viewToModelMapping.has(viewNode)) {
15711
+ len = 1;
15712
+ } else if (viewNode.is('$text')) {
15713
+ len = viewNode.data.length;
15714
+ } else if (!viewNode.is('uiElement')) {
15715
+ for (const child of viewNode.getChildren()){
15716
+ len += this._getModelLengthAndCache(child, viewContainer, modelOffset + len);
15717
+ }
15718
+ }
15719
+ this._cache.save(viewNode.parent, viewNode.index + 1, viewContainer, modelOffset + len);
15720
+ return len;
15721
+ }
15655
15722
  /**
15656
15723
  * Because we prefer positions in the text nodes over positions next to text nodes, if the view position was next to a text node,
15657
15724
  * it moves it into the text node instead.
@@ -15864,45 +15931,11 @@ Range.prototype.is = function(type) {
15864
15931
  } else {
15865
15932
  result = this.startTracking(viewContainer);
15866
15933
  }
15867
- const viewPosition = this._hoistViewPosition(result.viewPosition);
15868
15934
  return {
15869
15935
  modelOffset: result.modelOffset,
15870
- viewPosition
15936
+ viewPosition: result.viewPosition.clone()
15871
15937
  };
15872
15938
  }
15873
- /**
15874
- * Moves a view position to a preferred location.
15875
- *
15876
- * The view position is moved up from a non-tracked view element as long as it remains at the end of its current parent.
15877
- *
15878
- * See example below to understand when it is important:
15879
- *
15880
- * Starting state:
15881
- *
15882
- * ```
15883
- * <p>This is <strong>some <em>heavily <u>formatted</u>^ piece of</em></strong> text.</p>
15884
- * ```
15885
- *
15886
- * Then we remove " piece of " and invalidate some cache:
15887
- *
15888
- * ```
15889
- * <p>This is <strong>some <em>heavily <u>formatted</u>^</em></strong> text.</p>
15890
- * ```
15891
- *
15892
- * Now, if we ask for model offset after letter "d" in "formatted", we should get a position in " text", but we will get in `<em>`.
15893
- * For this scenario, we need to hoist the position.
15894
- *
15895
- * ```
15896
- * <p>This is <strong>some <em>heavily <u>formatted</u></em></strong>^ text.</p>
15897
- * ```
15898
- */ _hoistViewPosition(viewPosition) {
15899
- while(viewPosition.parent.parent && !this._cachedMapping.has(viewPosition.parent) && viewPosition.isAtEnd){
15900
- const parent = viewPosition.parent.parent;
15901
- const offset = parent.getChildIndex(viewPosition.parent) + 1;
15902
- viewPosition = new Position$1(parent, offset);
15903
- }
15904
- return viewPosition;
15905
- }
15906
15939
  /**
15907
15940
  * Starts tracking given `viewContainer`, which must be mapped to a model element or model document fragment.
15908
15941
  *