@ckeditor/ckeditor5-engine 43.1.0 → 43.2.0-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 +182 -99
- package/dist/index.js.map +1 -1
- package/dist/view/observer/domeventobserver.d.ts +5 -0
- package/package.json +2 -2
- package/src/dataprocessor/htmldataprocessor.js +1 -1
- package/src/model/nodelist.js +8 -1
- package/src/model/operation/transform.js +136 -62
- package/src/model/range.js +9 -12
- package/src/model/schema.js +2 -2
- package/src/view/domconverter.js +1 -1
- package/src/view/observer/domeventobserver.d.ts +5 -0
- package/src/view/observer/domeventobserver.js +6 -1
- package/src/view/observer/selectionobserver.js +1 -1
- package/src/view/renderer.js +11 -8
- package/src/view/styles/utils.js +1 -1
- package/src/view/stylesmap.js +8 -9
|
@@ -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.
|
|
3
|
+
"version": "43.2.0-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": "43.
|
|
27
|
+
"@ckeditor/ckeditor5-utils": "43.2.0-alpha.0",
|
|
28
28
|
"lodash-es": "4.17.21"
|
|
29
29
|
},
|
|
30
30
|
"author": "CKSource (http://cksource.com/)",
|
|
@@ -82,7 +82,7 @@ export default class HtmlDataProcessor {
|
|
|
82
82
|
// Wrap data with a <body> tag so leading non-layout nodes (like <script>, <style>, HTML comment)
|
|
83
83
|
// will be preserved in the body collection.
|
|
84
84
|
// Do it only for data that is not a full HTML document.
|
|
85
|
-
if (
|
|
85
|
+
if (!/<(?:html|body|head|meta)(?:\s[^>]*)?>/i.test(data.trim().slice(0, 10000))) {
|
|
86
86
|
data = `<body>${data}</body>`;
|
|
87
87
|
}
|
|
88
88
|
const document = this.domParser.parseFromString(data, 'text/html');
|
package/src/model/nodelist.js
CHANGED
|
@@ -67,7 +67,14 @@ export default class NodeList {
|
|
|
67
67
|
*/
|
|
68
68
|
getNodeStartOffset(node) {
|
|
69
69
|
const index = this.getNodeIndex(node);
|
|
70
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1224
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
1244
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1320
|
+
// For these two cases we will only transform `sourcePosition` and return early.
|
|
1251
1321
|
//
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
//
|
|
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 (
|
|
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,
|
|
1590
|
-
gyMoveSource = gyMoveSource._getTransformedByMove(a.sourcePosition,
|
|
1591
|
-
splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove(a.sourcePosition,
|
|
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,
|
|
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);
|
package/src/model/range.js
CHANGED
|
@@ -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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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.
|
package/src/model/schema.js
CHANGED
|
@@ -34,7 +34,7 @@ export default class Schema extends /* #__PURE__ */ ObservableMixin() {
|
|
|
34
34
|
/**
|
|
35
35
|
* A dictionary containing attribute properties.
|
|
36
36
|
*/
|
|
37
|
-
this._attributeProperties =
|
|
37
|
+
this._attributeProperties = Object.create(null);
|
|
38
38
|
/**
|
|
39
39
|
* Stores additional callbacks registered for schema items, which are evaluated when {@link ~Schema#checkChild} is called.
|
|
40
40
|
*
|
|
@@ -640,7 +640,7 @@ export default class Schema extends /* #__PURE__ */ ObservableMixin() {
|
|
|
640
640
|
* @param attributeName A name of the attribute.
|
|
641
641
|
*/
|
|
642
642
|
getAttributeProperties(attributeName) {
|
|
643
|
-
return this._attributeProperties[attributeName] ||
|
|
643
|
+
return this._attributeProperties[attributeName] || Object.create(null);
|
|
644
644
|
}
|
|
645
645
|
/**
|
|
646
646
|
* Returns the lowest {@link module:engine/model/schema~Schema#isLimit limit element} containing the entire
|
package/src/view/domconverter.js
CHANGED
|
@@ -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
|
|
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', (
|
|
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();
|
package/src/view/renderer.js
CHANGED
|
@@ -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
|
-
|
|
453
|
-
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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.
|
package/src/view/styles/utils.js
CHANGED
|
@@ -214,6 +214,6 @@ export function getPositionShorthandNormalizer(shorthand) {
|
|
|
214
214
|
* ```
|
|
215
215
|
*/
|
|
216
216
|
export function getShorthandValues(string) {
|
|
217
|
-
const matches = string.matchAll(CSS_SHORTHAND_VALUE_REGEXP);
|
|
217
|
+
const matches = string.trim().slice(0, 1500).matchAll(CSS_SHORTHAND_VALUE_REGEXP);
|
|
218
218
|
return Array.from(matches).map(i => i[0]);
|
|
219
219
|
}
|
package/src/view/stylesmap.js
CHANGED
|
@@ -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
|
|
472
|
+
for (const name of this._consumables.keys()) {
|
|
472
473
|
const style = this.getNormalized(name, styles);
|
|
473
|
-
if (style && typeof style
|
|
474
|
-
|
|
474
|
+
if (style && (typeof style != 'object' || Object.keys(style).length)) {
|
|
475
|
+
styleNamesKeysSet.add(name);
|
|
475
476
|
}
|
|
476
|
-
|
|
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
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
]);
|
|
480
|
+
for (const name of Object.keys(styles)) {
|
|
481
|
+
styleNamesKeysSet.add(name);
|
|
482
|
+
}
|
|
484
483
|
return Array.from(styleNamesKeysSet);
|
|
485
484
|
}
|
|
486
485
|
/**
|