@ckeditor/ckeditor5-engine 45.2.1-alpha.10 → 45.2.1-alpha.3

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.1-alpha.10",
3
+ "version": "45.2.1-alpha.3",
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.1-alpha.10",
27
+ "@ckeditor/ckeditor5-utils": "45.2.1-alpha.3",
28
28
  "es-toolkit": "1.32.0"
29
29
  },
30
30
  "author": "CKSource (http://cksource.com/)",
@@ -5,13 +5,14 @@
5
5
  /**
6
6
  * @module engine/controller/editingcontroller
7
7
  */
8
- import { CKEditorError, ObservableMixin } from '@ckeditor/ckeditor5-utils';
8
+ import { CKEditorError, ObservableMixin, env } from '@ckeditor/ckeditor5-utils';
9
9
  import RootEditableElement from '../view/rooteditableelement.js';
10
10
  import View from '../view/view.js';
11
11
  import Mapper from '../conversion/mapper.js';
12
12
  import DowncastDispatcher from '../conversion/downcastdispatcher.js';
13
13
  import { cleanSelection, convertCollapsedSelection, convertRangeSelection, insertAttributesAndChildren, insertText, remove } from '../conversion/downcasthelpers.js';
14
14
  import { convertSelectionChange } from '../conversion/upcasthelpers.js';
15
+ import { tryFixingRange } from '../model/utils/selection-post-fixer.js';
15
16
  // @if CK_DEBUG_ENGINE // const { dumpTrees, initDocumentDumping } = require( '../dev-utils/utils' );
16
17
  /**
17
18
  * A controller for the editing pipeline. The editing pipeline controls the {@link ~EditingController#model model} rendering,
@@ -75,6 +76,8 @@ export default class EditingController extends /* #__PURE__ */ ObservableMixin()
75
76
  }, { priority: 'low' });
76
77
  // Convert selection from the view to the model when it changes in the view.
77
78
  this.listenTo(this.view.document, 'selectionChange', convertSelectionChange(this.model, this.mapper));
79
+ // Fix `beforeinput` target ranges so that they map to the valid model ranges.
80
+ this.listenTo(this.view.document, 'beforeinput', fixTargetRanges(this.mapper, this.model.schema, this.view), { priority: 'high' });
78
81
  // Attach default model converters.
79
82
  this.downcastDispatcher.on('insert:$text', insertText(), { priority: 'lowest' });
80
83
  this.downcastDispatcher.on('insert', insertAttributesAndChildren(), { priority: 'lowest' });
@@ -179,3 +182,26 @@ export default class EditingController extends /* #__PURE__ */ ObservableMixin()
179
182
  });
180
183
  }
181
184
  }
185
+ /**
186
+ * Checks whether the target ranges provided by the `beforeInput` event can be properly mapped to model ranges and fixes them if needed.
187
+ *
188
+ * This is using the same logic as the selection post-fixer.
189
+ */
190
+ function fixTargetRanges(mapper, schema, view) {
191
+ return (evt, data) => {
192
+ // The Renderer is disabled while composing on non-android browsers, so we can't be sure that target ranges
193
+ // could be properly mapped to view and model because the DOM and view tree drifted apart.
194
+ if (view.document.isComposing && !env.isAndroid) {
195
+ return;
196
+ }
197
+ for (let i = 0; i < data.targetRanges.length; i++) {
198
+ const viewRange = data.targetRanges[i];
199
+ const modelRange = mapper.toModelRange(viewRange);
200
+ const correctedRange = tryFixingRange(modelRange, schema);
201
+ if (!correctedRange || correctedRange.isEqual(modelRange)) {
202
+ continue;
203
+ }
204
+ data.targetRanges[i] = mapper.toViewRange(correctedRange);
205
+ }
206
+ };
207
+ }
@@ -412,7 +412,7 @@ declare const MapperCache_base: {
412
412
  * * it is an internal tool, used by `Mapper`, transparent to the outside (no additional effort when developing a plugin or a converter),
413
413
  * * it stores all the necessary data internally, which makes it easier to disable or debug,
414
414
  * * it is optimized for initial downcast process (long insertions), which is crucial for editor init and data save,
415
- * * it does not save all possible positions mapping for memory considerations, although it is a possible improvement, which may increase
415
+ * * it does not save all possible positions for memory considerations, although it is a possible improvement, which may have increase
416
416
  * performance, as well as simplify some parts of the `MapperCache` logic.
417
417
  *
418
418
  * @internal
@@ -429,8 +429,13 @@ export declare class MapperCache extends /* #__PURE__ */ MapperCache_base {
429
429
  * values for model offsets that are after a view node. So, in essence, positions inside text nodes are not cached. However, it takes
430
430
  * from one to at most a few steps, to get from a cached position to a position that is inside a view text node.
431
431
  *
432
- * Additionally, only one item per `modelOffset` is cached. There can be several view positions that map to the same `modelOffset`.
433
- * Only the first save for `modelOffset` is stored.
432
+ * Additionally, only one item per `modelOffset` is cached. There can be several view nodes that "end" at the same `modelOffset`.
433
+ * In this case, we favour positions that are closer to the mapped item. For example
434
+ *
435
+ * * for model: `<paragraph>Some <$text bold=true italic=true>formatted</$text> text.</paragraph>`,
436
+ * * and view: `<p>Some <em><strong>formatted</strong></em> text.</p>`,
437
+ *
438
+ * for model offset `14` (after "d"), we store view position after `<em>` element (i.e. view position: at `<p>`, offset `2`).
434
439
  */
435
440
  private _cachedMapping;
436
441
  /**
@@ -464,9 +469,6 @@ export declare class MapperCache extends /* #__PURE__ */ MapperCache_base {
464
469
  * Saves cache for given view position mapping <-> model offset mapping. The view position should be after a node (i.e. it cannot
465
470
  * be the first position inside its parent, or in other words, `viewOffset` must be greater than `0`).
466
471
  *
467
- * Note, that if `modelOffset` for given `viewContainer` was already saved, the stored view position (i.e. parent+offset) will not
468
- * be overwritten. However, it is important to still save it, as we still store additional data related to cached view positions.
469
- *
470
472
  * @param viewParent View position parent.
471
473
  * @param viewOffset View position offset. Must be greater than `0`.
472
474
  * @param viewContainer Tracked view position ascendant (it may be the direct parent of the view position).
@@ -523,9 +525,6 @@ export declare class MapperCache extends /* #__PURE__ */ MapperCache_base {
523
525
  stopTracking(viewContainer: ViewElement | ViewDocumentFragment): void;
524
526
  /**
525
527
  * Invalidates cache inside `viewParent`, starting from given `index` in that parent.
526
- *
527
- * This method may clear a bit more cache than just what was saved after given `index`, but it is guaranteed that at least it
528
- * will invalidate everything after `index`.
529
528
  */
530
529
  private _clearCacheInsideParent;
531
530
  /**
@@ -533,17 +532,14 @@ export declare class MapperCache extends /* #__PURE__ */ MapperCache_base {
533
532
  */
534
533
  private _clearCacheAll;
535
534
  /**
536
- * Clears all the stored cache that is after given `viewNode`. The `viewNode` can be any node that is inside a tracked view element
535
+ * Clears all the stored cache starting before given `viewNode`. The `viewNode` can be any node that is inside a tracked view element
537
536
  * or view document fragment.
538
- *
539
- * In reality, this function may clear a bit more cache than just "starting after" `viewNode`, but it is guaranteed that at least
540
- * all cache after `viewNode` is invalidated.
541
537
  */
542
- private _clearCacheAfter;
538
+ private _clearCacheStartingBefore;
543
539
  /**
544
540
  * Clears all the cache in the cache list related to given `viewContainer`, starting from `index` (inclusive).
545
541
  */
546
- private _clearCacheFromCacheIndex;
542
+ private _clearCacheFromIndex;
547
543
  /**
548
544
  * Finds a cache item in the given cache list, which `modelOffset` is closest (but smaller or equal) to given `offset`.
549
545
  *
@@ -708,9 +704,10 @@ export type MapperViewToModelPositionEventData = {
708
704
  * and assuming `<paragraph>` and `<p>` are mapped, following example `CacheItem`s are possible:
709
705
  *
710
706
  * * `viewPosition` = `<p>`, 1; `modelOffset` = 5
707
+ * * `viewPosition` = `"bold"`, 2; `modelOffset` = 7
711
708
  * * `viewPosition` = `<strong>, 1; `modelOffset` = 9
709
+ * * `viewPosition` = `" text"`, 0; `modelOffset` = 9
712
710
  * * `viewPosition` = `<p>`, 2; `modelOffset` = 9
713
- * * `viewPosition` = `<p>`, 3; `modelOffset` = 14
714
711
  */
715
712
  type CacheItem = {
716
713
  viewPosition: ViewPosition;
@@ -708,7 +708,7 @@ export default class Mapper extends /* #__PURE__ */ EmitterMixin() {
708
708
  * * it is an internal tool, used by `Mapper`, transparent to the outside (no additional effort when developing a plugin or a converter),
709
709
  * * it stores all the necessary data internally, which makes it easier to disable or debug,
710
710
  * * it is optimized for initial downcast process (long insertions), which is crucial for editor init and data save,
711
- * * it does not save all possible positions mapping for memory considerations, although it is a possible improvement, which may increase
711
+ * * it does not save all possible positions for memory considerations, although it is a possible improvement, which may have increase
712
712
  * performance, as well as simplify some parts of the `MapperCache` logic.
713
713
  *
714
714
  * @internal
@@ -725,8 +725,13 @@ export class MapperCache extends /* #__PURE__ */ EmitterMixin() {
725
725
  * values for model offsets that are after a view node. So, in essence, positions inside text nodes are not cached. However, it takes
726
726
  * from one to at most a few steps, to get from a cached position to a position that is inside a view text node.
727
727
  *
728
- * Additionally, only one item per `modelOffset` is cached. There can be several view positions that map to the same `modelOffset`.
729
- * Only the first save for `modelOffset` is stored.
728
+ * Additionally, only one item per `modelOffset` is cached. There can be several view nodes that "end" at the same `modelOffset`.
729
+ * In this case, we favour positions that are closer to the mapped item. For example
730
+ *
731
+ * * for model: `<paragraph>Some <$text bold=true italic=true>formatted</$text> text.</paragraph>`,
732
+ * * and view: `<p>Some <em><strong>formatted</strong></em> text.</p>`,
733
+ *
734
+ * for model offset `14` (after "d"), we store view position after `<em>` element (i.e. view position: at `<p>`, offset `2`).
730
735
  */
731
736
  _cachedMapping = new WeakMap();
732
737
  /**
@@ -759,22 +764,13 @@ export class MapperCache extends /* #__PURE__ */ EmitterMixin() {
759
764
  * This is specified as a property to make it easier to set as an event callback and to later turn off that event.
760
765
  */
761
766
  _invalidateOnTextChangeCallback = (evt, viewNode) => {
762
- // It is enough to validate starting from "after the text node", because the first cache entry that we might want to invalidate
763
- // is the position after this text node.
764
- //
765
- // For example - assume following view and following view positions cached (marked by `^`): `<p>Foo^<strong>bar^</strong>^abc^</p>`.
766
- //
767
- // If we change text "bar", we only need to invalidate cached positions after it. Cached positions before the text are not changed.
768
- //
769
- this._clearCacheAfter(viewNode);
767
+ // Text node has changed. Clear all the cache starting from before this text node.
768
+ this._clearCacheStartingBefore(viewNode);
770
769
  };
771
770
  /**
772
771
  * Saves cache for given view position mapping <-> model offset mapping. The view position should be after a node (i.e. it cannot
773
772
  * be the first position inside its parent, or in other words, `viewOffset` must be greater than `0`).
774
773
  *
775
- * Note, that if `modelOffset` for given `viewContainer` was already saved, the stored view position (i.e. parent+offset) will not
776
- * be overwritten. However, it is important to still save it, as we still store additional data related to cached view positions.
777
- *
778
774
  * @param viewParent View position parent.
779
775
  * @param viewOffset View position offset. Must be greater than `0`.
780
776
  * @param viewContainer Tracked view position ascendant (it may be the direct parent of the view position).
@@ -787,18 +783,23 @@ export class MapperCache extends /* #__PURE__ */ EmitterMixin() {
787
783
  const cacheItem = cache.cacheMap.get(modelOffset);
788
784
  if (cacheItem) {
789
785
  // We already cached this offset. Don't overwrite the cache.
790
- // However, we still need to set a proper entry in `_nodeToCacheListIndex`. We can figure it out based on existing `cacheItem`.
791
786
  //
787
+ // This assumes that `Mapper` works in a way that we first cache the parent and only then cache children, as we prefer position
788
+ // after the parent ("closer" to the tracked ancestor). It might be safer to check which position is preferred (newly saved or
789
+ // the one currently in cache) but it would require additional processing. For now, `Mapper#_findPositionIn()` and
790
+ // `Mapper#getModelLength()` are implemented so that parents are cached before their children.
791
+ //
792
+ // So, don't create new cache if one already exists. Instead, only save `_nodeToCacheListIndex` value for the related view node.
793
+ const viewChild = viewParent.getChild(viewOffset - 1);
794
+ // Figure out what index to save with `viewChild`.
792
795
  // We have a `cacheItem` for the `modelOffset`, so we can get a `viewPosition` from there. Before that view position, there
793
796
  // must be a node. That node must have an index set. This will be the index we will want to use.
794
797
  // Since we expect `viewOffset` to be greater than 0, then in almost all cases `modelOffset` will be greater than 0 as well.
795
798
  // As a result, we can expect `cacheItem.viewPosition.nodeBefore` to be set.
796
799
  //
797
800
  // However, in an edge case, were the tracked element contains a 0-model-length view element as the first child (UI element or
798
- // an empty attribute element), then `modelOffset` will be 0, and `cacheItem.viewPosition` will be before any view node.
799
- // In such edge case, `cacheItem.viewPosition.nodeBefore` is `undefined`, so we set index to `0`.
800
- //
801
- const viewChild = viewParent.getChild(viewOffset - 1);
801
+ // an empty attribute element), then `modelOffset` will be 0, and `cacheItem` will be the first cache item, which is before any
802
+ // view node. In such edge case, `cacheItem.viewPosition.nodeBefore` is undefined, and we manually set to `0`.
802
803
  const index = cacheItem.viewPosition.nodeBefore ? this._nodeToCacheListIndex.get(cacheItem.viewPosition.nodeBefore) : 0;
803
804
  this._nodeToCacheListIndex.set(viewChild, index);
804
805
  return;
@@ -920,9 +921,6 @@ export class MapperCache extends /* #__PURE__ */ EmitterMixin() {
920
921
  }
921
922
  /**
922
923
  * Invalidates cache inside `viewParent`, starting from given `index` in that parent.
923
- *
924
- * This method may clear a bit more cache than just what was saved after given `index`, but it is guaranteed that at least it
925
- * will invalidate everything after `index`.
926
924
  */
927
925
  _clearCacheInsideParent(viewParent, index) {
928
926
  if (index == 0) {
@@ -933,16 +931,23 @@ export class MapperCache extends /* #__PURE__ */ EmitterMixin() {
933
931
  }
934
932
  else {
935
933
  // If this is not a tracked element, remove cache starting from before this element.
936
- // Since it is not a tracked element, it has to have a parent.
937
- this._clearCacheInsideParent(viewParent.parent, viewParent.index);
934
+ this._clearCacheStartingBefore(viewParent);
938
935
  }
939
936
  }
940
937
  else {
941
938
  // Change in the middle of the parent. Get a view node that's before the change.
942
939
  const lastValidNode = viewParent.getChild(index - 1);
943
- // Then, clear all cache after this view node.
940
+ // Then, clear all cache starting from before this view node.
941
+ //
942
+ // Possible performance improvement. We could have had `_clearCacheAfter( lastValidNode )` instead.
943
+ // If the `lastValidNode` is the last unchanged node, then we could clear everything AFTER it, not before.
944
+ // However, with the current setup, it didn't work properly and the actual gain wasn't that big on the tested data.
945
+ // The problem was with following example: <p>Foo<em><strong>Xyz</strong></em>Bar</p>.
946
+ // In this example we cache position after <em>, i.e. view position `<p>` 2 is saved with model offset 6.
947
+ // Now, if we add some text in `<em>`, we won't validate this cached item even though it gets outdated.
948
+ // So, if there's a need to have `_clearCacheAfter()`, we need to solve the above case first.
944
949
  //
945
- this._clearCacheAfter(lastValidNode);
950
+ this._clearCacheStartingBefore(lastValidNode);
946
951
  }
947
952
  }
948
953
  /**
@@ -950,7 +955,6 @@ export class MapperCache extends /* #__PURE__ */ EmitterMixin() {
950
955
  */
951
956
  _clearCacheAll(viewContainer) {
952
957
  const cache = this._cachedMapping.get(viewContainer);
953
- // TODO: Should clear `_nodeToCacheListIndex` too?
954
958
  if (cache.maxModelOffset > 0) {
955
959
  cache.maxModelOffset = 0;
956
960
  cache.cacheList.length = 1;
@@ -959,47 +963,46 @@ export class MapperCache extends /* #__PURE__ */ EmitterMixin() {
959
963
  }
960
964
  }
961
965
  /**
962
- * Clears all the stored cache that is after given `viewNode`. The `viewNode` can be any node that is inside a tracked view element
966
+ * Clears all the stored cache starting before given `viewNode`. The `viewNode` can be any node that is inside a tracked view element
963
967
  * or view document fragment.
964
- *
965
- * In reality, this function may clear a bit more cache than just "starting after" `viewNode`, but it is guaranteed that at least
966
- * all cache after `viewNode` is invalidated.
967
968
  */
968
- _clearCacheAfter(viewNode) {
969
+ _clearCacheStartingBefore(viewNode) {
969
970
  // To quickly invalidate the cache, we base on the cache list index stored with the node. See docs for `this._nodeToCacheListIndex`.
970
971
  const cacheListIndex = this._nodeToCacheListIndex.get(viewNode);
971
972
  // If there is no index stored, it means that this `viewNode` has not been cached yet.
972
973
  if (cacheListIndex === undefined) {
973
- // If the node is not cached, maybe it's parent is. We will try to invalidate the cache using the parent.
974
+ // If the node is not cached, maybe it's parent is. We will try to invalidate the cache starting from before the parent.
975
+ // Note, that there always must be a parent if we got here.
974
976
  const viewParent = viewNode.parent;
975
- // If the parent is a non-tracked element, try clearing the cache starting from the position before it.
976
- //
977
- // For example: `<p>Abc<strong>def<em>ghi</em></strong></p>`.
977
+ // If the parent is a non-tracked element, try clearing the cache starting before it.
978
978
  //
979
- // If `viewNode` is `<em>` in this case, and it was not cached yet, we will try to clear cache starting from before `<strong>`.
979
+ // This situation may happen e.g. if structure like `<p><strong><em>Foo</em></strong>...` was stepped over in
980
+ // `Mapper#_findPositionIn()` and the children are not cached yet, but the `<strong>` element is. If something changes
981
+ // inside this structure, make sure to invalidate all the cache after `<strong>`.
980
982
  //
981
983
  // If the parent is a tracked element, then it means there's no cache to clear (nothing after the element is cached).
982
- // In this case, there's nothing to do. We assume that there are no "holes" in caching in direct children of tracked element
983
- // (that is if some children is cached, then its previous sibling is cached too, and we would not end up inside this `if`).
984
- //
985
- // TODO: Most probably this `if` could be removed altogether, after recent changes in Mapper.
986
- // TODO: Now we cache all items one after another, so there aren't any "holes" anywhere, not only on top-level.
984
+ // In this case, there's nothing to do.
987
985
  //
988
986
  if (!this._cachedMapping.has(viewParent)) {
989
- this._clearCacheInsideParent(viewParent.parent, viewParent.index);
987
+ this._clearCacheStartingBefore(viewParent);
990
988
  }
991
989
  return;
992
990
  }
991
+ // Note: there was a consideration to save the `viewContainer` value together with `cacheListIndex` value.
992
+ // However, it is like it is on purpose. We want to find *current* mapped ancestor for the `viewNode`.
993
+ // This is an essential step to verify if the cache is still up-to-date.
994
+ // Actually, we could save `viewContainer` and compare it to current tracked ancestor to quickly invalidate.
995
+ // But this kinda happens with our flow and other assumptions around caching list index anyway.
993
996
  let viewContainer = viewNode.parent;
994
997
  while (!this._cachedMapping.has(viewContainer)) {
995
998
  viewContainer = viewContainer.parent;
996
999
  }
997
- this._clearCacheFromCacheIndex(viewContainer, cacheListIndex);
1000
+ this._clearCacheFromIndex(viewContainer, cacheListIndex);
998
1001
  }
999
1002
  /**
1000
1003
  * Clears all the cache in the cache list related to given `viewContainer`, starting from `index` (inclusive).
1001
1004
  */
1002
- _clearCacheFromCacheIndex(viewContainer, index) {
1005
+ _clearCacheFromIndex(viewContainer, index) {
1003
1006
  if (index === 0) {
1004
1007
  // Don't remove the first entry in the cache (this entry is always a mapping between view offset 0 <-> model offset 0,
1005
1008
  // and it is a default value that is always expected to be in the cache list).
package/src/index.d.ts CHANGED
@@ -118,4 +118,3 @@ export * from './view/styles/padding.js';
118
118
  export * from './view/styles/utils.js';
119
119
  export { getData as _getModelData, setData as _setModelData, parse as _parseModel, stringify as _stringifyModel } from './dev-utils/model.js';
120
120
  export { getData as _getViewData, setData as _setViewData, parse as _parseView, stringify as _stringifyView } from './dev-utils/view.js';
121
- export { tryFixingRange as _tryFixingModelRange } from './model/utils/selection-post-fixer.js';
package/src/index.js CHANGED
@@ -83,4 +83,3 @@ export * from './view/styles/utils.js';
83
83
  // Development / testing utils.
84
84
  export { getData as _getModelData, setData as _setModelData, parse as _parseModel, stringify as _stringifyModel } from './dev-utils/model.js';
85
85
  export { getData as _getViewData, setData as _setViewData, parse as _parseView, stringify as _stringifyView } from './dev-utils/view.js';
86
- export { tryFixingRange as _tryFixingModelRange } from './model/utils/selection-post-fixer.js';