@ckeditor/ckeditor5-engine 37.0.1 → 38.0.0-rc.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.
Files changed (51) hide show
  1. package/package.json +22 -22
  2. package/src/controller/editingcontroller.js +27 -1
  3. package/src/conversion/conversion.d.ts +5 -3
  4. package/src/conversion/downcasthelpers.js +1 -1
  5. package/src/index.d.ts +3 -1
  6. package/src/index.js +2 -0
  7. package/src/model/document.d.ts +8 -0
  8. package/src/model/document.js +1 -0
  9. package/src/model/markercollection.d.ts +3 -3
  10. package/src/model/markercollection.js +3 -3
  11. package/src/model/model.d.ts +38 -3
  12. package/src/model/model.js +37 -3
  13. package/src/model/operation/attributeoperation.d.ts +5 -0
  14. package/src/model/operation/attributeoperation.js +6 -0
  15. package/src/model/operation/detachoperation.d.ts +5 -0
  16. package/src/model/operation/detachoperation.js +6 -0
  17. package/src/model/operation/insertoperation.d.ts +5 -0
  18. package/src/model/operation/insertoperation.js +6 -0
  19. package/src/model/operation/markeroperation.d.ts +5 -0
  20. package/src/model/operation/markeroperation.js +18 -0
  21. package/src/model/operation/mergeoperation.d.ts +5 -0
  22. package/src/model/operation/mergeoperation.js +12 -0
  23. package/src/model/operation/moveoperation.d.ts +5 -0
  24. package/src/model/operation/moveoperation.js +9 -0
  25. package/src/model/operation/nooperation.d.ts +5 -0
  26. package/src/model/operation/nooperation.js +6 -0
  27. package/src/model/operation/operation.d.ts +7 -0
  28. package/src/model/operation/renameoperation.d.ts +5 -0
  29. package/src/model/operation/renameoperation.js +6 -0
  30. package/src/model/operation/rootattributeoperation.d.ts +5 -0
  31. package/src/model/operation/rootattributeoperation.js +6 -0
  32. package/src/model/operation/rootoperation.d.ts +5 -0
  33. package/src/model/operation/rootoperation.js +6 -0
  34. package/src/model/operation/splitoperation.d.ts +5 -0
  35. package/src/model/operation/splitoperation.js +14 -0
  36. package/src/model/selection.d.ts +13 -3
  37. package/src/model/selection.js +71 -6
  38. package/src/model/typecheckable.js +1 -1
  39. package/src/model/utils/insertcontent.js +4 -4
  40. package/src/model/utils/selection-post-fixer.d.ts +9 -0
  41. package/src/model/utils/selection-post-fixer.js +3 -1
  42. package/src/model/writer.d.ts +1 -1
  43. package/src/view/datatransfer.d.ts +4 -0
  44. package/src/view/datatransfer.js +6 -0
  45. package/src/view/domconverter.js +12 -2
  46. package/src/view/matcher.js +1 -1
  47. package/src/view/observer/bubblingemittermixin.js +1 -1
  48. package/src/view/observer/selectionobserver.js +1 -1
  49. package/src/view/position.js +1 -4
  50. package/src/view/typecheckable.js +1 -1
  51. package/src/view/view.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-engine",
3
- "version": "37.0.1",
3
+ "version": "38.0.0-rc.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": "^37.0.1",
26
+ "@ckeditor/ckeditor5-utils": "^38.0.0-rc.0",
27
27
  "lodash-es": "^4.17.15"
28
28
  },
29
29
  "devDependencies": {
30
- "@ckeditor/ckeditor5-basic-styles": "^37.0.1",
31
- "@ckeditor/ckeditor5-block-quote": "^37.0.1",
32
- "@ckeditor/ckeditor5-clipboard": "^37.0.1",
33
- "@ckeditor/ckeditor5-cloud-services": "^37.0.1",
34
- "@ckeditor/ckeditor5-core": "^37.0.1",
35
- "@ckeditor/ckeditor5-editor-classic": "^37.0.1",
36
- "@ckeditor/ckeditor5-enter": "^37.0.1",
37
- "@ckeditor/ckeditor5-essentials": "^37.0.1",
38
- "@ckeditor/ckeditor5-heading": "^37.0.1",
39
- "@ckeditor/ckeditor5-image": "^37.0.1",
40
- "@ckeditor/ckeditor5-link": "^37.0.1",
41
- "@ckeditor/ckeditor5-list": "^37.0.1",
42
- "@ckeditor/ckeditor5-mention": "^37.0.1",
43
- "@ckeditor/ckeditor5-paragraph": "^37.0.1",
44
- "@ckeditor/ckeditor5-table": "^37.0.1",
45
- "@ckeditor/ckeditor5-theme-lark": "^37.0.1",
46
- "@ckeditor/ckeditor5-typing": "^37.0.1",
47
- "@ckeditor/ckeditor5-ui": "^37.0.1",
48
- "@ckeditor/ckeditor5-undo": "^37.0.1",
49
- "@ckeditor/ckeditor5-widget": "^37.0.1",
30
+ "@ckeditor/ckeditor5-basic-styles": "^38.0.0-rc.0",
31
+ "@ckeditor/ckeditor5-block-quote": "^38.0.0-rc.0",
32
+ "@ckeditor/ckeditor5-clipboard": "^38.0.0-rc.0",
33
+ "@ckeditor/ckeditor5-cloud-services": "^38.0.0-rc.0",
34
+ "@ckeditor/ckeditor5-core": "^38.0.0-rc.0",
35
+ "@ckeditor/ckeditor5-editor-classic": "^38.0.0-rc.0",
36
+ "@ckeditor/ckeditor5-enter": "^38.0.0-rc.0",
37
+ "@ckeditor/ckeditor5-essentials": "^38.0.0-rc.0",
38
+ "@ckeditor/ckeditor5-heading": "^38.0.0-rc.0",
39
+ "@ckeditor/ckeditor5-image": "^38.0.0-rc.0",
40
+ "@ckeditor/ckeditor5-link": "^38.0.0-rc.0",
41
+ "@ckeditor/ckeditor5-list": "^38.0.0-rc.0",
42
+ "@ckeditor/ckeditor5-mention": "^38.0.0-rc.0",
43
+ "@ckeditor/ckeditor5-paragraph": "^38.0.0-rc.0",
44
+ "@ckeditor/ckeditor5-table": "^38.0.0-rc.0",
45
+ "@ckeditor/ckeditor5-theme-lark": "^38.0.0-rc.0",
46
+ "@ckeditor/ckeditor5-typing": "^38.0.0-rc.0",
47
+ "@ckeditor/ckeditor5-ui": "^38.0.0-rc.0",
48
+ "@ckeditor/ckeditor5-undo": "^38.0.0-rc.0",
49
+ "@ckeditor/ckeditor5-widget": "^38.0.0-rc.0",
50
50
  "typescript": "^4.8.4",
51
51
  "webpack": "^5.58.1",
52
52
  "webpack-cli": "^4.9.0"
@@ -5,13 +5,14 @@
5
5
  /**
6
6
  * @module engine/controller/editingcontroller
7
7
  */
8
- import { CKEditorError, ObservableMixin } from '@ckeditor/ckeditor5-utils';
8
+ import { CKEditorError, ObservableMixin, env } from '@ckeditor/ckeditor5-utils';
9
9
  import RootEditableElement from '../view/rooteditableelement';
10
10
  import View from '../view/view';
11
11
  import Mapper from '../conversion/mapper';
12
12
  import DowncastDispatcher from '../conversion/downcastdispatcher';
13
13
  import { clearAttributes, convertCollapsedSelection, convertRangeSelection, insertAttributesAndChildren, insertText, remove } from '../conversion/downcasthelpers';
14
14
  import { convertSelectionChange } from '../conversion/upcasthelpers';
15
+ import { tryFixingRange } from '../model/utils/selection-post-fixer';
15
16
  // @if CK_DEBUG_ENGINE // const { dumpTrees, initDocumentDumping } = require( '../dev-utils/utils' );
16
17
  /**
17
18
  * A controller for the editing pipeline. The editing pipeline controls the {@link ~EditingController#model model} rendering,
@@ -59,6 +60,8 @@ export default class EditingController extends ObservableMixin() {
59
60
  }, { priority: 'low' });
60
61
  // Convert selection from the view to the model when it changes in the view.
61
62
  this.listenTo(this.view.document, 'selectionChange', convertSelectionChange(this.model, this.mapper));
63
+ // Fix `beforeinput` target ranges so that they map to the valid model ranges.
64
+ this.listenTo(this.view.document, 'beforeinput', fixTargetRanges(this.mapper, this.model.schema, this.view), { priority: 'high' });
62
65
  // Attach default model converters.
63
66
  this.downcastDispatcher.on('insert:$text', insertText(), { priority: 'lowest' });
64
67
  this.downcastDispatcher.on('insert', insertAttributesAndChildren(), { priority: 'lowest' });
@@ -163,3 +166,26 @@ export default class EditingController extends ObservableMixin() {
163
166
  });
164
167
  }
165
168
  }
169
+ /**
170
+ * Checks whether the target ranges provided by the `beforeInput` event can be properly mapped to model ranges and fixes them if needed.
171
+ *
172
+ * This is using the same logic as the selection post-fixer.
173
+ */
174
+ function fixTargetRanges(mapper, schema, view) {
175
+ return (evt, data) => {
176
+ // The Renderer is disabled while composing on non-android browsers, so we can't be sure that target ranges
177
+ // could be properly mapped to view and model because the DOM and view tree drifted apart.
178
+ if (view.document.isComposing && !env.isAndroid) {
179
+ return;
180
+ }
181
+ for (let i = 0; i < data.targetRanges.length; i++) {
182
+ const viewRange = data.targetRanges[i];
183
+ const modelRange = mapper.toModelRange(viewRange);
184
+ const correctedRange = tryFixingRange(modelRange, schema);
185
+ if (!correctedRange || correctedRange.isEqual(modelRange)) {
186
+ continue;
187
+ }
188
+ data.targetRanges[i] = mapper.toViewRange(correctedRange);
189
+ }
190
+ };
191
+ }
@@ -72,9 +72,9 @@ export default class Conversion {
72
72
  addAlias(alias: `${string}Downcast`, dispatcher: DowncastDispatcher): void;
73
73
  addAlias(alias: `${string}Upcast`, dispatcher: UpcastDispatcher): void;
74
74
  addAlias(alias: string, dispatcher: DowncastDispatcher | UpcastDispatcher): void;
75
- for(groupName: 'downcast' | `${string}Downcast`): DowncastHelpers;
76
- for(groupName: 'upcast' | `${string}Upcast`): UpcastHelpers;
77
- for(groupName: string): DowncastHelpers | UpcastHelpers;
75
+ for(groupName: 'downcast' | 'dataDowncast' | 'editingDowncast'): DowncastHelpers;
76
+ for(groupName: 'upcast'): UpcastHelpers;
77
+ for<T extends string>(groupName: T): ConversionType<T>;
78
78
  /**
79
79
  * Sets up converters between the model and the view that convert a model element to a view element (and vice versa).
80
80
  * For example, the model `<paragraph>Foo</paragraph>` is turned into `<p>Foo</p>` in the view.
@@ -474,3 +474,5 @@ export default class Conversion {
474
474
  */
475
475
  private _createConversionHelpers;
476
476
  }
477
+ type ConversionType<T extends string> = T extends `${string}Downcast` ? DowncastHelpers : T extends `${string}Upcast` ? UpcastHelpers : DowncastHelpers | UpcastHelpers;
478
+ export {};
@@ -1958,7 +1958,7 @@ function createChangeReducerCallback(model) {
1958
1958
  }
1959
1959
  }
1960
1960
  else {
1961
- /* istanbul ignore else: This is always true because otherwise it would not register a reducer callback. */
1961
+ /* istanbul ignore else: This is always true because otherwise it would not register a reducer callback. -- @preserve */
1962
1962
  if (model.children) {
1963
1963
  return true;
1964
1964
  }
package/src/index.d.ts CHANGED
@@ -28,6 +28,7 @@ export { default as AttributeOperation } from './model/operation/attributeoperat
28
28
  export { default as RenameOperation } from './model/operation/renameoperation';
29
29
  export { default as RootAttributeOperation } from './model/operation/rootattributeoperation';
30
30
  export { default as RootOperation } from './model/operation/rootoperation';
31
+ export { default as NoOperation } from './model/operation/nooperation';
31
32
  export { transformSets } from './model/operation/transform';
32
33
  export { default as DocumentSelection, type DocumentSelectionChangeRangeEvent } from './model/documentselection';
33
34
  export { default as Range } from './model/range';
@@ -55,7 +56,7 @@ export type { default as Writer } from './model/writer';
55
56
  export { findOptimalInsertionRange } from './model/utils/findoptimalinsertionrange';
56
57
  export type { DocumentChangeEvent } from './model/document';
57
58
  export type { DocumentSelectionChangeEvent } from './model/documentselection';
58
- export type { ModelApplyOperationEvent, ModelDeleteContentEvent, ModelGetSelectedContentEvent, ModelInsertContentEvent, ModelInsertObjectEvent, ModelModifySelectionEvent } from './model/model';
59
+ export type { ModelApplyOperationEvent, ModelDeleteContentEvent, ModelGetSelectedContentEvent, ModelInsertContentEvent, ModelInsertObjectEvent, ModelModifySelectionEvent, ModelCanEditAtEvent } from './model/model';
59
60
  export type { SelectionChangeRangeEvent } from './model/selection';
60
61
  export { default as DataTransfer } from './view/datatransfer';
61
62
  export { default as DomConverter } from './view/domconverter';
@@ -66,6 +67,7 @@ export { default as ViewText } from './view/text';
66
67
  export { default as ViewElement, type ElementAttributes as ViewElementAttributes } from './view/element';
67
68
  export { default as ViewContainerElement } from './view/containerelement';
68
69
  export { default as ViewEditableElement } from './view/editableelement';
70
+ export { default as ViewRootEditableElement } from './view/rooteditableelement';
69
71
  export { default as ViewAttributeElement } from './view/attributeelement';
70
72
  export { default as ViewEmptyElement } from './view/emptyelement';
71
73
  export { default as ViewRawElement } from './view/rawelement';
package/src/index.js CHANGED
@@ -22,6 +22,7 @@ export { default as AttributeOperation } from './model/operation/attributeoperat
22
22
  export { default as RenameOperation } from './model/operation/renameoperation';
23
23
  export { default as RootAttributeOperation } from './model/operation/rootattributeoperation';
24
24
  export { default as RootOperation } from './model/operation/rootoperation';
25
+ export { default as NoOperation } from './model/operation/nooperation';
25
26
  export { transformSets } from './model/operation/transform';
26
27
  // Model.
27
28
  export { default as DocumentSelection } from './model/documentselection';
@@ -47,6 +48,7 @@ export { default as ViewText } from './view/text';
47
48
  export { default as ViewElement } from './view/element';
48
49
  export { default as ViewContainerElement } from './view/containerelement';
49
50
  export { default as ViewEditableElement } from './view/editableelement';
51
+ export { default as ViewRootEditableElement } from './view/rooteditableelement';
50
52
  export { default as ViewAttributeElement } from './view/attributeelement';
51
53
  export { default as ViewEmptyElement } from './view/emptyelement';
52
54
  export { default as ViewRawElement } from './view/rawelement';
@@ -56,6 +56,14 @@ export default class Document extends Document_base {
56
56
  * The model differ object. Its role is to buffer changes done on the model document and then calculate a diff of those changes.
57
57
  */
58
58
  readonly differ: Differ;
59
+ /**
60
+ * Defines whether the document is in a read-only mode.
61
+ *
62
+ * The user should not be able to change the data of a document that is read-only.
63
+ *
64
+ * @readonly
65
+ */
66
+ isReadOnly: boolean;
59
67
  /**
60
68
  * Post-fixer callbacks registered to the model document.
61
69
  */
@@ -41,6 +41,7 @@ export default class Document extends EmitterMixin() {
41
41
  this.selection = new DocumentSelection(this);
42
42
  this.roots = new Collection({ idProperty: 'rootName' });
43
43
  this.differ = new Differ(model.markers);
44
+ this.isReadOnly = false;
44
45
  this._postFixers = new Set();
45
46
  this._hasSelectionChangedFromTheLastChangeBlock = false;
46
47
  // Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
@@ -137,9 +137,9 @@ export interface MarkerData {
137
137
  }
138
138
  declare const Marker_base: import("@ckeditor/ckeditor5-utils").Mixed<typeof TypeCheckable, import("@ckeditor/ckeditor5-utils").Emitter>;
139
139
  /**
140
- * `Marker` is a continuous parts of model (like a range), is named and represent some kind of information about marked
141
- * part of model document. In contrary to {@link module:engine/model/node~Node nodes}, which are building blocks of
142
- * model document tree, markers are not stored directly in document tree but in
140
+ * `Marker` is a continuous part of the model (like a range), is named and represents some kind of information about the
141
+ * marked part of the model document. In contrary to {@link module:engine/model/node~Node nodes}, which are building blocks of
142
+ * the model document tree, markers are not stored directly in the document tree but in the
143
143
  * {@link module:engine/model/model~Model#markers model markers' collection}. Still, they are document data, by giving
144
144
  * additional meaning to the part of a model document between marker start and marker end.
145
145
  *
@@ -214,9 +214,9 @@ export default class MarkerCollection extends EmitterMixin() {
214
214
  }
215
215
  }
216
216
  /**
217
- * `Marker` is a continuous parts of model (like a range), is named and represent some kind of information about marked
218
- * part of model document. In contrary to {@link module:engine/model/node~Node nodes}, which are building blocks of
219
- * model document tree, markers are not stored directly in document tree but in
217
+ * `Marker` is a continuous part of the model (like a range), is named and represents some kind of information about the
218
+ * marked part of the model document. In contrary to {@link module:engine/model/node~Node nodes}, which are building blocks of
219
+ * the model document tree, markers are not stored directly in the document tree but in the
220
220
  * {@link module:engine/model/model~Model#markers model markers' collection}. Still, they are document data, by giving
221
221
  * additional meaning to the part of a model document between marker start and marker end.
222
222
  *
@@ -553,6 +553,20 @@ export default class Model extends Model_base {
553
553
  ignoreWhitespaces?: boolean;
554
554
  ignoreMarkers?: boolean;
555
555
  }): boolean;
556
+ /**
557
+ * Check whether given selectable is at a place in the model where it can be edited (returns `true`) or not (returns `false`).
558
+ *
559
+ * Should be used instead of {@link module:core/editor/editor~Editor#isReadOnly} to check whether a user action can happen at
560
+ * given selectable. It may be decorated and used differently in different environment (e.g. multi-root editor can disable
561
+ * a particular root).
562
+ *
563
+ * This method is decorated. Although this method accepts any parameter of `Selectable` type, the
564
+ * {@link ~Model#event:canEditAt `canEditAt` event} is fired with `selectable` normalized to an instance of
565
+ * {@link module:engine/model/selection~Selection} or {@link module:engine/model/documentselection~DocumentSelection}
566
+ *
567
+ * @fires canEditAt
568
+ */
569
+ canEditAt(selectable: Selectable): boolean;
556
570
  /**
557
571
  * Creates a position from the given root and path in that root.
558
572
  *
@@ -856,7 +870,7 @@ export type ModelInsertObjectEvent = {
856
870
  * Event fired when {@link ~Model#deleteContent} method is called.
857
871
  *
858
872
  * The {@link ~Model#deleteContent default action of that method} is implemented as a
859
- * listener to this event so it can be fully customized by the features.
873
+ * listener to this event, so it can be fully customized by the features.
860
874
  *
861
875
  * @eventName ~Model#deleteContent
862
876
  * @param args The arguments passed to the original method.
@@ -866,7 +880,7 @@ export type ModelDeleteContentEvent = DecoratedMethodEvent<Model, 'deleteContent
866
880
  * Event fired when {@link ~Model#modifySelection} method is called.
867
881
  *
868
882
  * The {@link ~Model#modifySelection default action of that method} is implemented as a
869
- * listener to this event so it can be fully customized by the features.
883
+ * listener to this event, so it can be fully customized by the features.
870
884
  *
871
885
  * @eventName ~Model#modifySelection
872
886
  * @param args The arguments passed to the original method.
@@ -876,10 +890,31 @@ export type ModelModifySelectionEvent = DecoratedMethodEvent<Model, 'modifySelec
876
890
  * Event fired when {@link ~Model#getSelectedContent} method is called.
877
891
  *
878
892
  * The {@link ~Model#getSelectedContent default action of that method} is implemented as a
879
- * listener to this event so it can be fully customized by the features.
893
+ * listener to this event, so it can be fully customized by the features.
880
894
  *
881
895
  * @eventName ~Model#getSelectedContent
882
896
  * @param args The arguments passed to the original method.
883
897
  */
884
898
  export type ModelGetSelectedContentEvent = DecoratedMethodEvent<Model, 'getSelectedContent'>;
899
+ /**
900
+ * Event fired when {@link ~Model#canEditAt} method is called.
901
+ *
902
+ * The {@link ~Model#canEditAt default action of that method} is implemented as a
903
+ * listener to this event, so it can be fully customized by the features.
904
+ *
905
+ * Although the original method accepts any parameter of `Selectable` type, this event is fired with `selectable` normalized
906
+ * to an instance of {@link module:engine/model/selection~Selection} or {@link module:engine/model/documentselection~DocumentSelection}.
907
+ *
908
+ * @eventName ~Model#canEditAt
909
+ * @param args The arguments passed to the original method.
910
+ */
911
+ export type ModelCanEditAtEvent = {
912
+ name: 'canEditAt';
913
+ args: [
914
+ [
915
+ selectable?: ModelSelection | DocumentSelection
916
+ ]
917
+ ];
918
+ return: boolean;
919
+ };
885
920
  export {};
@@ -106,6 +106,15 @@ export default class Model extends ObservableMixin() {
106
106
  this.on('insertObject', (evt, [element, selection, options]) => {
107
107
  evt.return = insertObject(this, element, selection, options);
108
108
  });
109
+ // The base implementation for "decorated" method with remapped arguments.
110
+ this.on('canEditAt', evt => {
111
+ const canEditAt = !this.document.isReadOnly;
112
+ evt.return = canEditAt;
113
+ if (!canEditAt) {
114
+ // Prevent further processing if the selection is at non-editable place.
115
+ evt.stop();
116
+ }
117
+ });
109
118
  // @if CK_DEBUG_ENGINE // initDocumentDumping( this.document );
110
119
  // @if CK_DEBUG_ENGINE // this.on( 'applyOperation', () => {
111
120
  // @if CK_DEBUG_ENGINE // dumpTrees( this.document, this.document.version );
@@ -168,7 +177,7 @@ export default class Model extends ObservableMixin() {
168
177
  }
169
178
  catch (err) {
170
179
  // @if CK_DEBUG // throw err;
171
- /* istanbul ignore next */
180
+ /* istanbul ignore next -- @preserve */
172
181
  CKEditorError.rethrowUnexpectedError(err, this);
173
182
  }
174
183
  }
@@ -191,7 +200,7 @@ export default class Model extends ObservableMixin() {
191
200
  }
192
201
  catch (err) {
193
202
  // @if CK_DEBUG // throw err;
194
- /* istanbul ignore next */
203
+ /* istanbul ignore next -- @preserve */
195
204
  CKEditorError.rethrowUnexpectedError(err, this);
196
205
  }
197
206
  }
@@ -619,6 +628,23 @@ export default class Model extends ObservableMixin() {
619
628
  }
620
629
  return false;
621
630
  }
631
+ /**
632
+ * Check whether given selectable is at a place in the model where it can be edited (returns `true`) or not (returns `false`).
633
+ *
634
+ * Should be used instead of {@link module:core/editor/editor~Editor#isReadOnly} to check whether a user action can happen at
635
+ * given selectable. It may be decorated and used differently in different environment (e.g. multi-root editor can disable
636
+ * a particular root).
637
+ *
638
+ * This method is decorated. Although this method accepts any parameter of `Selectable` type, the
639
+ * {@link ~Model#event:canEditAt `canEditAt` event} is fired with `selectable` normalized to an instance of
640
+ * {@link module:engine/model/selection~Selection} or {@link module:engine/model/documentselection~DocumentSelection}
641
+ *
642
+ * @fires canEditAt
643
+ */
644
+ canEditAt(selectable) {
645
+ const selection = normalizeSelectable(selectable);
646
+ return this.fire('canEditAt', [selection]);
647
+ }
622
648
  /**
623
649
  * Creates a position from the given root and path in that root.
624
650
  *
@@ -803,7 +829,15 @@ function normalizeSelectable(selectable, placeOrOffset) {
803
829
  return selectable;
804
830
  }
805
831
  if (selectable instanceof Node) {
806
- return new ModelSelection(selectable, placeOrOffset);
832
+ if (placeOrOffset || placeOrOffset === 0) {
833
+ return new ModelSelection(selectable, placeOrOffset);
834
+ }
835
+ else if (selectable.is('rootElement')) {
836
+ return new ModelSelection(selectable, 'in');
837
+ }
838
+ else {
839
+ return new ModelSelection(selectable, 'on');
840
+ }
807
841
  }
808
842
  return new ModelSelection(selectable);
809
843
  }
@@ -8,6 +8,7 @@
8
8
  import Operation from './operation';
9
9
  import Range from '../range';
10
10
  import type Document from '../document';
11
+ import type { Selectable } from '../selection';
11
12
  /**
12
13
  * Operation to change nodes' attribute.
13
14
  *
@@ -62,6 +63,10 @@ export default class AttributeOperation extends Operation {
62
63
  * @inheritDoc
63
64
  */
64
65
  get type(): 'addAttribute' | 'removeAttribute' | 'changeAttribute';
66
+ /**
67
+ * @inheritDoc
68
+ */
69
+ get affectedSelectable(): Selectable;
65
70
  /**
66
71
  * Creates and returns an operation that has the same parameters as this operation.
67
72
  */
@@ -56,6 +56,12 @@ export default class AttributeOperation extends Operation {
56
56
  return 'changeAttribute';
57
57
  }
58
58
  }
59
+ /**
60
+ * @inheritDoc
61
+ */
62
+ get affectedSelectable() {
63
+ return this.range.clone();
64
+ }
59
65
  /**
60
66
  * Creates and returns an operation that has the same parameters as this operation.
61
67
  */
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import Operation from './operation';
9
9
  import type Position from '../position';
10
+ import type { Selectable } from '../selection';
10
11
  /**
11
12
  * Operation to permanently remove node from detached root.
12
13
  * Note this operation is only a local operation and won't be send to the other clients.
@@ -34,6 +35,10 @@ export default class DetachOperation extends Operation {
34
35
  * @inheritDoc
35
36
  */
36
37
  get type(): 'detach';
38
+ /**
39
+ * @inheritDoc
40
+ */
41
+ get affectedSelectable(): Selectable;
37
42
  /**
38
43
  * @inheritDoc
39
44
  */
@@ -33,6 +33,12 @@ export default class DetachOperation extends Operation {
33
33
  get type() {
34
34
  return 'detach';
35
35
  }
36
+ /**
37
+ * @inheritDoc
38
+ */
39
+ get affectedSelectable() {
40
+ return null;
41
+ }
36
42
  /**
37
43
  * @inheritDoc
38
44
  */
@@ -9,6 +9,7 @@ import Operation from './operation';
9
9
  import Position from '../position';
10
10
  import NodeList from '../nodelist';
11
11
  import { type NodeSet } from './utils';
12
+ import type { Selectable } from '../selection';
12
13
  import type Document from '../document';
13
14
  /**
14
15
  * Operation to insert one or more nodes at given position in the model.
@@ -49,6 +50,10 @@ export default class InsertOperation extends Operation {
49
50
  * Total offset size of inserted nodes.
50
51
  */
51
52
  get howMany(): number;
53
+ /**
54
+ * @inheritDoc
55
+ */
56
+ get affectedSelectable(): Selectable;
52
57
  /**
53
58
  * Creates and returns an operation that has the same parameters as this operation.
54
59
  */
@@ -44,6 +44,12 @@ export default class InsertOperation extends Operation {
44
44
  get howMany() {
45
45
  return this.nodes.maxOffset;
46
46
  }
47
+ /**
48
+ * @inheritDoc
49
+ */
50
+ get affectedSelectable() {
51
+ return this.position.clone();
52
+ }
47
53
  /**
48
54
  * Creates and returns an operation that has the same parameters as this operation.
49
55
  */
@@ -9,6 +9,7 @@ import Operation from './operation';
9
9
  import Range from '../range';
10
10
  import type Document from '../document';
11
11
  import type MarkerCollection from '../markercollection';
12
+ import type { Selectable } from '../selection';
12
13
  export default class MarkerOperation extends Operation {
13
14
  /**
14
15
  * Marker name.
@@ -54,6 +55,10 @@ export default class MarkerOperation extends Operation {
54
55
  * @inheritDoc
55
56
  */
56
57
  get type(): 'marker';
58
+ /**
59
+ * @inheritDoc
60
+ */
61
+ get affectedSelectable(): Selectable;
57
62
  /**
58
63
  * Creates and returns an operation that has the same parameters as this operation.
59
64
  */
@@ -32,6 +32,24 @@ export default class MarkerOperation extends Operation {
32
32
  get type() {
33
33
  return 'marker';
34
34
  }
35
+ /**
36
+ * @inheritDoc
37
+ */
38
+ get affectedSelectable() {
39
+ const ranges = [];
40
+ if (this.oldRange) {
41
+ ranges.push(this.oldRange.clone());
42
+ }
43
+ if (this.newRange) {
44
+ if (this.oldRange) {
45
+ ranges.push(...this.newRange.getDifference(this.oldRange));
46
+ }
47
+ else {
48
+ ranges.push(this.newRange.clone());
49
+ }
50
+ }
51
+ return ranges;
52
+ }
35
53
  /**
36
54
  * Creates and returns an operation that has the same parameters as this operation.
37
55
  */
@@ -9,6 +9,7 @@ import Operation from './operation';
9
9
  import Position from '../position';
10
10
  import Range from '../range';
11
11
  import type Document from '../document';
12
+ import type { Selectable } from '../selection';
12
13
  /**
13
14
  * Operation to merge two {@link module:engine/model/element~Element elements}.
14
15
  *
@@ -59,6 +60,10 @@ export default class MergeOperation extends Operation {
59
60
  * The range starts at {@link ~MergeOperation#sourcePosition} and ends in the same parent, at `POSITIVE_INFINITY` offset.
60
61
  */
61
62
  get movedRange(): Range;
63
+ /**
64
+ * @inheritDoc
65
+ */
66
+ get affectedSelectable(): Selectable;
62
67
  /**
63
68
  * Creates and returns an operation that has the same parameters as this operation.
64
69
  */
@@ -63,6 +63,18 @@ export default class MergeOperation extends Operation {
63
63
  const end = this.sourcePosition.getShiftedBy(Number.POSITIVE_INFINITY);
64
64
  return new Range(this.sourcePosition, end);
65
65
  }
66
+ /**
67
+ * @inheritDoc
68
+ */
69
+ get affectedSelectable() {
70
+ const mergedElement = this.sourcePosition.parent;
71
+ return [
72
+ Range._createOn(mergedElement),
73
+ // These could be positions but `Selectable` type only supports `Iterable<Range>`.
74
+ Range._createFromPositionAndShift(this.targetPosition, 0),
75
+ Range._createFromPositionAndShift(this.graveyardPosition, 0)
76
+ ];
77
+ }
66
78
  /**
67
79
  * Creates and returns an operation that has the same parameters as this operation.
68
80
  */
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import Operation from './operation';
9
9
  import Position from '../position';
10
+ import type { Selectable } from '../selection';
10
11
  import type Document from '../document';
11
12
  /**
12
13
  * Operation to move a range of {@link module:engine/model/item~Item model items}
@@ -40,6 +41,10 @@ export default class MoveOperation extends Operation {
40
41
  * @inheritDoc
41
42
  */
42
43
  get type(): 'move' | 'remove' | 'reinsert';
44
+ /**
45
+ * @inheritDoc
46
+ */
47
+ get affectedSelectable(): Selectable;
43
48
  /**
44
49
  * Creates and returns an operation that has the same parameters as this operation.
45
50
  */
@@ -47,6 +47,15 @@ export default class MoveOperation extends Operation {
47
47
  }
48
48
  return 'move';
49
49
  }
50
+ /**
51
+ * @inheritDoc
52
+ */
53
+ get affectedSelectable() {
54
+ return [
55
+ Range._createFromPositionAndShift(this.sourcePosition, this.howMany),
56
+ Range._createFromPositionAndShift(this.targetPosition, 0)
57
+ ];
58
+ }
50
59
  /**
51
60
  * Creates and returns an operation that has the same parameters as this operation.
52
61
  */
@@ -6,6 +6,7 @@
6
6
  * @module engine/model/operation/nooperation
7
7
  */
8
8
  import Operation from './operation';
9
+ import type { Selectable } from '../selection';
9
10
  /**
10
11
  * Operation which is doing nothing ("empty operation", "do-nothing operation", "noop"). This is an operation,
11
12
  * which when executed does not change the tree model. It still has some parameters defined for transformation purposes.
@@ -16,6 +17,10 @@ import Operation from './operation';
16
17
  */
17
18
  export default class NoOperation extends Operation {
18
19
  get type(): 'noop';
20
+ /**
21
+ * @inheritDoc
22
+ */
23
+ get affectedSelectable(): Selectable;
19
24
  /**
20
25
  * Creates and returns an operation that has the same parameters as this operation.
21
26
  */
@@ -18,6 +18,12 @@ export default class NoOperation extends Operation {
18
18
  get type() {
19
19
  return 'noop';
20
20
  }
21
+ /**
22
+ * @inheritDoc
23
+ */
24
+ get affectedSelectable() {
25
+ return null;
26
+ }
21
27
  /**
22
28
  * Creates and returns an operation that has the same parameters as this operation.
23
29
  */
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import type Batch from '../batch';
6
6
  import type Document from '../document';
7
+ import type { Selectable } from '../selection';
7
8
  /**
8
9
  * @module engine/model/operation/operation
9
10
  */
@@ -38,6 +39,12 @@ export default abstract class Operation {
38
39
  * can be applied or `null` if the operation operates on detached (non-document) tree.
39
40
  */
40
41
  constructor(baseVersion: number | null);
42
+ /**
43
+ * A selectable that will be affected by the operation after it is executed.
44
+ *
45
+ * The exact returned parameter differs between operation types.
46
+ */
47
+ abstract get affectedSelectable(): Selectable;
41
48
  /**
42
49
  * Creates and returns an operation that has the same parameters as this operation.
43
50
  *
@@ -8,6 +8,7 @@
8
8
  import Operation from './operation';
9
9
  import Position from '../position';
10
10
  import type Document from '../document';
11
+ import type { Selectable } from '../selection';
11
12
  /**
12
13
  * Operation to change element's name.
13
14
  *
@@ -40,6 +41,10 @@ export default class RenameOperation extends Operation {
40
41
  * @inheritDoc
41
42
  */
42
43
  get type(): 'rename';
44
+ /**
45
+ * @inheritDoc
46
+ */
47
+ get affectedSelectable(): Selectable;
43
48
  /**
44
49
  * Creates and returns an operation that has the same parameters as this operation.
45
50
  *
@@ -38,6 +38,12 @@ export default class RenameOperation extends Operation {
38
38
  get type() {
39
39
  return 'rename';
40
40
  }
41
+ /**
42
+ * @inheritDoc
43
+ */
44
+ get affectedSelectable() {
45
+ return this.position.nodeAfter;
46
+ }
41
47
  /**
42
48
  * Creates and returns an operation that has the same parameters as this operation.
43
49
  *
@@ -8,6 +8,7 @@
8
8
  import Operation from './operation';
9
9
  import type Document from '../document';
10
10
  import type RootElement from '../rootelement';
11
+ import type { Selectable } from '../selection';
11
12
  /**
12
13
  * Operation to change root element's attribute. Using this class you can add, remove or change value of the attribute.
13
14
  *
@@ -55,6 +56,10 @@ export default class RootAttributeOperation extends Operation {
55
56
  * @inheritDoc
56
57
  */
57
58
  get type(): 'addRootAttribute' | 'removeRootAttribute' | 'changeRootAttribute';
59
+ /**
60
+ * @inheritDoc
61
+ */
62
+ get affectedSelectable(): Selectable;
58
63
  /**
59
64
  * Creates and returns an operation that has the same parameters as this operation.
60
65
  *
@@ -50,6 +50,12 @@ export default class RootAttributeOperation extends Operation {
50
50
  return 'changeRootAttribute';
51
51
  }
52
52
  }
53
+ /**
54
+ * @inheritDoc
55
+ */
56
+ get affectedSelectable() {
57
+ return this.root;
58
+ }
53
59
  /**
54
60
  * Creates and returns an operation that has the same parameters as this operation.
55
61
  *
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import Operation from './operation';
9
9
  import type Document from '../document';
10
+ import type { Selectable } from '../selection';
10
11
  /**
11
12
  * Operation that creates (or attaches) or detaches a root element.
12
13
  */
@@ -41,6 +42,10 @@ export default class RootOperation extends Operation {
41
42
  * @inheritDoc
42
43
  */
43
44
  get type(): 'addRoot' | 'detachRoot';
45
+ /**
46
+ * @inheritDoc
47
+ */
48
+ get affectedSelectable(): Selectable;
44
49
  /**
45
50
  * @inheritDoc
46
51
  */
@@ -41,6 +41,12 @@ export default class RootOperation extends Operation {
41
41
  get type() {
42
42
  return this.isAdd ? 'addRoot' : 'detachRoot';
43
43
  }
44
+ /**
45
+ * @inheritDoc
46
+ */
47
+ get affectedSelectable() {
48
+ return this._document.getRoot(this.rootName);
49
+ }
44
50
  /**
45
51
  * @inheritDoc
46
52
  */
@@ -9,6 +9,7 @@ import Operation from './operation';
9
9
  import Position from '../position';
10
10
  import Range from '../range';
11
11
  import type Document from '../document';
12
+ import type { Selectable } from '../selection';
12
13
  /**
13
14
  * Operation to split {@link module:engine/model/element~Element an element} at given
14
15
  * {@link module:engine/model/operation/splitoperation~SplitOperation#splitPosition split position} into two elements,
@@ -61,6 +62,10 @@ export default class SplitOperation extends Operation {
61
62
  * The range starts at {@link #splitPosition} and ends in the same parent, at `POSITIVE_INFINITY` offset.
62
63
  */
63
64
  get movedRange(): Range;
65
+ /**
66
+ * @inheritDoc
67
+ */
68
+ get affectedSelectable(): Selectable;
64
69
  /**
65
70
  * Creates and returns an operation that has the same parameters as this operation.
66
71
  *
@@ -65,6 +65,20 @@ export default class SplitOperation extends Operation {
65
65
  const end = this.splitPosition.getShiftedBy(Number.POSITIVE_INFINITY);
66
66
  return new Range(this.splitPosition, end);
67
67
  }
68
+ /**
69
+ * @inheritDoc
70
+ */
71
+ get affectedSelectable() {
72
+ // These could be positions but `Selectable` type only supports `Iterable<Range>`.
73
+ const ranges = [
74
+ Range._createFromPositionAndShift(this.splitPosition, 0),
75
+ Range._createFromPositionAndShift(this.insertionPosition, 0)
76
+ ];
77
+ if (this.graveyardPosition) {
78
+ ranges.push(Range._createFromPositionAndShift(this.graveyardPosition, 0));
79
+ }
80
+ return ranges;
81
+ }
68
82
  /**
69
83
  * Creates and returns an operation that has the same parameters as this operation.
70
84
  *
@@ -362,13 +362,23 @@ export default class Selection extends Selection_base {
362
362
  * </block>
363
363
  * ```
364
364
  *
365
- * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
366
- * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
365
+ * **Special case**: Selection ignores first and/or last blocks if nothing (from user perspective) is selected in them.
367
366
  *
368
367
  * ```xml
368
+ * // Selection ends and the beginning of the last block.
369
369
  * <paragraph>[a</paragraph>
370
370
  * <paragraph>b</paragraph>
371
- * <paragraph>]c</paragraph> // this block will not be returned
371
+ * <paragraph>]c</paragraph> // This block will not be returned
372
+ *
373
+ * // Selection begins at the end of the first block.
374
+ * <paragraph>a[</paragraph> // This block will not be returned
375
+ * <paragraph>b</paragraph>
376
+ * <paragraph>c]</paragraph>
377
+ *
378
+ * // Selection begings at the end of the first block and ends at the beginning of the last block.
379
+ * <paragraph>a[</paragraph> // This block will not be returned
380
+ * <paragraph>b</paragraph>
381
+ * <paragraph>]c</paragraph> // This block will not be returned
372
382
  * ```
373
383
  */
374
384
  getSelectedBlocks(): IterableIterator<Element>;
@@ -557,13 +557,23 @@ export default class Selection extends EmitterMixin(TypeCheckable) {
557
557
  * </block>
558
558
  * ```
559
559
  *
560
- * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
561
- * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
560
+ * **Special case**: Selection ignores first and/or last blocks if nothing (from user perspective) is selected in them.
562
561
  *
563
562
  * ```xml
563
+ * // Selection ends and the beginning of the last block.
564
564
  * <paragraph>[a</paragraph>
565
565
  * <paragraph>b</paragraph>
566
- * <paragraph>]c</paragraph> // this block will not be returned
566
+ * <paragraph>]c</paragraph> // This block will not be returned
567
+ *
568
+ * // Selection begins at the end of the first block.
569
+ * <paragraph>a[</paragraph> // This block will not be returned
570
+ * <paragraph>b</paragraph>
571
+ * <paragraph>c]</paragraph>
572
+ *
573
+ * // Selection begings at the end of the first block and ends at the beginning of the last block.
574
+ * <paragraph>a[</paragraph> // This block will not be returned
575
+ * <paragraph>b</paragraph>
576
+ * <paragraph>]c</paragraph> // This block will not be returned
567
577
  * ```
568
578
  */
569
579
  *getSelectedBlocks() {
@@ -571,7 +581,7 @@ export default class Selection extends EmitterMixin(TypeCheckable) {
571
581
  for (const range of this.getRanges()) {
572
582
  // Get start block of range in case of a collapsed range.
573
583
  const startBlock = getParentBlock(range.start, visited);
574
- if (startBlock && isTopBlockInRange(startBlock, range)) {
584
+ if (isStartBlockSelected(startBlock, range)) {
575
585
  yield startBlock;
576
586
  }
577
587
  for (const value of range.getWalker()) {
@@ -581,8 +591,7 @@ export default class Selection extends EmitterMixin(TypeCheckable) {
581
591
  }
582
592
  }
583
593
  const endBlock = getParentBlock(range.end, visited);
584
- // #984. Don't return the end block if the range ends right at its beginning.
585
- if (endBlock && !range.end.isTouching(Position._createAt(endBlock, 0)) && isTopBlockInRange(endBlock, range)) {
594
+ if (isEndBlockSelected(endBlock, range)) {
586
595
  yield endBlock;
587
596
  }
588
597
  }
@@ -709,6 +718,62 @@ function isTopBlockInRange(block, range) {
709
718
  const isParentInRange = range.containsRange(Range._createOn(parentBlock), true);
710
719
  return !isParentInRange;
711
720
  }
721
+ /**
722
+ * If a selection starts at the end of a block, that block is not returned as from the user's perspective this block wasn't selected.
723
+ * See [#11585](https://github.com/ckeditor/ckeditor5/issues/11585) for more details.
724
+ *
725
+ * ```xml
726
+ * <paragraph>a[</paragraph> // This block will not be returned
727
+ * <paragraph>b</paragraph>
728
+ * <paragraph>c]</paragraph>
729
+ * ```
730
+ *
731
+ * Collapsed selection is not affected by it:
732
+ *
733
+ * ```xml
734
+ * <paragraph>a[]</paragraph> // This block will be returned
735
+ * ```
736
+ */
737
+ function isStartBlockSelected(startBlock, range) {
738
+ if (!startBlock) {
739
+ return false;
740
+ }
741
+ if (range.isCollapsed || startBlock.isEmpty) {
742
+ return true;
743
+ }
744
+ if (range.start.isTouching(Position._createAt(startBlock, startBlock.maxOffset))) {
745
+ return false;
746
+ }
747
+ return isTopBlockInRange(startBlock, range);
748
+ }
749
+ /**
750
+ * If a selection ends at the beginning of a block, that block is not returned as from the user's perspective this block wasn't selected.
751
+ * See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
752
+ *
753
+ * ```xml
754
+ * <paragraph>[a</paragraph>
755
+ * <paragraph>b</paragraph>
756
+ * <paragraph>]c</paragraph> // this block will not be returned
757
+ * ```
758
+ *
759
+ * Collapsed selection is not affected by it:
760
+ *
761
+ * ```xml
762
+ * <paragraph>[]a</paragraph> // this block will be returned
763
+ * ```
764
+ */
765
+ function isEndBlockSelected(endBlock, range) {
766
+ if (!endBlock) {
767
+ return false;
768
+ }
769
+ if (range.isCollapsed || endBlock.isEmpty) {
770
+ return true;
771
+ }
772
+ if (range.end.isTouching(Position._createAt(endBlock, 0))) {
773
+ return false;
774
+ }
775
+ return isTopBlockInRange(endBlock, range);
776
+ }
712
777
  /**
713
778
  * Returns first ancestor block of a node.
714
779
  */
@@ -3,7 +3,7 @@
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
  export default class TypeCheckable {
6
- /* istanbul ignore next */
6
+ /* istanbul ignore next -- @preserve */
7
7
  is() {
8
8
  // There are a lot of overloads above.
9
9
  // Overriding method in derived classes remove them and only `is( type: string ): boolean` is visible which we don't want.
@@ -162,7 +162,7 @@ export default function insertContent(model, content, selectable) {
162
162
  selectionLiveRange.detach();
163
163
  }
164
164
  }
165
- /* istanbul ignore else */
165
+ /* istanbul ignore else -- @preserve */
166
166
  if (newRange) {
167
167
  if (selection instanceof DocumentSelection) {
168
168
  writer.setSelection(newRange);
@@ -253,7 +253,7 @@ class Insertion {
253
253
  // If the real end was after the last auto paragraph then update relevant properties.
254
254
  if (positionAfterNode.isAfter(positionAfterLastNode)) {
255
255
  this._lastNode = node;
256
- /* istanbul ignore if */
256
+ /* istanbul ignore if -- @preserve */
257
257
  if (this.position.parent != node || !this.position.isAtEnd) {
258
258
  // Algorithm's correctness check. We should never end up here but it's good to know that we did.
259
259
  // At this point the insertion position should be at the end of the last auto paragraph.
@@ -386,7 +386,7 @@ class Insertion {
386
386
  * @param node The node to insert.
387
387
  */
388
388
  _appendToFragment(node) {
389
- /* istanbul ignore if */
389
+ /* istanbul ignore if -- @preserve */
390
390
  if (!this.schema.checkChild(this.position, node)) {
391
391
  // Algorithm's correctness check. We should never end up here but it's good to know that we did.
392
392
  // Note that it would often be a silent issue if we insert node in a place where it's not allowed.
@@ -517,7 +517,7 @@ class Insertion {
517
517
  }
518
518
  const mergePosRight = LivePosition._createAfter(node);
519
519
  mergePosRight.stickiness = 'toNext';
520
- /* istanbul ignore if */
520
+ /* istanbul ignore if -- @preserve */
521
521
  if (!this.position.isEqual(mergePosRight)) {
522
522
  // Algorithm's correctness check. We should never end up here but it's good to know that we did.
523
523
  // At this point the insertion position should be after the node we'll merge. If it isn't,
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import Range from '../range';
6
6
  import type Model from '../model';
7
+ import type Schema from '../schema';
7
8
  /**
8
9
  * Injects selection post-fixer to the model.
9
10
  *
@@ -56,6 +57,14 @@ import type Model from '../model';
56
57
  * them to select `isLimit=true` elements.
57
58
  */
58
59
  export declare function injectSelectionPostFixer(model: Model): void;
60
+ /**
61
+ * Tries fixing a range if it's incorrect.
62
+ *
63
+ * **Note:** This helper is used by the selection post-fixer and to fix the `beforeinput` target ranges.
64
+ *
65
+ * @returns Returns fixed range or null if range is valid.
66
+ */
67
+ export declare function tryFixingRange(range: Range, schema: Schema): Range | null;
59
68
  /**
60
69
  * Returns a minimal non-intersecting array of ranges without duplicates.
61
70
  *
@@ -97,9 +97,11 @@ function selectionPostFixer(writer, model) {
97
97
  /**
98
98
  * Tries fixing a range if it's incorrect.
99
99
  *
100
+ * **Note:** This helper is used by the selection post-fixer and to fix the `beforeinput` target ranges.
101
+ *
100
102
  * @returns Returns fixed range or null if range is valid.
101
103
  */
102
- function tryFixingRange(range, schema) {
104
+ export function tryFixingRange(range, schema) {
103
105
  if (range.isCollapsed) {
104
106
  return tryFixingCollapsedRange(range, schema);
105
107
  }
@@ -472,7 +472,7 @@ export default class Writer {
472
472
  * @param element The element to rename.
473
473
  * @param newName New element name.
474
474
  */
475
- rename(element: Element, newName: string): void;
475
+ rename(element: Element | DocumentFragment, newName: string): void;
476
476
  /**
477
477
  * Splits elements starting from the given position and going to the top of the model tree as long as given
478
478
  * `limitElement` is reached. When `limitElement` is not defined then only the parent of the given position will be split.
@@ -59,6 +59,10 @@ export default class DataTransfer {
59
59
  */
60
60
  set dropEffect(value: DropEffect);
61
61
  get dropEffect(): DropEffect;
62
+ /**
63
+ * Set a preview image of the dragged content.
64
+ */
65
+ setDragImage(image: Element, x: number, y: number): void;
62
66
  /**
63
67
  * Whether the dragging operation was canceled.
64
68
  */
@@ -71,6 +71,12 @@ export default class DataTransfer {
71
71
  get dropEffect() {
72
72
  return this._native.dropEffect;
73
73
  }
74
+ /**
75
+ * Set a preview image of the dragged content.
76
+ */
77
+ setDragImage(image, x, y) {
78
+ this._native.setDragImage(image, x, y);
79
+ }
74
80
  /**
75
81
  * Whether the dragging operation was canceled.
76
82
  */
@@ -16,7 +16,7 @@ import ViewDocumentFragment from './documentfragment';
16
16
  import ViewTreeWalker from './treewalker';
17
17
  import { default as Matcher } from './matcher';
18
18
  import { BR_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER, MARKED_NBSP_FILLER, getDataWithoutFiller, isInlineFiller, startsWithFiller } from './filler';
19
- import { global, logWarning, indexOf, getAncestors, isText, isComment, first } from '@ckeditor/ckeditor5-utils';
19
+ import { global, logWarning, indexOf, getAncestors, isText, isComment, isValidAttributeName, first } from '@ckeditor/ckeditor5-utils';
20
20
  const BR_FILLER_REF = BR_FILLER(global.document); // eslint-disable-line new-cap
21
21
  const NBSP_FILLER_REF = NBSP_FILLER(global.document); // eslint-disable-line new-cap
22
22
  const MARKED_NBSP_FILLER_REF = MARKED_NBSP_FILLER(global.document); // eslint-disable-line new-cap
@@ -305,6 +305,15 @@ export default class DomConverter {
305
305
  if (!shouldRenderAttribute) {
306
306
  logWarning('domconverter-unsafe-attribute-detected', { domElement, key, value });
307
307
  }
308
+ if (!isValidAttributeName(key)) {
309
+ /**
310
+ * Invalid attribute name was ignored during rendering.
311
+ *
312
+ * @error domconverter-invalid-attribute-detected
313
+ */
314
+ logWarning('domconverter-invalid-attribute-detected', { domElement, key, value });
315
+ return;
316
+ }
308
317
  // The old value was safe but the new value is unsafe.
309
318
  if (domElement.hasAttribute(key) && !shouldRenderAttribute) {
310
319
  domElement.removeAttribute(key);
@@ -641,7 +650,8 @@ export default class DomConverter {
641
650
  }
642
651
  else {
643
652
  const domBefore = domParent.childNodes[domOffset - 1];
644
- if (isText(domBefore) && isInlineFiller(domBefore)) {
653
+ // Jump over an inline filler (and also on Firefox jump over a block filler while pressing backspace in an empty paragraph).
654
+ if (isText(domBefore) && isInlineFiller(domBefore) || domBefore && this.isBlockFiller(domBefore)) {
645
655
  return this.domPositionToView(domBefore.parentNode, indexOf(domBefore));
646
656
  }
647
657
  const viewBefore = isText(domBefore) ?
@@ -423,7 +423,7 @@ function matchAttributes(patterns, element) {
423
423
  */
424
424
  function matchClasses(patterns, element) {
425
425
  // We don't need `getter` here because patterns for classes are always normalized to `[ className, true ]`.
426
- return matchPatterns(patterns, element.getClassNames(), /* istanbul ignore next */ () => { });
426
+ return matchPatterns(patterns, element.getClassNames(), /* istanbul ignore next -- @preserve */ () => { });
427
427
  }
428
428
  /**
429
429
  * Checks if styles of provided element can be matched against provided patterns.
@@ -77,7 +77,7 @@ export default function BubblingEmitterMixin(base) {
77
77
  }
78
78
  catch (err) {
79
79
  // @if CK_DEBUG // throw err;
80
- /* istanbul ignore next */
80
+ /* istanbul ignore next -- @preserve */
81
81
  CKEditorError.rethrowUnexpectedError(err, this);
82
82
  }
83
83
  }
@@ -116,7 +116,7 @@ export default class SelectionObserver extends Observer {
116
116
  this._fireSelectionChangeDoneDebounced.cancel();
117
117
  this._documentIsSelectingInactivityTimeoutDebounced.cancel();
118
118
  }
119
- /* istanbul ignore next */
119
+ /* istanbul ignore next -- @preserve */
120
120
  _reportInfiniteLoop() {
121
121
  // @if CK_DEBUG // throw new Error(
122
122
  // @if CK_DEBUG // 'Selection change observer detected an infinite rendering loop.\n\n' +
@@ -209,11 +209,8 @@ export default class Position extends TypeCheckable {
209
209
  return 'before';
210
210
  case 'extension':
211
211
  return 'after';
212
- /* istanbul ignore next */
213
- case 'same':
214
- // Already covered by `this.isEqual` above. Added so TypeScript can infer `result` as number in `default` case.
215
- return 'same';
216
212
  default:
213
+ // Cast to number to avoid having 'same' as a type of `result`.
217
214
  return thisPath[result] < otherPath[result] ? 'before' : 'after';
218
215
  }
219
216
  }
@@ -6,7 +6,7 @@
6
6
  * @module engine/view/typecheckable
7
7
  */
8
8
  export default class TypeCheckable {
9
- /* istanbul ignore next */
9
+ /* istanbul ignore next -- @preserve */
10
10
  is() {
11
11
  // There are a lot of overloads above.
12
12
  // Overriding method in derived classes remove them and only `is( type: string ): boolean` is visible which we don't want.
package/src/view/view.js CHANGED
@@ -397,7 +397,7 @@ export default class View extends ObservableMixin() {
397
397
  }
398
398
  catch (err) {
399
399
  // @if CK_DEBUG // throw err;
400
- /* istanbul ignore next */
400
+ /* istanbul ignore next -- @preserve */
401
401
  CKEditorError.rethrowUnexpectedError(err, this);
402
402
  }
403
403
  }