@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.
@@ -49,6 +49,11 @@ export default abstract class DomEventObserver<EventType extends keyof HTMLEleme
49
49
  * Default value is `false`.
50
50
  */
51
51
  useCapture: boolean;
52
+ /**
53
+ * If set to `true`, indicates that the function specified by listener will never call `preventDefault()`.
54
+ * Default value is `false`.
55
+ */
56
+ usePassive: boolean;
52
57
  /**
53
58
  * Callback which should be called when the DOM event occurred. Note that the callback will not be called if
54
59
  * observer {@link #isEnabled is not enabled}.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-engine",
3
- "version": "43.1.1",
3
+ "version": "43.2.0-alpha.1",
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": "43.1.1",
27
+ "@ckeditor/ckeditor5-utils": "43.2.0-alpha.1",
28
28
  "lodash-es": "4.17.21"
29
29
  },
30
30
  "author": "CKSource (http://cksource.com/)",
@@ -67,7 +67,14 @@ export default class NodeList {
67
67
  */
68
68
  getNodeStartOffset(node) {
69
69
  const index = this.getNodeIndex(node);
70
- return index === null ? null : this._nodes.slice(0, index).reduce((sum, node) => sum + node.offsetSize, 0);
70
+ if (index === null) {
71
+ return null;
72
+ }
73
+ let sum = 0;
74
+ for (let i = 0; i < index; i++) {
75
+ sum += this._nodes[i].offsetSize;
76
+ }
77
+ return sum;
71
78
  }
72
79
  /**
73
80
  * Converts index to offset in node list.
@@ -319,6 +319,8 @@ export function transformSets(operationsA, operationsB, options) {
319
319
  operationsA.splice(i, 1, ...newOpsA);
320
320
  operationsB.splice(indexB, 1, ...newOpsB);
321
321
  }
322
+ handlePartialMarkerOperations(operationsA);
323
+ handlePartialMarkerOperations(operationsB);
322
324
  if (options.padWithNoOps) {
323
325
  // If no-operations padding is enabled, count how many extra `a` and `b` operations were generated.
324
326
  const brokenOperationsACount = operationsA.length - data.originalOperationsACount;
@@ -436,6 +438,9 @@ class ContextFactory {
436
438
  else {
437
439
  const range = Range._createFromPositionAndShift(opB.sourcePosition, opB.howMany);
438
440
  if (opA.splitPosition.hasSameParentAs(opB.sourcePosition) && range.containsPosition(opA.splitPosition)) {
441
+ // TODO: Potential bug -- we are saving offset value directly and it is not later updated during OT.
442
+ // TODO: This may cause a bug it here was an non-undone operation that may have impacted this offset.
443
+ // TODO: Similar error was with MarkerOperation relations, where full path was saved and never updated.
439
444
  const howMany = range.end.offset - opA.splitPosition.offset;
440
445
  const offset = opA.splitPosition.offset - range.start.offset;
441
446
  this._setRelation(opA, opB, { howMany, offset });
@@ -474,20 +479,7 @@ class ContextFactory {
474
479
  if (!markerRange) {
475
480
  return;
476
481
  }
477
- if (opB instanceof MoveOperation) {
478
- const movedRange = Range._createFromPositionAndShift(opB.sourcePosition, opB.howMany);
479
- const affectedLeft = movedRange.containsPosition(markerRange.start) ||
480
- movedRange.start.isEqual(markerRange.start);
481
- const affectedRight = movedRange.containsPosition(markerRange.end) ||
482
- movedRange.end.isEqual(markerRange.end);
483
- if ((affectedLeft || affectedRight) && !movedRange.containsRange(markerRange)) {
484
- this._setRelation(opA, opB, {
485
- side: affectedLeft ? 'left' : 'right',
486
- path: affectedLeft ? markerRange.start.path.slice() : markerRange.end.path.slice()
487
- });
488
- }
489
- }
490
- else if (opB instanceof MergeOperation) {
482
+ if (opB instanceof MergeOperation) {
491
483
  const wasInLeftElement = markerRange.start.isEqual(opB.targetPosition);
492
484
  const wasStartBeforeMergedElement = markerRange.start.isEqual(opB.deletionPosition);
493
485
  const wasEndBeforeMergedElement = markerRange.end.isEqual(opB.deletionPosition);
@@ -605,6 +597,57 @@ function padWithNoOps(operations, howMany) {
605
597
  operations.push(new NoOperation(0));
606
598
  }
607
599
  }
600
+ /**
601
+ * Transformed operations set may include marker operations which were broken into multiple marker operations during transformation.
602
+ * It represents marker range being broken into multiple pieces as the transformation was processed. Each partial marker operation is
603
+ * a piece of the original marker range.
604
+ *
605
+ * These partial marker operations ("marker range pieces") should be "glued" together if, after transformations, the ranges ended up
606
+ * next to each other.
607
+ *
608
+ * If the ranges did not end up next to each other, then partial marker operations should be discarded, as the marker range cannot
609
+ * be broken into two pieces.
610
+ *
611
+ * There is always one "reference" marker operation (the original operation) and there may be some partial marker operations. Partial
612
+ * marker operations have base version set to `-1`. If the `operations` set includes partial marker operations, then they are always
613
+ * after the original marker operation.
614
+ *
615
+ * See also `MarkerOperation` x `MoveOperation` transformation.
616
+ * See also https://github.com/ckeditor/ckeditor5/pull/17071.
617
+ */
618
+ function handlePartialMarkerOperations(operations) {
619
+ const markerOps = new Map();
620
+ for (let i = 0; i < operations.length; i++) {
621
+ const op = operations[i];
622
+ if (!(op instanceof MarkerOperation)) {
623
+ continue;
624
+ }
625
+ if (op.baseVersion !== -1) {
626
+ markerOps.set(op.name, {
627
+ op,
628
+ ranges: op.newRange ? [op.newRange] : []
629
+ });
630
+ }
631
+ else {
632
+ if (op.newRange) {
633
+ // `markerOps.get( op.name )` must exist because original marker operation is always before partial marker operations.
634
+ // If the original marker operation was changed to `NoOperation`, then the partial marker operations would be changed
635
+ // to `NoOperation` as well, so this is not a case.
636
+ markerOps.get(op.name).ranges.push(op.newRange);
637
+ }
638
+ operations.splice(i, 1);
639
+ i--;
640
+ }
641
+ }
642
+ for (const { op, ranges } of markerOps.values()) {
643
+ if (ranges.length) {
644
+ op.newRange = Range._createFromRanges(ranges);
645
+ }
646
+ else {
647
+ op.newRange = null;
648
+ }
649
+ }
650
+ }
608
651
  // -----------------------
609
652
  setTransformation(AttributeOperation, AttributeOperation, (a, b, context) => {
610
653
  // If operations in conflict, check if their ranges intersect and manage them properly.
@@ -943,27 +986,45 @@ setTransformation(MarkerOperation, MergeOperation, (a, b) => {
943
986
  }
944
987
  return [a];
945
988
  });
946
- setTransformation(MarkerOperation, MoveOperation, (a, b, context) => {
989
+ setTransformation(MarkerOperation, MoveOperation, (a, b) => {
990
+ const result = [a];
947
991
  if (a.oldRange) {
948
992
  a.oldRange = Range._createFromRanges(a.oldRange._getTransformedByMoveOperation(b));
949
993
  }
950
994
  if (a.newRange) {
951
- if (context.abRelation) {
952
- const aNewRange = Range._createFromRanges(a.newRange._getTransformedByMoveOperation(b));
953
- if (context.abRelation.side == 'left' && b.targetPosition.isEqual(a.newRange.start)) {
954
- a.newRange.end = aNewRange.end;
955
- a.newRange.start.path = context.abRelation.path;
956
- return [a];
957
- }
958
- else if (context.abRelation.side == 'right' && b.targetPosition.isEqual(a.newRange.end)) {
959
- a.newRange.start = aNewRange.start;
960
- a.newRange.end.path = context.abRelation.path;
961
- return [a];
962
- }
995
+ // In many simple cases the marker range will be kept integral after the transformation. For example, if some nodes
996
+ // were inserted before the range, or into the range, then the marker range is not broken into two.
997
+ //
998
+ // However, if some nodes are taken out of the range and moved somewhere else, or are moved into the range, then the marker
999
+ // range is "broken" into two or three pieces, and these pieces must be transformed and updated separately.
1000
+ //
1001
+ // When the marker range is transformed by move operation, as a result we get an array with one (simple case) or multiple
1002
+ // ("broken range" case) ranges.
1003
+ const ranges = a.newRange._getTransformedByMoveOperation(b);
1004
+ a.newRange = ranges[0];
1005
+ // If there are multiple ranges, we will create separate marker operations for each piece of the original marker range.
1006
+ // Since they will be marker operations, they will be processed through the transformation process.
1007
+ //
1008
+ // However, we cannot create multiple ranges for the same marker (for the same marker name). A marker has only one range.
1009
+ // So, we cannot really have multiple marker operations for the same marker. We will keep the track of the separate marker
1010
+ // operations to see, if after all transformations, the marker pieces are next to each other or not. If so, we will glue
1011
+ // them together to the original marker operation (`a`). If not, we will discard them. These extra operations will never
1012
+ // be executed, as they will only exist temporarily during the transformation process.
1013
+ //
1014
+ // We will call these additional marker operations "partial marker operations" and we will mark them with negative base version.
1015
+ //
1016
+ // See also `handlePartialMarkerOperations()`.
1017
+ // See also https://github.com/ckeditor/ckeditor5/pull/17071.
1018
+ //
1019
+ for (let i = 1; i < ranges.length; i++) {
1020
+ const op = a.clone();
1021
+ op.oldRange = null;
1022
+ op.newRange = ranges[i];
1023
+ op.baseVersion = -1;
1024
+ result.push(op);
963
1025
  }
964
- a.newRange = Range._createFromRanges(a.newRange._getTransformedByMoveOperation(b));
965
1026
  }
966
- return [a];
1027
+ return result;
967
1028
  });
968
1029
  setTransformation(MarkerOperation, SplitOperation, (a, b, context) => {
969
1030
  if (a.oldRange) {
@@ -978,6 +1039,9 @@ setTransformation(MarkerOperation, SplitOperation, (a, b, context) => {
978
1039
  else if (a.newRange.start.isEqual(b.splitPosition) && !context.abRelation.wasInLeftElement) {
979
1040
  a.newRange.start = Position._createAt(b.moveTargetPosition);
980
1041
  }
1042
+ else {
1043
+ a.newRange.start = aNewRange.start;
1044
+ }
981
1045
  if (a.newRange.end.isEqual(b.splitPosition) && context.abRelation.wasInRightElement) {
982
1046
  a.newRange.end = Position._createAt(b.moveTargetPosition);
983
1047
  }
@@ -1080,6 +1144,7 @@ setTransformation(MergeOperation, MergeOperation, (a, b, context) => {
1080
1144
  }
1081
1145
  }
1082
1146
  // The default case.
1147
+ // TODO: Possibly, there's a missing case for same `targetPosition` but different `sourcePosition`.
1083
1148
  //
1084
1149
  if (a.sourcePosition.hasSameParentAs(b.targetPosition)) {
1085
1150
  a.howMany += b.howMany;
@@ -1103,10 +1168,10 @@ setTransformation(MergeOperation, MoveOperation, (a, b, context) => {
1103
1168
  // was to have it all deleted, together with its children. From user experience point of view, moving back the
1104
1169
  // removed nodes might be unexpected. This means that in this scenario we will block the merging.
1105
1170
  //
1106
- // The exception of this rule would be if the remove operation was later undone.
1171
+ // The exception to this rule would be if the remove operation was later undone.
1107
1172
  //
1108
1173
  const removedRange = Range._createFromPositionAndShift(b.sourcePosition, b.howMany);
1109
- if (b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove) {
1174
+ if (b.type == 'remove' && !context.bWasUndone) {
1110
1175
  if (a.deletionPosition.hasSameParentAs(b.sourcePosition) && removedRange.containsPosition(a.sourcePosition)) {
1111
1176
  return [new NoOperation(0)];
1112
1177
  }
@@ -1204,59 +1269,66 @@ setTransformation(MergeOperation, SplitOperation, (a, b, context) => {
1204
1269
  // Case 1:
1205
1270
  //
1206
1271
  // Merge operation moves nodes to the place where split happens.
1207
- // This is a classic situation when there are two paragraphs, and there is a split (enter) after the first
1272
+ //
1273
+ // This is a classic situation when there are two paragraphs, and there is a split (enter) at the end of the first
1208
1274
  // paragraph and there is a merge (delete) at the beginning of the second paragraph:
1209
1275
  //
1210
1276
  // <p>Foo{}</p><p>[]Bar</p>.
1211
1277
  //
1212
- // Split is after `Foo`, while merge is from `Bar` to the end of `Foo`.
1278
+ // User A presses enter after `Foo`, while User B presses backspace before `Bar`. It is intuitive that after both operations, the
1279
+ // editor state should stay the same.
1213
1280
  //
1214
1281
  // State after split:
1215
- // <p>Foo</p><p></p><p>Bar</p>
1282
+ // <p>Foo</p><p></p><p>[]Bar</p>
1216
1283
  //
1217
- // Now, `Bar` should be merged to the new paragraph:
1284
+ // When this happens, `Bar` should be merged to the newly created paragraph, to maintain the editor state:
1218
1285
  // <p>Foo</p><p>Bar</p>
1219
1286
  //
1220
- // Instead of merging it to the original paragraph:
1287
+ // Another option is to merge into the original paragraph `Foo`, according to the `targetPosition`. This results in an incorrect state:
1221
1288
  // <p>FooBar</p><p></p>
1222
1289
  //
1223
- // This means that `targetPosition` needs to be transformed. This is the default case though.
1224
- // For example, if the split would be after `F`, `targetPosition` should also be transformed.
1290
+ // Also, consider an example where User A also writes something in the new paragraph:
1291
+ // <p>Foo</p><p>Xyz</p><p>[]Bar</p>
1292
+ //
1293
+ // 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 ]`,
1294
+ // and then transformed be insertion into `[ 1, 3 ]`.
1225
1295
  //
1226
- // There are three exceptions, though, when we want to keep `targetPosition` as it was.
1296
+ // So, usually we want to move `targetPosition` to the new paragraph when it is same as split position. This is how it is handled
1297
+ // in the default transformation (`_getTransformedBySplitOperation()`). We don't need a special case for this.
1227
1298
  //
1228
- // First exception is when the merge target position is inside an element (not at the end, as usual). This
1229
- // happens when the merge operation earlier was transformed by "the same" merge operation. If merge operation
1230
- // targets inside the element we want to keep the original target position (and not transform it) because
1231
- // we have additional context telling us that we want to merge to the original element. We can check if the
1232
- // merge operation points inside element by checking what is `SplitOperation#howMany`. Since merge target position
1233
- // is same as split position, if `howMany` is non-zero, it means that the merge target position is inside an element.
1299
+ // However, there are two exceptions, when we **do not** want to transform `targetPosition`, and we need a special case then.
1234
1300
  //
1235
- // Second exception is when the element to merge is in the graveyard and split operation uses it. In that case
1301
+ // These exceptions happen only if undo is involved. During OT, above presented case (`<p>Foo{}</p><p>[]Bar</p>`) is the only way
1302
+ // how `SplitOperation#splitPosition` and `MergeOperation#targetPosition` can be the same.
1303
+ //
1304
+ // First exception is when the element to merge is in the graveyard and split operation uses it. In that case
1236
1305
  // if target position would be transformed, the merge operation would target at the source position:
1237
1306
  //
1238
- // root: <p>Foo</p> graveyard: <p></p>
1307
+ // root: <p>Foo[]</p> graveyard: <p></p>
1239
1308
  //
1240
1309
  // SplitOperation: root [ 0, 3 ] using graveyard [ 0 ] (howMany = 0)
1241
1310
  // MergeOperation: graveyard [ 0, 0 ] -> root [ 0, 3 ] (howMany = 0)
1242
1311
  //
1243
- // Since split operation moves the graveyard node back to the root, the merge operation source position changes.
1244
- // We would like to merge from the empty <p> to the "Foo" <p>:
1245
- //
1246
- // root: <p>Foo</p><p></p> graveyard:
1312
+ // Since split operation moves the graveyard element back to the root (to path `[ 1 ]`), the merge operation `sourcePosition` changes.
1313
+ // After split we have: `<p>Foo</p><p></p>`, so `sourcePosition` is `[ 1, 0 ]`. But if `targetPosition` is transformed, then it
1314
+ // also becomes `[ 1, 0 ]`. In this case, we want to keep the `targetPosition` as it was.
1247
1315
  //
1248
- // MergeOperation#sourcePosition = root [ 1, 0 ]
1316
+ // Second exception is connected strictly with undo relations. If this `MergeOperation` was earlier transformed by
1317
+ // `MergeOperation` and we stored an information that earlier the target position was not affected, then here, when transforming by
1318
+ // `SplitOperation` we are not going to change it as well.
1249
1319
  //
1250
- // If `targetPosition` is transformed, it would become root [ 1, 0 ] as well. It has to be kept as it was.
1320
+ // For these two cases we will only transform `sourcePosition` and return early.
1251
1321
  //
1252
- // Third exception is connected with relations. If this happens during undo and we have explicit information
1253
- // that target position has not been affected by the operation which is undone by this split then this split should
1254
- // not move the target position either.
1322
+ // Note, that earlier there was also third special case here. `targetPosition` was not transformed, if it pointed into the middle of
1323
+ // target element, not into its end (as usual). This can also happen only with undo involved. However, it wasn't always a correct
1324
+ // solution, as in some cases we actually wanted to transform `targetPosition`. Also, this case usually happens together with the second
1325
+ // case described above. There is only one scenario that we have in our unit tests, where this third case happened without second case.
1326
+ // However, this scenario went fine no matter if we transformed `targetPosition` or not. That's because this happened in the middle
1327
+ // of transformation process and the operation was correctly transformed later on.
1255
1328
  //
1256
1329
  if (a.targetPosition.isEqual(b.splitPosition)) {
1257
- const mergeInside = b.howMany != 0;
1258
1330
  const mergeSplittingElement = b.graveyardPosition && a.deletionPosition.isEqual(b.graveyardPosition);
1259
- if (mergeInside || mergeSplittingElement || context.abRelation == 'mergeTargetNotMoved') {
1331
+ if (mergeSplittingElement || context.abRelation == 'mergeTargetNotMoved') {
1260
1332
  a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation(b);
1261
1333
  return [a];
1262
1334
  }
@@ -1585,12 +1657,14 @@ setTransformation(MoveOperation, MergeOperation, (a, b, context) => {
1585
1657
  const results = [];
1586
1658
  let gyMoveSource = b.graveyardPosition.clone();
1587
1659
  let splitNodesMoveSource = b.targetPosition._getTransformedByMergeOperation(b);
1660
+ // `a.targetPosition` points to graveyard, so it was probably affected by `b` (which moved merged element to the graveyard).
1661
+ const aTarget = a.targetPosition.getTransformedByOperation(b);
1588
1662
  if (a.howMany > 1) {
1589
- results.push(new MoveOperation(a.sourcePosition, a.howMany - 1, a.targetPosition, 0));
1590
- gyMoveSource = gyMoveSource._getTransformedByMove(a.sourcePosition, a.targetPosition, a.howMany - 1);
1591
- splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove(a.sourcePosition, a.targetPosition, a.howMany - 1);
1663
+ results.push(new MoveOperation(a.sourcePosition, a.howMany - 1, aTarget, 0));
1664
+ gyMoveSource = gyMoveSource._getTransformedByMove(a.sourcePosition, aTarget, a.howMany - 1);
1665
+ splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove(a.sourcePosition, aTarget, a.howMany - 1);
1592
1666
  }
1593
- const gyMoveTarget = b.deletionPosition._getCombined(a.sourcePosition, a.targetPosition);
1667
+ const gyMoveTarget = b.deletionPosition._getCombined(a.sourcePosition, aTarget);
1594
1668
  const gyMove = new MoveOperation(gyMoveSource, 1, gyMoveTarget, 0);
1595
1669
  const splitNodesMoveTargetPath = gyMove.getMovedRangeStart().path.slice();
1596
1670
  splitNodesMoveTargetPath.push(0);
@@ -820,28 +820,25 @@ export default class Range extends TypeCheckable {
820
820
  // If we are going to return just a one range, one of the ranges need to be the reference one.
821
821
  // Other ranges will be stuck to that range, if possible.
822
822
  const ref = ranges[0];
823
- // 2. Sort all the ranges so it's easier to process them.
823
+ // 2. Sort all the ranges, so it's easier to process them.
824
824
  ranges.sort((a, b) => {
825
825
  return a.start.isAfter(b.start) ? 1 : -1;
826
826
  });
827
827
  // 3. Check at which index the reference range is now.
828
828
  const refIndex = ranges.indexOf(ref);
829
829
  // 4. At this moment we don't need the original range.
830
- // We are going to modify the result and we need to return a new instance of Range.
830
+ // We are going to modify the result, and we need to return a new instance of Range.
831
831
  // We have to create a copy of the reference range.
832
832
  const result = new this(ref.start, ref.end);
833
833
  // 5. Ranges should be checked and glued starting from the range that is closest to the reference range.
834
834
  // Since ranges are sorted, start with the range with index that is closest to reference range index.
835
- if (refIndex > 0) {
836
- // eslint-disable-next-line no-constant-condition
837
- for (let i = refIndex - 1; true; i++) {
838
- if (ranges[i].end.isEqual(result.start)) {
839
- result.start = Position._createAt(ranges[i].start);
840
- }
841
- else {
842
- // If ranges are not starting/ending at the same position there is no point in looking further.
843
- break;
844
- }
835
+ for (let i = refIndex - 1; i >= 0; i--) {
836
+ if (ranges[i].end.isEqual(result.start)) {
837
+ result.start = Position._createAt(ranges[i].start);
838
+ }
839
+ else {
840
+ // If ranges are not starting/ending at the same position there is no point in looking further.
841
+ break;
845
842
  }
846
843
  }
847
844
  // 6. Ranges should be checked and glued starting from the range that is closest to the reference range.
@@ -129,7 +129,7 @@ export default class DomConverter {
129
129
  if (viewElement) {
130
130
  this._domToViewMapping.delete(domElement);
131
131
  this._viewToDomMapping.delete(viewElement);
132
- for (const child of Array.from(domElement.children)) {
132
+ for (const child of domElement.children) {
133
133
  this.unbindDomElement(child);
134
134
  }
135
135
  }
@@ -45,6 +45,11 @@ export default abstract class DomEventObserver<EventType extends keyof HTMLEleme
45
45
  * Default value is `false`.
46
46
  */
47
47
  useCapture: boolean;
48
+ /**
49
+ * If set to `true`, indicates that the function specified by listener will never call `preventDefault()`.
50
+ * Default value is `false`.
51
+ */
52
+ usePassive: boolean;
48
53
  /**
49
54
  * Callback which should be called when the DOM event occurred. Note that the callback will not be called if
50
55
  * observer {@link #isEnabled is not enabled}.
@@ -42,6 +42,11 @@ export default class DomEventObserver extends Observer {
42
42
  * Default value is `false`.
43
43
  */
44
44
  this.useCapture = false;
45
+ /**
46
+ * If set to `true`, indicates that the function specified by listener will never call `preventDefault()`.
47
+ * Default value is `false`.
48
+ */
49
+ this.usePassive = false;
45
50
  }
46
51
  /**
47
52
  * @inheritDoc
@@ -53,7 +58,7 @@ export default class DomEventObserver extends Observer {
53
58
  if (this.isEnabled && !this.checkShouldIgnoreEventFromTarget(domEvent.target)) {
54
59
  this.onDomEvent(domEvent);
55
60
  }
56
- }, { useCapture: this.useCapture });
61
+ }, { useCapture: this.useCapture, usePassive: this.usePassive });
57
62
  });
58
63
  }
59
64
  /**
@@ -70,7 +70,7 @@ export default class SelectionObserver extends Observer {
70
70
  // This listener is using capture mode to make sure that selection is upcasted before any other
71
71
  // handler would like to check it and update (for example table multi cell selection).
72
72
  this.listenTo(domDocument, 'mouseup', endDocumentIsSelecting, { priority: 'highest', useCapture: true });
73
- this.listenTo(domDocument, 'selectionchange', (evt, domEvent) => {
73
+ this.listenTo(domDocument, 'selectionchange', () => {
74
74
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
75
75
  // @if CK_DEBUG_TYPING // _debouncedLine();
76
76
  // @if CK_DEBUG_TYPING // const domSelection = domDocument.defaultView!.getSelection();
@@ -449,19 +449,22 @@ export default class Renderer extends /* #__PURE__ */ ObservableMixin() {
449
449
  // in 'this._updateChildrenMappings()' so it will be processed separately.
450
450
  return;
451
451
  }
452
- const domAttrKeys = Array.from(domElement.attributes).map(attr => attr.name);
453
- const viewAttrKeys = viewElement.getAttributeKeys();
454
- // Add or overwrite attributes.
455
- for (const key of viewAttrKeys) {
456
- this.domConverter.setDomElementAttribute(domElement, key, viewElement.getAttribute(key), viewElement);
457
- }
458
- // Remove from DOM attributes which do not exists in the view.
459
- for (const key of domAttrKeys) {
452
+ // Remove attributes from DOM elements if they do not exist in the view.
453
+ //
454
+ // Note: It is important to first remove DOM attributes and then set new ones, because some view attributes may be renamed
455
+ // as they are set on DOM (due to unsafe attributes handling). If we set the view attribute first, and then remove
456
+ // non-existing DOM attributes, then we would remove the attribute that we just set.
457
+ for (const domAttr of domElement.attributes) {
458
+ const key = domAttr.name;
460
459
  // All other attributes not present in the DOM should be removed.
461
460
  if (!viewElement.hasAttribute(key)) {
462
461
  this.domConverter.removeDomElementAttribute(domElement, key);
463
462
  }
464
463
  }
464
+ // Add or overwrite attributes.
465
+ for (const key of viewElement.getAttributeKeys()) {
466
+ this.domConverter.setDomElementAttribute(domElement, key, viewElement.getAttribute(key), viewElement);
467
+ }
465
468
  }
466
469
  /**
467
470
  * Checks if elements child list needs to be updated and possibly updates it.
@@ -467,20 +467,19 @@ export class StylesProcessor {
467
467
  * @param styles Object holding normalized styles.
468
468
  */
469
469
  getStyleNames(styles) {
470
+ const styleNamesKeysSet = new Set();
470
471
  // Find all extractable styles that have a value.
471
- const expandedStyleNames = Array.from(this._consumables.keys()).filter(name => {
472
+ for (const name of this._consumables.keys()) {
472
473
  const style = this.getNormalized(name, styles);
473
- if (style && typeof style == 'object') {
474
- return Object.keys(style).length;
474
+ if (style && (typeof style != 'object' || Object.keys(style).length)) {
475
+ styleNamesKeysSet.add(name);
475
476
  }
476
- return style;
477
- });
477
+ }
478
478
  // For simple styles (for example `color`) we don't have a map of those styles
479
479
  // but they are 1 to 1 with normalized object keys.
480
- const styleNamesKeysSet = new Set([
481
- ...expandedStyleNames,
482
- ...Object.keys(styles)
483
- ]);
480
+ for (const name of Object.keys(styles)) {
481
+ styleNamesKeysSet.add(name);
482
+ }
484
483
  return Array.from(styleNamesKeysSet);
485
484
  }
486
485
  /**