@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-engine",
3
- "version": "45.2.0-alpha.7",
3
+ "version": "45.2.1-alpha.0",
4
4
  "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
5
5
  "keywords": [
6
6
  "wysiwyg",
@@ -24,7 +24,7 @@
24
24
  "type": "module",
25
25
  "main": "src/index.js",
26
26
  "dependencies": {
27
- "@ckeditor/ckeditor5-utils": "45.2.0-alpha.7",
27
+ "@ckeditor/ckeditor5-utils": "45.2.1-alpha.0",
28
28
  "es-toolkit": "1.32.0"
29
29
  },
30
30
  "author": "CKSource (http://cksource.com/)",
@@ -357,6 +357,17 @@ export default class Mapper extends /* #__PURE__ */ Mapper_base {
357
357
  * @returns View position mapped to `targetModelOffset`.
358
358
  */
359
359
  private _findPositionStartingFrom;
360
+ /**
361
+ * Gets the length of the view element in the model and updates cache values after each view item it visits.
362
+ *
363
+ * See also {@link #getModelLength}.
364
+ *
365
+ * @param viewNode View node.
366
+ * @param viewContainer Ancestor of `viewNode` that is a mapped view element.
367
+ * @param modelOffset Model offset at which the `viewNode` starts.
368
+ * @returns Length of the node in the tree model.
369
+ */
370
+ private _getModelLengthAndCache;
360
371
  /**
361
372
  * Because we prefer positions in the text nodes over positions next to text nodes, if the view position was next to a text node,
362
373
  * it moves it into the text node instead.
@@ -494,33 +505,6 @@ export declare class MapperCache extends /* #__PURE__ */ MapperCache_base {
494
505
  * @param modelOffset Model offset in a model element or document fragment, which is mapped to `viewContainer`.
495
506
  */
496
507
  getClosest(viewContainer: ViewElement | ViewDocumentFragment, modelOffset: number): CacheItem;
497
- /**
498
- * Moves a view position to a preferred location.
499
- *
500
- * The view position is moved up from a non-tracked view element as long as it remains at the end of its current parent.
501
- *
502
- * See example below to understand when it is important:
503
- *
504
- * Starting state:
505
- *
506
- * ```
507
- * <p>This is <strong>some <em>heavily <u>formatted</u>^ piece of</em></strong> text.</p>
508
- * ```
509
- *
510
- * Then we remove " piece of " and invalidate some cache:
511
- *
512
- * ```
513
- * <p>This is <strong>some <em>heavily <u>formatted</u>^</em></strong> text.</p>
514
- * ```
515
- *
516
- * 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>`.
517
- * For this scenario, we need to hoist the position.
518
- *
519
- * ```
520
- * <p>This is <strong>some <em>heavily <u>formatted</u></em></strong>^ text.</p>
521
- * ```
522
- */
523
- private _hoistViewPosition;
524
508
  /**
525
509
  * Starts tracking given `viewContainer`, which must be mapped to a model element or model document fragment.
526
510
  *
@@ -564,26 +564,64 @@ export default class Mapper extends /* #__PURE__ */ EmitterMixin() {
564
564
  else {
565
565
  viewOffset = viewParent.parent.getChildIndex(viewParent) + 1;
566
566
  viewParent = viewParent.parent;
567
+ // Cache view position after stepping out of the view element to make sure that all visited view positions are cached.
568
+ // Otherwise, cache invalidation may work incorrectly.
569
+ if (useCache) {
570
+ this._cache.save(viewParent, viewOffset, viewContainer, traversedModelOffset);
571
+ }
567
572
  continue;
568
573
  }
569
574
  }
570
- lastLength = this.getModelLength(viewNode);
575
+ if (useCache) {
576
+ lastLength = this._getModelLengthAndCache(viewNode, viewContainer, traversedModelOffset);
577
+ }
578
+ else {
579
+ lastLength = this.getModelLength(viewNode);
580
+ }
571
581
  traversedModelOffset += lastLength;
572
582
  viewOffset++;
573
- if (useCache) {
574
- // Note, that we cache the view position before and after a visited element here, so before we (possibly) "enter" it
575
- // (see `else` below).
576
- //
577
- // Since `MapperCache#save` does not overwrite already cached model offsets, this way the cached position is set to
578
- // a correct location, that is the closest to the mapped `viewContainer`.
579
- //
580
- // However, in some cases, we still need to "hoist" the cached position (see `MapperCache#_hoistViewPosition()`).
581
- this._cache.save(viewParent, viewOffset, viewContainer, traversedModelOffset);
583
+ }
584
+ let viewPosition = new ViewPosition(viewParent, viewOffset);
585
+ if (useCache) {
586
+ // Make sure to hoist view position and save cache for all view positions along the way that have the same `modelOffset`.
587
+ //
588
+ // Consider example view:
589
+ //
590
+ // <p>Foo<strong><i>bar<em>xyz</em></i></strong>abc</p>
591
+ //
592
+ // Lets assume that we looked for model offset `9`, starting from `6` (as it was previously cached value).
593
+ // In this case we will only traverse over `<em>xyz</em>` and cache view positions after "xyz" and after `</em>`.
594
+ // After stepping over `<em>xyz</em>`, we will stop processing this view, as we will reach target model offset `9`.
595
+ //
596
+ // However, position before `</strong>` and before `abc` are also valid positions for model offset `9`.
597
+ // Additionally, `Mapper` is supposed to return "hoisted" view positions, that is, we prefer positions that are closer to
598
+ // the mapped `viewContainer`. If a position is nested inside attribute elements, it should be "moved up" if possible.
599
+ //
600
+ // As we hoist the view position, we need to make sure that all view positions valid for model offset `9` are cached.
601
+ // This is necessary for cache invalidation to work correctly.
602
+ //
603
+ // 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
604
+ // all necessary values. See example:
605
+ //
606
+ // <p>Foo<strong><i>bar<em>xyz</em>^</i></strong>abc</p>
607
+ // <p>Foo<strong><i>bar<em>xyz</em></i>^</strong>abc</p>
608
+ // <p>Foo<strong><i>bar<em>xyz</em></i></strong>^abc</p>
609
+ //
610
+ while (viewPosition.isAtEnd && viewPosition.parent !== viewContainer && viewPosition.parent.parent) {
611
+ const cacheViewParent = viewPosition.parent.parent;
612
+ const cacheViewOffset = cacheViewParent.getChildIndex(viewPosition.parent) + 1;
613
+ this._cache.save(cacheViewParent, cacheViewOffset, viewContainer, traversedModelOffset);
614
+ viewPosition = new ViewPosition(cacheViewParent, cacheViewOffset);
582
615
  }
583
616
  }
584
617
  if (traversedModelOffset == targetModelOffset) {
585
618
  // If it equals we found the position.
586
- return this._moveViewPositionToTextNode(new ViewPosition(viewParent, viewOffset));
619
+ //
620
+ // Try moving view position into a text node if possible, as the editor engine prefers positions inside view text nodes.
621
+ //
622
+ // <p>Foo<strong><i>bar<em>xyz</em></i></strong>[]abc</p> --> <p>Foo<strong><i>bar<em>xyz</em></i></strong>{}abc</p>
623
+ //
624
+ return this._moveViewPositionToTextNode(viewPosition);
587
625
  }
588
626
  else {
589
627
  // If it is higher we overstepped with the last traversed view node.
@@ -591,6 +629,32 @@ export default class Mapper extends /* #__PURE__ */ EmitterMixin() {
591
629
  return this._findPositionStartingFrom(new ViewPosition(viewNode, 0), traversedModelOffset - lastLength, targetModelOffset, viewContainer, useCache);
592
630
  }
593
631
  }
632
+ /**
633
+ * Gets the length of the view element in the model and updates cache values after each view item it visits.
634
+ *
635
+ * See also {@link #getModelLength}.
636
+ *
637
+ * @param viewNode View node.
638
+ * @param viewContainer Ancestor of `viewNode` that is a mapped view element.
639
+ * @param modelOffset Model offset at which the `viewNode` starts.
640
+ * @returns Length of the node in the tree model.
641
+ */
642
+ _getModelLengthAndCache(viewNode, viewContainer, modelOffset) {
643
+ let len = 0;
644
+ if (this._viewToModelMapping.has(viewNode)) {
645
+ len = 1;
646
+ }
647
+ else if (viewNode.is('$text')) {
648
+ len = viewNode.data.length;
649
+ }
650
+ else if (!viewNode.is('uiElement')) {
651
+ for (const child of viewNode.getChildren()) {
652
+ len += this._getModelLengthAndCache(child, viewContainer, modelOffset + len);
653
+ }
654
+ }
655
+ this._cache.save(viewNode.parent, viewNode.index + 1, viewContainer, modelOffset + len);
656
+ return len;
657
+ }
594
658
  /**
595
659
  * Because we prefer positions in the text nodes over positions next to text nodes, if the view position was next to a text node,
596
660
  * it moves it into the text node instead.
@@ -812,46 +876,11 @@ export class MapperCache extends /* #__PURE__ */ EmitterMixin() {
812
876
  else {
813
877
  result = this.startTracking(viewContainer);
814
878
  }
815
- const viewPosition = this._hoistViewPosition(result.viewPosition);
816
879
  return {
817
880
  modelOffset: result.modelOffset,
818
- viewPosition
881
+ viewPosition: result.viewPosition.clone()
819
882
  };
820
883
  }
821
- /**
822
- * Moves a view position to a preferred location.
823
- *
824
- * The view position is moved up from a non-tracked view element as long as it remains at the end of its current parent.
825
- *
826
- * See example below to understand when it is important:
827
- *
828
- * Starting state:
829
- *
830
- * ```
831
- * <p>This is <strong>some <em>heavily <u>formatted</u>^ piece of</em></strong> text.</p>
832
- * ```
833
- *
834
- * Then we remove " piece of " and invalidate some cache:
835
- *
836
- * ```
837
- * <p>This is <strong>some <em>heavily <u>formatted</u>^</em></strong> text.</p>
838
- * ```
839
- *
840
- * 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>`.
841
- * For this scenario, we need to hoist the position.
842
- *
843
- * ```
844
- * <p>This is <strong>some <em>heavily <u>formatted</u></em></strong>^ text.</p>
845
- * ```
846
- */
847
- _hoistViewPosition(viewPosition) {
848
- while (viewPosition.parent.parent && !this._cachedMapping.has(viewPosition.parent) && viewPosition.isAtEnd) {
849
- const parent = viewPosition.parent.parent;
850
- const offset = parent.getChildIndex(viewPosition.parent) + 1;
851
- viewPosition = new ViewPosition(parent, offset);
852
- }
853
- return viewPosition;
854
- }
855
884
  /**
856
885
  * Starts tracking given `viewContainer`, which must be mapped to a model element or model document fragment.
857
886
  *
@@ -494,6 +494,13 @@ export default class DomConverter {
494
494
  if (startsWithFiller(domParent)) {
495
495
  offset += INLINE_FILLER_LENGTH;
496
496
  }
497
+ // In case someone uses outdated view position, but DOM text node was already changed while typing.
498
+ // See: https://github.com/ckeditor/ckeditor5/issues/18648.
499
+ // Note that when checking Renderer#_isSelectionInInlineFiller() this might be other element
500
+ // than a text node as it is triggered before applying view changes to the DOM.
501
+ if (domParent.data && offset > domParent.data.length) {
502
+ offset = domParent.data.length;
503
+ }
497
504
  return { parent: domParent, offset };
498
505
  }
499
506
  else {