@ckeditor/ckeditor5-engine 39.0.2 → 40.0.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 (241) hide show
  1. package/package.json +2 -2
  2. package/src/controller/datacontroller.d.ts +334 -334
  3. package/src/controller/datacontroller.js +481 -481
  4. package/src/controller/editingcontroller.d.ts +98 -98
  5. package/src/controller/editingcontroller.js +191 -191
  6. package/src/conversion/conversion.d.ts +478 -478
  7. package/src/conversion/conversion.js +601 -601
  8. package/src/conversion/conversionhelpers.d.ts +26 -26
  9. package/src/conversion/conversionhelpers.js +32 -32
  10. package/src/conversion/downcastdispatcher.d.ts +562 -562
  11. package/src/conversion/downcastdispatcher.js +547 -547
  12. package/src/conversion/downcasthelpers.d.ts +1226 -1226
  13. package/src/conversion/downcasthelpers.js +2183 -2183
  14. package/src/conversion/mapper.d.ts +503 -503
  15. package/src/conversion/mapper.js +536 -536
  16. package/src/conversion/modelconsumable.d.ts +201 -201
  17. package/src/conversion/modelconsumable.js +333 -333
  18. package/src/conversion/upcastdispatcher.d.ts +492 -492
  19. package/src/conversion/upcastdispatcher.js +460 -460
  20. package/src/conversion/upcasthelpers.d.ts +499 -499
  21. package/src/conversion/upcasthelpers.js +950 -950
  22. package/src/conversion/viewconsumable.d.ts +369 -369
  23. package/src/conversion/viewconsumable.js +532 -532
  24. package/src/dataprocessor/basichtmlwriter.d.ts +18 -18
  25. package/src/dataprocessor/basichtmlwriter.js +19 -19
  26. package/src/dataprocessor/dataprocessor.d.ts +61 -61
  27. package/src/dataprocessor/dataprocessor.js +5 -5
  28. package/src/dataprocessor/htmldataprocessor.d.ts +76 -76
  29. package/src/dataprocessor/htmldataprocessor.js +96 -96
  30. package/src/dataprocessor/htmlwriter.d.ts +16 -16
  31. package/src/dataprocessor/htmlwriter.js +5 -5
  32. package/src/dataprocessor/xmldataprocessor.d.ts +90 -90
  33. package/src/dataprocessor/xmldataprocessor.js +108 -108
  34. package/src/dev-utils/model.d.ts +124 -124
  35. package/src/dev-utils/model.js +395 -395
  36. package/src/dev-utils/operationreplayer.d.ts +51 -51
  37. package/src/dev-utils/operationreplayer.js +112 -112
  38. package/src/dev-utils/utils.d.ts +37 -37
  39. package/src/dev-utils/utils.js +73 -73
  40. package/src/dev-utils/view.d.ts +319 -319
  41. package/src/dev-utils/view.js +967 -967
  42. package/src/index.d.ts +114 -114
  43. package/src/index.js +78 -78
  44. package/src/model/batch.d.ts +106 -106
  45. package/src/model/batch.js +96 -96
  46. package/src/model/differ.d.ts +387 -387
  47. package/src/model/differ.js +1149 -1149
  48. package/src/model/document.d.ts +272 -272
  49. package/src/model/document.js +361 -361
  50. package/src/model/documentfragment.d.ts +200 -200
  51. package/src/model/documentfragment.js +306 -306
  52. package/src/model/documentselection.d.ts +420 -420
  53. package/src/model/documentselection.js +993 -993
  54. package/src/model/element.d.ts +165 -165
  55. package/src/model/element.js +281 -281
  56. package/src/model/history.d.ts +114 -114
  57. package/src/model/history.js +207 -207
  58. package/src/model/item.d.ts +14 -14
  59. package/src/model/item.js +5 -5
  60. package/src/model/liveposition.d.ts +77 -77
  61. package/src/model/liveposition.js +93 -93
  62. package/src/model/liverange.d.ts +102 -102
  63. package/src/model/liverange.js +120 -120
  64. package/src/model/markercollection.d.ts +335 -335
  65. package/src/model/markercollection.js +403 -403
  66. package/src/model/model.d.ts +919 -919
  67. package/src/model/model.js +842 -842
  68. package/src/model/node.d.ts +256 -256
  69. package/src/model/node.js +375 -375
  70. package/src/model/nodelist.d.ts +91 -91
  71. package/src/model/nodelist.js +163 -163
  72. package/src/model/operation/attributeoperation.d.ts +103 -103
  73. package/src/model/operation/attributeoperation.js +148 -148
  74. package/src/model/operation/detachoperation.d.ts +60 -60
  75. package/src/model/operation/detachoperation.js +77 -77
  76. package/src/model/operation/insertoperation.d.ts +90 -90
  77. package/src/model/operation/insertoperation.js +135 -135
  78. package/src/model/operation/markeroperation.d.ts +91 -91
  79. package/src/model/operation/markeroperation.js +107 -107
  80. package/src/model/operation/mergeoperation.d.ts +100 -100
  81. package/src/model/operation/mergeoperation.js +167 -167
  82. package/src/model/operation/moveoperation.d.ts +96 -96
  83. package/src/model/operation/moveoperation.js +164 -164
  84. package/src/model/operation/nooperation.d.ts +38 -38
  85. package/src/model/operation/nooperation.js +48 -48
  86. package/src/model/operation/operation.d.ts +96 -96
  87. package/src/model/operation/operation.js +62 -62
  88. package/src/model/operation/operationfactory.d.ts +18 -18
  89. package/src/model/operation/operationfactory.js +44 -44
  90. package/src/model/operation/renameoperation.d.ts +83 -83
  91. package/src/model/operation/renameoperation.js +115 -115
  92. package/src/model/operation/rootattributeoperation.d.ts +98 -98
  93. package/src/model/operation/rootattributeoperation.js +155 -155
  94. package/src/model/operation/rootoperation.d.ts +76 -76
  95. package/src/model/operation/rootoperation.js +90 -90
  96. package/src/model/operation/splitoperation.d.ts +109 -109
  97. package/src/model/operation/splitoperation.js +194 -194
  98. package/src/model/operation/transform.d.ts +100 -100
  99. package/src/model/operation/transform.js +1985 -1985
  100. package/src/model/operation/utils.d.ts +71 -71
  101. package/src/model/operation/utils.js +213 -213
  102. package/src/model/position.d.ts +539 -539
  103. package/src/model/position.js +979 -979
  104. package/src/model/range.d.ts +458 -458
  105. package/src/model/range.js +875 -875
  106. package/src/model/rootelement.d.ts +60 -60
  107. package/src/model/rootelement.js +74 -74
  108. package/src/model/schema.d.ts +1186 -1186
  109. package/src/model/schema.js +1242 -1242
  110. package/src/model/selection.d.ts +482 -482
  111. package/src/model/selection.js +789 -789
  112. package/src/model/text.d.ts +66 -66
  113. package/src/model/text.js +85 -85
  114. package/src/model/textproxy.d.ts +144 -144
  115. package/src/model/textproxy.js +189 -189
  116. package/src/model/treewalker.d.ts +186 -186
  117. package/src/model/treewalker.js +244 -244
  118. package/src/model/typecheckable.d.ts +285 -285
  119. package/src/model/typecheckable.js +16 -16
  120. package/src/model/utils/autoparagraphing.d.ts +37 -37
  121. package/src/model/utils/autoparagraphing.js +63 -63
  122. package/src/model/utils/deletecontent.d.ts +58 -58
  123. package/src/model/utils/deletecontent.js +488 -488
  124. package/src/model/utils/findoptimalinsertionrange.d.ts +32 -32
  125. package/src/model/utils/findoptimalinsertionrange.js +57 -57
  126. package/src/model/utils/getselectedcontent.d.ts +30 -30
  127. package/src/model/utils/getselectedcontent.js +125 -125
  128. package/src/model/utils/insertcontent.d.ts +46 -46
  129. package/src/model/utils/insertcontent.js +705 -705
  130. package/src/model/utils/insertobject.d.ts +44 -44
  131. package/src/model/utils/insertobject.js +139 -139
  132. package/src/model/utils/modifyselection.d.ts +48 -48
  133. package/src/model/utils/modifyselection.js +186 -186
  134. package/src/model/utils/selection-post-fixer.d.ts +74 -74
  135. package/src/model/utils/selection-post-fixer.js +260 -260
  136. package/src/model/writer.d.ts +851 -851
  137. package/src/model/writer.js +1306 -1306
  138. package/src/view/attributeelement.d.ts +108 -108
  139. package/src/view/attributeelement.js +184 -184
  140. package/src/view/containerelement.d.ts +49 -49
  141. package/src/view/containerelement.js +80 -80
  142. package/src/view/datatransfer.d.ts +79 -79
  143. package/src/view/datatransfer.js +98 -98
  144. package/src/view/document.d.ts +184 -184
  145. package/src/view/document.js +120 -120
  146. package/src/view/documentfragment.d.ts +149 -149
  147. package/src/view/documentfragment.js +228 -228
  148. package/src/view/documentselection.d.ts +306 -306
  149. package/src/view/documentselection.js +256 -256
  150. package/src/view/domconverter.d.ts +640 -640
  151. package/src/view/domconverter.js +1450 -1425
  152. package/src/view/downcastwriter.d.ts +996 -996
  153. package/src/view/downcastwriter.js +1696 -1696
  154. package/src/view/editableelement.d.ts +62 -62
  155. package/src/view/editableelement.js +62 -62
  156. package/src/view/element.d.ts +468 -468
  157. package/src/view/element.js +724 -724
  158. package/src/view/elementdefinition.d.ts +87 -87
  159. package/src/view/elementdefinition.js +5 -5
  160. package/src/view/emptyelement.d.ts +41 -41
  161. package/src/view/emptyelement.js +73 -73
  162. package/src/view/filler.d.ts +111 -111
  163. package/src/view/filler.js +150 -150
  164. package/src/view/item.d.ts +14 -14
  165. package/src/view/item.js +5 -5
  166. package/src/view/matcher.d.ts +486 -486
  167. package/src/view/matcher.js +507 -507
  168. package/src/view/node.d.ts +163 -163
  169. package/src/view/node.js +228 -228
  170. package/src/view/observer/arrowkeysobserver.d.ts +45 -45
  171. package/src/view/observer/arrowkeysobserver.js +40 -40
  172. package/src/view/observer/bubblingemittermixin.d.ts +166 -166
  173. package/src/view/observer/bubblingemittermixin.js +172 -172
  174. package/src/view/observer/bubblingeventinfo.d.ts +47 -47
  175. package/src/view/observer/bubblingeventinfo.js +37 -37
  176. package/src/view/observer/clickobserver.d.ts +43 -43
  177. package/src/view/observer/clickobserver.js +29 -29
  178. package/src/view/observer/compositionobserver.d.ts +82 -82
  179. package/src/view/observer/compositionobserver.js +60 -60
  180. package/src/view/observer/domeventdata.d.ts +50 -50
  181. package/src/view/observer/domeventdata.js +47 -47
  182. package/src/view/observer/domeventobserver.d.ts +73 -73
  183. package/src/view/observer/domeventobserver.js +79 -79
  184. package/src/view/observer/fakeselectionobserver.d.ts +47 -47
  185. package/src/view/observer/fakeselectionobserver.js +91 -91
  186. package/src/view/observer/focusobserver.d.ts +82 -82
  187. package/src/view/observer/focusobserver.js +86 -86
  188. package/src/view/observer/inputobserver.d.ts +86 -86
  189. package/src/view/observer/inputobserver.js +164 -164
  190. package/src/view/observer/keyobserver.d.ts +66 -66
  191. package/src/view/observer/keyobserver.js +39 -39
  192. package/src/view/observer/mouseobserver.d.ts +89 -89
  193. package/src/view/observer/mouseobserver.js +29 -29
  194. package/src/view/observer/mutationobserver.d.ts +86 -86
  195. package/src/view/observer/mutationobserver.js +206 -206
  196. package/src/view/observer/observer.d.ts +89 -89
  197. package/src/view/observer/observer.js +84 -84
  198. package/src/view/observer/selectionobserver.d.ts +148 -148
  199. package/src/view/observer/selectionobserver.js +202 -202
  200. package/src/view/observer/tabobserver.d.ts +46 -46
  201. package/src/view/observer/tabobserver.js +42 -42
  202. package/src/view/placeholder.d.ts +96 -96
  203. package/src/view/placeholder.js +267 -267
  204. package/src/view/position.d.ts +189 -189
  205. package/src/view/position.js +324 -324
  206. package/src/view/range.d.ts +279 -279
  207. package/src/view/range.js +430 -430
  208. package/src/view/rawelement.d.ts +73 -73
  209. package/src/view/rawelement.js +105 -105
  210. package/src/view/renderer.d.ts +265 -265
  211. package/src/view/renderer.js +999 -999
  212. package/src/view/rooteditableelement.d.ts +41 -41
  213. package/src/view/rooteditableelement.js +69 -69
  214. package/src/view/selection.d.ts +375 -375
  215. package/src/view/selection.js +559 -559
  216. package/src/view/styles/background.d.ts +33 -33
  217. package/src/view/styles/background.js +74 -74
  218. package/src/view/styles/border.d.ts +43 -43
  219. package/src/view/styles/border.js +316 -316
  220. package/src/view/styles/margin.d.ts +29 -29
  221. package/src/view/styles/margin.js +34 -34
  222. package/src/view/styles/padding.d.ts +29 -29
  223. package/src/view/styles/padding.js +34 -34
  224. package/src/view/styles/utils.d.ts +93 -93
  225. package/src/view/styles/utils.js +219 -219
  226. package/src/view/stylesmap.d.ts +675 -675
  227. package/src/view/stylesmap.js +766 -766
  228. package/src/view/text.d.ts +74 -74
  229. package/src/view/text.js +93 -93
  230. package/src/view/textproxy.d.ts +97 -97
  231. package/src/view/textproxy.js +124 -124
  232. package/src/view/treewalker.d.ts +195 -195
  233. package/src/view/treewalker.js +327 -327
  234. package/src/view/typecheckable.d.ts +448 -448
  235. package/src/view/typecheckable.js +19 -19
  236. package/src/view/uielement.d.ts +96 -96
  237. package/src/view/uielement.js +182 -182
  238. package/src/view/upcastwriter.d.ts +417 -417
  239. package/src/view/upcastwriter.js +359 -359
  240. package/src/view/view.d.ts +487 -487
  241. 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
+ }