@ckeditor/ckeditor5-engine 38.0.1 → 38.1.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 +2 -31
- package/src/controller/datacontroller.js +1 -2
- package/src/conversion/downcasthelpers.d.ts +1 -1
- package/src/conversion/downcasthelpers.js +1 -1
- package/src/conversion/upcasthelpers.js +2 -2
- package/src/index.d.ts +2 -1
- package/src/index.js +1 -0
- package/src/model/documentselection.js +27 -9
- package/src/model/treewalker.js +29 -35
- package/src/view/observer/inputobserver.js +13 -2
- package/src/view/styles/utils.js +5 -4
- package/src/view/treewalker.js +52 -62
- package/src/view/view.d.ts +46 -1
- package/src/view/view.js +25 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ckeditor/ckeditor5-engine",
|
|
3
|
-
"version": "38.0
|
|
3
|
+
"version": "38.1.0",
|
|
4
4
|
"description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wysiwyg",
|
|
@@ -23,34 +23,9 @@
|
|
|
23
23
|
],
|
|
24
24
|
"main": "src/index.js",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@ckeditor/ckeditor5-utils": "
|
|
26
|
+
"@ckeditor/ckeditor5-utils": "38.1.0",
|
|
27
27
|
"lodash-es": "^4.17.15"
|
|
28
28
|
},
|
|
29
|
-
"devDependencies": {
|
|
30
|
-
"@ckeditor/ckeditor5-basic-styles": "^38.0.1",
|
|
31
|
-
"@ckeditor/ckeditor5-block-quote": "^38.0.1",
|
|
32
|
-
"@ckeditor/ckeditor5-clipboard": "^38.0.1",
|
|
33
|
-
"@ckeditor/ckeditor5-cloud-services": "^38.0.1",
|
|
34
|
-
"@ckeditor/ckeditor5-core": "^38.0.1",
|
|
35
|
-
"@ckeditor/ckeditor5-editor-classic": "^38.0.1",
|
|
36
|
-
"@ckeditor/ckeditor5-enter": "^38.0.1",
|
|
37
|
-
"@ckeditor/ckeditor5-essentials": "^38.0.1",
|
|
38
|
-
"@ckeditor/ckeditor5-heading": "^38.0.1",
|
|
39
|
-
"@ckeditor/ckeditor5-image": "^38.0.1",
|
|
40
|
-
"@ckeditor/ckeditor5-link": "^38.0.1",
|
|
41
|
-
"@ckeditor/ckeditor5-list": "^38.0.1",
|
|
42
|
-
"@ckeditor/ckeditor5-mention": "^38.0.1",
|
|
43
|
-
"@ckeditor/ckeditor5-paragraph": "^38.0.1",
|
|
44
|
-
"@ckeditor/ckeditor5-table": "^38.0.1",
|
|
45
|
-
"@ckeditor/ckeditor5-theme-lark": "^38.0.1",
|
|
46
|
-
"@ckeditor/ckeditor5-typing": "^38.0.1",
|
|
47
|
-
"@ckeditor/ckeditor5-ui": "^38.0.1",
|
|
48
|
-
"@ckeditor/ckeditor5-undo": "^38.0.1",
|
|
49
|
-
"@ckeditor/ckeditor5-widget": "^38.0.1",
|
|
50
|
-
"typescript": "^4.8.4",
|
|
51
|
-
"webpack": "^5.58.1",
|
|
52
|
-
"webpack-cli": "^4.9.0"
|
|
53
|
-
},
|
|
54
29
|
"engines": {
|
|
55
30
|
"node": ">=16.0.0",
|
|
56
31
|
"npm": ">=5.7.1"
|
|
@@ -72,9 +47,5 @@
|
|
|
72
47
|
"ckeditor5-metadata.json",
|
|
73
48
|
"CHANGELOG.md"
|
|
74
49
|
],
|
|
75
|
-
"scripts": {
|
|
76
|
-
"build": "tsc -p ./tsconfig.json",
|
|
77
|
-
"postversion": "npm run build"
|
|
78
|
-
},
|
|
79
50
|
"types": "src/index.d.ts"
|
|
80
51
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @module engine/controller/datacontroller
|
|
7
7
|
*/
|
|
8
|
-
import { CKEditorError, EmitterMixin, ObservableMixin } from '@ckeditor/ckeditor5-utils';
|
|
8
|
+
import { CKEditorError, EmitterMixin, ObservableMixin, logWarning } from '@ckeditor/ckeditor5-utils';
|
|
9
9
|
import Mapper from '../conversion/mapper';
|
|
10
10
|
import DowncastDispatcher from '../conversion/downcastdispatcher';
|
|
11
11
|
import { insertAttributesAndChildren, insertText } from '../conversion/downcasthelpers';
|
|
@@ -17,7 +17,6 @@ import ViewDowncastWriter from '../view/downcastwriter';
|
|
|
17
17
|
import ModelRange from '../model/range';
|
|
18
18
|
import { autoParagraphEmptyRoots } from '../model/utils/autoparagraphing';
|
|
19
19
|
import HtmlDataProcessor from '../dataprocessor/htmldataprocessor';
|
|
20
|
-
import { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
|
|
21
20
|
/**
|
|
22
21
|
* Controller for the data pipeline. The data pipeline controls how data is retrieved from the document
|
|
23
22
|
* and set inside it. Hence, the controller features two methods which allow to {@link ~DataController#get get}
|
|
@@ -236,7 +236,7 @@ export default class DowncastHelpers extends ConversionHelpers<DowncastDispatche
|
|
|
236
236
|
* } );
|
|
237
237
|
* ```
|
|
238
238
|
*
|
|
239
|
-
* The `
|
|
239
|
+
* The `createSlot()` function can also take a callback that allows filtering which children of the model element
|
|
240
240
|
* should be converted into this slot.
|
|
241
241
|
*
|
|
242
242
|
* Imagine a table feature where for this model structure:
|
|
@@ -221,7 +221,7 @@ export default class DowncastHelpers extends ConversionHelpers {
|
|
|
221
221
|
* } );
|
|
222
222
|
* ```
|
|
223
223
|
*
|
|
224
|
-
* The `
|
|
224
|
+
* The `createSlot()` function can also take a callback that allows filtering which children of the model element
|
|
225
225
|
* should be converted into this slot.
|
|
226
226
|
*
|
|
227
227
|
* Imagine a table feature where for this model structure:
|
|
@@ -639,8 +639,8 @@ function upcastDataToMarker(config) {
|
|
|
639
639
|
//
|
|
640
640
|
// This hack probably would not be needed if attributes are upcasted separately.
|
|
641
641
|
//
|
|
642
|
-
const basePriority = priorities.
|
|
643
|
-
const maxPriority = priorities.
|
|
642
|
+
const basePriority = priorities.low;
|
|
643
|
+
const maxPriority = priorities.highest;
|
|
644
644
|
const priorityFactor = priorities.get(config.converterPriority) / maxPriority; // Number in range [ -1, 1 ].
|
|
645
645
|
dispatcher.on('element', upcastAttributeToMarker(normalizedConfig), { priority: basePriority + priorityFactor });
|
|
646
646
|
};
|
package/src/index.d.ts
CHANGED
|
@@ -73,6 +73,7 @@ export { default as ViewEmptyElement } from './view/emptyelement';
|
|
|
73
73
|
export { default as ViewRawElement } from './view/rawelement';
|
|
74
74
|
export { default as ViewUIElement } from './view/uielement';
|
|
75
75
|
export { default as ViewDocumentFragment } from './view/documentfragment';
|
|
76
|
+
export { default as ViewTreeWalker, type TreeWalkerValue as ViewTreeWalkerValue } from './view/treewalker';
|
|
76
77
|
export type { default as ViewElementDefinition } from './view/elementdefinition';
|
|
77
78
|
export type { default as ViewDocumentSelection } from './view/documentselection';
|
|
78
79
|
export { default as AttributeElement } from './view/attributeelement';
|
|
@@ -103,7 +104,7 @@ export type { ViewDocumentMouseDownEvent, ViewDocumentMouseUpEvent, ViewDocument
|
|
|
103
104
|
export type { ViewDocumentTabEvent } from './view/observer/tabobserver';
|
|
104
105
|
export type { ViewDocumentClickEvent } from './view/observer/clickobserver';
|
|
105
106
|
export type { ViewDocumentSelectionChangeEvent } from './view/observer/selectionobserver';
|
|
106
|
-
export type { ViewRenderEvent } from './view/view';
|
|
107
|
+
export type { ViewRenderEvent, ViewScrollToTheSelectionEvent } from './view/view';
|
|
107
108
|
export { StylesProcessor, type BoxSides } from './view/stylesmap';
|
|
108
109
|
export * from './view/styles/background';
|
|
109
110
|
export * from './view/styles/border';
|
package/src/index.js
CHANGED
|
@@ -54,6 +54,7 @@ export { default as ViewEmptyElement } from './view/emptyelement';
|
|
|
54
54
|
export { default as ViewRawElement } from './view/rawelement';
|
|
55
55
|
export { default as ViewUIElement } from './view/uielement';
|
|
56
56
|
export { default as ViewDocumentFragment } from './view/documentfragment';
|
|
57
|
+
export { default as ViewTreeWalker } from './view/treewalker';
|
|
57
58
|
export { default as AttributeElement } from './view/attributeelement';
|
|
58
59
|
export { getFillerOffset } from './view/containerelement';
|
|
59
60
|
// View / Observer.
|
|
@@ -889,27 +889,27 @@ class LiveSelection extends Selection {
|
|
|
889
889
|
// When gravity is overridden then don't take node before into consideration.
|
|
890
890
|
if (!this.isGravityOverridden) {
|
|
891
891
|
// ...look at the node before caret and take attributes from it if it is a character node.
|
|
892
|
-
attrs =
|
|
892
|
+
attrs = getTextAttributes(nodeBefore, schema);
|
|
893
893
|
}
|
|
894
894
|
// 3. If not, look at the node after caret...
|
|
895
895
|
if (!attrs) {
|
|
896
|
-
attrs =
|
|
896
|
+
attrs = getTextAttributes(nodeAfter, schema);
|
|
897
897
|
}
|
|
898
898
|
// 4. If not, try to find the first character on the left, that is in the same node.
|
|
899
899
|
// When gravity is overridden then don't take node before into consideration.
|
|
900
900
|
if (!this.isGravityOverridden && !attrs) {
|
|
901
901
|
let node = nodeBefore;
|
|
902
|
-
while (node && !
|
|
902
|
+
while (node && !attrs) {
|
|
903
903
|
node = node.previousSibling;
|
|
904
|
-
attrs =
|
|
904
|
+
attrs = getTextAttributes(node, schema);
|
|
905
905
|
}
|
|
906
906
|
}
|
|
907
907
|
// 5. If not found, try to find the first character on the right, that is in the same node.
|
|
908
908
|
if (!attrs) {
|
|
909
909
|
let node = nodeAfter;
|
|
910
|
-
while (node && !
|
|
910
|
+
while (node && !attrs) {
|
|
911
911
|
node = node.nextSibling;
|
|
912
|
-
attrs =
|
|
912
|
+
attrs = getTextAttributes(node, schema);
|
|
913
913
|
}
|
|
914
914
|
}
|
|
915
915
|
// 6. If not found, selection should retrieve attributes from parent.
|
|
@@ -937,13 +937,31 @@ class LiveSelection extends Selection {
|
|
|
937
937
|
/**
|
|
938
938
|
* Helper function for {@link module:engine/model/liveselection~LiveSelection#_updateAttributes}.
|
|
939
939
|
*
|
|
940
|
-
* It
|
|
940
|
+
* It checks if the passed model item is a text node (or text proxy) and, if so, returns it's attributes.
|
|
941
|
+
* If not, it checks if item is an inline object and does the same. Otherwise it returns `null`.
|
|
941
942
|
*/
|
|
942
|
-
function
|
|
943
|
+
function getTextAttributes(node, schema) {
|
|
944
|
+
if (!node) {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
943
947
|
if (node instanceof TextProxy || node instanceof Text) {
|
|
944
948
|
return node.getAttributes();
|
|
945
949
|
}
|
|
946
|
-
|
|
950
|
+
if (!schema.isInline(node)) {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
// Stop on inline elements (such as `<softBreak>`) that are not objects (such as `<imageInline>` or `<mathml>`).
|
|
954
|
+
if (!schema.isObject(node)) {
|
|
955
|
+
return [];
|
|
956
|
+
}
|
|
957
|
+
const attributes = [];
|
|
958
|
+
// Collect all attributes that can be applied to the text node.
|
|
959
|
+
for (const [key, value] of node.getAttributes()) {
|
|
960
|
+
if (schema.checkAttribute('$text', key)) {
|
|
961
|
+
attributes.push([key, value]);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return attributes;
|
|
947
965
|
}
|
|
948
966
|
/**
|
|
949
967
|
* Removes selection attributes from element which is not empty anymore.
|
package/src/model/treewalker.js
CHANGED
|
@@ -126,7 +126,7 @@ export default class TreeWalker {
|
|
|
126
126
|
// Get node just after the current position.
|
|
127
127
|
// Use a highly optimized version instead of checking the text node first and then getting the node after. See #6582.
|
|
128
128
|
const textNodeAtPosition = getTextNodeAtPosition(position, parent);
|
|
129
|
-
const node = textNodeAtPosition
|
|
129
|
+
const node = textNodeAtPosition || getNodeAfterPosition(position, parent, textNodeAtPosition);
|
|
130
130
|
if (node instanceof Element) {
|
|
131
131
|
if (!this.shallow) {
|
|
132
132
|
// Manual operations on path internals for optimization purposes. Here and in the rest of the method.
|
|
@@ -134,12 +134,16 @@ export default class TreeWalker {
|
|
|
134
134
|
this._visitedParent = node;
|
|
135
135
|
}
|
|
136
136
|
else {
|
|
137
|
+
// We are past the walker boundaries.
|
|
138
|
+
if (this.boundaries && this.boundaries.end.isBefore(position)) {
|
|
139
|
+
return { done: true, value: undefined };
|
|
140
|
+
}
|
|
137
141
|
position.offset++;
|
|
138
142
|
}
|
|
139
143
|
this._position = position;
|
|
140
144
|
return formatReturnValue('elementStart', node, previousPosition, position, 1);
|
|
141
145
|
}
|
|
142
|
-
|
|
146
|
+
if (node instanceof Text) {
|
|
143
147
|
let charactersCount;
|
|
144
148
|
if (this.singleCharacters) {
|
|
145
149
|
charactersCount = 1;
|
|
@@ -157,19 +161,15 @@ export default class TreeWalker {
|
|
|
157
161
|
this._position = position;
|
|
158
162
|
return formatReturnValue('text', item, previousPosition, position, charactersCount);
|
|
159
163
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return this._next();
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
return formatReturnValue('elementEnd', parent, previousPosition, position);
|
|
171
|
-
}
|
|
164
|
+
// `node` is not set, we reached the end of current `parent`.
|
|
165
|
+
position.path.pop();
|
|
166
|
+
position.offset++;
|
|
167
|
+
this._position = position;
|
|
168
|
+
this._visitedParent = parent.parent;
|
|
169
|
+
if (this.ignoreElementEnd) {
|
|
170
|
+
return this._next();
|
|
172
171
|
}
|
|
172
|
+
return formatReturnValue('elementEnd', parent, previousPosition, position);
|
|
173
173
|
}
|
|
174
174
|
/**
|
|
175
175
|
* Makes a step backward in model. Moves the {@link #position} to the previous position and returns the encountered value.
|
|
@@ -190,26 +190,22 @@ export default class TreeWalker {
|
|
|
190
190
|
// Use a highly optimized version instead of checking the text node first and then getting the node before. See #6582.
|
|
191
191
|
const positionParent = position.parent;
|
|
192
192
|
const textNodeAtPosition = getTextNodeAtPosition(position, positionParent);
|
|
193
|
-
const node = textNodeAtPosition
|
|
193
|
+
const node = textNodeAtPosition || getNodeBeforePosition(position, positionParent, textNodeAtPosition);
|
|
194
194
|
if (node instanceof Element) {
|
|
195
195
|
position.offset--;
|
|
196
|
-
if (
|
|
197
|
-
position.path.push(node.maxOffset);
|
|
198
|
-
this._position = position;
|
|
199
|
-
this._visitedParent = node;
|
|
200
|
-
if (this.ignoreElementEnd) {
|
|
201
|
-
return this._previous();
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
return formatReturnValue('elementEnd', node, previousPosition, position);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
196
|
+
if (this.shallow) {
|
|
208
197
|
this._position = position;
|
|
209
198
|
return formatReturnValue('elementStart', node, previousPosition, position, 1);
|
|
210
199
|
}
|
|
200
|
+
position.path.push(node.maxOffset);
|
|
201
|
+
this._position = position;
|
|
202
|
+
this._visitedParent = node;
|
|
203
|
+
if (this.ignoreElementEnd) {
|
|
204
|
+
return this._previous();
|
|
205
|
+
}
|
|
206
|
+
return formatReturnValue('elementEnd', node, previousPosition, position);
|
|
211
207
|
}
|
|
212
|
-
|
|
208
|
+
if (node instanceof Text) {
|
|
213
209
|
let charactersCount;
|
|
214
210
|
if (this.singleCharacters) {
|
|
215
211
|
charactersCount = 1;
|
|
@@ -227,13 +223,11 @@ export default class TreeWalker {
|
|
|
227
223
|
this._position = position;
|
|
228
224
|
return formatReturnValue('text', item, previousPosition, position, charactersCount);
|
|
229
225
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return formatReturnValue('elementStart', parent, previousPosition, position, 1);
|
|
236
|
-
}
|
|
226
|
+
// `node` is not set, we reached the beginning of current `parent`.
|
|
227
|
+
position.path.pop();
|
|
228
|
+
this._position = position;
|
|
229
|
+
this._visitedParent = parent.parent;
|
|
230
|
+
return formatReturnValue('elementStart', parent, previousPosition, position, 1);
|
|
237
231
|
}
|
|
238
232
|
}
|
|
239
233
|
function formatReturnValue(type, item, previousPosition, nextPosition, length) {
|
|
@@ -70,8 +70,19 @@ export default class InputObserver extends DomEventObserver {
|
|
|
70
70
|
}
|
|
71
71
|
else if (domTargetRanges.length) {
|
|
72
72
|
targetRanges = domTargetRanges.map(domRange => {
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
// Sometimes browser provides range that starts before editable node.
|
|
74
|
+
// We try to fall back to collapsed range at the valid end position.
|
|
75
|
+
// See https://github.com/ckeditor/ckeditor5/issues/14411.
|
|
76
|
+
// See https://github.com/ckeditor/ckeditor5/issues/14050.
|
|
77
|
+
const viewStart = view.domConverter.domPositionToView(domRange.startContainer, domRange.startOffset);
|
|
78
|
+
const viewEnd = view.domConverter.domPositionToView(domRange.endContainer, domRange.endOffset);
|
|
79
|
+
if (viewStart) {
|
|
80
|
+
return view.createRange(viewStart, viewEnd);
|
|
81
|
+
}
|
|
82
|
+
else if (viewEnd) {
|
|
83
|
+
return view.createRange(viewEnd);
|
|
84
|
+
}
|
|
85
|
+
}).filter((range) => !!range);
|
|
75
86
|
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
|
|
76
87
|
// @if CK_DEBUG_TYPING // console.info( '%c[InputObserver]%c using target ranges:',
|
|
77
88
|
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', targetRanges
|
package/src/view/styles/utils.js
CHANGED
|
@@ -7,6 +7,9 @@ const RGB_COLOR_REGEXP = /^rgb\([ ]?([0-9]{1,3}[ %]?,[ ]?){2,3}[0-9]{1,3}[ %]?\)
|
|
|
7
7
|
const RGBA_COLOR_REGEXP = /^rgba\([ ]?([0-9]{1,3}[ %]?,[ ]?){3}(1|[0-9]+%|[0]?\.?[0-9]+)\)$/i;
|
|
8
8
|
const HSL_COLOR_REGEXP = /^hsl\([ ]?([0-9]{1,3}[ %]?[,]?[ ]*){3}(1|[0-9]+%|[0]?\.?[0-9]+)?\)$/i;
|
|
9
9
|
const HSLA_COLOR_REGEXP = /^hsla\([ ]?([0-9]{1,3}[ %]?,[ ]?){2,3}(1|[0-9]+%|[0]?\.?[0-9]+)\)$/i;
|
|
10
|
+
// Note: This regexp hardcodes a single level of nested () for values such as `calc( var( ...) + ...)`.
|
|
11
|
+
// If this gets more complex, a proper parser should be used instead.
|
|
12
|
+
const CSS_SHORTHAND_VALUE_REGEXP = /\w+\((?:[^()]|\([^()]*\))*\)|\S+/gi;
|
|
10
13
|
const COLOR_NAMES = new Set([
|
|
11
14
|
// CSS Level 1
|
|
12
15
|
'black', 'silver', 'gray', 'white', 'maroon', 'red', 'purple', 'fuchsia',
|
|
@@ -211,8 +214,6 @@ export function getPositionShorthandNormalizer(shorthand) {
|
|
|
211
214
|
* ```
|
|
212
215
|
*/
|
|
213
216
|
export function getShorthandValues(string) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
.split(' ')
|
|
217
|
-
.map(string => string.replace(/,/g, ', ')); // Restore original notation.
|
|
217
|
+
const matches = string.matchAll(CSS_SHORTHAND_VALUE_REGEXP);
|
|
218
|
+
return Array.from(matches).map(i => i[0]);
|
|
218
219
|
}
|
package/src/view/treewalker.js
CHANGED
|
@@ -136,36 +136,38 @@ export default class TreeWalker {
|
|
|
136
136
|
position = new Position(node, 0);
|
|
137
137
|
}
|
|
138
138
|
else {
|
|
139
|
+
// We are past the walker boundaries.
|
|
140
|
+
if (this.boundaries && this.boundaries.end.isBefore(position)) {
|
|
141
|
+
return { done: true, value: undefined };
|
|
142
|
+
}
|
|
139
143
|
position.offset++;
|
|
140
144
|
}
|
|
141
145
|
this._position = position;
|
|
142
146
|
return this._formatReturnValue('elementStart', node, previousPosition, position, 1);
|
|
143
147
|
}
|
|
144
|
-
|
|
148
|
+
if (node instanceof Text) {
|
|
145
149
|
if (this.singleCharacters) {
|
|
146
150
|
position = new Position(node, 0);
|
|
147
151
|
this._position = position;
|
|
148
152
|
return this._next();
|
|
149
153
|
}
|
|
154
|
+
let charactersCount = node.data.length;
|
|
155
|
+
let item;
|
|
156
|
+
// If text stick out of walker range, we need to cut it and wrap in TextProxy.
|
|
157
|
+
if (node == this._boundaryEndParent) {
|
|
158
|
+
charactersCount = this.boundaries.end.offset;
|
|
159
|
+
item = new TextProxy(node, 0, charactersCount);
|
|
160
|
+
position = Position._createAfter(item);
|
|
161
|
+
}
|
|
150
162
|
else {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (node == this._boundaryEndParent) {
|
|
155
|
-
charactersCount = this.boundaries.end.offset;
|
|
156
|
-
item = new TextProxy(node, 0, charactersCount);
|
|
157
|
-
position = Position._createAfter(item);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
item = new TextProxy(node, 0, node.data.length);
|
|
161
|
-
// If not just keep moving forward.
|
|
162
|
-
position.offset++;
|
|
163
|
-
}
|
|
164
|
-
this._position = position;
|
|
165
|
-
return this._formatReturnValue('text', item, previousPosition, position, charactersCount);
|
|
163
|
+
item = new TextProxy(node, 0, node.data.length);
|
|
164
|
+
// If not just keep moving forward.
|
|
165
|
+
position.offset++;
|
|
166
166
|
}
|
|
167
|
+
this._position = position;
|
|
168
|
+
return this._formatReturnValue('text', item, previousPosition, position, charactersCount);
|
|
167
169
|
}
|
|
168
|
-
|
|
170
|
+
if (typeof node == 'string') {
|
|
169
171
|
let textLength;
|
|
170
172
|
if (this.singleCharacters) {
|
|
171
173
|
textLength = 1;
|
|
@@ -180,17 +182,13 @@ export default class TreeWalker {
|
|
|
180
182
|
this._position = position;
|
|
181
183
|
return this._formatReturnValue('text', textProxy, previousPosition, position, textLength);
|
|
182
184
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return this._next();
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
return this._formatReturnValue('elementEnd', parent, previousPosition, position);
|
|
192
|
-
}
|
|
185
|
+
// `node` is not set, we reached the end of current `parent`.
|
|
186
|
+
position = Position._createAfter(parent);
|
|
187
|
+
this._position = position;
|
|
188
|
+
if (this.ignoreElementEnd) {
|
|
189
|
+
return this._next();
|
|
193
190
|
}
|
|
191
|
+
return this._formatReturnValue('elementEnd', parent, previousPosition, position);
|
|
194
192
|
}
|
|
195
193
|
/**
|
|
196
194
|
* Makes a step backward in view. Moves the {@link #position} to the previous position and returns the encountered value.
|
|
@@ -222,48 +220,42 @@ export default class TreeWalker {
|
|
|
222
220
|
node = parent.getChild(position.offset - 1);
|
|
223
221
|
}
|
|
224
222
|
if (node instanceof Element) {
|
|
225
|
-
if (
|
|
226
|
-
position = new Position(node, node.childCount);
|
|
227
|
-
this._position = position;
|
|
228
|
-
if (this.ignoreElementEnd) {
|
|
229
|
-
return this._previous();
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
return this._formatReturnValue('elementEnd', node, previousPosition, position);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
223
|
+
if (this.shallow) {
|
|
236
224
|
position.offset--;
|
|
237
225
|
this._position = position;
|
|
238
226
|
return this._formatReturnValue('elementStart', node, previousPosition, position, 1);
|
|
239
227
|
}
|
|
228
|
+
position = new Position(node, node.childCount);
|
|
229
|
+
this._position = position;
|
|
230
|
+
if (this.ignoreElementEnd) {
|
|
231
|
+
return this._previous();
|
|
232
|
+
}
|
|
233
|
+
return this._formatReturnValue('elementEnd', node, previousPosition, position);
|
|
240
234
|
}
|
|
241
|
-
|
|
235
|
+
if (node instanceof Text) {
|
|
242
236
|
if (this.singleCharacters) {
|
|
243
237
|
position = new Position(node, node.data.length);
|
|
244
238
|
this._position = position;
|
|
245
239
|
return this._previous();
|
|
246
240
|
}
|
|
241
|
+
let charactersCount = node.data.length;
|
|
242
|
+
let item;
|
|
243
|
+
// If text stick out of walker range, we need to cut it and wrap in TextProxy.
|
|
244
|
+
if (node == this._boundaryStartParent) {
|
|
245
|
+
const offset = this.boundaries.start.offset;
|
|
246
|
+
item = new TextProxy(node, offset, node.data.length - offset);
|
|
247
|
+
charactersCount = item.data.length;
|
|
248
|
+
position = Position._createBefore(item);
|
|
249
|
+
}
|
|
247
250
|
else {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (node == this._boundaryStartParent) {
|
|
252
|
-
const offset = this.boundaries.start.offset;
|
|
253
|
-
item = new TextProxy(node, offset, node.data.length - offset);
|
|
254
|
-
charactersCount = item.data.length;
|
|
255
|
-
position = Position._createBefore(item);
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
item = new TextProxy(node, 0, node.data.length);
|
|
259
|
-
// If not just keep moving backward.
|
|
260
|
-
position.offset--;
|
|
261
|
-
}
|
|
262
|
-
this._position = position;
|
|
263
|
-
return this._formatReturnValue('text', item, previousPosition, position, charactersCount);
|
|
251
|
+
item = new TextProxy(node, 0, node.data.length);
|
|
252
|
+
// If not just keep moving backward.
|
|
253
|
+
position.offset--;
|
|
264
254
|
}
|
|
255
|
+
this._position = position;
|
|
256
|
+
return this._formatReturnValue('text', item, previousPosition, position, charactersCount);
|
|
265
257
|
}
|
|
266
|
-
|
|
258
|
+
if (typeof node == 'string') {
|
|
267
259
|
let textLength;
|
|
268
260
|
if (!this.singleCharacters) {
|
|
269
261
|
// Check if text stick out of walker range.
|
|
@@ -278,12 +270,10 @@ export default class TreeWalker {
|
|
|
278
270
|
this._position = position;
|
|
279
271
|
return this._formatReturnValue('text', textProxy, previousPosition, position, textLength);
|
|
280
272
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return this._formatReturnValue('elementStart', parent, previousPosition, position, 1);
|
|
286
|
-
}
|
|
273
|
+
// `node` is not set, we reached the beginning of current `parent`.
|
|
274
|
+
position = Position._createBefore(parent);
|
|
275
|
+
this._position = position;
|
|
276
|
+
return this._formatReturnValue('elementStart', parent, previousPosition, position, 1);
|
|
287
277
|
}
|
|
288
278
|
/**
|
|
289
279
|
* Format returned data and adjust `previousPosition` and `nextPosition` if reach the bound of the {@link module:engine/view/text~Text}.
|
package/src/view/view.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type Element from './element';
|
|
|
18
18
|
import type Node from './node';
|
|
19
19
|
import type Item from './item';
|
|
20
20
|
type IfTrue<T> = T extends true ? true : never;
|
|
21
|
+
type DomRange = globalThis.Range;
|
|
21
22
|
declare const View_base: {
|
|
22
23
|
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
|
23
24
|
prototype: import("@ckeditor/ckeditor5-utils").Observable;
|
|
@@ -186,6 +187,9 @@ export default class View extends View_base {
|
|
|
186
187
|
* Scrolls the page viewport and {@link #domRoots} with their ancestors to reveal the
|
|
187
188
|
* caret, **if not already visible to the user**.
|
|
188
189
|
*
|
|
190
|
+
* **Note**: Calling this method fires the {@link module:engine/view/view~ViewScrollToTheSelectionEvent} event that
|
|
191
|
+
* allows custom behaviors.
|
|
192
|
+
*
|
|
189
193
|
* @param options Additional configuration of the scrolling behavior.
|
|
190
194
|
* @param options.viewportOffset A distance between the DOM selection and the viewport boundary to be maintained
|
|
191
195
|
* while scrolling to the selection (default is 20px). Setting this value to `0` will reveal the selection precisely at
|
|
@@ -199,7 +203,12 @@ export default class View extends View_base {
|
|
|
199
203
|
* whether it is already visible or not. This option will only work when `alignToTop` is `true`.
|
|
200
204
|
*/
|
|
201
205
|
scrollToTheSelection<T extends boolean, U extends IfTrue<T>>({ alignToTop, forceScroll, viewportOffset, ancestorOffset }?: {
|
|
202
|
-
readonly viewportOffset?: number
|
|
206
|
+
readonly viewportOffset?: number | {
|
|
207
|
+
top: number;
|
|
208
|
+
bottom: number;
|
|
209
|
+
left: number;
|
|
210
|
+
right: number;
|
|
211
|
+
};
|
|
203
212
|
readonly ancestorOffset?: number;
|
|
204
213
|
readonly alignToTop?: T;
|
|
205
214
|
readonly forceScroll?: U;
|
|
@@ -432,4 +441,40 @@ export type ViewRenderEvent = {
|
|
|
432
441
|
name: 'render';
|
|
433
442
|
args: [];
|
|
434
443
|
};
|
|
444
|
+
/**
|
|
445
|
+
* An event fired at the moment of {@link module:engine/view/view~View#scrollToTheSelection} being called. It
|
|
446
|
+
* carries two objects in its payload (`args`):
|
|
447
|
+
*
|
|
448
|
+
* * The first argument is the {@link module:engine/view/view~ViewScrollToTheSelectionEventData object containing data} that gets
|
|
449
|
+
* passed down to the {@link module:utils/dom/scroll~scrollViewportToShowTarget} helper. If some event listener modifies it, it can
|
|
450
|
+
* adjust the behavior of the scrolling (e.g. include additional `viewportOffset`).
|
|
451
|
+
* * The second argument corresponds to the original arguments passed to {@link module:utils/dom/scroll~scrollViewportToShowTarget}.
|
|
452
|
+
* It allows listeners to re-execute the `scrollViewportToShowTarget()` method with its original arguments if there is such a need,
|
|
453
|
+
* for instance, if the integration requires re–scrolling after certain interaction.
|
|
454
|
+
*
|
|
455
|
+
* @eventName ~View#scrollToTheSelection
|
|
456
|
+
*/
|
|
457
|
+
export type ViewScrollToTheSelectionEvent = {
|
|
458
|
+
name: 'scrollToTheSelection';
|
|
459
|
+
args: [
|
|
460
|
+
ViewScrollToTheSelectionEventData,
|
|
461
|
+
Parameters<View['scrollToTheSelection']>[0]
|
|
462
|
+
];
|
|
463
|
+
};
|
|
464
|
+
/**
|
|
465
|
+
* An object passed down to the {@link module:utils/dom/scroll~scrollViewportToShowTarget} helper while calling
|
|
466
|
+
* {@link module:engine/view/view~View#scrollToTheSelection}.
|
|
467
|
+
*/
|
|
468
|
+
export type ViewScrollToTheSelectionEventData = {
|
|
469
|
+
target: DomRange;
|
|
470
|
+
viewportOffset: {
|
|
471
|
+
top: number;
|
|
472
|
+
bottom: number;
|
|
473
|
+
left: number;
|
|
474
|
+
right: number;
|
|
475
|
+
};
|
|
476
|
+
ancestorOffset: number;
|
|
477
|
+
alignToTop?: boolean;
|
|
478
|
+
forceScroll?: boolean;
|
|
479
|
+
};
|
|
435
480
|
export {};
|
package/src/view/view.js
CHANGED
|
@@ -24,6 +24,7 @@ import TabObserver from './observer/tabobserver';
|
|
|
24
24
|
import { CKEditorError, ObservableMixin, scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils';
|
|
25
25
|
import { injectUiElementHandling } from './uielement';
|
|
26
26
|
import { injectQuirksHandling } from './filler';
|
|
27
|
+
import { cloneDeep } from 'lodash-es';
|
|
27
28
|
/**
|
|
28
29
|
* Editor's view controller class. Its main responsibility is DOM - View management for editing purposes, to provide
|
|
29
30
|
* abstraction over the DOM structure and events and hide all browsers quirks.
|
|
@@ -283,6 +284,9 @@ export default class View extends ObservableMixin() {
|
|
|
283
284
|
* Scrolls the page viewport and {@link #domRoots} with their ancestors to reveal the
|
|
284
285
|
* caret, **if not already visible to the user**.
|
|
285
286
|
*
|
|
287
|
+
* **Note**: Calling this method fires the {@link module:engine/view/view~ViewScrollToTheSelectionEvent} event that
|
|
288
|
+
* allows custom behaviors.
|
|
289
|
+
*
|
|
286
290
|
* @param options Additional configuration of the scrolling behavior.
|
|
287
291
|
* @param options.viewportOffset A distance between the DOM selection and the viewport boundary to be maintained
|
|
288
292
|
* while scrolling to the selection (default is 20px). Setting this value to `0` will reveal the selection precisely at
|
|
@@ -297,15 +301,28 @@ export default class View extends ObservableMixin() {
|
|
|
297
301
|
*/
|
|
298
302
|
scrollToTheSelection({ alignToTop, forceScroll, viewportOffset = 20, ancestorOffset = 20 } = {}) {
|
|
299
303
|
const range = this.document.selection.getFirstRange();
|
|
300
|
-
if (range) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
304
|
+
if (!range) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// Clone to make sure properties like `viewportOffset` are not mutated in the event listeners.
|
|
308
|
+
const originalArgs = cloneDeep({ alignToTop, forceScroll, viewportOffset, ancestorOffset });
|
|
309
|
+
if (typeof viewportOffset === 'number') {
|
|
310
|
+
viewportOffset = {
|
|
311
|
+
top: viewportOffset,
|
|
312
|
+
bottom: viewportOffset,
|
|
313
|
+
left: viewportOffset,
|
|
314
|
+
right: viewportOffset
|
|
315
|
+
};
|
|
308
316
|
}
|
|
317
|
+
const options = {
|
|
318
|
+
target: this.domConverter.viewRangeToDom(range),
|
|
319
|
+
viewportOffset,
|
|
320
|
+
ancestorOffset,
|
|
321
|
+
alignToTop,
|
|
322
|
+
forceScroll
|
|
323
|
+
};
|
|
324
|
+
this.fire('scrollToTheSelection', options, originalArgs);
|
|
325
|
+
scrollViewportToShowTarget(options);
|
|
309
326
|
}
|
|
310
327
|
/**
|
|
311
328
|
* It will focus DOM element representing {@link module:engine/view/editableelement~EditableElement EditableElement}
|