@ckeditor/ckeditor5-engine 38.0.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-engine",
3
- "version": "38.0.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": "^38.0.0",
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.0",
31
- "@ckeditor/ckeditor5-block-quote": "^38.0.0",
32
- "@ckeditor/ckeditor5-clipboard": "^38.0.0",
33
- "@ckeditor/ckeditor5-cloud-services": "^38.0.0",
34
- "@ckeditor/ckeditor5-core": "^38.0.0",
35
- "@ckeditor/ckeditor5-editor-classic": "^38.0.0",
36
- "@ckeditor/ckeditor5-enter": "^38.0.0",
37
- "@ckeditor/ckeditor5-essentials": "^38.0.0",
38
- "@ckeditor/ckeditor5-heading": "^38.0.0",
39
- "@ckeditor/ckeditor5-image": "^38.0.0",
40
- "@ckeditor/ckeditor5-link": "^38.0.0",
41
- "@ckeditor/ckeditor5-list": "^38.0.0",
42
- "@ckeditor/ckeditor5-mention": "^38.0.0",
43
- "@ckeditor/ckeditor5-paragraph": "^38.0.0",
44
- "@ckeditor/ckeditor5-table": "^38.0.0",
45
- "@ckeditor/ckeditor5-theme-lark": "^38.0.0",
46
- "@ckeditor/ckeditor5-typing": "^38.0.0",
47
- "@ckeditor/ckeditor5-ui": "^38.0.0",
48
- "@ckeditor/ckeditor5-undo": "^38.0.0",
49
- "@ckeditor/ckeditor5-widget": "^38.0.0",
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 `slorFor()` function can also take a callback that allows filtering which children of the model element
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 `slorFor()` function can also take a callback that allows filtering which children of the model element
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.get('low');
643
- const maxPriority = priorities.get('highest');
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 = getAttrsIfCharacter(nodeBefore);
892
+ attrs = getTextAttributes(nodeBefore, schema);
893
893
  }
894
894
  // 3. If not, look at the node after caret...
895
895
  if (!attrs) {
896
- attrs = getAttrsIfCharacter(nodeAfter);
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 && !schema.isInline(node) && !attrs) {
902
+ while (node && !attrs) {
903
903
  node = node.previousSibling;
904
- attrs = getAttrsIfCharacter(node);
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 && !schema.isInline(node) && !attrs) {
910
+ while (node && !attrs) {
911
911
  node = node.nextSibling;
912
- attrs = getAttrsIfCharacter(node);
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 takes model item, checks whether it is a text node (or text proxy) and, if so, returns it's attributes. If not, returns `null`.
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 getAttrsIfCharacter(node) {
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
- return null;
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.
@@ -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 ? textNodeAtPosition : getNodeAfterPosition(position, parent, 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
- else if (node instanceof Text) {
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
- else {
161
- // `node` is not set, we reached the end of current `parent`.
162
- position.path.pop();
163
- position.offset++;
164
- this._position = position;
165
- this._visitedParent = parent.parent;
166
- if (this.ignoreElementEnd) {
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 ? textNodeAtPosition : getNodeBeforePosition(position, positionParent, textNodeAtPosition);
193
+ const node = textNodeAtPosition || getNodeBeforePosition(position, positionParent, textNodeAtPosition);
194
194
  if (node instanceof Element) {
195
195
  position.offset--;
196
- if (!this.shallow) {
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
- else if (node instanceof Text) {
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
- else {
231
- // `node` is not set, we reached the beginning of current `parent`.
232
- position.path.pop();
233
- this._position = position;
234
- this._visitedParent = parent.parent;
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
- return view.domConverter.domRangeToView(domRange);
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
@@ -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
- return string
215
- .replace(/, /g, ',') // Exclude comma from spaces evaluation as values are separated by spaces.
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
  }
@@ -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
- else if (node instanceof Text) {
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
- let charactersCount = node.data.length;
152
- let item;
153
- // If text stick out of walker range, we need to cut it and wrap in TextProxy.
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
- else if (typeof node == 'string') {
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
- else {
184
- // `node` is not set, we reached the end of current `parent`.
185
- position = Position._createAfter(parent);
186
- this._position = position;
187
- if (this.ignoreElementEnd) {
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 (!this.shallow) {
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
- else if (node instanceof Text) {
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
- let charactersCount = node.data.length;
249
- let item;
250
- // If text stick out of walker range, we need to cut it and wrap in TextProxy.
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
- else if (typeof node == 'string') {
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
- else {
282
- // `node` is not set, we reached the beginning of current `parent`.
283
- position = Position._createBefore(parent);
284
- this._position = position;
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}.
@@ -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
- scrollViewportToShowTarget({
302
- target: this.domConverter.viewRangeToDom(range),
303
- viewportOffset,
304
- ancestorOffset,
305
- alignToTop,
306
- forceScroll
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}