@ckeditor/ckeditor5-engine 35.1.0 → 35.2.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 +22 -22
- package/src/model/nodelist.js +2 -1
- package/src/model/position.js +118 -37
- package/src/model/utils/insertcontent.js +104 -1
- package/src/view/observer/selectionobserver.js +11 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ckeditor/ckeditor5-engine",
|
|
3
|
-
"version": "35.
|
|
3
|
+
"version": "35.2.0",
|
|
4
4
|
"description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wysiwyg",
|
|
@@ -23,30 +23,30 @@
|
|
|
23
23
|
],
|
|
24
24
|
"main": "src/index.js",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@ckeditor/ckeditor5-utils": "^35.
|
|
26
|
+
"@ckeditor/ckeditor5-utils": "^35.2.0",
|
|
27
27
|
"lodash-es": "^4.17.15"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@ckeditor/ckeditor5-basic-styles": "^35.
|
|
31
|
-
"@ckeditor/ckeditor5-block-quote": "^35.
|
|
32
|
-
"@ckeditor/ckeditor5-clipboard": "^35.
|
|
33
|
-
"@ckeditor/ckeditor5-cloud-services": "^35.
|
|
34
|
-
"@ckeditor/ckeditor5-core": "^35.
|
|
35
|
-
"@ckeditor/ckeditor5-editor-classic": "^35.
|
|
36
|
-
"@ckeditor/ckeditor5-enter": "^35.
|
|
37
|
-
"@ckeditor/ckeditor5-essentials": "^35.
|
|
38
|
-
"@ckeditor/ckeditor5-heading": "^35.
|
|
39
|
-
"@ckeditor/ckeditor5-image": "^35.
|
|
40
|
-
"@ckeditor/ckeditor5-link": "^35.
|
|
41
|
-
"@ckeditor/ckeditor5-list": "^35.
|
|
42
|
-
"@ckeditor/ckeditor5-mention": "^35.
|
|
43
|
-
"@ckeditor/ckeditor5-paragraph": "^35.
|
|
44
|
-
"@ckeditor/ckeditor5-table": "^35.
|
|
45
|
-
"@ckeditor/ckeditor5-theme-lark": "^35.
|
|
46
|
-
"@ckeditor/ckeditor5-typing": "^35.
|
|
47
|
-
"@ckeditor/ckeditor5-ui": "^35.
|
|
48
|
-
"@ckeditor/ckeditor5-undo": "^35.
|
|
49
|
-
"@ckeditor/ckeditor5-widget": "^35.
|
|
30
|
+
"@ckeditor/ckeditor5-basic-styles": "^35.2.0",
|
|
31
|
+
"@ckeditor/ckeditor5-block-quote": "^35.2.0",
|
|
32
|
+
"@ckeditor/ckeditor5-clipboard": "^35.2.0",
|
|
33
|
+
"@ckeditor/ckeditor5-cloud-services": "^35.2.0",
|
|
34
|
+
"@ckeditor/ckeditor5-core": "^35.2.0",
|
|
35
|
+
"@ckeditor/ckeditor5-editor-classic": "^35.2.0",
|
|
36
|
+
"@ckeditor/ckeditor5-enter": "^35.2.0",
|
|
37
|
+
"@ckeditor/ckeditor5-essentials": "^35.2.0",
|
|
38
|
+
"@ckeditor/ckeditor5-heading": "^35.2.0",
|
|
39
|
+
"@ckeditor/ckeditor5-image": "^35.2.0",
|
|
40
|
+
"@ckeditor/ckeditor5-link": "^35.2.0",
|
|
41
|
+
"@ckeditor/ckeditor5-list": "^35.2.0",
|
|
42
|
+
"@ckeditor/ckeditor5-mention": "^35.2.0",
|
|
43
|
+
"@ckeditor/ckeditor5-paragraph": "^35.2.0",
|
|
44
|
+
"@ckeditor/ckeditor5-table": "^35.2.0",
|
|
45
|
+
"@ckeditor/ckeditor5-theme-lark": "^35.2.0",
|
|
46
|
+
"@ckeditor/ckeditor5-typing": "^35.2.0",
|
|
47
|
+
"@ckeditor/ckeditor5-ui": "^35.2.0",
|
|
48
|
+
"@ckeditor/ckeditor5-undo": "^35.2.0",
|
|
49
|
+
"@ckeditor/ckeditor5-widget": "^35.2.0",
|
|
50
50
|
"typescript": "^4.6.4",
|
|
51
51
|
"webpack": "^5.58.1",
|
|
52
52
|
"webpack-cli": "^4.9.0"
|
package/src/model/nodelist.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import Node from './node';
|
|
9
9
|
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
|
|
10
|
+
import spliceArray from '@ckeditor/ckeditor5-utils/src/splicearray';
|
|
10
11
|
/**
|
|
11
12
|
* Provides an interface to operate on a list of {@link module:engine/model/node~Node nodes}. `NodeList` is used internally
|
|
12
13
|
* in classes like {@link module:engine/model/element~Element Element}
|
|
@@ -165,7 +166,7 @@ export default class NodeList {
|
|
|
165
166
|
throw new CKEditorError('model-nodelist-insertnodes-not-node', this);
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
|
-
this._nodes.
|
|
169
|
+
this._nodes = spliceArray(this._nodes, Array.from(nodes), index, 0);
|
|
169
170
|
}
|
|
170
171
|
/**
|
|
171
172
|
* Removes one or more nodes starting at the given index.
|
package/src/model/position.js
CHANGED
|
@@ -442,48 +442,46 @@ export default class Position extends TypeCheckable {
|
|
|
442
442
|
* @returns {Boolean} True if positions touch.
|
|
443
443
|
*/
|
|
444
444
|
isTouching(otherPosition) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const compare = this.compareWith(otherPosition);
|
|
448
|
-
switch (compare) {
|
|
449
|
-
case 'same':
|
|
450
|
-
return true;
|
|
451
|
-
case 'before':
|
|
452
|
-
left = Position._createAt(this);
|
|
453
|
-
right = Position._createAt(otherPosition);
|
|
454
|
-
break;
|
|
455
|
-
case 'after':
|
|
456
|
-
left = Position._createAt(otherPosition);
|
|
457
|
-
right = Position._createAt(this);
|
|
458
|
-
break;
|
|
459
|
-
default:
|
|
460
|
-
return false;
|
|
445
|
+
if (this.root !== otherPosition.root) {
|
|
446
|
+
return false;
|
|
461
447
|
}
|
|
462
|
-
|
|
463
|
-
let
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
448
|
+
const commonLevel = Math.min(this.path.length, otherPosition.path.length);
|
|
449
|
+
for (let level = 0; level < commonLevel; level++) {
|
|
450
|
+
const diff = this.path[level] - otherPosition.path[level];
|
|
451
|
+
// Positions are spread by a node, so they are not touching.
|
|
452
|
+
if (diff < -1 || diff > 1) {
|
|
453
|
+
return false;
|
|
467
454
|
}
|
|
468
|
-
if (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
left.path = left.path.slice(0, -1);
|
|
473
|
-
leftParent = leftParent.parent;
|
|
474
|
-
left.offset++;
|
|
455
|
+
else if (diff === 1) {
|
|
456
|
+
// `otherPosition` is on the left.
|
|
457
|
+
// `this` is on the right.
|
|
458
|
+
return checkTouchingBranch(otherPosition, this, level);
|
|
475
459
|
}
|
|
476
|
-
else {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
right.path = right.path.slice(0, -1);
|
|
460
|
+
else if (diff === -1) {
|
|
461
|
+
// `this` is on the left.
|
|
462
|
+
// `otherPosition` is on the right.
|
|
463
|
+
return checkTouchingBranch(this, otherPosition, level);
|
|
481
464
|
}
|
|
465
|
+
// `diff === 0`.
|
|
466
|
+
// Positions are inside the same element on this level, compare deeper.
|
|
467
|
+
}
|
|
468
|
+
// If we ended up here, it means that positions paths have the same beginning.
|
|
469
|
+
// If the paths have the same length, then it means that they are identical, so the positions are same.
|
|
470
|
+
if (this.path.length === otherPosition.path.length) {
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
// If positions have different length of paths, then the common part is the same.
|
|
474
|
+
// In this case, the "shorter" position is on the left, the "longer" position is on the right.
|
|
475
|
+
//
|
|
476
|
+
// If the positions are touching, the "longer" position must have only zeroes. For example:
|
|
477
|
+
// [ 1, 2 ] vs [ 1, 2, 0 ]
|
|
478
|
+
// [ 1, 2 ] vs [ 1, 2, 0, 0, 0 ]
|
|
479
|
+
else if (this.path.length > otherPosition.path.length) {
|
|
480
|
+
return checkOnlyZeroes(this.path, commonLevel);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
return checkOnlyZeroes(otherPosition.path, commonLevel);
|
|
482
484
|
}
|
|
483
|
-
// TypeScript compiler thinks that the flow can reach the end of this function without `return` and requires `undefined` or `void`
|
|
484
|
-
// as the return type. This `throw` convinces the compiler that the flow won't pass till the end.
|
|
485
|
-
/* istanbul ignore next */
|
|
486
|
-
throw new Error('unreachable code');
|
|
487
485
|
}
|
|
488
486
|
/**
|
|
489
487
|
* Checks if two positions are in the same parent.
|
|
@@ -1008,3 +1006,86 @@ export function getNodeBeforePosition(position, positionParent, textNode) {
|
|
|
1008
1006
|
}
|
|
1009
1007
|
return positionParent.getChild(positionParent.offsetToIndex(position.offset) - 1);
|
|
1010
1008
|
}
|
|
1009
|
+
// This is a helper function for `Position#isTouching()`.
|
|
1010
|
+
//
|
|
1011
|
+
// It checks whether to given positions are touching, considering that they have the same root and paths
|
|
1012
|
+
// until given level, and at given level they differ by 1 (so they are branching at `level` point).
|
|
1013
|
+
//
|
|
1014
|
+
// The exact requirements for touching positions are described in `Position#isTouching()` and also
|
|
1015
|
+
// in the body of this function.
|
|
1016
|
+
//
|
|
1017
|
+
// @param {module:engine/model/position~Position} left Position "on the left" (it is before `right`).
|
|
1018
|
+
// @param {module:engine/model/position~Position} right Position "on the right" (it is after `left`).
|
|
1019
|
+
// @param {Number} level Level on which the positions are different.
|
|
1020
|
+
// @returns {Boolean}
|
|
1021
|
+
function checkTouchingBranch(left, right, level) {
|
|
1022
|
+
if (level + 1 === left.path.length) {
|
|
1023
|
+
// Left position does not have any more entries after the point where the positions differ.
|
|
1024
|
+
// [ 2 ] vs [ 3 ]
|
|
1025
|
+
// [ 2 ] vs [ 3, 0, 0 ]
|
|
1026
|
+
// The positions are spread by node at [ 2 ].
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
if (!checkOnlyZeroes(right.path, level + 1)) {
|
|
1030
|
+
// Right position does not have only zeroes, so we have situation like:
|
|
1031
|
+
// [ 2, maxOffset ] vs [ 3, 1 ]
|
|
1032
|
+
// [ 2, maxOffset ] vs [ 3, 1, 0, 0 ]
|
|
1033
|
+
// The positions are spread by node at [ 3, 0 ].
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
if (!checkOnlyMaxOffset(left, level + 1)) {
|
|
1037
|
+
// Left position does not have only max offsets, so we have situation like:
|
|
1038
|
+
// [ 2, 4 ] vs [ 3 ]
|
|
1039
|
+
// [ 2, 4 ] vs [ 3, 0, 0 ]
|
|
1040
|
+
// The positions are spread by node at [ 2, 5 ].
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
// Left position has only max offsets and right position has only zeroes or nothing.
|
|
1044
|
+
// [ 2, maxOffset ] vs [ 3 ]
|
|
1045
|
+
// [ 2, maxOffset, maxOffset ] vs [ 3, 0 ]
|
|
1046
|
+
// There are not elements between positions. The positions are touching.
|
|
1047
|
+
return true;
|
|
1048
|
+
}
|
|
1049
|
+
// Checks whether for given array, starting from given index until the end of the array, all items are `0`s.
|
|
1050
|
+
//
|
|
1051
|
+
// This is a helper function for `Position#isTouching()`.
|
|
1052
|
+
//
|
|
1053
|
+
// @private
|
|
1054
|
+
// @param {Array.<Number>} arr Array to check.
|
|
1055
|
+
// @param {Number} idx Index to start checking from.
|
|
1056
|
+
// @returns {Boolean}
|
|
1057
|
+
function checkOnlyZeroes(arr, idx) {
|
|
1058
|
+
while (idx < arr.length) {
|
|
1059
|
+
if (arr[idx] !== 0) {
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1062
|
+
idx++;
|
|
1063
|
+
}
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
1066
|
+
// Checks whether for given position, starting from given path level, whether the position is at the end of
|
|
1067
|
+
// its parent and whether each element on the path to the position is also at at the end of its parent.
|
|
1068
|
+
//
|
|
1069
|
+
// This is a helper function for `Position#isTouching()`.
|
|
1070
|
+
//
|
|
1071
|
+
// @private
|
|
1072
|
+
// @param {module:engine/model/position~Position} pos Position to check.
|
|
1073
|
+
// @param {Number} level Level to start checking from.
|
|
1074
|
+
// @returns {Boolean}
|
|
1075
|
+
function checkOnlyMaxOffset(pos, level) {
|
|
1076
|
+
let parent = pos.parent;
|
|
1077
|
+
let idx = pos.path.length - 1;
|
|
1078
|
+
let add = 0;
|
|
1079
|
+
while (idx >= level) {
|
|
1080
|
+
if (pos.path[idx] + add !== parent.maxOffset) {
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
// After the first check, we "go up", and check whether the position's parent-parent is the last element.
|
|
1084
|
+
// However, we need to add 1 to the value in the path to "simulate" moving the path after the parent.
|
|
1085
|
+
// It happens just once.
|
|
1086
|
+
add = 1;
|
|
1087
|
+
idx--;
|
|
1088
|
+
parent = parent.parent;
|
|
1089
|
+
}
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
@@ -12,6 +12,7 @@ import Position from '../position';
|
|
|
12
12
|
import Range from '../range';
|
|
13
13
|
import Selection from '../selection';
|
|
14
14
|
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
|
|
15
|
+
import { LiveRange } from '../../index';
|
|
15
16
|
/**
|
|
16
17
|
* Inserts content into the editor (specified selection) as one would expect the paste functionality to work.
|
|
17
18
|
*
|
|
@@ -60,15 +61,117 @@ export default function insertContent(model, content, selectable, placeOrOffset)
|
|
|
60
61
|
model.deleteContent(selection, { doNotAutoparagraph: true });
|
|
61
62
|
}
|
|
62
63
|
const insertion = new Insertion(model, writer, selection.anchor);
|
|
64
|
+
const fakeMarkerElements = [];
|
|
63
65
|
let nodesToInsert;
|
|
64
66
|
if (content.is('documentFragment')) {
|
|
67
|
+
// If document fragment has any markers, these markers should be inserted into the model as well.
|
|
68
|
+
if (content.markers.size) {
|
|
69
|
+
const markersPosition = [];
|
|
70
|
+
for (const [name, range] of content.markers) {
|
|
71
|
+
const { start, end } = range;
|
|
72
|
+
const isCollapsed = start.isEqual(end);
|
|
73
|
+
markersPosition.push({ position: start, name, isCollapsed }, { position: end, name, isCollapsed });
|
|
74
|
+
}
|
|
75
|
+
// Markers position is sorted backwards to ensure that the insertion of fake markers will not change
|
|
76
|
+
// the position of the next markers.
|
|
77
|
+
markersPosition.sort(({ position: posA }, { position: posB }) => posA.isBefore(posB) ? 1 : -1);
|
|
78
|
+
for (const { position, name, isCollapsed } of markersPosition) {
|
|
79
|
+
let fakeElement = null;
|
|
80
|
+
let collapsed = null;
|
|
81
|
+
const isAtBeginning = position.parent === content && position.isAtStart;
|
|
82
|
+
const isAtEnd = position.parent === content && position.isAtEnd;
|
|
83
|
+
// We have two ways of handling markers. In general, we want to add temporary <$marker> model elements to
|
|
84
|
+
// represent marker boundaries. These elements will be inserted into content together with the rest
|
|
85
|
+
// of the document fragment. After insertion is done, positions for these elements will be read
|
|
86
|
+
// and proper, actual markers will be created in the model and fake elements will be removed.
|
|
87
|
+
//
|
|
88
|
+
// However, if the <$marker> element is at the beginning or at the end of the document fragment,
|
|
89
|
+
// it may affect how the inserted content is merged with current model, impacting the insertion
|
|
90
|
+
// result. To avoid that, we don't add <$marker> elements at these positions. Instead, we will use
|
|
91
|
+
// `Insertion#getAffectedRange()` to figure out new positions for these marker boundaries.
|
|
92
|
+
if (!isAtBeginning && !isAtEnd) {
|
|
93
|
+
fakeElement = writer.createElement('$marker');
|
|
94
|
+
writer.insert(fakeElement, position);
|
|
95
|
+
}
|
|
96
|
+
else if (isCollapsed) {
|
|
97
|
+
// Save whether the collapsed marker was at the beginning or at the end of document fragment
|
|
98
|
+
// to know where to create it after the insertion is done.
|
|
99
|
+
collapsed = isAtBeginning ? 'start' : 'end';
|
|
100
|
+
}
|
|
101
|
+
fakeMarkerElements.push({
|
|
102
|
+
name,
|
|
103
|
+
element: fakeElement,
|
|
104
|
+
collapsed
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
65
108
|
nodesToInsert = content.getChildren();
|
|
66
109
|
}
|
|
67
110
|
else {
|
|
68
111
|
nodesToInsert = [content];
|
|
69
112
|
}
|
|
70
113
|
insertion.handleNodes(nodesToInsert);
|
|
71
|
-
|
|
114
|
+
let newRange = insertion.getSelectionRange();
|
|
115
|
+
if (content.is('documentFragment') && fakeMarkerElements.length) {
|
|
116
|
+
// After insertion was done, the selection was set but the model contains fake <$marker> elements.
|
|
117
|
+
// These <$marker> elements will be now removed. Because of that, we will need to fix the selection.
|
|
118
|
+
// We will create a live range that will automatically be update as <$marker> elements are removed.
|
|
119
|
+
const selectionLiveRange = newRange ? LiveRange.fromRange(newRange) : null;
|
|
120
|
+
// Marker name -> [ start position, end position ].
|
|
121
|
+
const markersData = {};
|
|
122
|
+
// Note: `fakeMarkerElements` are sorted backwards. However, now, we want to handle the markers
|
|
123
|
+
// from the beginning, so that existing <$marker> elements do not affect markers positions.
|
|
124
|
+
// This is why we iterate from the end to the start.
|
|
125
|
+
for (let i = fakeMarkerElements.length - 1; i >= 0; i--) {
|
|
126
|
+
const { name, element, collapsed } = fakeMarkerElements[i];
|
|
127
|
+
const isStartBoundary = !markersData[name];
|
|
128
|
+
if (isStartBoundary) {
|
|
129
|
+
markersData[name] = [];
|
|
130
|
+
}
|
|
131
|
+
if (element) {
|
|
132
|
+
// Read fake marker element position to learn where the marker should be created.
|
|
133
|
+
const elementPosition = writer.createPositionAt(element, 'before');
|
|
134
|
+
markersData[name].push(elementPosition);
|
|
135
|
+
writer.remove(element);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// If the fake marker element does not exist, it means that the marker boundary was at the beginning or at the end.
|
|
139
|
+
const rangeOnInsertion = insertion.getAffectedRange();
|
|
140
|
+
if (!rangeOnInsertion) {
|
|
141
|
+
// If affected range is `null` it means that nothing was in the document fragment or all content was filtered out.
|
|
142
|
+
// Some markers that were in the filtered content may be removed (partially or totally).
|
|
143
|
+
// Let's handle only those markers that were at the beginning or at the end of the document fragment.
|
|
144
|
+
if (collapsed) {
|
|
145
|
+
markersData[name].push(insertion.position);
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (collapsed) {
|
|
150
|
+
// If the marker was collapsed at the beginning or at the end of the document fragment,
|
|
151
|
+
// put both boundaries at the beginning or at the end of inserted range (to keep the marker collapsed).
|
|
152
|
+
markersData[name].push(rangeOnInsertion[collapsed]);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
markersData[name].push(isStartBoundary ? rangeOnInsertion.start : rangeOnInsertion.end);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const [name, [start, end]] of Object.entries(markersData)) {
|
|
160
|
+
// For now, we ignore markers if they are included in the filtered-out content.
|
|
161
|
+
// In the future implementation we will improve that case to create markers that are not filtered out completely.
|
|
162
|
+
if (start && end && start.root === end.root) {
|
|
163
|
+
writer.addMarker(name, {
|
|
164
|
+
usingOperation: true,
|
|
165
|
+
affectsData: true,
|
|
166
|
+
range: new Range(start, end)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (selectionLiveRange) {
|
|
171
|
+
newRange = selectionLiveRange.toRange();
|
|
172
|
+
selectionLiveRange.detach();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
72
175
|
/* istanbul ignore else */
|
|
73
176
|
if (newRange) {
|
|
74
177
|
if (selection instanceof DocumentSelection) {
|
|
@@ -109,6 +109,12 @@ export default class SelectionObserver extends Observer {
|
|
|
109
109
|
this._documentIsSelectingInactivityTimeoutDebounced();
|
|
110
110
|
};
|
|
111
111
|
const endDocumentIsSelecting = () => {
|
|
112
|
+
if (!this.document.isSelecting) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Make sure that model selection is up-to-date at the end of selecting process.
|
|
116
|
+
// Sometimes `selectionchange` events could arrive after the `mouseup` event and that selection could be already outdated.
|
|
117
|
+
this._handleSelectionChange(null, domDocument);
|
|
112
118
|
this.document.isSelecting = false;
|
|
113
119
|
// The safety timeout can be canceled when the document leaves the "is selecting" state.
|
|
114
120
|
this._documentIsSelectingInactivityTimeoutDebounced.cancel();
|
|
@@ -117,13 +123,15 @@ export default class SelectionObserver extends Observer {
|
|
|
117
123
|
// (e.g. by holding the mouse button and moving the cursor). The state resets when they either released
|
|
118
124
|
// the mouse button or interrupted the process by pressing or releasing any key.
|
|
119
125
|
this.listenTo(domElement, 'selectstart', startDocumentIsSelecting, { priority: 'highest' });
|
|
120
|
-
this.listenTo(domElement, 'keydown', endDocumentIsSelecting, { priority: 'highest' });
|
|
121
|
-
this.listenTo(domElement, 'keyup', endDocumentIsSelecting, { priority: 'highest' });
|
|
126
|
+
this.listenTo(domElement, 'keydown', endDocumentIsSelecting, { priority: 'highest', useCapture: true });
|
|
127
|
+
this.listenTo(domElement, 'keyup', endDocumentIsSelecting, { priority: 'highest', useCapture: true });
|
|
122
128
|
// Add document-wide listeners only once. This method could be called for multiple editing roots.
|
|
123
129
|
if (this._documents.has(domDocument)) {
|
|
124
130
|
return;
|
|
125
131
|
}
|
|
126
|
-
|
|
132
|
+
// This listener is using capture mode to make sure that selection is upcasted before any other
|
|
133
|
+
// handler would like to check it and update (for example table multi cell selection).
|
|
134
|
+
this.listenTo(domDocument, 'mouseup', endDocumentIsSelecting, { priority: 'highest', useCapture: true });
|
|
127
135
|
this.listenTo(domDocument, 'selectionchange', (evt, domEvent) => {
|
|
128
136
|
this._handleSelectionChange(domEvent, domDocument);
|
|
129
137
|
// Defer the safety timeout when the selection changes (e.g. the user keeps extending the selection
|