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