@ckeditor/ckeditor5-engine 35.0.1 → 35.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +4 -4
  2. package/package.json +30 -24
  3. package/src/controller/datacontroller.js +467 -561
  4. package/src/controller/editingcontroller.js +168 -204
  5. package/src/conversion/conversion.js +541 -565
  6. package/src/conversion/conversionhelpers.js +24 -28
  7. package/src/conversion/downcastdispatcher.js +457 -686
  8. package/src/conversion/downcasthelpers.js +1583 -1965
  9. package/src/conversion/mapper.js +518 -707
  10. package/src/conversion/modelconsumable.js +240 -283
  11. package/src/conversion/upcastdispatcher.js +372 -718
  12. package/src/conversion/upcasthelpers.js +707 -818
  13. package/src/conversion/viewconsumable.js +524 -581
  14. package/src/dataprocessor/basichtmlwriter.js +12 -16
  15. package/src/dataprocessor/dataprocessor.js +5 -0
  16. package/src/dataprocessor/htmldataprocessor.js +100 -116
  17. package/src/dataprocessor/htmlwriter.js +1 -18
  18. package/src/dataprocessor/xmldataprocessor.js +116 -137
  19. package/src/dev-utils/model.js +260 -352
  20. package/src/dev-utils/operationreplayer.js +106 -126
  21. package/src/dev-utils/utils.js +34 -51
  22. package/src/dev-utils/view.js +632 -753
  23. package/src/index.js +0 -11
  24. package/src/model/batch.js +111 -127
  25. package/src/model/differ.js +988 -1233
  26. package/src/model/document.js +340 -449
  27. package/src/model/documentfragment.js +327 -364
  28. package/src/model/documentselection.js +996 -1189
  29. package/src/model/element.js +306 -410
  30. package/src/model/history.js +224 -262
  31. package/src/model/item.js +5 -0
  32. package/src/model/liveposition.js +84 -145
  33. package/src/model/liverange.js +108 -185
  34. package/src/model/markercollection.js +379 -480
  35. package/src/model/model.js +883 -1034
  36. package/src/model/node.js +419 -463
  37. package/src/model/nodelist.js +176 -201
  38. package/src/model/operation/attributeoperation.js +153 -182
  39. package/src/model/operation/detachoperation.js +64 -83
  40. package/src/model/operation/insertoperation.js +135 -166
  41. package/src/model/operation/markeroperation.js +114 -140
  42. package/src/model/operation/mergeoperation.js +163 -191
  43. package/src/model/operation/moveoperation.js +157 -187
  44. package/src/model/operation/nooperation.js +28 -38
  45. package/src/model/operation/operation.js +106 -125
  46. package/src/model/operation/operationfactory.js +30 -34
  47. package/src/model/operation/renameoperation.js +109 -135
  48. package/src/model/operation/rootattributeoperation.js +155 -188
  49. package/src/model/operation/splitoperation.js +196 -232
  50. package/src/model/operation/transform.js +1833 -2204
  51. package/src/model/operation/utils.js +140 -204
  52. package/src/model/position.js +980 -1053
  53. package/src/model/range.js +910 -1028
  54. package/src/model/rootelement.js +77 -97
  55. package/src/model/schema.js +1189 -1835
  56. package/src/model/selection.js +745 -862
  57. package/src/model/text.js +90 -114
  58. package/src/model/textproxy.js +204 -240
  59. package/src/model/treewalker.js +316 -397
  60. package/src/model/typecheckable.js +16 -0
  61. package/src/model/utils/autoparagraphing.js +32 -44
  62. package/src/model/utils/deletecontent.js +334 -418
  63. package/src/model/utils/findoptimalinsertionrange.js +25 -36
  64. package/src/model/utils/getselectedcontent.js +96 -118
  65. package/src/model/utils/insertcontent.js +757 -773
  66. package/src/model/utils/insertobject.js +96 -119
  67. package/src/model/utils/modifyselection.js +120 -158
  68. package/src/model/utils/selection-post-fixer.js +153 -201
  69. package/src/model/writer.js +1305 -1474
  70. package/src/view/attributeelement.js +189 -225
  71. package/src/view/containerelement.js +75 -85
  72. package/src/view/document.js +172 -215
  73. package/src/view/documentfragment.js +200 -249
  74. package/src/view/documentselection.js +338 -367
  75. package/src/view/domconverter.js +1370 -1617
  76. package/src/view/downcastwriter.js +1747 -2076
  77. package/src/view/editableelement.js +81 -97
  78. package/src/view/element.js +739 -890
  79. package/src/view/elementdefinition.js +5 -0
  80. package/src/view/emptyelement.js +82 -92
  81. package/src/view/filler.js +35 -50
  82. package/src/view/item.js +5 -0
  83. package/src/view/matcher.js +260 -559
  84. package/src/view/node.js +274 -360
  85. package/src/view/observer/arrowkeysobserver.js +19 -28
  86. package/src/view/observer/bubblingemittermixin.js +120 -263
  87. package/src/view/observer/bubblingeventinfo.js +47 -55
  88. package/src/view/observer/clickobserver.js +7 -13
  89. package/src/view/observer/compositionobserver.js +14 -24
  90. package/src/view/observer/domeventdata.js +57 -67
  91. package/src/view/observer/domeventobserver.js +40 -64
  92. package/src/view/observer/fakeselectionobserver.js +81 -96
  93. package/src/view/observer/focusobserver.js +45 -61
  94. package/src/view/observer/inputobserver.js +7 -13
  95. package/src/view/observer/keyobserver.js +17 -27
  96. package/src/view/observer/mouseobserver.js +7 -14
  97. package/src/view/observer/mutationobserver.js +220 -315
  98. package/src/view/observer/observer.js +81 -102
  99. package/src/view/observer/selectionobserver.js +199 -246
  100. package/src/view/observer/tabobserver.js +23 -36
  101. package/src/view/placeholder.js +128 -173
  102. package/src/view/position.js +350 -401
  103. package/src/view/range.js +453 -513
  104. package/src/view/rawelement.js +85 -112
  105. package/src/view/renderer.js +874 -1018
  106. package/src/view/rooteditableelement.js +80 -90
  107. package/src/view/selection.js +608 -689
  108. package/src/view/styles/background.js +43 -44
  109. package/src/view/styles/border.js +220 -276
  110. package/src/view/styles/margin.js +8 -17
  111. package/src/view/styles/padding.js +8 -16
  112. package/src/view/styles/utils.js +127 -160
  113. package/src/view/stylesmap.js +728 -905
  114. package/src/view/text.js +102 -126
  115. package/src/view/textproxy.js +144 -170
  116. package/src/view/treewalker.js +383 -479
  117. package/src/view/typecheckable.js +19 -0
  118. package/src/view/uielement.js +166 -187
  119. package/src/view/upcastwriter.js +395 -449
  120. package/src/view/view.js +569 -664
  121. package/src/dataprocessor/dataprocessor.jsdoc +0 -64
  122. package/src/model/item.jsdoc +0 -14
  123. package/src/view/elementdefinition.jsdoc +0 -59
  124. package/src/view/item.jsdoc +0 -14
@@ -2,19 +2,17 @@
2
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module engine/model/utils/insertcontent
8
7
  */
9
-
10
- import Position from '../position';
11
- import LivePosition from '../liveposition';
8
+ import DocumentSelection from '../documentselection';
12
9
  import Element from '../element';
10
+ import LivePosition from '../liveposition';
11
+ import Position from '../position';
13
12
  import Range from '../range';
14
- import DocumentSelection from '../documentselection';
15
13
  import Selection from '../selection';
16
14
  import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
17
-
15
+ import { LiveRange } from '../../index';
18
16
  /**
19
17
  * Inserts content into the editor (specified selection) as one would expect the paste functionality to work.
20
18
  *
@@ -47,778 +45,764 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
47
45
  * would return the model to the state before the insertion. If no changes were preformed by `insertContent`, returns a range collapsed
48
46
  * at the insertion position.
49
47
  */
50
- export default function insertContent( model, content, selectable, placeOrOffset ) {
51
- return model.change( writer => {
52
- let selection;
53
-
54
- if ( !selectable ) {
55
- selection = model.document.selection;
56
- } else if ( selectable instanceof Selection || selectable instanceof DocumentSelection ) {
57
- selection = selectable;
58
- } else {
59
- selection = writer.createSelection( selectable, placeOrOffset );
60
- }
61
-
62
- if ( !selection.isCollapsed ) {
63
- model.deleteContent( selection, { doNotAutoparagraph: true } );
64
- }
65
-
66
- const insertion = new Insertion( model, writer, selection.anchor );
67
-
68
- let nodesToInsert;
69
-
70
- if ( content.is( 'documentFragment' ) ) {
71
- nodesToInsert = content.getChildren();
72
- } else {
73
- nodesToInsert = [ content ];
74
- }
75
-
76
- insertion.handleNodes( nodesToInsert );
77
-
78
- const newRange = insertion.getSelectionRange();
79
-
80
- /* istanbul ignore else */
81
- if ( newRange ) {
82
- if ( selection instanceof DocumentSelection ) {
83
- writer.setSelection( newRange );
84
- } else {
85
- selection.setTo( newRange );
86
- }
87
- } else {
88
- // We are not testing else because it's a safe check for unpredictable edge cases:
89
- // an insertion without proper range to select.
90
- //
91
- // @if CK_DEBUG // console.warn( 'Cannot determine a proper selection range after insertion.' );
92
- }
93
-
94
- const affectedRange = insertion.getAffectedRange() || model.createRange( selection.anchor );
95
-
96
- insertion.destroy();
97
-
98
- return affectedRange;
99
- } );
48
+ export default function insertContent(model, content, selectable, placeOrOffset) {
49
+ return model.change(writer => {
50
+ let selection;
51
+ if (!selectable) {
52
+ selection = model.document.selection;
53
+ }
54
+ else if (selectable instanceof Selection || selectable instanceof DocumentSelection) {
55
+ selection = selectable;
56
+ }
57
+ else {
58
+ selection = writer.createSelection(selectable, placeOrOffset);
59
+ }
60
+ if (!selection.isCollapsed) {
61
+ model.deleteContent(selection, { doNotAutoparagraph: true });
62
+ }
63
+ const insertion = new Insertion(model, writer, selection.anchor);
64
+ const fakeMarkerElements = [];
65
+ let nodesToInsert;
66
+ if (content.is('documentFragment')) {
67
+ // If document fragment has any markers, these markers should be inserted into the model as well.
68
+ if (content.markers.size) {
69
+ const markersPosition = [];
70
+ for (const [name, range] of content.markers) {
71
+ const { start, end } = range;
72
+ const isCollapsed = start.isEqual(end);
73
+ markersPosition.push({ position: start, name, isCollapsed }, { position: end, name, isCollapsed });
74
+ }
75
+ // Markers position is sorted backwards to ensure that the insertion of fake markers will not change
76
+ // the position of the next markers.
77
+ markersPosition.sort(({ position: posA }, { position: posB }) => posA.isBefore(posB) ? 1 : -1);
78
+ for (const { position, name, isCollapsed } of markersPosition) {
79
+ let fakeElement = null;
80
+ let collapsed = null;
81
+ const isAtBeginning = position.parent === content && position.isAtStart;
82
+ const isAtEnd = position.parent === content && position.isAtEnd;
83
+ // We have two ways of handling markers. In general, we want to add temporary <$marker> model elements to
84
+ // represent marker boundaries. These elements will be inserted into content together with the rest
85
+ // of the document fragment. After insertion is done, positions for these elements will be read
86
+ // and proper, actual markers will be created in the model and fake elements will be removed.
87
+ //
88
+ // However, if the <$marker> element is at the beginning or at the end of the document fragment,
89
+ // it may affect how the inserted content is merged with current model, impacting the insertion
90
+ // result. To avoid that, we don't add <$marker> elements at these positions. Instead, we will use
91
+ // `Insertion#getAffectedRange()` to figure out new positions for these marker boundaries.
92
+ if (!isAtBeginning && !isAtEnd) {
93
+ fakeElement = writer.createElement('$marker');
94
+ writer.insert(fakeElement, position);
95
+ }
96
+ else if (isCollapsed) {
97
+ // Save whether the collapsed marker was at the beginning or at the end of document fragment
98
+ // to know where to create it after the insertion is done.
99
+ collapsed = isAtBeginning ? 'start' : 'end';
100
+ }
101
+ fakeMarkerElements.push({
102
+ name,
103
+ element: fakeElement,
104
+ collapsed
105
+ });
106
+ }
107
+ }
108
+ nodesToInsert = content.getChildren();
109
+ }
110
+ else {
111
+ nodesToInsert = [content];
112
+ }
113
+ insertion.handleNodes(nodesToInsert);
114
+ let newRange = insertion.getSelectionRange();
115
+ if (content.is('documentFragment') && fakeMarkerElements.length) {
116
+ // After insertion was done, the selection was set but the model contains fake <$marker> elements.
117
+ // These <$marker> elements will be now removed. Because of that, we will need to fix the selection.
118
+ // We will create a live range that will automatically be update as <$marker> elements are removed.
119
+ const selectionLiveRange = newRange ? LiveRange.fromRange(newRange) : null;
120
+ // Marker name -> [ start position, end position ].
121
+ const markersData = {};
122
+ // Note: `fakeMarkerElements` are sorted backwards. However, now, we want to handle the markers
123
+ // from the beginning, so that existing <$marker> elements do not affect markers positions.
124
+ // This is why we iterate from the end to the start.
125
+ for (let i = fakeMarkerElements.length - 1; i >= 0; i--) {
126
+ const { name, element, collapsed } = fakeMarkerElements[i];
127
+ const isStartBoundary = !markersData[name];
128
+ if (isStartBoundary) {
129
+ markersData[name] = [];
130
+ }
131
+ if (element) {
132
+ // Read fake marker element position to learn where the marker should be created.
133
+ const elementPosition = writer.createPositionAt(element, 'before');
134
+ markersData[name].push(elementPosition);
135
+ writer.remove(element);
136
+ }
137
+ else {
138
+ // If the fake marker element does not exist, it means that the marker boundary was at the beginning or at the end.
139
+ const rangeOnInsertion = insertion.getAffectedRange();
140
+ if (!rangeOnInsertion) {
141
+ // If affected range is `null` it means that nothing was in the document fragment or all content was filtered out.
142
+ // Some markers that were in the filtered content may be removed (partially or totally).
143
+ // Let's handle only those markers that were at the beginning or at the end of the document fragment.
144
+ if (collapsed) {
145
+ markersData[name].push(insertion.position);
146
+ }
147
+ continue;
148
+ }
149
+ if (collapsed) {
150
+ // If the marker was collapsed at the beginning or at the end of the document fragment,
151
+ // put both boundaries at the beginning or at the end of inserted range (to keep the marker collapsed).
152
+ markersData[name].push(rangeOnInsertion[collapsed]);
153
+ }
154
+ else {
155
+ markersData[name].push(isStartBoundary ? rangeOnInsertion.start : rangeOnInsertion.end);
156
+ }
157
+ }
158
+ }
159
+ for (const [name, [start, end]] of Object.entries(markersData)) {
160
+ // For now, we ignore markers if they are included in the filtered-out content.
161
+ // In the future implementation we will improve that case to create markers that are not filtered out completely.
162
+ if (start && end && start.root === end.root) {
163
+ writer.addMarker(name, {
164
+ usingOperation: true,
165
+ affectsData: true,
166
+ range: new Range(start, end)
167
+ });
168
+ }
169
+ }
170
+ if (selectionLiveRange) {
171
+ newRange = selectionLiveRange.toRange();
172
+ selectionLiveRange.detach();
173
+ }
174
+ }
175
+ /* istanbul ignore else */
176
+ if (newRange) {
177
+ if (selection instanceof DocumentSelection) {
178
+ writer.setSelection(newRange);
179
+ }
180
+ else {
181
+ selection.setTo(newRange);
182
+ }
183
+ }
184
+ else {
185
+ // We are not testing else because it's a safe check for unpredictable edge cases:
186
+ // an insertion without proper range to select.
187
+ //
188
+ // @if CK_DEBUG // console.warn( 'Cannot determine a proper selection range after insertion.' );
189
+ }
190
+ const affectedRange = insertion.getAffectedRange() || model.createRange(selection.anchor);
191
+ insertion.destroy();
192
+ return affectedRange;
193
+ });
100
194
  }
101
-
102
195
  /**
103
196
  * Utility class for performing content insertion.
104
197
  *
105
198
  * @private
106
199
  */
107
200
  class Insertion {
108
- constructor( model, writer, position ) {
109
- /**
110
- * The model in context of which the insertion should be performed.
111
- *
112
- * @member {module:engine/model~Model} #model
113
- */
114
- this.model = model;
115
-
116
- /**
117
- * Batch to which operations will be added.
118
- *
119
- * @member {module:engine/controller/writer~Batch} #writer
120
- */
121
- this.writer = writer;
122
-
123
- /**
124
- * The position at which (or near which) the next node will be inserted.
125
- *
126
- * @member {module:engine/model/position~Position} #position
127
- */
128
- this.position = position;
129
-
130
- /**
131
- * Elements with which the inserted elements can be merged.
132
- *
133
- * <p>x^</p><p>y</p> + <p>z</p> (can merge to <p>x</p>)
134
- * <p>x</p><p>^y</p> + <p>z</p> (can merge to <p>y</p>)
135
- * <p>x^y</p> + <p>z</p> (can merge to <p>xy</p> which will be split during the action,
136
- * so both its pieces will be added to this set)
137
- *
138
- *
139
- * @member {Set} #canMergeWith
140
- */
141
- this.canMergeWith = new Set( [ this.position.parent ] );
142
-
143
- /**
144
- * Schema of the model.
145
- *
146
- * @member {module:engine/model/schema~Schema} #schema
147
- */
148
- this.schema = model.schema;
149
-
150
- /**
151
- * The temporary DocumentFragment used for grouping multiple nodes for single insert operation.
152
- *
153
- * @private
154
- * @type {module:engine/model/documentfragment~DocumentFragment}
155
- */
156
- this._documentFragment = writer.createDocumentFragment();
157
-
158
- /**
159
- * The current position in the temporary DocumentFragment.
160
- *
161
- * @private
162
- * @type {module:engine/model/position~Position}
163
- */
164
- this._documentFragmentPosition = writer.createPositionAt( this._documentFragment, 0 );
165
-
166
- /**
167
- * The reference to the first inserted node.
168
- *
169
- * @private
170
- * @type {module:engine/model/node~Node}
171
- */
172
- this._firstNode = null;
173
-
174
- /**
175
- * The reference to the last inserted node.
176
- *
177
- * @private
178
- * @type {module:engine/model/node~Node}
179
- */
180
- this._lastNode = null;
181
-
182
- /**
183
- * The reference to the last auto paragraph node.
184
- *
185
- * @private
186
- * @type {module:engine/model/node~Node}
187
- */
188
- this._lastAutoParagraph = null;
189
-
190
- /**
191
- * The array of nodes that should be cleaned of not allowed attributes.
192
- *
193
- * @private
194
- * @type {Array.<module:engine/model/node~Node>}
195
- */
196
- this._filterAttributesOf = [];
197
-
198
- /**
199
- * Beginning of the affected range. See {@link module:engine/model/utils/insertcontent~Insertion#getAffectedRange}.
200
- *
201
- * @private
202
- * @member {module:engine/model/liveposition~LivePosition|null} #_affectedStart
203
- */
204
- this._affectedStart = null;
205
-
206
- /**
207
- * End of the affected range. See {@link module:engine/model/utils/insertcontent~Insertion#getAffectedRange}.
208
- *
209
- * @private
210
- * @member {module:engine/model/liveposition~LivePosition|null} #_affectedEnd
211
- */
212
- this._affectedEnd = null;
213
- }
214
-
215
- /**
216
- * Handles insertion of a set of nodes.
217
- *
218
- * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes to insert.
219
- */
220
- handleNodes( nodes ) {
221
- for ( const node of Array.from( nodes ) ) {
222
- this._handleNode( node );
223
- }
224
-
225
- // Insert nodes collected in temporary DocumentFragment.
226
- this._insertPartialFragment();
227
-
228
- // If there was an auto paragraph then we might need to adjust the end of insertion.
229
- if ( this._lastAutoParagraph ) {
230
- this._updateLastNodeFromAutoParagraph( this._lastAutoParagraph );
231
- }
232
-
233
- // After the content was inserted we may try to merge it with its next sibling if the selection was in it initially.
234
- // Merging with the previous sibling was performed just after inserting the first node to the document.
235
- this._mergeOnRight();
236
-
237
- // TMP this will become a post-fixer.
238
- this.schema.removeDisallowedAttributes( this._filterAttributesOf, this.writer );
239
- this._filterAttributesOf = [];
240
- }
241
-
242
- /**
243
- * Updates the last node after the auto paragraphing.
244
- *
245
- * @private
246
- * @param {module:engine/model/node~Node} node The last auto paragraphing node.
247
- */
248
- _updateLastNodeFromAutoParagraph( node ) {
249
- const positionAfterLastNode = this.writer.createPositionAfter( this._lastNode );
250
- const positionAfterNode = this.writer.createPositionAfter( node );
251
-
252
- // If the real end was after the last auto paragraph then update relevant properties.
253
- if ( positionAfterNode.isAfter( positionAfterLastNode ) ) {
254
- this._lastNode = node;
255
-
256
- /* istanbul ignore if */
257
- if ( this.position.parent != node || !this.position.isAtEnd ) {
258
- // Algorithm's correctness check. We should never end up here but it's good to know that we did.
259
- // At this point the insertion position should be at the end of the last auto paragraph.
260
- // Note: This error is documented in other place in this file.
261
- throw new CKEditorError( 'insertcontent-invalid-insertion-position', this );
262
- }
263
-
264
- this.position = positionAfterNode;
265
- this._setAffectedBoundaries( this.position );
266
- }
267
- }
268
-
269
- /**
270
- * Returns range to be selected after insertion.
271
- * Returns `null` if there is no valid range to select after insertion.
272
- *
273
- * @returns {module:engine/model/range~Range|null}
274
- */
275
- getSelectionRange() {
276
- if ( this.nodeToSelect ) {
277
- return Range._createOn( this.nodeToSelect );
278
- }
279
-
280
- return this.model.schema.getNearestSelectionRange( this.position );
281
- }
282
-
283
- /**
284
- * Returns a range which contains all the performed changes. This is a range that, if removed, would return the model to the state
285
- * before the insertion. Returns `null` if no changes were done.
286
- *
287
- * @returns {module:engine/model/range~Range|null}
288
- */
289
- getAffectedRange() {
290
- if ( !this._affectedStart ) {
291
- return null;
292
- }
293
-
294
- return new Range( this._affectedStart, this._affectedEnd );
295
- }
296
-
297
- /**
298
- * Destroys `Insertion` instance.
299
- */
300
- destroy() {
301
- if ( this._affectedStart ) {
302
- this._affectedStart.detach();
303
- }
304
-
305
- if ( this._affectedEnd ) {
306
- this._affectedEnd.detach();
307
- }
308
- }
309
-
310
- /**
311
- * Handles insertion of a single node.
312
- *
313
- * @private
314
- * @param {module:engine/model/node~Node} node
315
- */
316
- _handleNode( node ) {
317
- // Let's handle object in a special way.
318
- // * They should never be merged with other elements.
319
- // * If they are not allowed in any of the selection ancestors, they could be either autoparagraphed or totally removed.
320
- if ( this.schema.isObject( node ) ) {
321
- this._handleObject( node );
322
-
323
- return;
324
- }
325
-
326
- // Try to find a place for the given node.
327
-
328
- // Check if a node can be inserted in the given position or it would be accepted if a paragraph would be inserted.
329
- // Inserts the auto paragraph if it would allow for insertion.
330
- let isAllowed = this._checkAndAutoParagraphToAllowedPosition( node );
331
-
332
- if ( !isAllowed ) {
333
- // Split the position.parent's branch up to a point where the node can be inserted.
334
- // If it isn't allowed in the whole branch, then of course don't split anything.
335
- isAllowed = this._checkAndSplitToAllowedPosition( node );
336
-
337
- if ( !isAllowed ) {
338
- this._handleDisallowedNode( node );
339
-
340
- return;
341
- }
342
- }
343
-
344
- // Add node to the current temporary DocumentFragment.
345
- this._appendToFragment( node );
346
-
347
- // Store the first and last nodes for easy access for merging with sibling nodes.
348
- if ( !this._firstNode ) {
349
- this._firstNode = node;
350
- }
351
-
352
- this._lastNode = node;
353
- }
354
-
355
- /**
356
- * Inserts the temporary DocumentFragment into the model.
357
- *
358
- * @private
359
- */
360
- _insertPartialFragment() {
361
- if ( this._documentFragment.isEmpty ) {
362
- return;
363
- }
364
-
365
- const livePosition = LivePosition.fromPosition( this.position, 'toNext' );
366
-
367
- this._setAffectedBoundaries( this.position );
368
-
369
- // If the very first node of the whole insertion process is inserted, insert it separately for OT reasons (undo).
370
- // Note: there can be multiple calls to `_insertPartialFragment()` during one insertion process.
371
- // Note: only the very first node can be merged so we have to do separate operation only for it.
372
- if ( this._documentFragment.getChild( 0 ) == this._firstNode ) {
373
- this.writer.insert( this._firstNode, this.position );
374
-
375
- // We must merge the first node just after inserting it to avoid problems with OT.
376
- // (See: https://github.com/ckeditor/ckeditor5/pull/8773#issuecomment-760945652).
377
- this._mergeOnLeft();
378
-
379
- this.position = livePosition.toPosition();
380
- }
381
-
382
- // Insert the remaining nodes from document fragment.
383
- if ( !this._documentFragment.isEmpty ) {
384
- this.writer.insert( this._documentFragment, this.position );
385
- }
386
-
387
- this._documentFragmentPosition = this.writer.createPositionAt( this._documentFragment, 0 );
388
-
389
- this.position = livePosition.toPosition();
390
- livePosition.detach();
391
- }
392
-
393
- /**
394
- * @private
395
- * @param {module:engine/model/element~Element} node The object element.
396
- */
397
- _handleObject( node ) {
398
- // Try finding it a place in the tree.
399
- if ( this._checkAndSplitToAllowedPosition( node ) ) {
400
- this._appendToFragment( node );
401
- }
402
- // Try autoparagraphing.
403
- else {
404
- this._tryAutoparagraphing( node );
405
- }
406
- }
407
-
408
- /**
409
- * @private
410
- * @param {module:engine/model/node~Node} node The disallowed node which needs to be handled.
411
- */
412
- _handleDisallowedNode( node ) {
413
- // If the node is an element, try inserting its children (strip the parent).
414
- if ( node.is( 'element' ) ) {
415
- this.handleNodes( node.getChildren() );
416
- }
417
- // If text is not allowed, try autoparagraphing it.
418
- else {
419
- this._tryAutoparagraphing( node );
420
- }
421
- }
422
-
423
- /**
424
- * Append a node to the temporary DocumentFragment.
425
- *
426
- * @private
427
- * @param {module:engine/model/node~Node} node The node to insert.
428
- */
429
- _appendToFragment( node ) {
430
- /* istanbul ignore if */
431
- if ( !this.schema.checkChild( this.position, node ) ) {
432
- // Algorithm's correctness check. We should never end up here but it's good to know that we did.
433
- // Note that it would often be a silent issue if we insert node in a place where it's not allowed.
434
-
435
- /**
436
- * Given node cannot be inserted on the given position.
437
- *
438
- * @error insertcontent-wrong-position
439
- * @param {module:engine/model/node~Node} node Node to insert.
440
- * @param {module:engine/model/position~Position} position Position to insert the node at.
441
- */
442
- throw new CKEditorError(
443
- 'insertcontent-wrong-position',
444
- this,
445
- { node, position: this.position }
446
- );
447
- }
448
-
449
- this.writer.insert( node, this._documentFragmentPosition );
450
- this._documentFragmentPosition = this._documentFragmentPosition.getShiftedBy( node.offsetSize );
451
-
452
- // The last inserted object should be selected because we can't put a collapsed selection after it.
453
- if ( this.schema.isObject( node ) && !this.schema.checkChild( this.position, '$text' ) ) {
454
- this.nodeToSelect = node;
455
- } else {
456
- this.nodeToSelect = null;
457
- }
458
-
459
- this._filterAttributesOf.push( node );
460
- }
461
-
462
- /**
463
- * Sets `_affectedStart` and `_affectedEnd` to the given `position`. Should be used before a change is done during insertion process to
464
- * mark the affected range.
465
- *
466
- * This method is used before inserting a node or splitting a parent node. `_affectedStart` and `_affectedEnd` are also changed
467
- * during merging, but the logic there is more complicated so it is left out of this function.
468
- *
469
- * @private
470
- * @param {module:engine/model/position~Position} position
471
- */
472
- _setAffectedBoundaries( position ) {
473
- // Set affected boundaries stickiness so that those position will "expand" when something is inserted in between them:
474
- // <paragraph>Foo][bar</paragraph> -> <paragraph>Foo]xx[bar</paragraph>
475
- // This is why it cannot be a range but two separate positions.
476
- if ( !this._affectedStart ) {
477
- this._affectedStart = LivePosition.fromPosition( position, 'toPrevious' );
478
- }
479
-
480
- // If `_affectedEnd` is before the new boundary position, expand `_affectedEnd`. This can happen if first inserted node was
481
- // inserted into the parent but the next node is moved-out of that parent:
482
- // (1) <paragraph>Foo][</paragraph> -> <paragraph>Foo]xx[</paragraph>
483
- // (2) <paragraph>Foo]xx[</paragraph> -> <paragraph>Foo]xx</paragraph><widget></widget>[
484
- if ( !this._affectedEnd || this._affectedEnd.isBefore( position ) ) {
485
- if ( this._affectedEnd ) {
486
- this._affectedEnd.detach();
487
- }
488
-
489
- this._affectedEnd = LivePosition.fromPosition( position, 'toNext' );
490
- }
491
- }
492
-
493
- /**
494
- * Merges the previous sibling of the first node if it should be merged.
495
- *
496
- * After the content was inserted we may try to merge it with its siblings.
497
- * This should happen only if the selection was in those elements initially.
498
- *
499
- * @private
500
- */
501
- _mergeOnLeft() {
502
- const node = this._firstNode;
503
-
504
- if ( !( node instanceof Element ) ) {
505
- return;
506
- }
507
-
508
- if ( !this._canMergeLeft( node ) ) {
509
- return;
510
- }
511
-
512
- const mergePosLeft = LivePosition._createBefore( node );
513
- mergePosLeft.stickiness = 'toNext';
514
-
515
- const livePosition = LivePosition.fromPosition( this.position, 'toNext' );
516
-
517
- // If `_affectedStart` is sames as merge position, it means that the element "marked" by `_affectedStart` is going to be
518
- // removed and its contents will be moved. This won't transform `LivePosition` so `_affectedStart` needs to be moved
519
- // by hand to properly reflect affected range. (Due to `_affectedStart` and `_affectedEnd` stickiness, the "range" is
520
- // shown as `][`).
521
- //
522
- // Example - insert `<paragraph>Abc</paragraph><paragraph>Xyz</paragraph>` at the end of `<paragraph>Foo^</paragraph>`:
523
- //
524
- // <paragraph>Foo</paragraph><paragraph>Bar</paragraph> -->
525
- // <paragraph>Foo</paragraph>]<paragraph>Abc</paragraph><paragraph>Xyz</paragraph>[<paragraph>Bar</paragraph> -->
526
- // <paragraph>Foo]Abc</paragraph><paragraph>Xyz</paragraph>[<paragraph>Bar</paragraph>
527
- //
528
- // Note, that if we are here then something must have been inserted, so `_affectedStart` and `_affectedEnd` have to be set.
529
- if ( this._affectedStart.isEqual( mergePosLeft ) ) {
530
- this._affectedStart.detach();
531
- this._affectedStart = LivePosition._createAt( mergePosLeft.nodeBefore, 'end', 'toPrevious' );
532
- }
533
-
534
- // We need to update the references to the first and last nodes if they will be merged into the previous sibling node
535
- // because the reference would point to the removed node.
536
- //
537
- // <p>A^A</p> + <p>X</p>
538
- //
539
- // <p>A</p>^<p>A</p>
540
- // <p>A</p><p>X</p><p>A</p>
541
- // <p>AX</p><p>A</p>
542
- // <p>AXA</p>
543
- if ( this._firstNode === this._lastNode ) {
544
- this._firstNode = mergePosLeft.nodeBefore;
545
- this._lastNode = mergePosLeft.nodeBefore;
546
- }
547
-
548
- this.writer.merge( mergePosLeft );
549
-
550
- // If only one element (the merged one) is in the "affected range", also move the affected range end appropriately.
551
- //
552
- // Example - insert `<paragraph>Abc</paragraph>` at the of `<paragraph>Foo^</paragraph>`:
553
- //
554
- // <paragraph>Foo</paragraph><paragraph>Bar</paragraph> -->
555
- // <paragraph>Foo</paragraph>]<paragraph>Abc</paragraph>[<paragraph>Bar</paragraph> -->
556
- // <paragraph>Foo]Abc</paragraph>[<paragraph>Bar</paragraph> -->
557
- // <paragraph>Foo]Abc[</paragraph><paragraph>Bar</paragraph>
558
- if ( mergePosLeft.isEqual( this._affectedEnd ) && this._firstNode === this._lastNode ) {
559
- this._affectedEnd.detach();
560
- this._affectedEnd = LivePosition._createAt( mergePosLeft.nodeBefore, 'end', 'toNext' );
561
- }
562
-
563
- this.position = livePosition.toPosition();
564
- livePosition.detach();
565
-
566
- // After merge elements that were marked by _insert() to be filtered might be gone so
567
- // we need to mark the new container.
568
- this._filterAttributesOf.push( this.position.parent );
569
-
570
- mergePosLeft.detach();
571
- }
572
-
573
- /**
574
- * Merges the next sibling of the last node if it should be merged.
575
- *
576
- * After the content was inserted we may try to merge it with its siblings.
577
- * This should happen only if the selection was in those elements initially.
578
- *
579
- * @private
580
- */
581
- _mergeOnRight() {
582
- const node = this._lastNode;
583
-
584
- if ( !( node instanceof Element ) ) {
585
- return;
586
- }
587
-
588
- if ( !this._canMergeRight( node ) ) {
589
- return;
590
- }
591
-
592
- const mergePosRight = LivePosition._createAfter( node );
593
- mergePosRight.stickiness = 'toNext';
594
-
595
- /* istanbul ignore if */
596
- if ( !this.position.isEqual( mergePosRight ) ) {
597
- // Algorithm's correctness check. We should never end up here but it's good to know that we did.
598
- // At this point the insertion position should be after the node we'll merge. If it isn't,
599
- // it should need to be secured as in the left merge case.
600
- /**
601
- * An internal error occurred when merging inserted content with its siblings.
602
- * The insertion position should equal the merge position.
603
- *
604
- * If you encountered this error, report it back to the CKEditor 5 team
605
- * with as many details as possible regarding the content being inserted and the insertion position.
606
- *
607
- * @error insertcontent-invalid-insertion-position
608
- */
609
- throw new CKEditorError( 'insertcontent-invalid-insertion-position', this );
610
- }
611
-
612
- // Move the position to the previous node, so it isn't moved to the graveyard on merge.
613
- // <p>x</p>[]<p>y</p> => <p>x[]</p><p>y</p>
614
- this.position = Position._createAt( mergePosRight.nodeBefore, 'end' );
615
-
616
- // Explanation of setting position stickiness to `'toPrevious'`:
617
- // OK: <p>xx[]</p> + <p>yy</p> => <p>xx[]yy</p> (when sticks to previous)
618
- // NOK: <p>xx[]</p> + <p>yy</p> => <p>xxyy[]</p> (when sticks to next)
619
- const livePosition = LivePosition.fromPosition( this.position, 'toPrevious' );
620
-
621
- // See comment in `_mergeOnLeft()` on moving `_affectedStart`.
622
- if ( this._affectedEnd.isEqual( mergePosRight ) ) {
623
- this._affectedEnd.detach();
624
- this._affectedEnd = LivePosition._createAt( mergePosRight.nodeBefore, 'end', 'toNext' );
625
- }
626
-
627
- // We need to update the references to the first and last nodes if they will be merged into the previous sibling node
628
- // because the reference would point to the removed node.
629
- //
630
- // <p>A^A</p> + <p>X</p>
631
- //
632
- // <p>A</p>^<p>A</p>
633
- // <p>A</p><p>X</p><p>A</p>
634
- // <p>AX</p><p>A</p>
635
- // <p>AXA</p>
636
- if ( this._firstNode === this._lastNode ) {
637
- this._firstNode = mergePosRight.nodeBefore;
638
- this._lastNode = mergePosRight.nodeBefore;
639
- }
640
-
641
- this.writer.merge( mergePosRight );
642
-
643
- // See comment in `_mergeOnLeft()` on moving `_affectedStart`.
644
- if ( mergePosRight.getShiftedBy( -1 ).isEqual( this._affectedStart ) && this._firstNode === this._lastNode ) {
645
- this._affectedStart.detach();
646
- this._affectedStart = LivePosition._createAt( mergePosRight.nodeBefore, 0, 'toPrevious' );
647
- }
648
-
649
- this.position = livePosition.toPosition();
650
- livePosition.detach();
651
-
652
- // After merge elements that were marked by _insert() to be filtered might be gone so
653
- // we need to mark the new container.
654
- this._filterAttributesOf.push( this.position.parent );
655
-
656
- mergePosRight.detach();
657
- }
658
-
659
- /**
660
- * Checks whether specified node can be merged with previous sibling element.
661
- *
662
- * @private
663
- * @param {module:engine/model/node~Node} node The node which could potentially be merged.
664
- * @returns {Boolean}
665
- */
666
- _canMergeLeft( node ) {
667
- const previousSibling = node.previousSibling;
668
-
669
- return ( previousSibling instanceof Element ) &&
670
- this.canMergeWith.has( previousSibling ) &&
671
- this.model.schema.checkMerge( previousSibling, node );
672
- }
673
-
674
- /**
675
- * Checks whether specified node can be merged with next sibling element.
676
- *
677
- * @private
678
- * @param {module:engine/model/node~Node} node The node which could potentially be merged.
679
- * @returns {Boolean}
680
- */
681
- _canMergeRight( node ) {
682
- const nextSibling = node.nextSibling;
683
-
684
- return ( nextSibling instanceof Element ) &&
685
- this.canMergeWith.has( nextSibling ) &&
686
- this.model.schema.checkMerge( node, nextSibling );
687
- }
688
-
689
- /**
690
- * Tries wrapping the node in a new paragraph and inserting it this way.
691
- *
692
- * @private
693
- * @param {module:engine/model/node~Node} node The node which needs to be autoparagraphed.
694
- */
695
- _tryAutoparagraphing( node ) {
696
- const paragraph = this.writer.createElement( 'paragraph' );
697
-
698
- // Do not autoparagraph if the paragraph won't be allowed there,
699
- // cause that would lead to an infinite loop. The paragraph would be rejected in
700
- // the next _handleNode() call and we'd be here again.
701
- if ( this._getAllowedIn( this.position.parent, paragraph ) && this.schema.checkChild( paragraph, node ) ) {
702
- paragraph._appendChild( node );
703
- this._handleNode( paragraph );
704
- }
705
- }
706
-
707
- /**
708
- * Checks if a node can be inserted in the given position or it would be accepted if a paragraph would be inserted.
709
- * It also handles inserting the paragraph.
710
- *
711
- * @private
712
- * @param {module:engine/model/node~Node} node The node.
713
- * @returns {Boolean} Whether an allowed position was found.
714
- * `false` is returned if the node isn't allowed at the current position or in auto paragraph, `true` if was.
715
- */
716
- _checkAndAutoParagraphToAllowedPosition( node ) {
717
- if ( this.schema.checkChild( this.position.parent, node ) ) {
718
- return true;
719
- }
720
-
721
- // Do not auto paragraph if the paragraph won't be allowed there,
722
- // cause that would lead to an infinite loop. The paragraph would be rejected in
723
- // the next _handleNode() call and we'd be here again.
724
- if ( !this.schema.checkChild( this.position.parent, 'paragraph' ) || !this.schema.checkChild( 'paragraph', node ) ) {
725
- return false;
726
- }
727
-
728
- // Insert nodes collected in temporary DocumentFragment if the position parent needs change to process further nodes.
729
- this._insertPartialFragment();
730
-
731
- // Insert a paragraph and move insertion position to it.
732
- const paragraph = this.writer.createElement( 'paragraph' );
733
-
734
- this.writer.insert( paragraph, this.position );
735
- this._setAffectedBoundaries( this.position );
736
-
737
- this._lastAutoParagraph = paragraph;
738
- this.position = this.writer.createPositionAt( paragraph, 0 );
739
-
740
- return true;
741
- }
742
-
743
- /**
744
- * @private
745
- * @param {module:engine/model/node~Node} node
746
- * @returns {Boolean} Whether an allowed position was found.
747
- * `false` is returned if the node isn't allowed at any position up in the tree, `true` if was.
748
- */
749
- _checkAndSplitToAllowedPosition( node ) {
750
- const allowedIn = this._getAllowedIn( this.position.parent, node );
751
-
752
- if ( !allowedIn ) {
753
- return false;
754
- }
755
-
756
- // Insert nodes collected in temporary DocumentFragment if the position parent needs change to process further nodes.
757
- if ( allowedIn != this.position.parent ) {
758
- this._insertPartialFragment();
759
- }
760
-
761
- while ( allowedIn != this.position.parent ) {
762
- if ( this.position.isAtStart ) {
763
- // If insertion position is at the beginning of the parent, move it out instead of splitting.
764
- // <p>^Foo</p> -> ^<p>Foo</p>
765
- const parent = this.position.parent;
766
-
767
- this.position = this.writer.createPositionBefore( parent );
768
-
769
- // Special case – parent is empty (<p>^</p>).
770
- //
771
- // 1. parent.isEmpty
772
- // We can remove the element after moving insertion position out of it.
773
- //
774
- // 2. parent.parent === allowedIn
775
- // However parent should remain in place when allowed element is above limit element in document tree.
776
- // For example there shouldn't be allowed to remove empty paragraph from tableCell, when is pasted
777
- // content allowed in $root.
778
- if ( parent.isEmpty && parent.parent === allowedIn ) {
779
- this.writer.remove( parent );
780
- }
781
- } else if ( this.position.isAtEnd ) {
782
- // If insertion position is at the end of the parent, move it out instead of splitting.
783
- // <p>Foo^</p> -> <p>Foo</p>^
784
- this.position = this.writer.createPositionAfter( this.position.parent );
785
- } else {
786
- const tempPos = this.writer.createPositionAfter( this.position.parent );
787
-
788
- this._setAffectedBoundaries( this.position );
789
- this.writer.split( this.position );
790
-
791
- this.position = tempPos;
792
-
793
- this.canMergeWith.add( this.position.nodeAfter );
794
- }
795
- }
796
-
797
- return true;
798
- }
799
-
800
- /**
801
- * Gets the element in which the given node is allowed. It checks the passed element and all its ancestors.
802
- *
803
- * @private
804
- * @param {module:engine/model/element~Element} contextElement The element in which context the node should be checked.
805
- * @param {module:engine/model/node~Node} childNode The node to check.
806
- * @returns {module:engine/model/element~Element|null}
807
- */
808
- _getAllowedIn( contextElement, childNode ) {
809
- if ( this.schema.checkChild( contextElement, childNode ) ) {
810
- return contextElement;
811
- }
812
-
813
- // If the child wasn't allowed in the context element and the element is a limit there's no point in
814
- // checking any further towards the root. This is it: the limit is unsplittable and there's nothing
815
- // we can do about it. Without this check, the algorithm will analyze parent of the limit and may create
816
- // an illusion of the child being allowed. There's no way to insert it down there, though. It results in
817
- // infinite loops.
818
- if ( this.schema.isLimit( contextElement ) ) {
819
- return null;
820
- }
821
-
822
- return this._getAllowedIn( contextElement.parent, childNode );
823
- }
201
+ constructor(model, writer, position) {
202
+ /**
203
+ * The model in context of which the insertion should be performed.
204
+ *
205
+ * @member {module:engine/model~Model} #model
206
+ */
207
+ this.model = model;
208
+ /**
209
+ * Batch to which operations will be added.
210
+ *
211
+ * @member {module:engine/controller/writer~Batch} #writer
212
+ */
213
+ this.writer = writer;
214
+ /**
215
+ * The position at which (or near which) the next node will be inserted.
216
+ *
217
+ * @member {module:engine/model/position~Position} #position
218
+ */
219
+ this.position = position;
220
+ /**
221
+ * Elements with which the inserted elements can be merged.
222
+ *
223
+ * <p>x^</p><p>y</p> + <p>z</p> (can merge to <p>x</p>)
224
+ * <p>x</p><p>^y</p> + <p>z</p> (can merge to <p>y</p>)
225
+ * <p>x^y</p> + <p>z</p> (can merge to <p>xy</p> which will be split during the action,
226
+ * so both its pieces will be added to this set)
227
+ *
228
+ *
229
+ * @member {Set} #canMergeWith
230
+ */
231
+ this.canMergeWith = new Set([this.position.parent]);
232
+ /**
233
+ * Schema of the model.
234
+ *
235
+ * @member {module:engine/model/schema~Schema} #schema
236
+ */
237
+ this.schema = model.schema;
238
+ /**
239
+ * The temporary DocumentFragment used for grouping multiple nodes for single insert operation.
240
+ *
241
+ * @private
242
+ * @type {module:engine/model/documentfragment~DocumentFragment}
243
+ */
244
+ this._documentFragment = writer.createDocumentFragment();
245
+ /**
246
+ * The current position in the temporary DocumentFragment.
247
+ *
248
+ * @private
249
+ * @type {module:engine/model/position~Position}
250
+ */
251
+ this._documentFragmentPosition = writer.createPositionAt(this._documentFragment, 0);
252
+ /**
253
+ * The reference to the first inserted node.
254
+ *
255
+ * @private
256
+ * @type {module:engine/model/node~Node}
257
+ */
258
+ this._firstNode = null;
259
+ /**
260
+ * The reference to the last inserted node.
261
+ *
262
+ * @private
263
+ * @type {module:engine/model/node~Node}
264
+ */
265
+ this._lastNode = null;
266
+ /**
267
+ * The reference to the last auto paragraph node.
268
+ *
269
+ * @private
270
+ * @type {module:engine/model/node~Node}
271
+ */
272
+ this._lastAutoParagraph = null;
273
+ /**
274
+ * The array of nodes that should be cleaned of not allowed attributes.
275
+ *
276
+ * @private
277
+ * @type {Array.<module:engine/model/node~Node>}
278
+ */
279
+ this._filterAttributesOf = [];
280
+ /**
281
+ * Beginning of the affected range. See {@link module:engine/model/utils/insertcontent~Insertion#getAffectedRange}.
282
+ *
283
+ * @private
284
+ * @member {module:engine/model/liveposition~LivePosition|null} #_affectedStart
285
+ */
286
+ this._affectedStart = null;
287
+ /**
288
+ * End of the affected range. See {@link module:engine/model/utils/insertcontent~Insertion#getAffectedRange}.
289
+ *
290
+ * @private
291
+ * @member {module:engine/model/liveposition~LivePosition|null} #_affectedEnd
292
+ */
293
+ this._affectedEnd = null;
294
+ }
295
+ /**
296
+ * Handles insertion of a set of nodes.
297
+ *
298
+ * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes to insert.
299
+ */
300
+ handleNodes(nodes) {
301
+ for (const node of Array.from(nodes)) {
302
+ this._handleNode(node);
303
+ }
304
+ // Insert nodes collected in temporary DocumentFragment.
305
+ this._insertPartialFragment();
306
+ // If there was an auto paragraph then we might need to adjust the end of insertion.
307
+ if (this._lastAutoParagraph) {
308
+ this._updateLastNodeFromAutoParagraph(this._lastAutoParagraph);
309
+ }
310
+ // After the content was inserted we may try to merge it with its next sibling if the selection was in it initially.
311
+ // Merging with the previous sibling was performed just after inserting the first node to the document.
312
+ this._mergeOnRight();
313
+ // TMP this will become a post-fixer.
314
+ this.schema.removeDisallowedAttributes(this._filterAttributesOf, this.writer);
315
+ this._filterAttributesOf = [];
316
+ }
317
+ /**
318
+ * Updates the last node after the auto paragraphing.
319
+ *
320
+ * @private
321
+ * @param {module:engine/model/node~Node} node The last auto paragraphing node.
322
+ */
323
+ _updateLastNodeFromAutoParagraph(node) {
324
+ const positionAfterLastNode = this.writer.createPositionAfter(this._lastNode);
325
+ const positionAfterNode = this.writer.createPositionAfter(node);
326
+ // If the real end was after the last auto paragraph then update relevant properties.
327
+ if (positionAfterNode.isAfter(positionAfterLastNode)) {
328
+ this._lastNode = node;
329
+ /* istanbul ignore if */
330
+ if (this.position.parent != node || !this.position.isAtEnd) {
331
+ // Algorithm's correctness check. We should never end up here but it's good to know that we did.
332
+ // At this point the insertion position should be at the end of the last auto paragraph.
333
+ // Note: This error is documented in other place in this file.
334
+ throw new CKEditorError('insertcontent-invalid-insertion-position', this);
335
+ }
336
+ this.position = positionAfterNode;
337
+ this._setAffectedBoundaries(this.position);
338
+ }
339
+ }
340
+ /**
341
+ * Returns range to be selected after insertion.
342
+ * Returns `null` if there is no valid range to select after insertion.
343
+ *
344
+ * @returns {module:engine/model/range~Range|null}
345
+ */
346
+ getSelectionRange() {
347
+ if (this._nodeToSelect) {
348
+ return Range._createOn(this._nodeToSelect);
349
+ }
350
+ return this.model.schema.getNearestSelectionRange(this.position);
351
+ }
352
+ /**
353
+ * Returns a range which contains all the performed changes. This is a range that, if removed, would return the model to the state
354
+ * before the insertion. Returns `null` if no changes were done.
355
+ *
356
+ * @returns {module:engine/model/range~Range|null}
357
+ */
358
+ getAffectedRange() {
359
+ if (!this._affectedStart) {
360
+ return null;
361
+ }
362
+ return new Range(this._affectedStart, this._affectedEnd);
363
+ }
364
+ /**
365
+ * Destroys `Insertion` instance.
366
+ */
367
+ destroy() {
368
+ if (this._affectedStart) {
369
+ this._affectedStart.detach();
370
+ }
371
+ if (this._affectedEnd) {
372
+ this._affectedEnd.detach();
373
+ }
374
+ }
375
+ /**
376
+ * Handles insertion of a single node.
377
+ *
378
+ * @private
379
+ * @param {module:engine/model/node~Node} node
380
+ */
381
+ _handleNode(node) {
382
+ // Let's handle object in a special way.
383
+ // * They should never be merged with other elements.
384
+ // * If they are not allowed in any of the selection ancestors, they could be either autoparagraphed or totally removed.
385
+ if (this.schema.isObject(node)) {
386
+ this._handleObject(node);
387
+ return;
388
+ }
389
+ // Try to find a place for the given node.
390
+ // Check if a node can be inserted in the given position or it would be accepted if a paragraph would be inserted.
391
+ // Inserts the auto paragraph if it would allow for insertion.
392
+ let isAllowed = this._checkAndAutoParagraphToAllowedPosition(node);
393
+ if (!isAllowed) {
394
+ // Split the position.parent's branch up to a point where the node can be inserted.
395
+ // If it isn't allowed in the whole branch, then of course don't split anything.
396
+ isAllowed = this._checkAndSplitToAllowedPosition(node);
397
+ if (!isAllowed) {
398
+ this._handleDisallowedNode(node);
399
+ return;
400
+ }
401
+ }
402
+ // Add node to the current temporary DocumentFragment.
403
+ this._appendToFragment(node);
404
+ // Store the first and last nodes for easy access for merging with sibling nodes.
405
+ if (!this._firstNode) {
406
+ this._firstNode = node;
407
+ }
408
+ this._lastNode = node;
409
+ }
410
+ /**
411
+ * Inserts the temporary DocumentFragment into the model.
412
+ *
413
+ * @private
414
+ */
415
+ _insertPartialFragment() {
416
+ if (this._documentFragment.isEmpty) {
417
+ return;
418
+ }
419
+ const livePosition = LivePosition.fromPosition(this.position, 'toNext');
420
+ this._setAffectedBoundaries(this.position);
421
+ // If the very first node of the whole insertion process is inserted, insert it separately for OT reasons (undo).
422
+ // Note: there can be multiple calls to `_insertPartialFragment()` during one insertion process.
423
+ // Note: only the very first node can be merged so we have to do separate operation only for it.
424
+ if (this._documentFragment.getChild(0) == this._firstNode) {
425
+ this.writer.insert(this._firstNode, this.position);
426
+ // We must merge the first node just after inserting it to avoid problems with OT.
427
+ // (See: https://github.com/ckeditor/ckeditor5/pull/8773#issuecomment-760945652).
428
+ this._mergeOnLeft();
429
+ this.position = livePosition.toPosition();
430
+ }
431
+ // Insert the remaining nodes from document fragment.
432
+ if (!this._documentFragment.isEmpty) {
433
+ this.writer.insert(this._documentFragment, this.position);
434
+ }
435
+ this._documentFragmentPosition = this.writer.createPositionAt(this._documentFragment, 0);
436
+ this.position = livePosition.toPosition();
437
+ livePosition.detach();
438
+ }
439
+ /**
440
+ * @private
441
+ * @param {module:engine/model/element~Element} node The object element.
442
+ */
443
+ _handleObject(node) {
444
+ // Try finding it a place in the tree.
445
+ if (this._checkAndSplitToAllowedPosition(node)) {
446
+ this._appendToFragment(node);
447
+ }
448
+ // Try autoparagraphing.
449
+ else {
450
+ this._tryAutoparagraphing(node);
451
+ }
452
+ }
453
+ /**
454
+ * @private
455
+ * @param {module:engine/model/node~Node} node The disallowed node which needs to be handled.
456
+ */
457
+ _handleDisallowedNode(node) {
458
+ // If the node is an element, try inserting its children (strip the parent).
459
+ if (node.is('element')) {
460
+ this.handleNodes(node.getChildren());
461
+ }
462
+ // If text is not allowed, try autoparagraphing it.
463
+ else {
464
+ this._tryAutoparagraphing(node);
465
+ }
466
+ }
467
+ /**
468
+ * Append a node to the temporary DocumentFragment.
469
+ *
470
+ * @private
471
+ * @param {module:engine/model/node~Node} node The node to insert.
472
+ */
473
+ _appendToFragment(node) {
474
+ /* istanbul ignore if */
475
+ if (!this.schema.checkChild(this.position, node)) {
476
+ // Algorithm's correctness check. We should never end up here but it's good to know that we did.
477
+ // Note that it would often be a silent issue if we insert node in a place where it's not allowed.
478
+ /**
479
+ * Given node cannot be inserted on the given position.
480
+ *
481
+ * @error insertcontent-wrong-position
482
+ * @param {module:engine/model/node~Node} node Node to insert.
483
+ * @param {module:engine/model/position~Position} position Position to insert the node at.
484
+ */
485
+ throw new CKEditorError('insertcontent-wrong-position', this, { node, position: this.position });
486
+ }
487
+ this.writer.insert(node, this._documentFragmentPosition);
488
+ this._documentFragmentPosition = this._documentFragmentPosition.getShiftedBy(node.offsetSize);
489
+ // The last inserted object should be selected because we can't put a collapsed selection after it.
490
+ if (this.schema.isObject(node) && !this.schema.checkChild(this.position, '$text')) {
491
+ this._nodeToSelect = node;
492
+ }
493
+ else {
494
+ this._nodeToSelect = null;
495
+ }
496
+ this._filterAttributesOf.push(node);
497
+ }
498
+ /**
499
+ * Sets `_affectedStart` and `_affectedEnd` to the given `position`. Should be used before a change is done during insertion process to
500
+ * mark the affected range.
501
+ *
502
+ * This method is used before inserting a node or splitting a parent node. `_affectedStart` and `_affectedEnd` are also changed
503
+ * during merging, but the logic there is more complicated so it is left out of this function.
504
+ *
505
+ * @private
506
+ * @param {module:engine/model/position~Position} position
507
+ */
508
+ _setAffectedBoundaries(position) {
509
+ // Set affected boundaries stickiness so that those position will "expand" when something is inserted in between them:
510
+ // <paragraph>Foo][bar</paragraph> -> <paragraph>Foo]xx[bar</paragraph>
511
+ // This is why it cannot be a range but two separate positions.
512
+ if (!this._affectedStart) {
513
+ this._affectedStart = LivePosition.fromPosition(position, 'toPrevious');
514
+ }
515
+ // If `_affectedEnd` is before the new boundary position, expand `_affectedEnd`. This can happen if first inserted node was
516
+ // inserted into the parent but the next node is moved-out of that parent:
517
+ // (1) <paragraph>Foo][</paragraph> -> <paragraph>Foo]xx[</paragraph>
518
+ // (2) <paragraph>Foo]xx[</paragraph> -> <paragraph>Foo]xx</paragraph><widget></widget>[
519
+ if (!this._affectedEnd || this._affectedEnd.isBefore(position)) {
520
+ if (this._affectedEnd) {
521
+ this._affectedEnd.detach();
522
+ }
523
+ this._affectedEnd = LivePosition.fromPosition(position, 'toNext');
524
+ }
525
+ }
526
+ /**
527
+ * Merges the previous sibling of the first node if it should be merged.
528
+ *
529
+ * After the content was inserted we may try to merge it with its siblings.
530
+ * This should happen only if the selection was in those elements initially.
531
+ *
532
+ * @private
533
+ */
534
+ _mergeOnLeft() {
535
+ const node = this._firstNode;
536
+ if (!(node instanceof Element)) {
537
+ return;
538
+ }
539
+ if (!this._canMergeLeft(node)) {
540
+ return;
541
+ }
542
+ const mergePosLeft = LivePosition._createBefore(node);
543
+ mergePosLeft.stickiness = 'toNext';
544
+ const livePosition = LivePosition.fromPosition(this.position, 'toNext');
545
+ // If `_affectedStart` is sames as merge position, it means that the element "marked" by `_affectedStart` is going to be
546
+ // removed and its contents will be moved. This won't transform `LivePosition` so `_affectedStart` needs to be moved
547
+ // by hand to properly reflect affected range. (Due to `_affectedStart` and `_affectedEnd` stickiness, the "range" is
548
+ // shown as `][`).
549
+ //
550
+ // Example - insert `<paragraph>Abc</paragraph><paragraph>Xyz</paragraph>` at the end of `<paragraph>Foo^</paragraph>`:
551
+ //
552
+ // <paragraph>Foo</paragraph><paragraph>Bar</paragraph> -->
553
+ // <paragraph>Foo</paragraph>]<paragraph>Abc</paragraph><paragraph>Xyz</paragraph>[<paragraph>Bar</paragraph> -->
554
+ // <paragraph>Foo]Abc</paragraph><paragraph>Xyz</paragraph>[<paragraph>Bar</paragraph>
555
+ //
556
+ // Note, that if we are here then something must have been inserted, so `_affectedStart` and `_affectedEnd` have to be set.
557
+ if (this._affectedStart.isEqual(mergePosLeft)) {
558
+ this._affectedStart.detach();
559
+ this._affectedStart = LivePosition._createAt(mergePosLeft.nodeBefore, 'end', 'toPrevious');
560
+ }
561
+ // We need to update the references to the first and last nodes if they will be merged into the previous sibling node
562
+ // because the reference would point to the removed node.
563
+ //
564
+ // <p>A^A</p> + <p>X</p>
565
+ //
566
+ // <p>A</p>^<p>A</p>
567
+ // <p>A</p><p>X</p><p>A</p>
568
+ // <p>AX</p><p>A</p>
569
+ // <p>AXA</p>
570
+ if (this._firstNode === this._lastNode) {
571
+ this._firstNode = mergePosLeft.nodeBefore;
572
+ this._lastNode = mergePosLeft.nodeBefore;
573
+ }
574
+ this.writer.merge(mergePosLeft);
575
+ // If only one element (the merged one) is in the "affected range", also move the affected range end appropriately.
576
+ //
577
+ // Example - insert `<paragraph>Abc</paragraph>` at the of `<paragraph>Foo^</paragraph>`:
578
+ //
579
+ // <paragraph>Foo</paragraph><paragraph>Bar</paragraph> -->
580
+ // <paragraph>Foo</paragraph>]<paragraph>Abc</paragraph>[<paragraph>Bar</paragraph> -->
581
+ // <paragraph>Foo]Abc</paragraph>[<paragraph>Bar</paragraph> -->
582
+ // <paragraph>Foo]Abc[</paragraph><paragraph>Bar</paragraph>
583
+ if (mergePosLeft.isEqual(this._affectedEnd) && this._firstNode === this._lastNode) {
584
+ this._affectedEnd.detach();
585
+ this._affectedEnd = LivePosition._createAt(mergePosLeft.nodeBefore, 'end', 'toNext');
586
+ }
587
+ this.position = livePosition.toPosition();
588
+ livePosition.detach();
589
+ // After merge elements that were marked by _insert() to be filtered might be gone so
590
+ // we need to mark the new container.
591
+ this._filterAttributesOf.push(this.position.parent);
592
+ mergePosLeft.detach();
593
+ }
594
+ /**
595
+ * Merges the next sibling of the last node if it should be merged.
596
+ *
597
+ * After the content was inserted we may try to merge it with its siblings.
598
+ * This should happen only if the selection was in those elements initially.
599
+ *
600
+ * @private
601
+ */
602
+ _mergeOnRight() {
603
+ const node = this._lastNode;
604
+ if (!(node instanceof Element)) {
605
+ return;
606
+ }
607
+ if (!this._canMergeRight(node)) {
608
+ return;
609
+ }
610
+ const mergePosRight = LivePosition._createAfter(node);
611
+ mergePosRight.stickiness = 'toNext';
612
+ /* istanbul ignore if */
613
+ if (!this.position.isEqual(mergePosRight)) {
614
+ // Algorithm's correctness check. We should never end up here but it's good to know that we did.
615
+ // At this point the insertion position should be after the node we'll merge. If it isn't,
616
+ // it should need to be secured as in the left merge case.
617
+ /**
618
+ * An internal error occurred when merging inserted content with its siblings.
619
+ * The insertion position should equal the merge position.
620
+ *
621
+ * If you encountered this error, report it back to the CKEditor 5 team
622
+ * with as many details as possible regarding the content being inserted and the insertion position.
623
+ *
624
+ * @error insertcontent-invalid-insertion-position
625
+ */
626
+ throw new CKEditorError('insertcontent-invalid-insertion-position', this);
627
+ }
628
+ // Move the position to the previous node, so it isn't moved to the graveyard on merge.
629
+ // <p>x</p>[]<p>y</p> => <p>x[]</p><p>y</p>
630
+ this.position = Position._createAt(mergePosRight.nodeBefore, 'end');
631
+ // Explanation of setting position stickiness to `'toPrevious'`:
632
+ // OK: <p>xx[]</p> + <p>yy</p> => <p>xx[]yy</p> (when sticks to previous)
633
+ // NOK: <p>xx[]</p> + <p>yy</p> => <p>xxyy[]</p> (when sticks to next)
634
+ const livePosition = LivePosition.fromPosition(this.position, 'toPrevious');
635
+ // See comment in `_mergeOnLeft()` on moving `_affectedStart`.
636
+ if (this._affectedEnd.isEqual(mergePosRight)) {
637
+ this._affectedEnd.detach();
638
+ this._affectedEnd = LivePosition._createAt(mergePosRight.nodeBefore, 'end', 'toNext');
639
+ }
640
+ // We need to update the references to the first and last nodes if they will be merged into the previous sibling node
641
+ // because the reference would point to the removed node.
642
+ //
643
+ // <p>A^A</p> + <p>X</p>
644
+ //
645
+ // <p>A</p>^<p>A</p>
646
+ // <p>A</p><p>X</p><p>A</p>
647
+ // <p>AX</p><p>A</p>
648
+ // <p>AXA</p>
649
+ if (this._firstNode === this._lastNode) {
650
+ this._firstNode = mergePosRight.nodeBefore;
651
+ this._lastNode = mergePosRight.nodeBefore;
652
+ }
653
+ this.writer.merge(mergePosRight);
654
+ // See comment in `_mergeOnLeft()` on moving `_affectedStart`.
655
+ if (mergePosRight.getShiftedBy(-1).isEqual(this._affectedStart) && this._firstNode === this._lastNode) {
656
+ this._affectedStart.detach();
657
+ this._affectedStart = LivePosition._createAt(mergePosRight.nodeBefore, 0, 'toPrevious');
658
+ }
659
+ this.position = livePosition.toPosition();
660
+ livePosition.detach();
661
+ // After merge elements that were marked by _insert() to be filtered might be gone so
662
+ // we need to mark the new container.
663
+ this._filterAttributesOf.push(this.position.parent);
664
+ mergePosRight.detach();
665
+ }
666
+ /**
667
+ * Checks whether specified node can be merged with previous sibling element.
668
+ *
669
+ * @private
670
+ * @param {module:engine/model/node~Node} node The node which could potentially be merged.
671
+ * @returns {Boolean}
672
+ */
673
+ _canMergeLeft(node) {
674
+ const previousSibling = node.previousSibling;
675
+ return (previousSibling instanceof Element) &&
676
+ this.canMergeWith.has(previousSibling) &&
677
+ this.model.schema.checkMerge(previousSibling, node);
678
+ }
679
+ /**
680
+ * Checks whether specified node can be merged with next sibling element.
681
+ *
682
+ * @private
683
+ * @param {module:engine/model/node~Node} node The node which could potentially be merged.
684
+ * @returns {Boolean}
685
+ */
686
+ _canMergeRight(node) {
687
+ const nextSibling = node.nextSibling;
688
+ return (nextSibling instanceof Element) &&
689
+ this.canMergeWith.has(nextSibling) &&
690
+ this.model.schema.checkMerge(node, nextSibling);
691
+ }
692
+ /**
693
+ * Tries wrapping the node in a new paragraph and inserting it this way.
694
+ *
695
+ * @private
696
+ * @param {module:engine/model/node~Node} node The node which needs to be autoparagraphed.
697
+ */
698
+ _tryAutoparagraphing(node) {
699
+ const paragraph = this.writer.createElement('paragraph');
700
+ // Do not autoparagraph if the paragraph won't be allowed there,
701
+ // cause that would lead to an infinite loop. The paragraph would be rejected in
702
+ // the next _handleNode() call and we'd be here again.
703
+ if (this._getAllowedIn(this.position.parent, paragraph) && this.schema.checkChild(paragraph, node)) {
704
+ paragraph._appendChild(node);
705
+ this._handleNode(paragraph);
706
+ }
707
+ }
708
+ /**
709
+ * Checks if a node can be inserted in the given position or it would be accepted if a paragraph would be inserted.
710
+ * It also handles inserting the paragraph.
711
+ *
712
+ * @private
713
+ * @param {module:engine/model/node~Node} node The node.
714
+ * @returns {Boolean} Whether an allowed position was found.
715
+ * `false` is returned if the node isn't allowed at the current position or in auto paragraph, `true` if was.
716
+ */
717
+ _checkAndAutoParagraphToAllowedPosition(node) {
718
+ if (this.schema.checkChild(this.position.parent, node)) {
719
+ return true;
720
+ }
721
+ // Do not auto paragraph if the paragraph won't be allowed there,
722
+ // cause that would lead to an infinite loop. The paragraph would be rejected in
723
+ // the next _handleNode() call and we'd be here again.
724
+ if (!this.schema.checkChild(this.position.parent, 'paragraph') || !this.schema.checkChild('paragraph', node)) {
725
+ return false;
726
+ }
727
+ // Insert nodes collected in temporary DocumentFragment if the position parent needs change to process further nodes.
728
+ this._insertPartialFragment();
729
+ // Insert a paragraph and move insertion position to it.
730
+ const paragraph = this.writer.createElement('paragraph');
731
+ this.writer.insert(paragraph, this.position);
732
+ this._setAffectedBoundaries(this.position);
733
+ this._lastAutoParagraph = paragraph;
734
+ this.position = this.writer.createPositionAt(paragraph, 0);
735
+ return true;
736
+ }
737
+ /**
738
+ * @private
739
+ * @param {module:engine/model/node~Node} node
740
+ * @returns {Boolean} Whether an allowed position was found.
741
+ * `false` is returned if the node isn't allowed at any position up in the tree, `true` if was.
742
+ */
743
+ _checkAndSplitToAllowedPosition(node) {
744
+ const allowedIn = this._getAllowedIn(this.position.parent, node);
745
+ if (!allowedIn) {
746
+ return false;
747
+ }
748
+ // Insert nodes collected in temporary DocumentFragment if the position parent needs change to process further nodes.
749
+ if (allowedIn != this.position.parent) {
750
+ this._insertPartialFragment();
751
+ }
752
+ while (allowedIn != this.position.parent) {
753
+ if (this.position.isAtStart) {
754
+ // If insertion position is at the beginning of the parent, move it out instead of splitting.
755
+ // <p>^Foo</p> -> ^<p>Foo</p>
756
+ const parent = this.position.parent;
757
+ this.position = this.writer.createPositionBefore(parent);
758
+ // Special case – parent is empty (<p>^</p>).
759
+ //
760
+ // 1. parent.isEmpty
761
+ // We can remove the element after moving insertion position out of it.
762
+ //
763
+ // 2. parent.parent === allowedIn
764
+ // However parent should remain in place when allowed element is above limit element in document tree.
765
+ // For example there shouldn't be allowed to remove empty paragraph from tableCell, when is pasted
766
+ // content allowed in $root.
767
+ if (parent.isEmpty && parent.parent === allowedIn) {
768
+ this.writer.remove(parent);
769
+ }
770
+ }
771
+ else if (this.position.isAtEnd) {
772
+ // If insertion position is at the end of the parent, move it out instead of splitting.
773
+ // <p>Foo^</p> -> <p>Foo</p>^
774
+ this.position = this.writer.createPositionAfter(this.position.parent);
775
+ }
776
+ else {
777
+ const tempPos = this.writer.createPositionAfter(this.position.parent);
778
+ this._setAffectedBoundaries(this.position);
779
+ this.writer.split(this.position);
780
+ this.position = tempPos;
781
+ this.canMergeWith.add(this.position.nodeAfter);
782
+ }
783
+ }
784
+ return true;
785
+ }
786
+ /**
787
+ * Gets the element in which the given node is allowed. It checks the passed element and all its ancestors.
788
+ *
789
+ * @private
790
+ * @param {module:engine/model/element~Element} contextElement The element in which context the node should be checked.
791
+ * @param {module:engine/model/node~Node} childNode The node to check.
792
+ * @returns {module:engine/model/element~Element|null}
793
+ */
794
+ _getAllowedIn(contextElement, childNode) {
795
+ if (this.schema.checkChild(contextElement, childNode)) {
796
+ return contextElement;
797
+ }
798
+ // If the child wasn't allowed in the context element and the element is a limit there's no point in
799
+ // checking any further towards the root. This is it: the limit is unsplittable and there's nothing
800
+ // we can do about it. Without this check, the algorithm will analyze parent of the limit and may create
801
+ // an illusion of the child being allowed. There's no way to insert it down there, though. It results in
802
+ // infinite loops.
803
+ if (this.schema.isLimit(contextElement)) {
804
+ return null;
805
+ }
806
+ return this._getAllowedIn(contextElement.parent, childNode);
807
+ }
824
808
  }