@ckeditor/ckeditor5-engine 34.2.0 → 35.1.0

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