@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,1306 +1,1306 @@
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/writer
7
- */
8
- import AttributeOperation from './operation/attributeoperation';
9
- import DetachOperation from './operation/detachoperation';
10
- import InsertOperation from './operation/insertoperation';
11
- import MarkerOperation from './operation/markeroperation';
12
- import MergeOperation from './operation/mergeoperation';
13
- import MoveOperation from './operation/moveoperation';
14
- import RenameOperation from './operation/renameoperation';
15
- import RootAttributeOperation from './operation/rootattributeoperation';
16
- import RootOperation from './operation/rootoperation';
17
- import SplitOperation from './operation/splitoperation';
18
- import DocumentFragment from './documentfragment';
19
- import DocumentSelection from './documentselection';
20
- import Element from './element';
21
- import Position from './position';
22
- import Range from './range';
23
- import RootElement from './rootelement';
24
- import Text from './text';
25
- import { CKEditorError, logWarning, toMap } from '@ckeditor/ckeditor5-utils';
26
- /**
27
- * The model can only be modified by using the writer. It should be used whenever you want to create a node, modify
28
- * child nodes, attributes or text, set the selection's position and its attributes.
29
- *
30
- * The instance of the writer is only available in the {@link module:engine/model/model~Model#change `change()`} or
31
- * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`}.
32
- *
33
- * ```ts
34
- * model.change( writer => {
35
- * writer.insertText( 'foo', paragraph, 'end' );
36
- * } );
37
- * ```
38
- *
39
- * Note that the writer should never be stored and used outside of the `change()` and
40
- * `enqueueChange()` blocks.
41
- *
42
- * Note that writer's methods do not check the {@link module:engine/model/schema~Schema}. It is possible
43
- * to create incorrect model structures by using the writer. Read more about in
44
- * {@glink framework/deep-dive/schema#who-checks-the-schema "Who checks the schema?"}.
45
- *
46
- * @see module:engine/model/model~Model#change
47
- * @see module:engine/model/model~Model#enqueueChange
48
- */
49
- export default class Writer {
50
- /**
51
- * Creates a writer instance.
52
- *
53
- * **Note:** It is not recommended to use it directly. Use {@link module:engine/model/model~Model#change `Model#change()`} or
54
- * {@link module:engine/model/model~Model#enqueueChange `Model#enqueueChange()`} instead.
55
- *
56
- * @internal
57
- */
58
- constructor(model, batch) {
59
- this.model = model;
60
- this.batch = batch;
61
- }
62
- /**
63
- * Creates a new {@link module:engine/model/text~Text text node}.
64
- *
65
- * ```ts
66
- * writer.createText( 'foo' );
67
- * writer.createText( 'foo', { bold: true } );
68
- * ```
69
- *
70
- * @param data Text data.
71
- * @param attributes Text attributes.
72
- * @returns {module:engine/model/text~Text} Created text node.
73
- */
74
- createText(data, attributes) {
75
- return new Text(data, attributes);
76
- }
77
- /**
78
- * Creates a new {@link module:engine/model/element~Element element}.
79
- *
80
- * ```ts
81
- * writer.createElement( 'paragraph' );
82
- * writer.createElement( 'paragraph', { alignment: 'center' } );
83
- * ```
84
- *
85
- * @param name Name of the element.
86
- * @param attributes Elements attributes.
87
- * @returns Created element.
88
- */
89
- createElement(name, attributes) {
90
- return new Element(name, attributes);
91
- }
92
- /**
93
- * Creates a new {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
94
- *
95
- * @returns Created document fragment.
96
- */
97
- createDocumentFragment() {
98
- return new DocumentFragment();
99
- }
100
- /**
101
- * Creates a copy of the element and returns it. Created element has the same name and attributes as the original element.
102
- * If clone is deep, the original element's children are also cloned. If not, then empty element is returned.
103
- *
104
- * @param element The element to clone.
105
- * @param deep If set to `true` clones element and all its children recursively. When set to `false`,
106
- * element will be cloned without any child.
107
- */
108
- cloneElement(element, deep = true) {
109
- return element._clone(deep);
110
- }
111
- /**
112
- * Inserts item on given position.
113
- *
114
- * ```ts
115
- * const paragraph = writer.createElement( 'paragraph' );
116
- * writer.insert( paragraph, position );
117
- * ```
118
- *
119
- * Instead of using position you can use parent and offset:
120
- *
121
- * ```ts
122
- * const text = writer.createText( 'foo' );
123
- * writer.insert( text, paragraph, 5 );
124
- * ```
125
- *
126
- * You can also use `end` instead of the offset to insert at the end:
127
- *
128
- * ```ts
129
- * const text = writer.createText( 'foo' );
130
- * writer.insert( text, paragraph, 'end' );
131
- * ```
132
- *
133
- * Or insert before or after another element:
134
- *
135
- * ```ts
136
- * const paragraph = writer.createElement( 'paragraph' );
137
- * writer.insert( paragraph, anotherParagraph, 'after' );
138
- * ```
139
- *
140
- * These parameters works the same way as {@link #createPositionAt `writer.createPositionAt()`}.
141
- *
142
- * Note that if the item already has parent it will be removed from the previous parent.
143
- *
144
- * Note that you cannot re-insert a node from a document to a different document or a document fragment. In this case,
145
- * `model-writer-insert-forbidden-move` is thrown.
146
- *
147
- * If you want to move {@link module:engine/model/range~Range range} instead of an
148
- * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move `Writer#move()`}.
149
- *
150
- * **Note:** For a paste-like content insertion mechanism see
151
- * {@link module:engine/model/model~Model#insertContent `model.insertContent()`}.
152
- *
153
- * @param item Item or document fragment to insert.
154
- * @param offset Offset or one of the flags. Used only when second parameter is a {@link module:engine/model/item~Item model item}.
155
- */
156
- insert(item, itemOrPosition, offset = 0) {
157
- this._assertWriterUsedCorrectly();
158
- if (item instanceof Text && item.data == '') {
159
- return;
160
- }
161
- const position = Position._createAt(itemOrPosition, offset);
162
- // If item has a parent already.
163
- if (item.parent) {
164
- // We need to check if item is going to be inserted within the same document.
165
- if (isSameTree(item.root, position.root)) {
166
- // If it's we just need to move it.
167
- this.move(Range._createOn(item), position);
168
- return;
169
- }
170
- // If it isn't the same root.
171
- else {
172
- if (item.root.document) {
173
- /**
174
- * Cannot move a node from a document to a different tree.
175
- * It is forbidden to move a node that was already in a document outside of it.
176
- *
177
- * @error model-writer-insert-forbidden-move
178
- */
179
- throw new CKEditorError('model-writer-insert-forbidden-move', this);
180
- }
181
- else {
182
- // Move between two different document fragments or from document fragment to a document is possible.
183
- // In that case, remove the item from it's original parent.
184
- this.remove(item);
185
- }
186
- }
187
- }
188
- const version = position.root.document ? position.root.document.version : null;
189
- const insert = new InsertOperation(position, item, version);
190
- if (item instanceof Text) {
191
- insert.shouldReceiveAttributes = true;
192
- }
193
- this.batch.addOperation(insert);
194
- this.model.applyOperation(insert);
195
- // When element is a DocumentFragment we need to move its markers to Document#markers.
196
- if (item instanceof DocumentFragment) {
197
- for (const [markerName, markerRange] of item.markers) {
198
- // We need to migrate marker range from DocumentFragment to Document.
199
- const rangeRootPosition = Position._createAt(markerRange.root, 0);
200
- const range = new Range(markerRange.start._getCombined(rangeRootPosition, position), markerRange.end._getCombined(rangeRootPosition, position));
201
- const options = { range, usingOperation: true, affectsData: true };
202
- if (this.model.markers.has(markerName)) {
203
- this.updateMarker(markerName, options);
204
- }
205
- else {
206
- this.addMarker(markerName, options);
207
- }
208
- }
209
- }
210
- }
211
- insertText(text, attributes, // Too complicated when not using `any`.
212
- itemOrPosition, // Too complicated when not using `any`.
213
- offset // Too complicated when not using `any`.
214
- ) {
215
- if (attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position) {
216
- this.insert(this.createText(text), attributes, itemOrPosition);
217
- }
218
- else {
219
- this.insert(this.createText(text, attributes), itemOrPosition, offset);
220
- }
221
- }
222
- insertElement(name, attributes, // Too complicated when not using `any`.
223
- itemOrPositionOrOffset, // Too complicated when not using `any`.
224
- offset // Too complicated when not using `any`.
225
- ) {
226
- if (attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position) {
227
- this.insert(this.createElement(name), attributes, itemOrPositionOrOffset);
228
- }
229
- else {
230
- this.insert(this.createElement(name, attributes), itemOrPositionOrOffset, offset);
231
- }
232
- }
233
- /**
234
- * Inserts item at the end of the given parent.
235
- *
236
- * ```ts
237
- * const paragraph = writer.createElement( 'paragraph' );
238
- * writer.append( paragraph, root );
239
- * ```
240
- *
241
- * Note that if the item already has parent it will be removed from the previous parent.
242
- *
243
- * If you want to move {@link module:engine/model/range~Range range} instead of an
244
- * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move `Writer#move()`}.
245
- *
246
- * @param item Item or document fragment to insert.
247
- */
248
- append(item, parent) {
249
- this.insert(item, parent, 'end');
250
- }
251
- appendText(text, attributes, parent) {
252
- if (attributes instanceof DocumentFragment || attributes instanceof Element) {
253
- this.insert(this.createText(text), attributes, 'end');
254
- }
255
- else {
256
- this.insert(this.createText(text, attributes), parent, 'end');
257
- }
258
- }
259
- appendElement(name, attributes, parent) {
260
- if (attributes instanceof DocumentFragment || attributes instanceof Element) {
261
- this.insert(this.createElement(name), attributes, 'end');
262
- }
263
- else {
264
- this.insert(this.createElement(name, attributes), parent, 'end');
265
- }
266
- }
267
- /**
268
- * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
269
- * or on a {@link module:engine/model/range~Range range}.
270
- *
271
- * @param key Attribute key.
272
- * @param value Attribute new value.
273
- * @param itemOrRange Model item or range on which the attribute will be set.
274
- */
275
- setAttribute(key, value, itemOrRange) {
276
- this._assertWriterUsedCorrectly();
277
- if (itemOrRange instanceof Range) {
278
- const ranges = itemOrRange.getMinimalFlatRanges();
279
- for (const range of ranges) {
280
- setAttributeOnRange(this, key, value, range);
281
- }
282
- }
283
- else {
284
- setAttributeOnItem(this, key, value, itemOrRange);
285
- }
286
- }
287
- /**
288
- * Sets values of attributes on a {@link module:engine/model/item~Item model item}
289
- * or on a {@link module:engine/model/range~Range range}.
290
- *
291
- * ```ts
292
- * writer.setAttributes( {
293
- * bold: true,
294
- * italic: true
295
- * }, range );
296
- * ```
297
- *
298
- * @param attributes Attributes keys and values.
299
- * @param itemOrRange Model item or range on which the attributes will be set.
300
- */
301
- setAttributes(attributes, itemOrRange) {
302
- for (const [key, val] of toMap(attributes)) {
303
- this.setAttribute(key, val, itemOrRange);
304
- }
305
- }
306
- /**
307
- * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
308
- * or from a {@link module:engine/model/range~Range range}.
309
- *
310
- * @param key Attribute key.
311
- * @param itemOrRange Model item or range from which the attribute will be removed.
312
- */
313
- removeAttribute(key, itemOrRange) {
314
- this._assertWriterUsedCorrectly();
315
- if (itemOrRange instanceof Range) {
316
- const ranges = itemOrRange.getMinimalFlatRanges();
317
- for (const range of ranges) {
318
- setAttributeOnRange(this, key, null, range);
319
- }
320
- }
321
- else {
322
- setAttributeOnItem(this, key, null, itemOrRange);
323
- }
324
- }
325
- /**
326
- * Removes all attributes from all elements in the range or from the given item.
327
- *
328
- * @param itemOrRange Model item or range from which all attributes will be removed.
329
- */
330
- clearAttributes(itemOrRange) {
331
- this._assertWriterUsedCorrectly();
332
- const removeAttributesFromItem = (item) => {
333
- for (const attribute of item.getAttributeKeys()) {
334
- this.removeAttribute(attribute, item);
335
- }
336
- };
337
- if (!(itemOrRange instanceof Range)) {
338
- removeAttributesFromItem(itemOrRange);
339
- }
340
- else {
341
- for (const item of itemOrRange.getItems()) {
342
- removeAttributesFromItem(item);
343
- }
344
- }
345
- }
346
- /**
347
- * Moves all items in the source range to the target position.
348
- *
349
- * ```ts
350
- * writer.move( sourceRange, targetPosition );
351
- * ```
352
- *
353
- * Instead of the target position you can use parent and offset or define that range should be moved to the end
354
- * or before or after chosen item:
355
- *
356
- * ```ts
357
- * // Moves all items in the range to the paragraph at offset 5:
358
- * writer.move( sourceRange, paragraph, 5 );
359
- * // Moves all items in the range to the end of a blockquote:
360
- * writer.move( sourceRange, blockquote, 'end' );
361
- * // Moves all items in the range to a position after an image:
362
- * writer.move( sourceRange, image, 'after' );
363
- * ```
364
- *
365
- * These parameters work the same way as {@link #createPositionAt `writer.createPositionAt()`}.
366
- *
367
- * Note that items can be moved only within the same tree. It means that you can move items within the same root
368
- * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
369
- * but you can not move items from document fragment to the document or from one detached element to another. Use
370
- * {@link module:engine/model/writer~Writer#insert} in such cases.
371
- *
372
- * @param range Source range.
373
- * @param offset Offset or one of the flags. Used only when second parameter is a {@link module:engine/model/item~Item model item}.
374
- */
375
- move(range, itemOrPosition, offset) {
376
- this._assertWriterUsedCorrectly();
377
- if (!(range instanceof Range)) {
378
- /**
379
- * Invalid range to move.
380
- *
381
- * @error writer-move-invalid-range
382
- */
383
- throw new CKEditorError('writer-move-invalid-range', this);
384
- }
385
- if (!range.isFlat) {
386
- /**
387
- * Range to move is not flat.
388
- *
389
- * @error writer-move-range-not-flat
390
- */
391
- throw new CKEditorError('writer-move-range-not-flat', this);
392
- }
393
- const position = Position._createAt(itemOrPosition, offset);
394
- // Do not move anything if the move target is same as moved range start.
395
- if (position.isEqual(range.start)) {
396
- return;
397
- }
398
- // If part of the marker is removed, create additional marker operation for undo purposes.
399
- this._addOperationForAffectedMarkers('move', range);
400
- if (!isSameTree(range.root, position.root)) {
401
- /**
402
- * Range is going to be moved within not the same document. Please use
403
- * {@link module:engine/model/writer~Writer#insert insert} instead.
404
- *
405
- * @error writer-move-different-document
406
- */
407
- throw new CKEditorError('writer-move-different-document', this);
408
- }
409
- const version = range.root.document ? range.root.document.version : null;
410
- const operation = new MoveOperation(range.start, range.end.offset - range.start.offset, position, version);
411
- this.batch.addOperation(operation);
412
- this.model.applyOperation(operation);
413
- }
414
- /**
415
- * Removes given model {@link module:engine/model/item~Item item} or {@link module:engine/model/range~Range range}.
416
- *
417
- * @param itemOrRange Model item or range to remove.
418
- */
419
- remove(itemOrRange) {
420
- this._assertWriterUsedCorrectly();
421
- const rangeToRemove = itemOrRange instanceof Range ? itemOrRange : Range._createOn(itemOrRange);
422
- const ranges = rangeToRemove.getMinimalFlatRanges().reverse();
423
- for (const flat of ranges) {
424
- // If part of the marker is removed, create additional marker operation for undo purposes.
425
- this._addOperationForAffectedMarkers('move', flat);
426
- applyRemoveOperation(flat.start, flat.end.offset - flat.start.offset, this.batch, this.model);
427
- }
428
- }
429
- /**
430
- * Merges two siblings at the given position.
431
- *
432
- * Node before and after the position have to be an element. Otherwise `writer-merge-no-element-before` or
433
- * `writer-merge-no-element-after` error will be thrown.
434
- *
435
- * @param position Position between merged elements.
436
- */
437
- merge(position) {
438
- this._assertWriterUsedCorrectly();
439
- const nodeBefore = position.nodeBefore;
440
- const nodeAfter = position.nodeAfter;
441
- // If part of the marker is removed, create additional marker operation for undo purposes.
442
- this._addOperationForAffectedMarkers('merge', position);
443
- if (!(nodeBefore instanceof Element)) {
444
- /**
445
- * Node before merge position must be an element.
446
- *
447
- * @error writer-merge-no-element-before
448
- */
449
- throw new CKEditorError('writer-merge-no-element-before', this);
450
- }
451
- if (!(nodeAfter instanceof Element)) {
452
- /**
453
- * Node after merge position must be an element.
454
- *
455
- * @error writer-merge-no-element-after
456
- */
457
- throw new CKEditorError('writer-merge-no-element-after', this);
458
- }
459
- if (!position.root.document) {
460
- this._mergeDetached(position);
461
- }
462
- else {
463
- this._merge(position);
464
- }
465
- }
466
- /**
467
- * Shortcut for {@link module:engine/model/model~Model#createPositionFromPath `Model#createPositionFromPath()`}.
468
- *
469
- * @param root Root of the position.
470
- * @param path Position path. See {@link module:engine/model/position~Position#path}.
471
- * @param stickiness Position stickiness. See {@link module:engine/model/position~PositionStickiness}.
472
- */
473
- createPositionFromPath(root, path, stickiness) {
474
- return this.model.createPositionFromPath(root, path, stickiness);
475
- }
476
- /**
477
- * Shortcut for {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}.
478
- *
479
- * @param offset Offset or one of the flags. Used only when first parameter is a {@link module:engine/model/item~Item model item}.
480
- */
481
- createPositionAt(itemOrPosition, offset) {
482
- return this.model.createPositionAt(itemOrPosition, offset);
483
- }
484
- /**
485
- * Shortcut for {@link module:engine/model/model~Model#createPositionAfter `Model#createPositionAfter()`}.
486
- *
487
- * @param item Item after which the position should be placed.
488
- */
489
- createPositionAfter(item) {
490
- return this.model.createPositionAfter(item);
491
- }
492
- /**
493
- * Shortcut for {@link module:engine/model/model~Model#createPositionBefore `Model#createPositionBefore()`}.
494
- *
495
- * @param item Item after which the position should be placed.
496
- */
497
- createPositionBefore(item) {
498
- return this.model.createPositionBefore(item);
499
- }
500
- /**
501
- * Shortcut for {@link module:engine/model/model~Model#createRange `Model#createRange()`}.
502
- *
503
- * @param start Start position.
504
- * @param end End position. If not set, range will be collapsed at `start` position.
505
- */
506
- createRange(start, end) {
507
- return this.model.createRange(start, end);
508
- }
509
- /**
510
- * Shortcut for {@link module:engine/model/model~Model#createRangeIn `Model#createRangeIn()`}.
511
- *
512
- * @param element Element which is a parent for the range.
513
- */
514
- createRangeIn(element) {
515
- return this.model.createRangeIn(element);
516
- }
517
- /**
518
- * Shortcut for {@link module:engine/model/model~Model#createRangeOn `Model#createRangeOn()`}.
519
- *
520
- * @param element Element which is a parent for the range.
521
- */
522
- createRangeOn(element) {
523
- return this.model.createRangeOn(element);
524
- }
525
- createSelection(...args) {
526
- return this.model.createSelection(...args);
527
- }
528
- /**
529
- * Performs merge action in a detached tree.
530
- *
531
- * @param position Position between merged elements.
532
- */
533
- _mergeDetached(position) {
534
- const nodeBefore = position.nodeBefore;
535
- const nodeAfter = position.nodeAfter;
536
- this.move(Range._createIn(nodeAfter), Position._createAt(nodeBefore, 'end'));
537
- this.remove(nodeAfter);
538
- }
539
- /**
540
- * Performs merge action in a non-detached tree.
541
- *
542
- * @param position Position between merged elements.
543
- */
544
- _merge(position) {
545
- const targetPosition = Position._createAt(position.nodeBefore, 'end');
546
- const sourcePosition = Position._createAt(position.nodeAfter, 0);
547
- const graveyard = position.root.document.graveyard;
548
- const graveyardPosition = new Position(graveyard, [0]);
549
- const version = position.root.document.version;
550
- const merge = new MergeOperation(sourcePosition, position.nodeAfter.maxOffset, targetPosition, graveyardPosition, version);
551
- this.batch.addOperation(merge);
552
- this.model.applyOperation(merge);
553
- }
554
- /**
555
- * Renames the given element.
556
- *
557
- * @param element The element to rename.
558
- * @param newName New element name.
559
- */
560
- rename(element, newName) {
561
- this._assertWriterUsedCorrectly();
562
- if (!(element instanceof Element)) {
563
- /**
564
- * Trying to rename an object which is not an instance of Element.
565
- *
566
- * @error writer-rename-not-element-instance
567
- */
568
- throw new CKEditorError('writer-rename-not-element-instance', this);
569
- }
570
- const version = element.root.document ? element.root.document.version : null;
571
- const renameOperation = new RenameOperation(Position._createBefore(element), element.name, newName, version);
572
- this.batch.addOperation(renameOperation);
573
- this.model.applyOperation(renameOperation);
574
- }
575
- /**
576
- * Splits elements starting from the given position and going to the top of the model tree as long as given
577
- * `limitElement` is reached. When `limitElement` is not defined then only the parent of the given position will be split.
578
- *
579
- * The element needs to have a parent. It cannot be a root element nor a document fragment.
580
- * The `writer-split-element-no-parent` error will be thrown if you try to split an element with no parent.
581
- *
582
- * @param position Position of split.
583
- * @param limitElement Stop splitting when this element will be reached.
584
- * @returns Split result with properties:
585
- * * `position` - Position between split elements.
586
- * * `range` - Range that stars from the end of the first split element and ends at the beginning of the first copy element.
587
- */
588
- split(position, limitElement) {
589
- this._assertWriterUsedCorrectly();
590
- let splitElement = position.parent;
591
- if (!splitElement.parent) {
592
- /**
593
- * Element with no parent can not be split.
594
- *
595
- * @error writer-split-element-no-parent
596
- */
597
- throw new CKEditorError('writer-split-element-no-parent', this);
598
- }
599
- // When limit element is not defined lets set splitElement parent as limit.
600
- if (!limitElement) {
601
- limitElement = splitElement.parent;
602
- }
603
- if (!position.parent.getAncestors({ includeSelf: true }).includes(limitElement)) {
604
- /**
605
- * Limit element is not a position ancestor.
606
- *
607
- * @error writer-split-invalid-limit-element
608
- */
609
- throw new CKEditorError('writer-split-invalid-limit-element', this);
610
- }
611
- // We need to cache elements that will be created as a result of the first split because
612
- // we need to create a range from the end of the first split element to the beginning of the
613
- // first copy element. This should be handled by LiveRange but it doesn't work on detached nodes.
614
- let firstSplitElement;
615
- let firstCopyElement;
616
- do {
617
- const version = splitElement.root.document ? splitElement.root.document.version : null;
618
- const howMany = splitElement.maxOffset - position.offset;
619
- const insertionPosition = SplitOperation.getInsertionPosition(position);
620
- const split = new SplitOperation(position, howMany, insertionPosition, null, version);
621
- this.batch.addOperation(split);
622
- this.model.applyOperation(split);
623
- // Cache result of the first split.
624
- if (!firstSplitElement && !firstCopyElement) {
625
- firstSplitElement = splitElement;
626
- firstCopyElement = position.parent.nextSibling;
627
- }
628
- position = this.createPositionAfter(position.parent);
629
- splitElement = position.parent;
630
- } while (splitElement !== limitElement);
631
- return {
632
- position,
633
- range: new Range(Position._createAt(firstSplitElement, 'end'), Position._createAt(firstCopyElement, 0))
634
- };
635
- }
636
- /**
637
- * Wraps the given range with the given element or with a new element (if a string was passed).
638
- *
639
- * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat `Range#isFlat`}).
640
- * If not, an error will be thrown.
641
- *
642
- * @param range Range to wrap.
643
- * @param elementOrString Element or name of element to wrap the range with.
644
- */
645
- wrap(range, elementOrString) {
646
- this._assertWriterUsedCorrectly();
647
- if (!range.isFlat) {
648
- /**
649
- * Range to wrap is not flat.
650
- *
651
- * @error writer-wrap-range-not-flat
652
- */
653
- throw new CKEditorError('writer-wrap-range-not-flat', this);
654
- }
655
- const element = elementOrString instanceof Element ? elementOrString : new Element(elementOrString);
656
- if (element.childCount > 0) {
657
- /**
658
- * Element to wrap with is not empty.
659
- *
660
- * @error writer-wrap-element-not-empty
661
- */
662
- throw new CKEditorError('writer-wrap-element-not-empty', this);
663
- }
664
- if (element.parent !== null) {
665
- /**
666
- * Element to wrap with is already attached to a tree model.
667
- *
668
- * @error writer-wrap-element-attached
669
- */
670
- throw new CKEditorError('writer-wrap-element-attached', this);
671
- }
672
- this.insert(element, range.start);
673
- // Shift the range-to-wrap because we just inserted an element before that range.
674
- const shiftedRange = new Range(range.start.getShiftedBy(1), range.end.getShiftedBy(1));
675
- this.move(shiftedRange, Position._createAt(element, 0));
676
- }
677
- /**
678
- * Unwraps children of the given element – all its children are moved before it and then the element is removed.
679
- * Throws error if you try to unwrap an element which does not have a parent.
680
- *
681
- * @param element Element to unwrap.
682
- */
683
- unwrap(element) {
684
- this._assertWriterUsedCorrectly();
685
- if (element.parent === null) {
686
- /**
687
- * Trying to unwrap an element which has no parent.
688
- *
689
- * @error writer-unwrap-element-no-parent
690
- */
691
- throw new CKEditorError('writer-unwrap-element-no-parent', this);
692
- }
693
- this.move(Range._createIn(element), this.createPositionAfter(element));
694
- this.remove(element);
695
- }
696
- /**
697
- * Adds a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
698
- * changes in the document and updates its range automatically, when model tree changes.
699
- *
700
- * As the first parameter you can set marker name.
701
- *
702
- * The required `options.usingOperation` parameter lets you decide if the marker should be managed by operations or not. See
703
- * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
704
- * markers managed by operations and not-managed by operations.
705
- *
706
- * The `options.affectsData` parameter, which defaults to `false`, allows you to define if a marker affects the data. It should be
707
- * `true` when the marker change changes the data returned by the
708
- * {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`} method.
709
- * When set to `true` it fires the {@link module:engine/model/document~Document#event:change:data `change:data`} event.
710
- * When set to `false` it fires the {@link module:engine/model/document~Document#event:change `change`} event.
711
- *
712
- * Create marker directly base on marker's name:
713
- *
714
- * ```ts
715
- * addMarker( markerName, { range, usingOperation: false } );
716
- * ```
717
- *
718
- * Create marker using operation:
719
- *
720
- * ```ts
721
- * addMarker( markerName, { range, usingOperation: true } );
722
- * ```
723
- *
724
- * Create marker that affects the editor data:
725
- *
726
- * ```ts
727
- * addMarker( markerName, { range, usingOperation: false, affectsData: true } );
728
- * ```
729
- *
730
- * Note: For efficiency reasons, it's best to create and keep as little markers as possible.
731
- *
732
- * @see module:engine/model/markercollection~Marker
733
- * @param name Name of a marker to create - must be unique.
734
- * @param options.usingOperation Flag indicating that the marker should be added by MarkerOperation.
735
- * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
736
- * @param options.range Marker range.
737
- * @param options.affectsData Flag indicating that the marker changes the editor data.
738
- * @returns Marker that was set.
739
- */
740
- addMarker(name, options) {
741
- this._assertWriterUsedCorrectly();
742
- if (!options || typeof options.usingOperation != 'boolean') {
743
- /**
744
- * The `options.usingOperation` parameter is required when adding a new marker.
745
- *
746
- * @error writer-addmarker-no-usingoperation
747
- */
748
- throw new CKEditorError('writer-addmarker-no-usingoperation', this);
749
- }
750
- const usingOperation = options.usingOperation;
751
- const range = options.range;
752
- const affectsData = options.affectsData === undefined ? false : options.affectsData;
753
- if (this.model.markers.has(name)) {
754
- /**
755
- * Marker with provided name already exists.
756
- *
757
- * @error writer-addmarker-marker-exists
758
- */
759
- throw new CKEditorError('writer-addmarker-marker-exists', this);
760
- }
761
- if (!range) {
762
- /**
763
- * Range parameter is required when adding a new marker.
764
- *
765
- * @error writer-addmarker-no-range
766
- */
767
- throw new CKEditorError('writer-addmarker-no-range', this);
768
- }
769
- if (!usingOperation) {
770
- return this.model.markers._set(name, range, usingOperation, affectsData);
771
- }
772
- applyMarkerOperation(this, name, null, range, affectsData);
773
- return this.model.markers.get(name);
774
- }
775
- /**
776
- * Adds, updates or refreshes a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
777
- * changes in the document and updates its range automatically, when model tree changes. Still, it is possible to change the
778
- * marker's range directly using this method.
779
- *
780
- * As the first parameter you can set marker name or instance. If none of them is provided, new marker, with a unique
781
- * name is created and returned.
782
- *
783
- * **Note**: If you want to change the {@link module:engine/view/element~Element view element} of the marker while its data in the model
784
- * remains the same, use the dedicated {@link module:engine/controller/editingcontroller~EditingController#reconvertMarker} method.
785
- *
786
- * The `options.usingOperation` parameter lets you change if the marker should be managed by operations or not. See
787
- * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
788
- * markers managed by operations and not-managed by operations. It is possible to change this option for an existing marker.
789
- *
790
- * The `options.affectsData` parameter, which defaults to `false`, allows you to define if a marker affects the data. It should be
791
- * `true` when the marker change changes the data returned by
792
- * the {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`} method.
793
- * When set to `true` it fires the {@link module:engine/model/document~Document#event:change:data `change:data`} event.
794
- * When set to `false` it fires the {@link module:engine/model/document~Document#event:change `change`} event.
795
- *
796
- * Update marker directly base on marker's name:
797
- *
798
- * ```ts
799
- * updateMarker( markerName, { range } );
800
- * ```
801
- *
802
- * Update marker using operation:
803
- *
804
- * ```ts
805
- * updateMarker( marker, { range, usingOperation: true } );
806
- * updateMarker( markerName, { range, usingOperation: true } );
807
- * ```
808
- *
809
- * Change marker's option (start using operations to manage it):
810
- *
811
- * ```ts
812
- * updateMarker( marker, { usingOperation: true } );
813
- * ```
814
- *
815
- * Change marker's option (inform the engine, that the marker does not affect the data anymore):
816
- *
817
- * ```ts
818
- * updateMarker( markerName, { affectsData: false } );
819
- * ```
820
- *
821
- * @see module:engine/model/markercollection~Marker
822
- * @param markerOrName Name of a marker to update, or a marker instance.
823
- * @param options If options object is not defined then marker will be refreshed by triggering
824
- * downcast conversion for this marker with the same data.
825
- * @param options.range Marker range to update.
826
- * @param options.usingOperation Flag indicated whether the marker should be added by MarkerOperation.
827
- * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
828
- * @param options.affectsData Flag indicating that the marker changes the editor data.
829
- */
830
- updateMarker(markerOrName, options) {
831
- this._assertWriterUsedCorrectly();
832
- const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
833
- const currentMarker = this.model.markers.get(markerName);
834
- if (!currentMarker) {
835
- /**
836
- * Marker with provided name does not exist and will not be updated.
837
- *
838
- * @error writer-updatemarker-marker-not-exists
839
- */
840
- throw new CKEditorError('writer-updatemarker-marker-not-exists', this);
841
- }
842
- if (!options) {
843
- /**
844
- * The usage of `writer.updateMarker()` only to reconvert (refresh) a
845
- * {@link module:engine/model/markercollection~Marker model marker} was deprecated and may not work in the future.
846
- * Please update your code to use
847
- * {@link module:engine/controller/editingcontroller~EditingController#reconvertMarker `editor.editing.reconvertMarker()`}
848
- * instead.
849
- *
850
- * @error writer-updatemarker-reconvert-using-editingcontroller
851
- * @param markerName The name of the updated marker.
852
- */
853
- logWarning('writer-updatemarker-reconvert-using-editingcontroller', { markerName });
854
- this.model.markers._refresh(currentMarker);
855
- return;
856
- }
857
- const hasUsingOperationDefined = typeof options.usingOperation == 'boolean';
858
- const affectsDataDefined = typeof options.affectsData == 'boolean';
859
- // Use previously defined marker's affectsData if the property is not provided.
860
- const affectsData = affectsDataDefined ? options.affectsData : currentMarker.affectsData;
861
- if (!hasUsingOperationDefined && !options.range && !affectsDataDefined) {
862
- /**
863
- * One of the options is required - provide range, usingOperations or affectsData.
864
- *
865
- * @error writer-updatemarker-wrong-options
866
- */
867
- throw new CKEditorError('writer-updatemarker-wrong-options', this);
868
- }
869
- const currentRange = currentMarker.getRange();
870
- const updatedRange = options.range ? options.range : currentRange;
871
- if (hasUsingOperationDefined && options.usingOperation !== currentMarker.managedUsingOperations) {
872
- // The marker type is changed so it's necessary to create proper operations.
873
- if (options.usingOperation) {
874
- // If marker changes to a managed one treat this as synchronizing existing marker.
875
- // Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
876
- applyMarkerOperation(this, markerName, null, updatedRange, affectsData);
877
- }
878
- else {
879
- // If marker changes to a marker that do not use operations then we need to create additional operation
880
- // that removes that marker first.
881
- applyMarkerOperation(this, markerName, currentRange, null, affectsData);
882
- // Although not managed the marker itself should stay in model and its range should be preserver or changed to passed range.
883
- this.model.markers._set(markerName, updatedRange, undefined, affectsData);
884
- }
885
- return;
886
- }
887
- // Marker's type doesn't change so update it accordingly.
888
- if (currentMarker.managedUsingOperations) {
889
- applyMarkerOperation(this, markerName, currentRange, updatedRange, affectsData);
890
- }
891
- else {
892
- this.model.markers._set(markerName, updatedRange, undefined, affectsData);
893
- }
894
- }
895
- /**
896
- * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
897
- * The marker is removed accordingly to how it has been created, so if the marker was created using operation,
898
- * it will be destroyed using operation.
899
- *
900
- * @param markerOrName Marker or marker name to remove.
901
- */
902
- removeMarker(markerOrName) {
903
- this._assertWriterUsedCorrectly();
904
- const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
905
- if (!this.model.markers.has(name)) {
906
- /**
907
- * Trying to remove marker which does not exist.
908
- *
909
- * @error writer-removemarker-no-marker
910
- */
911
- throw new CKEditorError('writer-removemarker-no-marker', this);
912
- }
913
- const marker = this.model.markers.get(name);
914
- if (!marker.managedUsingOperations) {
915
- this.model.markers._remove(name);
916
- return;
917
- }
918
- const oldRange = marker.getRange();
919
- applyMarkerOperation(this, name, oldRange, null, marker.affectsData);
920
- }
921
- /**
922
- * Adds a new root to the document (or re-attaches a {@link #detachRoot detached root}).
923
- *
924
- * Throws an error, if trying to add a root that is already added and attached.
925
- *
926
- * @param rootName Name of the added root.
927
- * @param elementName The element name. Defaults to `'$root'` which also has some basic schema defined
928
- * (e.g. `$block` elements are allowed inside the `$root`). Make sure to define a proper schema if you use a different name.
929
- * @returns The added root element.
930
- */
931
- addRoot(rootName, elementName = '$root') {
932
- this._assertWriterUsedCorrectly();
933
- const root = this.model.document.getRoot(rootName);
934
- if (root && root.isAttached()) {
935
- /**
936
- * Root with provided name already exists and is attached.
937
- *
938
- * @error writer-addroot-root-exists
939
- */
940
- throw new CKEditorError('writer-addroot-root-exists', this);
941
- }
942
- const document = this.model.document;
943
- const operation = new RootOperation(rootName, elementName, true, document, document.version);
944
- this.batch.addOperation(operation);
945
- this.model.applyOperation(operation);
946
- return this.model.document.getRoot(rootName);
947
- }
948
- /**
949
- * Detaches the root from the document.
950
- *
951
- * All content and markers are removed from the root upon detaching. New content and new markers cannot be added to the root, as long
952
- * as it is detached.
953
- *
954
- * A root cannot be fully removed from the document, it can be only detached. A root is permanently removed only after you
955
- * re-initialize the editor and do not specify the root in the initial data.
956
- *
957
- * A detached root can be re-attached using {@link #addRoot}.
958
- *
959
- * Throws an error if the root does not exist or the root is already detached.
960
- *
961
- * @param rootOrName Name of the detached root.
962
- */
963
- detachRoot(rootOrName) {
964
- this._assertWriterUsedCorrectly();
965
- const root = typeof rootOrName == 'string' ? this.model.document.getRoot(rootOrName) : rootOrName;
966
- if (!root || !root.isAttached()) {
967
- /**
968
- * Root with provided name does not exist or is already detached.
969
- *
970
- * @error writer-detachroot-no-root
971
- */
972
- throw new CKEditorError('writer-detachroot-no-root', this);
973
- }
974
- // First, remove all markers from the root. It is better to do it before removing stuff for undo purposes.
975
- // However, looking through all the markers may not be the best performance wise. But there's no better solution for now.
976
- for (const marker of this.model.markers) {
977
- if (marker.getRange().root === root) {
978
- this.removeMarker(marker);
979
- }
980
- }
981
- // Remove all attributes from the root.
982
- for (const key of root.getAttributeKeys()) {
983
- this.removeAttribute(key, root);
984
- }
985
- // Remove all contents of the root.
986
- this.remove(this.createRangeIn(root));
987
- // Finally, detach the root.
988
- const document = this.model.document;
989
- const operation = new RootOperation(root.rootName, root.name, false, document, document.version);
990
- this.batch.addOperation(operation);
991
- this.model.applyOperation(operation);
992
- }
993
- setSelection(...args) {
994
- this._assertWriterUsedCorrectly();
995
- this.model.document.selection._setTo(...args);
996
- }
997
- /**
998
- * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
999
- *
1000
- * The location can be specified in the same form as
1001
- * {@link #createPositionAt `writer.createPositionAt()`} parameters.
1002
- *
1003
- * @param itemOrPosition
1004
- * @param offset Offset or one of the flags. Used only when first parameter is a {@link module:engine/model/item~Item model item}.
1005
- */
1006
- setSelectionFocus(itemOrPosition, offset) {
1007
- this._assertWriterUsedCorrectly();
1008
- this.model.document.selection._setFocus(itemOrPosition, offset);
1009
- }
1010
- setSelectionAttribute(keyOrObjectOrIterable, value) {
1011
- this._assertWriterUsedCorrectly();
1012
- if (typeof keyOrObjectOrIterable === 'string') {
1013
- this._setSelectionAttribute(keyOrObjectOrIterable, value);
1014
- }
1015
- else {
1016
- for (const [key, value] of toMap(keyOrObjectOrIterable)) {
1017
- this._setSelectionAttribute(key, value);
1018
- }
1019
- }
1020
- }
1021
- /**
1022
- * Removes attribute(s) with given key(s) from the selection.
1023
- *
1024
- * Remove one attribute:
1025
- *
1026
- * ```ts
1027
- * writer.removeSelectionAttribute( 'italic' );
1028
- * ```
1029
- *
1030
- * Remove multiple attributes:
1031
- *
1032
- * ```ts
1033
- * writer.removeSelectionAttribute( [ 'italic', 'bold' ] );
1034
- * ```
1035
- *
1036
- * @param keyOrIterableOfKeys Key of the attribute to remove or an iterable of attribute keys to remove.
1037
- */
1038
- removeSelectionAttribute(keyOrIterableOfKeys) {
1039
- this._assertWriterUsedCorrectly();
1040
- if (typeof keyOrIterableOfKeys === 'string') {
1041
- this._removeSelectionAttribute(keyOrIterableOfKeys);
1042
- }
1043
- else {
1044
- for (const key of keyOrIterableOfKeys) {
1045
- this._removeSelectionAttribute(key);
1046
- }
1047
- }
1048
- }
1049
- /**
1050
- * Temporarily changes the {@link module:engine/model/documentselection~DocumentSelection#isGravityOverridden gravity}
1051
- * of the selection from left to right.
1052
- *
1053
- * The gravity defines from which direction the selection inherits its attributes. If it's the default left gravity,
1054
- * then the selection (after being moved by the user) inherits attributes from its left-hand side.
1055
- * This method allows to temporarily override this behavior by forcing the gravity to the right.
1056
- *
1057
- * For the following model fragment:
1058
- *
1059
- * ```xml
1060
- * <$text bold="true" linkHref="url">bar[]</$text><$text bold="true">biz</$text>
1061
- * ```
1062
- *
1063
- * * Default gravity: selection will have the `bold` and `linkHref` attributes.
1064
- * * Overridden gravity: selection will have `bold` attribute.
1065
- *
1066
- * **Note**: It returns an unique identifier which is required to restore the gravity. It guarantees the symmetry
1067
- * of the process.
1068
- *
1069
- * @returns The unique id which allows restoring the gravity.
1070
- */
1071
- overrideSelectionGravity() {
1072
- return this.model.document.selection._overrideGravity();
1073
- }
1074
- /**
1075
- * Restores {@link ~Writer#overrideSelectionGravity} gravity to default.
1076
- *
1077
- * Restoring the gravity is only possible using the unique identifier returned by
1078
- * {@link ~Writer#overrideSelectionGravity}. Note that the gravity remains overridden as long as won't be restored
1079
- * the same number of times it was overridden.
1080
- *
1081
- * @param uid The unique id returned by {@link ~Writer#overrideSelectionGravity}.
1082
- */
1083
- restoreSelectionGravity(uid) {
1084
- this.model.document.selection._restoreGravity(uid);
1085
- }
1086
- /**
1087
- * @param key Key of the attribute to remove.
1088
- * @param value Attribute value.
1089
- */
1090
- _setSelectionAttribute(key, value) {
1091
- const selection = this.model.document.selection;
1092
- // Store attribute in parent element if the selection is collapsed in an empty node.
1093
- if (selection.isCollapsed && selection.anchor.parent.isEmpty) {
1094
- const storeKey = DocumentSelection._getStoreAttributeKey(key);
1095
- this.setAttribute(storeKey, value, selection.anchor.parent);
1096
- }
1097
- selection._setAttribute(key, value);
1098
- }
1099
- /**
1100
- * @param key Key of the attribute to remove.
1101
- */
1102
- _removeSelectionAttribute(key) {
1103
- const selection = this.model.document.selection;
1104
- // Remove stored attribute from parent element if the selection is collapsed in an empty node.
1105
- if (selection.isCollapsed && selection.anchor.parent.isEmpty) {
1106
- const storeKey = DocumentSelection._getStoreAttributeKey(key);
1107
- this.removeAttribute(storeKey, selection.anchor.parent);
1108
- }
1109
- selection._removeAttribute(key);
1110
- }
1111
- /**
1112
- * Throws `writer-detached-writer-tries-to-modify-model` error when the writer is used outside of the `change()` block.
1113
- */
1114
- _assertWriterUsedCorrectly() {
1115
- /**
1116
- * Trying to use a writer outside a {@link module:engine/model/model~Model#change `change()`} or
1117
- * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`} blocks.
1118
- *
1119
- * The writer can only be used inside these blocks which ensures that the model
1120
- * can only be changed during such "sessions".
1121
- *
1122
- * @error writer-incorrect-use
1123
- */
1124
- if (this.model._currentWriter !== this) {
1125
- throw new CKEditorError('writer-incorrect-use', this);
1126
- }
1127
- }
1128
- /**
1129
- * For given action `type` and `positionOrRange` where the action happens, this function finds all affected markers
1130
- * and applies a marker operation with the new marker range equal to the current range. Thanks to this, the marker range
1131
- * can be later correctly processed during undo.
1132
- *
1133
- * @param type Writer action type.
1134
- * @param positionOrRange Position or range where the writer action happens.
1135
- */
1136
- _addOperationForAffectedMarkers(type, positionOrRange) {
1137
- for (const marker of this.model.markers) {
1138
- if (!marker.managedUsingOperations) {
1139
- continue;
1140
- }
1141
- const markerRange = marker.getRange();
1142
- let isAffected = false;
1143
- if (type === 'move') {
1144
- const range = positionOrRange;
1145
- isAffected =
1146
- range.containsPosition(markerRange.start) ||
1147
- range.start.isEqual(markerRange.start) ||
1148
- range.containsPosition(markerRange.end) ||
1149
- range.end.isEqual(markerRange.end);
1150
- }
1151
- else {
1152
- // if type === 'merge'.
1153
- const position = positionOrRange;
1154
- const elementBefore = position.nodeBefore;
1155
- const elementAfter = position.nodeAfter;
1156
- // Start: <p>Foo[</p><p>Bar]</p>
1157
- // After merge: <p>Foo[Bar]</p>
1158
- // After undoing split: <p>Foo</p><p>[Bar]</p> <-- incorrect, needs remembering for undo.
1159
- //
1160
- const affectedInLeftElement = markerRange.start.parent == elementBefore && markerRange.start.isAtEnd;
1161
- // Start: <p>[Foo</p><p>]Bar</p>
1162
- // After merge: <p>[Foo]Bar</p>
1163
- // After undoing split: <p>[Foo]</p><p>Bar</p> <-- incorrect, needs remembering for undo.
1164
- //
1165
- const affectedInRightElement = markerRange.end.parent == elementAfter && markerRange.end.offset == 0;
1166
- // Start: <p>[Foo</p>]<p>Bar</p>
1167
- // After merge: <p>[Foo]Bar</p>
1168
- // After undoing split: <p>[Foo]</p><p>Bar</p> <-- incorrect, needs remembering for undo.
1169
- //
1170
- const affectedAfterLeftElement = markerRange.end.nodeAfter == elementAfter;
1171
- // Start: <p>Foo</p>[<p>Bar]</p>
1172
- // After merge: <p>Foo[Bar]</p>
1173
- // After undoing split: <p>Foo</p><p>[Bar]</p> <-- incorrect, needs remembering for undo.
1174
- //
1175
- const affectedBeforeRightElement = markerRange.start.nodeAfter == elementAfter;
1176
- isAffected = affectedInLeftElement || affectedInRightElement || affectedAfterLeftElement || affectedBeforeRightElement;
1177
- }
1178
- if (isAffected) {
1179
- this.updateMarker(marker.name, { range: markerRange });
1180
- }
1181
- }
1182
- }
1183
- }
1184
- /**
1185
- * Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.
1186
- *
1187
- * Because attribute operation needs to have the same attribute value on the whole range, this function splits
1188
- * the range into smaller parts.
1189
- *
1190
- * Given `range` must be flat.
1191
- */
1192
- function setAttributeOnRange(writer, key, value, range) {
1193
- const model = writer.model;
1194
- const doc = model.document;
1195
- // Position of the last split, the beginning of the new range.
1196
- let lastSplitPosition = range.start;
1197
- // Currently position in the scanning range. Because we need value after the position, it is not a current
1198
- // position of the iterator but the previous one (we need to iterate one more time to get the value after).
1199
- let position;
1200
- // Value before the currently position.
1201
- let valueBefore;
1202
- // Value after the currently position.
1203
- let valueAfter;
1204
- for (const val of range.getWalker({ shallow: true })) {
1205
- valueAfter = val.item.getAttribute(key);
1206
- // At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
1207
- // because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
1208
- if (position && valueBefore != valueAfter) {
1209
- // if valueBefore == value there is nothing to change, so we add operation only if these values are different.
1210
- if (valueBefore != value) {
1211
- addOperation();
1212
- }
1213
- lastSplitPosition = position;
1214
- }
1215
- position = val.nextPosition;
1216
- valueBefore = valueAfter;
1217
- }
1218
- // Because position in the loop is not the iterator position (see let position comment), the last position in
1219
- // the while loop will be last but one position in the range. We need to check the last position manually.
1220
- if (position instanceof Position && position != lastSplitPosition && valueBefore != value) {
1221
- addOperation();
1222
- }
1223
- function addOperation() {
1224
- const range = new Range(lastSplitPosition, position);
1225
- const version = range.root.document ? doc.version : null;
1226
- const operation = new AttributeOperation(range, key, valueBefore, value, version);
1227
- writer.batch.addOperation(operation);
1228
- model.applyOperation(operation);
1229
- }
1230
- }
1231
- /**
1232
- * Sets given attribute to the given node. When attribute value is null then attribute will be removed.
1233
- */
1234
- function setAttributeOnItem(writer, key, value, item) {
1235
- const model = writer.model;
1236
- const doc = model.document;
1237
- const previousValue = item.getAttribute(key);
1238
- let range, operation;
1239
- if (previousValue != value) {
1240
- const isRootChanged = item.root === item;
1241
- if (isRootChanged) {
1242
- // If we change attributes of root element, we have to use `RootAttributeOperation`.
1243
- const version = item.document ? doc.version : null;
1244
- operation = new RootAttributeOperation(item, key, previousValue, value, version);
1245
- }
1246
- else {
1247
- range = new Range(Position._createBefore(item), writer.createPositionAfter(item));
1248
- const version = range.root.document ? doc.version : null;
1249
- operation = new AttributeOperation(range, key, previousValue, value, version);
1250
- }
1251
- writer.batch.addOperation(operation);
1252
- model.applyOperation(operation);
1253
- }
1254
- }
1255
- /**
1256
- * Creates and applies marker operation to {@link module:engine/model/operation/operation~Operation operation}.
1257
- */
1258
- function applyMarkerOperation(writer, name, oldRange, newRange, affectsData) {
1259
- const model = writer.model;
1260
- const doc = model.document;
1261
- const operation = new MarkerOperation(name, oldRange, newRange, model.markers, !!affectsData, doc.version);
1262
- writer.batch.addOperation(operation);
1263
- model.applyOperation(operation);
1264
- }
1265
- /**
1266
- * Creates `MoveOperation` or `DetachOperation` that removes `howMany` nodes starting from `position`.
1267
- * The operation will be applied on given model instance and added to given operation instance.
1268
- *
1269
- * @param position Position from which nodes are removed.
1270
- * @param howMany Number of nodes to remove.
1271
- * @param batch Batch to which the operation will be added.
1272
- * @param model Model instance on which operation will be applied.
1273
- */
1274
- function applyRemoveOperation(position, howMany, batch, model) {
1275
- let operation;
1276
- if (position.root.document) {
1277
- const doc = model.document;
1278
- const graveyardPosition = new Position(doc.graveyard, [0]);
1279
- operation = new MoveOperation(position, howMany, graveyardPosition, doc.version);
1280
- }
1281
- else {
1282
- operation = new DetachOperation(position, howMany);
1283
- }
1284
- batch.addOperation(operation);
1285
- model.applyOperation(operation);
1286
- }
1287
- /**
1288
- * Returns `true` if both root elements are the same element or both are documents root elements.
1289
- *
1290
- * Elements in the same tree can be moved (for instance you can move element form one documents root to another, or
1291
- * within the same document fragment), but when element supposed to be moved from document fragment to the document, or
1292
- * to another document it should be removed and inserted to avoid problems with OT. This is because features like undo or
1293
- * collaboration may track changes on the document but ignore changes on detached fragments and should not get
1294
- * unexpected `move` operation.
1295
- */
1296
- function isSameTree(rootA, rootB) {
1297
- // If it is the same root this is the same tree.
1298
- if (rootA === rootB) {
1299
- return true;
1300
- }
1301
- // If both roots are documents root it is operation within the document what we still treat as the same tree.
1302
- if (rootA instanceof RootElement && rootB instanceof RootElement) {
1303
- return true;
1304
- }
1305
- return false;
1306
- }
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/writer
7
+ */
8
+ import AttributeOperation from './operation/attributeoperation';
9
+ import DetachOperation from './operation/detachoperation';
10
+ import InsertOperation from './operation/insertoperation';
11
+ import MarkerOperation from './operation/markeroperation';
12
+ import MergeOperation from './operation/mergeoperation';
13
+ import MoveOperation from './operation/moveoperation';
14
+ import RenameOperation from './operation/renameoperation';
15
+ import RootAttributeOperation from './operation/rootattributeoperation';
16
+ import RootOperation from './operation/rootoperation';
17
+ import SplitOperation from './operation/splitoperation';
18
+ import DocumentFragment from './documentfragment';
19
+ import DocumentSelection from './documentselection';
20
+ import Element from './element';
21
+ import Position from './position';
22
+ import Range from './range';
23
+ import RootElement from './rootelement';
24
+ import Text from './text';
25
+ import { CKEditorError, logWarning, toMap } from '@ckeditor/ckeditor5-utils';
26
+ /**
27
+ * The model can only be modified by using the writer. It should be used whenever you want to create a node, modify
28
+ * child nodes, attributes or text, set the selection's position and its attributes.
29
+ *
30
+ * The instance of the writer is only available in the {@link module:engine/model/model~Model#change `change()`} or
31
+ * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`}.
32
+ *
33
+ * ```ts
34
+ * model.change( writer => {
35
+ * writer.insertText( 'foo', paragraph, 'end' );
36
+ * } );
37
+ * ```
38
+ *
39
+ * Note that the writer should never be stored and used outside of the `change()` and
40
+ * `enqueueChange()` blocks.
41
+ *
42
+ * Note that writer's methods do not check the {@link module:engine/model/schema~Schema}. It is possible
43
+ * to create incorrect model structures by using the writer. Read more about in
44
+ * {@glink framework/deep-dive/schema#who-checks-the-schema "Who checks the schema?"}.
45
+ *
46
+ * @see module:engine/model/model~Model#change
47
+ * @see module:engine/model/model~Model#enqueueChange
48
+ */
49
+ export default class Writer {
50
+ /**
51
+ * Creates a writer instance.
52
+ *
53
+ * **Note:** It is not recommended to use it directly. Use {@link module:engine/model/model~Model#change `Model#change()`} or
54
+ * {@link module:engine/model/model~Model#enqueueChange `Model#enqueueChange()`} instead.
55
+ *
56
+ * @internal
57
+ */
58
+ constructor(model, batch) {
59
+ this.model = model;
60
+ this.batch = batch;
61
+ }
62
+ /**
63
+ * Creates a new {@link module:engine/model/text~Text text node}.
64
+ *
65
+ * ```ts
66
+ * writer.createText( 'foo' );
67
+ * writer.createText( 'foo', { bold: true } );
68
+ * ```
69
+ *
70
+ * @param data Text data.
71
+ * @param attributes Text attributes.
72
+ * @returns {module:engine/model/text~Text} Created text node.
73
+ */
74
+ createText(data, attributes) {
75
+ return new Text(data, attributes);
76
+ }
77
+ /**
78
+ * Creates a new {@link module:engine/model/element~Element element}.
79
+ *
80
+ * ```ts
81
+ * writer.createElement( 'paragraph' );
82
+ * writer.createElement( 'paragraph', { alignment: 'center' } );
83
+ * ```
84
+ *
85
+ * @param name Name of the element.
86
+ * @param attributes Elements attributes.
87
+ * @returns Created element.
88
+ */
89
+ createElement(name, attributes) {
90
+ return new Element(name, attributes);
91
+ }
92
+ /**
93
+ * Creates a new {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
94
+ *
95
+ * @returns Created document fragment.
96
+ */
97
+ createDocumentFragment() {
98
+ return new DocumentFragment();
99
+ }
100
+ /**
101
+ * Creates a copy of the element and returns it. Created element has the same name and attributes as the original element.
102
+ * If clone is deep, the original element's children are also cloned. If not, then empty element is returned.
103
+ *
104
+ * @param element The element to clone.
105
+ * @param deep If set to `true` clones element and all its children recursively. When set to `false`,
106
+ * element will be cloned without any child.
107
+ */
108
+ cloneElement(element, deep = true) {
109
+ return element._clone(deep);
110
+ }
111
+ /**
112
+ * Inserts item on given position.
113
+ *
114
+ * ```ts
115
+ * const paragraph = writer.createElement( 'paragraph' );
116
+ * writer.insert( paragraph, position );
117
+ * ```
118
+ *
119
+ * Instead of using position you can use parent and offset:
120
+ *
121
+ * ```ts
122
+ * const text = writer.createText( 'foo' );
123
+ * writer.insert( text, paragraph, 5 );
124
+ * ```
125
+ *
126
+ * You can also use `end` instead of the offset to insert at the end:
127
+ *
128
+ * ```ts
129
+ * const text = writer.createText( 'foo' );
130
+ * writer.insert( text, paragraph, 'end' );
131
+ * ```
132
+ *
133
+ * Or insert before or after another element:
134
+ *
135
+ * ```ts
136
+ * const paragraph = writer.createElement( 'paragraph' );
137
+ * writer.insert( paragraph, anotherParagraph, 'after' );
138
+ * ```
139
+ *
140
+ * These parameters works the same way as {@link #createPositionAt `writer.createPositionAt()`}.
141
+ *
142
+ * Note that if the item already has parent it will be removed from the previous parent.
143
+ *
144
+ * Note that you cannot re-insert a node from a document to a different document or a document fragment. In this case,
145
+ * `model-writer-insert-forbidden-move` is thrown.
146
+ *
147
+ * If you want to move {@link module:engine/model/range~Range range} instead of an
148
+ * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move `Writer#move()`}.
149
+ *
150
+ * **Note:** For a paste-like content insertion mechanism see
151
+ * {@link module:engine/model/model~Model#insertContent `model.insertContent()`}.
152
+ *
153
+ * @param item Item or document fragment to insert.
154
+ * @param offset Offset or one of the flags. Used only when second parameter is a {@link module:engine/model/item~Item model item}.
155
+ */
156
+ insert(item, itemOrPosition, offset = 0) {
157
+ this._assertWriterUsedCorrectly();
158
+ if (item instanceof Text && item.data == '') {
159
+ return;
160
+ }
161
+ const position = Position._createAt(itemOrPosition, offset);
162
+ // If item has a parent already.
163
+ if (item.parent) {
164
+ // We need to check if item is going to be inserted within the same document.
165
+ if (isSameTree(item.root, position.root)) {
166
+ // If it's we just need to move it.
167
+ this.move(Range._createOn(item), position);
168
+ return;
169
+ }
170
+ // If it isn't the same root.
171
+ else {
172
+ if (item.root.document) {
173
+ /**
174
+ * Cannot move a node from a document to a different tree.
175
+ * It is forbidden to move a node that was already in a document outside of it.
176
+ *
177
+ * @error model-writer-insert-forbidden-move
178
+ */
179
+ throw new CKEditorError('model-writer-insert-forbidden-move', this);
180
+ }
181
+ else {
182
+ // Move between two different document fragments or from document fragment to a document is possible.
183
+ // In that case, remove the item from it's original parent.
184
+ this.remove(item);
185
+ }
186
+ }
187
+ }
188
+ const version = position.root.document ? position.root.document.version : null;
189
+ const insert = new InsertOperation(position, item, version);
190
+ if (item instanceof Text) {
191
+ insert.shouldReceiveAttributes = true;
192
+ }
193
+ this.batch.addOperation(insert);
194
+ this.model.applyOperation(insert);
195
+ // When element is a DocumentFragment we need to move its markers to Document#markers.
196
+ if (item instanceof DocumentFragment) {
197
+ for (const [markerName, markerRange] of item.markers) {
198
+ // We need to migrate marker range from DocumentFragment to Document.
199
+ const rangeRootPosition = Position._createAt(markerRange.root, 0);
200
+ const range = new Range(markerRange.start._getCombined(rangeRootPosition, position), markerRange.end._getCombined(rangeRootPosition, position));
201
+ const options = { range, usingOperation: true, affectsData: true };
202
+ if (this.model.markers.has(markerName)) {
203
+ this.updateMarker(markerName, options);
204
+ }
205
+ else {
206
+ this.addMarker(markerName, options);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ insertText(text, attributes, // Too complicated when not using `any`.
212
+ itemOrPosition, // Too complicated when not using `any`.
213
+ offset // Too complicated when not using `any`.
214
+ ) {
215
+ if (attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position) {
216
+ this.insert(this.createText(text), attributes, itemOrPosition);
217
+ }
218
+ else {
219
+ this.insert(this.createText(text, attributes), itemOrPosition, offset);
220
+ }
221
+ }
222
+ insertElement(name, attributes, // Too complicated when not using `any`.
223
+ itemOrPositionOrOffset, // Too complicated when not using `any`.
224
+ offset // Too complicated when not using `any`.
225
+ ) {
226
+ if (attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position) {
227
+ this.insert(this.createElement(name), attributes, itemOrPositionOrOffset);
228
+ }
229
+ else {
230
+ this.insert(this.createElement(name, attributes), itemOrPositionOrOffset, offset);
231
+ }
232
+ }
233
+ /**
234
+ * Inserts item at the end of the given parent.
235
+ *
236
+ * ```ts
237
+ * const paragraph = writer.createElement( 'paragraph' );
238
+ * writer.append( paragraph, root );
239
+ * ```
240
+ *
241
+ * Note that if the item already has parent it will be removed from the previous parent.
242
+ *
243
+ * If you want to move {@link module:engine/model/range~Range range} instead of an
244
+ * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move `Writer#move()`}.
245
+ *
246
+ * @param item Item or document fragment to insert.
247
+ */
248
+ append(item, parent) {
249
+ this.insert(item, parent, 'end');
250
+ }
251
+ appendText(text, attributes, parent) {
252
+ if (attributes instanceof DocumentFragment || attributes instanceof Element) {
253
+ this.insert(this.createText(text), attributes, 'end');
254
+ }
255
+ else {
256
+ this.insert(this.createText(text, attributes), parent, 'end');
257
+ }
258
+ }
259
+ appendElement(name, attributes, parent) {
260
+ if (attributes instanceof DocumentFragment || attributes instanceof Element) {
261
+ this.insert(this.createElement(name), attributes, 'end');
262
+ }
263
+ else {
264
+ this.insert(this.createElement(name, attributes), parent, 'end');
265
+ }
266
+ }
267
+ /**
268
+ * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
269
+ * or on a {@link module:engine/model/range~Range range}.
270
+ *
271
+ * @param key Attribute key.
272
+ * @param value Attribute new value.
273
+ * @param itemOrRange Model item or range on which the attribute will be set.
274
+ */
275
+ setAttribute(key, value, itemOrRange) {
276
+ this._assertWriterUsedCorrectly();
277
+ if (itemOrRange instanceof Range) {
278
+ const ranges = itemOrRange.getMinimalFlatRanges();
279
+ for (const range of ranges) {
280
+ setAttributeOnRange(this, key, value, range);
281
+ }
282
+ }
283
+ else {
284
+ setAttributeOnItem(this, key, value, itemOrRange);
285
+ }
286
+ }
287
+ /**
288
+ * Sets values of attributes on a {@link module:engine/model/item~Item model item}
289
+ * or on a {@link module:engine/model/range~Range range}.
290
+ *
291
+ * ```ts
292
+ * writer.setAttributes( {
293
+ * bold: true,
294
+ * italic: true
295
+ * }, range );
296
+ * ```
297
+ *
298
+ * @param attributes Attributes keys and values.
299
+ * @param itemOrRange Model item or range on which the attributes will be set.
300
+ */
301
+ setAttributes(attributes, itemOrRange) {
302
+ for (const [key, val] of toMap(attributes)) {
303
+ this.setAttribute(key, val, itemOrRange);
304
+ }
305
+ }
306
+ /**
307
+ * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
308
+ * or from a {@link module:engine/model/range~Range range}.
309
+ *
310
+ * @param key Attribute key.
311
+ * @param itemOrRange Model item or range from which the attribute will be removed.
312
+ */
313
+ removeAttribute(key, itemOrRange) {
314
+ this._assertWriterUsedCorrectly();
315
+ if (itemOrRange instanceof Range) {
316
+ const ranges = itemOrRange.getMinimalFlatRanges();
317
+ for (const range of ranges) {
318
+ setAttributeOnRange(this, key, null, range);
319
+ }
320
+ }
321
+ else {
322
+ setAttributeOnItem(this, key, null, itemOrRange);
323
+ }
324
+ }
325
+ /**
326
+ * Removes all attributes from all elements in the range or from the given item.
327
+ *
328
+ * @param itemOrRange Model item or range from which all attributes will be removed.
329
+ */
330
+ clearAttributes(itemOrRange) {
331
+ this._assertWriterUsedCorrectly();
332
+ const removeAttributesFromItem = (item) => {
333
+ for (const attribute of item.getAttributeKeys()) {
334
+ this.removeAttribute(attribute, item);
335
+ }
336
+ };
337
+ if (!(itemOrRange instanceof Range)) {
338
+ removeAttributesFromItem(itemOrRange);
339
+ }
340
+ else {
341
+ for (const item of itemOrRange.getItems()) {
342
+ removeAttributesFromItem(item);
343
+ }
344
+ }
345
+ }
346
+ /**
347
+ * Moves all items in the source range to the target position.
348
+ *
349
+ * ```ts
350
+ * writer.move( sourceRange, targetPosition );
351
+ * ```
352
+ *
353
+ * Instead of the target position you can use parent and offset or define that range should be moved to the end
354
+ * or before or after chosen item:
355
+ *
356
+ * ```ts
357
+ * // Moves all items in the range to the paragraph at offset 5:
358
+ * writer.move( sourceRange, paragraph, 5 );
359
+ * // Moves all items in the range to the end of a blockquote:
360
+ * writer.move( sourceRange, blockquote, 'end' );
361
+ * // Moves all items in the range to a position after an image:
362
+ * writer.move( sourceRange, image, 'after' );
363
+ * ```
364
+ *
365
+ * These parameters work the same way as {@link #createPositionAt `writer.createPositionAt()`}.
366
+ *
367
+ * Note that items can be moved only within the same tree. It means that you can move items within the same root
368
+ * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
369
+ * but you can not move items from document fragment to the document or from one detached element to another. Use
370
+ * {@link module:engine/model/writer~Writer#insert} in such cases.
371
+ *
372
+ * @param range Source range.
373
+ * @param offset Offset or one of the flags. Used only when second parameter is a {@link module:engine/model/item~Item model item}.
374
+ */
375
+ move(range, itemOrPosition, offset) {
376
+ this._assertWriterUsedCorrectly();
377
+ if (!(range instanceof Range)) {
378
+ /**
379
+ * Invalid range to move.
380
+ *
381
+ * @error writer-move-invalid-range
382
+ */
383
+ throw new CKEditorError('writer-move-invalid-range', this);
384
+ }
385
+ if (!range.isFlat) {
386
+ /**
387
+ * Range to move is not flat.
388
+ *
389
+ * @error writer-move-range-not-flat
390
+ */
391
+ throw new CKEditorError('writer-move-range-not-flat', this);
392
+ }
393
+ const position = Position._createAt(itemOrPosition, offset);
394
+ // Do not move anything if the move target is same as moved range start.
395
+ if (position.isEqual(range.start)) {
396
+ return;
397
+ }
398
+ // If part of the marker is removed, create additional marker operation for undo purposes.
399
+ this._addOperationForAffectedMarkers('move', range);
400
+ if (!isSameTree(range.root, position.root)) {
401
+ /**
402
+ * Range is going to be moved within not the same document. Please use
403
+ * {@link module:engine/model/writer~Writer#insert insert} instead.
404
+ *
405
+ * @error writer-move-different-document
406
+ */
407
+ throw new CKEditorError('writer-move-different-document', this);
408
+ }
409
+ const version = range.root.document ? range.root.document.version : null;
410
+ const operation = new MoveOperation(range.start, range.end.offset - range.start.offset, position, version);
411
+ this.batch.addOperation(operation);
412
+ this.model.applyOperation(operation);
413
+ }
414
+ /**
415
+ * Removes given model {@link module:engine/model/item~Item item} or {@link module:engine/model/range~Range range}.
416
+ *
417
+ * @param itemOrRange Model item or range to remove.
418
+ */
419
+ remove(itemOrRange) {
420
+ this._assertWriterUsedCorrectly();
421
+ const rangeToRemove = itemOrRange instanceof Range ? itemOrRange : Range._createOn(itemOrRange);
422
+ const ranges = rangeToRemove.getMinimalFlatRanges().reverse();
423
+ for (const flat of ranges) {
424
+ // If part of the marker is removed, create additional marker operation for undo purposes.
425
+ this._addOperationForAffectedMarkers('move', flat);
426
+ applyRemoveOperation(flat.start, flat.end.offset - flat.start.offset, this.batch, this.model);
427
+ }
428
+ }
429
+ /**
430
+ * Merges two siblings at the given position.
431
+ *
432
+ * Node before and after the position have to be an element. Otherwise `writer-merge-no-element-before` or
433
+ * `writer-merge-no-element-after` error will be thrown.
434
+ *
435
+ * @param position Position between merged elements.
436
+ */
437
+ merge(position) {
438
+ this._assertWriterUsedCorrectly();
439
+ const nodeBefore = position.nodeBefore;
440
+ const nodeAfter = position.nodeAfter;
441
+ // If part of the marker is removed, create additional marker operation for undo purposes.
442
+ this._addOperationForAffectedMarkers('merge', position);
443
+ if (!(nodeBefore instanceof Element)) {
444
+ /**
445
+ * Node before merge position must be an element.
446
+ *
447
+ * @error writer-merge-no-element-before
448
+ */
449
+ throw new CKEditorError('writer-merge-no-element-before', this);
450
+ }
451
+ if (!(nodeAfter instanceof Element)) {
452
+ /**
453
+ * Node after merge position must be an element.
454
+ *
455
+ * @error writer-merge-no-element-after
456
+ */
457
+ throw new CKEditorError('writer-merge-no-element-after', this);
458
+ }
459
+ if (!position.root.document) {
460
+ this._mergeDetached(position);
461
+ }
462
+ else {
463
+ this._merge(position);
464
+ }
465
+ }
466
+ /**
467
+ * Shortcut for {@link module:engine/model/model~Model#createPositionFromPath `Model#createPositionFromPath()`}.
468
+ *
469
+ * @param root Root of the position.
470
+ * @param path Position path. See {@link module:engine/model/position~Position#path}.
471
+ * @param stickiness Position stickiness. See {@link module:engine/model/position~PositionStickiness}.
472
+ */
473
+ createPositionFromPath(root, path, stickiness) {
474
+ return this.model.createPositionFromPath(root, path, stickiness);
475
+ }
476
+ /**
477
+ * Shortcut for {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}.
478
+ *
479
+ * @param offset Offset or one of the flags. Used only when first parameter is a {@link module:engine/model/item~Item model item}.
480
+ */
481
+ createPositionAt(itemOrPosition, offset) {
482
+ return this.model.createPositionAt(itemOrPosition, offset);
483
+ }
484
+ /**
485
+ * Shortcut for {@link module:engine/model/model~Model#createPositionAfter `Model#createPositionAfter()`}.
486
+ *
487
+ * @param item Item after which the position should be placed.
488
+ */
489
+ createPositionAfter(item) {
490
+ return this.model.createPositionAfter(item);
491
+ }
492
+ /**
493
+ * Shortcut for {@link module:engine/model/model~Model#createPositionBefore `Model#createPositionBefore()`}.
494
+ *
495
+ * @param item Item after which the position should be placed.
496
+ */
497
+ createPositionBefore(item) {
498
+ return this.model.createPositionBefore(item);
499
+ }
500
+ /**
501
+ * Shortcut for {@link module:engine/model/model~Model#createRange `Model#createRange()`}.
502
+ *
503
+ * @param start Start position.
504
+ * @param end End position. If not set, range will be collapsed at `start` position.
505
+ */
506
+ createRange(start, end) {
507
+ return this.model.createRange(start, end);
508
+ }
509
+ /**
510
+ * Shortcut for {@link module:engine/model/model~Model#createRangeIn `Model#createRangeIn()`}.
511
+ *
512
+ * @param element Element which is a parent for the range.
513
+ */
514
+ createRangeIn(element) {
515
+ return this.model.createRangeIn(element);
516
+ }
517
+ /**
518
+ * Shortcut for {@link module:engine/model/model~Model#createRangeOn `Model#createRangeOn()`}.
519
+ *
520
+ * @param element Element which is a parent for the range.
521
+ */
522
+ createRangeOn(element) {
523
+ return this.model.createRangeOn(element);
524
+ }
525
+ createSelection(...args) {
526
+ return this.model.createSelection(...args);
527
+ }
528
+ /**
529
+ * Performs merge action in a detached tree.
530
+ *
531
+ * @param position Position between merged elements.
532
+ */
533
+ _mergeDetached(position) {
534
+ const nodeBefore = position.nodeBefore;
535
+ const nodeAfter = position.nodeAfter;
536
+ this.move(Range._createIn(nodeAfter), Position._createAt(nodeBefore, 'end'));
537
+ this.remove(nodeAfter);
538
+ }
539
+ /**
540
+ * Performs merge action in a non-detached tree.
541
+ *
542
+ * @param position Position between merged elements.
543
+ */
544
+ _merge(position) {
545
+ const targetPosition = Position._createAt(position.nodeBefore, 'end');
546
+ const sourcePosition = Position._createAt(position.nodeAfter, 0);
547
+ const graveyard = position.root.document.graveyard;
548
+ const graveyardPosition = new Position(graveyard, [0]);
549
+ const version = position.root.document.version;
550
+ const merge = new MergeOperation(sourcePosition, position.nodeAfter.maxOffset, targetPosition, graveyardPosition, version);
551
+ this.batch.addOperation(merge);
552
+ this.model.applyOperation(merge);
553
+ }
554
+ /**
555
+ * Renames the given element.
556
+ *
557
+ * @param element The element to rename.
558
+ * @param newName New element name.
559
+ */
560
+ rename(element, newName) {
561
+ this._assertWriterUsedCorrectly();
562
+ if (!(element instanceof Element)) {
563
+ /**
564
+ * Trying to rename an object which is not an instance of Element.
565
+ *
566
+ * @error writer-rename-not-element-instance
567
+ */
568
+ throw new CKEditorError('writer-rename-not-element-instance', this);
569
+ }
570
+ const version = element.root.document ? element.root.document.version : null;
571
+ const renameOperation = new RenameOperation(Position._createBefore(element), element.name, newName, version);
572
+ this.batch.addOperation(renameOperation);
573
+ this.model.applyOperation(renameOperation);
574
+ }
575
+ /**
576
+ * Splits elements starting from the given position and going to the top of the model tree as long as given
577
+ * `limitElement` is reached. When `limitElement` is not defined then only the parent of the given position will be split.
578
+ *
579
+ * The element needs to have a parent. It cannot be a root element nor a document fragment.
580
+ * The `writer-split-element-no-parent` error will be thrown if you try to split an element with no parent.
581
+ *
582
+ * @param position Position of split.
583
+ * @param limitElement Stop splitting when this element will be reached.
584
+ * @returns Split result with properties:
585
+ * * `position` - Position between split elements.
586
+ * * `range` - Range that stars from the end of the first split element and ends at the beginning of the first copy element.
587
+ */
588
+ split(position, limitElement) {
589
+ this._assertWriterUsedCorrectly();
590
+ let splitElement = position.parent;
591
+ if (!splitElement.parent) {
592
+ /**
593
+ * Element with no parent can not be split.
594
+ *
595
+ * @error writer-split-element-no-parent
596
+ */
597
+ throw new CKEditorError('writer-split-element-no-parent', this);
598
+ }
599
+ // When limit element is not defined lets set splitElement parent as limit.
600
+ if (!limitElement) {
601
+ limitElement = splitElement.parent;
602
+ }
603
+ if (!position.parent.getAncestors({ includeSelf: true }).includes(limitElement)) {
604
+ /**
605
+ * Limit element is not a position ancestor.
606
+ *
607
+ * @error writer-split-invalid-limit-element
608
+ */
609
+ throw new CKEditorError('writer-split-invalid-limit-element', this);
610
+ }
611
+ // We need to cache elements that will be created as a result of the first split because
612
+ // we need to create a range from the end of the first split element to the beginning of the
613
+ // first copy element. This should be handled by LiveRange but it doesn't work on detached nodes.
614
+ let firstSplitElement;
615
+ let firstCopyElement;
616
+ do {
617
+ const version = splitElement.root.document ? splitElement.root.document.version : null;
618
+ const howMany = splitElement.maxOffset - position.offset;
619
+ const insertionPosition = SplitOperation.getInsertionPosition(position);
620
+ const split = new SplitOperation(position, howMany, insertionPosition, null, version);
621
+ this.batch.addOperation(split);
622
+ this.model.applyOperation(split);
623
+ // Cache result of the first split.
624
+ if (!firstSplitElement && !firstCopyElement) {
625
+ firstSplitElement = splitElement;
626
+ firstCopyElement = position.parent.nextSibling;
627
+ }
628
+ position = this.createPositionAfter(position.parent);
629
+ splitElement = position.parent;
630
+ } while (splitElement !== limitElement);
631
+ return {
632
+ position,
633
+ range: new Range(Position._createAt(firstSplitElement, 'end'), Position._createAt(firstCopyElement, 0))
634
+ };
635
+ }
636
+ /**
637
+ * Wraps the given range with the given element or with a new element (if a string was passed).
638
+ *
639
+ * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat `Range#isFlat`}).
640
+ * If not, an error will be thrown.
641
+ *
642
+ * @param range Range to wrap.
643
+ * @param elementOrString Element or name of element to wrap the range with.
644
+ */
645
+ wrap(range, elementOrString) {
646
+ this._assertWriterUsedCorrectly();
647
+ if (!range.isFlat) {
648
+ /**
649
+ * Range to wrap is not flat.
650
+ *
651
+ * @error writer-wrap-range-not-flat
652
+ */
653
+ throw new CKEditorError('writer-wrap-range-not-flat', this);
654
+ }
655
+ const element = elementOrString instanceof Element ? elementOrString : new Element(elementOrString);
656
+ if (element.childCount > 0) {
657
+ /**
658
+ * Element to wrap with is not empty.
659
+ *
660
+ * @error writer-wrap-element-not-empty
661
+ */
662
+ throw new CKEditorError('writer-wrap-element-not-empty', this);
663
+ }
664
+ if (element.parent !== null) {
665
+ /**
666
+ * Element to wrap with is already attached to a tree model.
667
+ *
668
+ * @error writer-wrap-element-attached
669
+ */
670
+ throw new CKEditorError('writer-wrap-element-attached', this);
671
+ }
672
+ this.insert(element, range.start);
673
+ // Shift the range-to-wrap because we just inserted an element before that range.
674
+ const shiftedRange = new Range(range.start.getShiftedBy(1), range.end.getShiftedBy(1));
675
+ this.move(shiftedRange, Position._createAt(element, 0));
676
+ }
677
+ /**
678
+ * Unwraps children of the given element – all its children are moved before it and then the element is removed.
679
+ * Throws error if you try to unwrap an element which does not have a parent.
680
+ *
681
+ * @param element Element to unwrap.
682
+ */
683
+ unwrap(element) {
684
+ this._assertWriterUsedCorrectly();
685
+ if (element.parent === null) {
686
+ /**
687
+ * Trying to unwrap an element which has no parent.
688
+ *
689
+ * @error writer-unwrap-element-no-parent
690
+ */
691
+ throw new CKEditorError('writer-unwrap-element-no-parent', this);
692
+ }
693
+ this.move(Range._createIn(element), this.createPositionAfter(element));
694
+ this.remove(element);
695
+ }
696
+ /**
697
+ * Adds a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
698
+ * changes in the document and updates its range automatically, when model tree changes.
699
+ *
700
+ * As the first parameter you can set marker name.
701
+ *
702
+ * The required `options.usingOperation` parameter lets you decide if the marker should be managed by operations or not. See
703
+ * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
704
+ * markers managed by operations and not-managed by operations.
705
+ *
706
+ * The `options.affectsData` parameter, which defaults to `false`, allows you to define if a marker affects the data. It should be
707
+ * `true` when the marker change changes the data returned by the
708
+ * {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`} method.
709
+ * When set to `true` it fires the {@link module:engine/model/document~Document#event:change:data `change:data`} event.
710
+ * When set to `false` it fires the {@link module:engine/model/document~Document#event:change `change`} event.
711
+ *
712
+ * Create marker directly base on marker's name:
713
+ *
714
+ * ```ts
715
+ * addMarker( markerName, { range, usingOperation: false } );
716
+ * ```
717
+ *
718
+ * Create marker using operation:
719
+ *
720
+ * ```ts
721
+ * addMarker( markerName, { range, usingOperation: true } );
722
+ * ```
723
+ *
724
+ * Create marker that affects the editor data:
725
+ *
726
+ * ```ts
727
+ * addMarker( markerName, { range, usingOperation: false, affectsData: true } );
728
+ * ```
729
+ *
730
+ * Note: For efficiency reasons, it's best to create and keep as little markers as possible.
731
+ *
732
+ * @see module:engine/model/markercollection~Marker
733
+ * @param name Name of a marker to create - must be unique.
734
+ * @param options.usingOperation Flag indicating that the marker should be added by MarkerOperation.
735
+ * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
736
+ * @param options.range Marker range.
737
+ * @param options.affectsData Flag indicating that the marker changes the editor data.
738
+ * @returns Marker that was set.
739
+ */
740
+ addMarker(name, options) {
741
+ this._assertWriterUsedCorrectly();
742
+ if (!options || typeof options.usingOperation != 'boolean') {
743
+ /**
744
+ * The `options.usingOperation` parameter is required when adding a new marker.
745
+ *
746
+ * @error writer-addmarker-no-usingoperation
747
+ */
748
+ throw new CKEditorError('writer-addmarker-no-usingoperation', this);
749
+ }
750
+ const usingOperation = options.usingOperation;
751
+ const range = options.range;
752
+ const affectsData = options.affectsData === undefined ? false : options.affectsData;
753
+ if (this.model.markers.has(name)) {
754
+ /**
755
+ * Marker with provided name already exists.
756
+ *
757
+ * @error writer-addmarker-marker-exists
758
+ */
759
+ throw new CKEditorError('writer-addmarker-marker-exists', this);
760
+ }
761
+ if (!range) {
762
+ /**
763
+ * Range parameter is required when adding a new marker.
764
+ *
765
+ * @error writer-addmarker-no-range
766
+ */
767
+ throw new CKEditorError('writer-addmarker-no-range', this);
768
+ }
769
+ if (!usingOperation) {
770
+ return this.model.markers._set(name, range, usingOperation, affectsData);
771
+ }
772
+ applyMarkerOperation(this, name, null, range, affectsData);
773
+ return this.model.markers.get(name);
774
+ }
775
+ /**
776
+ * Adds, updates or refreshes a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
777
+ * changes in the document and updates its range automatically, when model tree changes. Still, it is possible to change the
778
+ * marker's range directly using this method.
779
+ *
780
+ * As the first parameter you can set marker name or instance. If none of them is provided, new marker, with a unique
781
+ * name is created and returned.
782
+ *
783
+ * **Note**: If you want to change the {@link module:engine/view/element~Element view element} of the marker while its data in the model
784
+ * remains the same, use the dedicated {@link module:engine/controller/editingcontroller~EditingController#reconvertMarker} method.
785
+ *
786
+ * The `options.usingOperation` parameter lets you change if the marker should be managed by operations or not. See
787
+ * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
788
+ * markers managed by operations and not-managed by operations. It is possible to change this option for an existing marker.
789
+ *
790
+ * The `options.affectsData` parameter, which defaults to `false`, allows you to define if a marker affects the data. It should be
791
+ * `true` when the marker change changes the data returned by
792
+ * the {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`} method.
793
+ * When set to `true` it fires the {@link module:engine/model/document~Document#event:change:data `change:data`} event.
794
+ * When set to `false` it fires the {@link module:engine/model/document~Document#event:change `change`} event.
795
+ *
796
+ * Update marker directly base on marker's name:
797
+ *
798
+ * ```ts
799
+ * updateMarker( markerName, { range } );
800
+ * ```
801
+ *
802
+ * Update marker using operation:
803
+ *
804
+ * ```ts
805
+ * updateMarker( marker, { range, usingOperation: true } );
806
+ * updateMarker( markerName, { range, usingOperation: true } );
807
+ * ```
808
+ *
809
+ * Change marker's option (start using operations to manage it):
810
+ *
811
+ * ```ts
812
+ * updateMarker( marker, { usingOperation: true } );
813
+ * ```
814
+ *
815
+ * Change marker's option (inform the engine, that the marker does not affect the data anymore):
816
+ *
817
+ * ```ts
818
+ * updateMarker( markerName, { affectsData: false } );
819
+ * ```
820
+ *
821
+ * @see module:engine/model/markercollection~Marker
822
+ * @param markerOrName Name of a marker to update, or a marker instance.
823
+ * @param options If options object is not defined then marker will be refreshed by triggering
824
+ * downcast conversion for this marker with the same data.
825
+ * @param options.range Marker range to update.
826
+ * @param options.usingOperation Flag indicated whether the marker should be added by MarkerOperation.
827
+ * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
828
+ * @param options.affectsData Flag indicating that the marker changes the editor data.
829
+ */
830
+ updateMarker(markerOrName, options) {
831
+ this._assertWriterUsedCorrectly();
832
+ const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
833
+ const currentMarker = this.model.markers.get(markerName);
834
+ if (!currentMarker) {
835
+ /**
836
+ * Marker with provided name does not exist and will not be updated.
837
+ *
838
+ * @error writer-updatemarker-marker-not-exists
839
+ */
840
+ throw new CKEditorError('writer-updatemarker-marker-not-exists', this);
841
+ }
842
+ if (!options) {
843
+ /**
844
+ * The usage of `writer.updateMarker()` only to reconvert (refresh) a
845
+ * {@link module:engine/model/markercollection~Marker model marker} was deprecated and may not work in the future.
846
+ * Please update your code to use
847
+ * {@link module:engine/controller/editingcontroller~EditingController#reconvertMarker `editor.editing.reconvertMarker()`}
848
+ * instead.
849
+ *
850
+ * @error writer-updatemarker-reconvert-using-editingcontroller
851
+ * @param markerName The name of the updated marker.
852
+ */
853
+ logWarning('writer-updatemarker-reconvert-using-editingcontroller', { markerName });
854
+ this.model.markers._refresh(currentMarker);
855
+ return;
856
+ }
857
+ const hasUsingOperationDefined = typeof options.usingOperation == 'boolean';
858
+ const affectsDataDefined = typeof options.affectsData == 'boolean';
859
+ // Use previously defined marker's affectsData if the property is not provided.
860
+ const affectsData = affectsDataDefined ? options.affectsData : currentMarker.affectsData;
861
+ if (!hasUsingOperationDefined && !options.range && !affectsDataDefined) {
862
+ /**
863
+ * One of the options is required - provide range, usingOperations or affectsData.
864
+ *
865
+ * @error writer-updatemarker-wrong-options
866
+ */
867
+ throw new CKEditorError('writer-updatemarker-wrong-options', this);
868
+ }
869
+ const currentRange = currentMarker.getRange();
870
+ const updatedRange = options.range ? options.range : currentRange;
871
+ if (hasUsingOperationDefined && options.usingOperation !== currentMarker.managedUsingOperations) {
872
+ // The marker type is changed so it's necessary to create proper operations.
873
+ if (options.usingOperation) {
874
+ // If marker changes to a managed one treat this as synchronizing existing marker.
875
+ // Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
876
+ applyMarkerOperation(this, markerName, null, updatedRange, affectsData);
877
+ }
878
+ else {
879
+ // If marker changes to a marker that do not use operations then we need to create additional operation
880
+ // that removes that marker first.
881
+ applyMarkerOperation(this, markerName, currentRange, null, affectsData);
882
+ // Although not managed the marker itself should stay in model and its range should be preserver or changed to passed range.
883
+ this.model.markers._set(markerName, updatedRange, undefined, affectsData);
884
+ }
885
+ return;
886
+ }
887
+ // Marker's type doesn't change so update it accordingly.
888
+ if (currentMarker.managedUsingOperations) {
889
+ applyMarkerOperation(this, markerName, currentRange, updatedRange, affectsData);
890
+ }
891
+ else {
892
+ this.model.markers._set(markerName, updatedRange, undefined, affectsData);
893
+ }
894
+ }
895
+ /**
896
+ * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
897
+ * The marker is removed accordingly to how it has been created, so if the marker was created using operation,
898
+ * it will be destroyed using operation.
899
+ *
900
+ * @param markerOrName Marker or marker name to remove.
901
+ */
902
+ removeMarker(markerOrName) {
903
+ this._assertWriterUsedCorrectly();
904
+ const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
905
+ if (!this.model.markers.has(name)) {
906
+ /**
907
+ * Trying to remove marker which does not exist.
908
+ *
909
+ * @error writer-removemarker-no-marker
910
+ */
911
+ throw new CKEditorError('writer-removemarker-no-marker', this);
912
+ }
913
+ const marker = this.model.markers.get(name);
914
+ if (!marker.managedUsingOperations) {
915
+ this.model.markers._remove(name);
916
+ return;
917
+ }
918
+ const oldRange = marker.getRange();
919
+ applyMarkerOperation(this, name, oldRange, null, marker.affectsData);
920
+ }
921
+ /**
922
+ * Adds a new root to the document (or re-attaches a {@link #detachRoot detached root}).
923
+ *
924
+ * Throws an error, if trying to add a root that is already added and attached.
925
+ *
926
+ * @param rootName Name of the added root.
927
+ * @param elementName The element name. Defaults to `'$root'` which also has some basic schema defined
928
+ * (e.g. `$block` elements are allowed inside the `$root`). Make sure to define a proper schema if you use a different name.
929
+ * @returns The added root element.
930
+ */
931
+ addRoot(rootName, elementName = '$root') {
932
+ this._assertWriterUsedCorrectly();
933
+ const root = this.model.document.getRoot(rootName);
934
+ if (root && root.isAttached()) {
935
+ /**
936
+ * Root with provided name already exists and is attached.
937
+ *
938
+ * @error writer-addroot-root-exists
939
+ */
940
+ throw new CKEditorError('writer-addroot-root-exists', this);
941
+ }
942
+ const document = this.model.document;
943
+ const operation = new RootOperation(rootName, elementName, true, document, document.version);
944
+ this.batch.addOperation(operation);
945
+ this.model.applyOperation(operation);
946
+ return this.model.document.getRoot(rootName);
947
+ }
948
+ /**
949
+ * Detaches the root from the document.
950
+ *
951
+ * All content and markers are removed from the root upon detaching. New content and new markers cannot be added to the root, as long
952
+ * as it is detached.
953
+ *
954
+ * A root cannot be fully removed from the document, it can be only detached. A root is permanently removed only after you
955
+ * re-initialize the editor and do not specify the root in the initial data.
956
+ *
957
+ * A detached root can be re-attached using {@link #addRoot}.
958
+ *
959
+ * Throws an error if the root does not exist or the root is already detached.
960
+ *
961
+ * @param rootOrName Name of the detached root.
962
+ */
963
+ detachRoot(rootOrName) {
964
+ this._assertWriterUsedCorrectly();
965
+ const root = typeof rootOrName == 'string' ? this.model.document.getRoot(rootOrName) : rootOrName;
966
+ if (!root || !root.isAttached()) {
967
+ /**
968
+ * Root with provided name does not exist or is already detached.
969
+ *
970
+ * @error writer-detachroot-no-root
971
+ */
972
+ throw new CKEditorError('writer-detachroot-no-root', this);
973
+ }
974
+ // First, remove all markers from the root. It is better to do it before removing stuff for undo purposes.
975
+ // However, looking through all the markers may not be the best performance wise. But there's no better solution for now.
976
+ for (const marker of this.model.markers) {
977
+ if (marker.getRange().root === root) {
978
+ this.removeMarker(marker);
979
+ }
980
+ }
981
+ // Remove all attributes from the root.
982
+ for (const key of root.getAttributeKeys()) {
983
+ this.removeAttribute(key, root);
984
+ }
985
+ // Remove all contents of the root.
986
+ this.remove(this.createRangeIn(root));
987
+ // Finally, detach the root.
988
+ const document = this.model.document;
989
+ const operation = new RootOperation(root.rootName, root.name, false, document, document.version);
990
+ this.batch.addOperation(operation);
991
+ this.model.applyOperation(operation);
992
+ }
993
+ setSelection(...args) {
994
+ this._assertWriterUsedCorrectly();
995
+ this.model.document.selection._setTo(...args);
996
+ }
997
+ /**
998
+ * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
999
+ *
1000
+ * The location can be specified in the same form as
1001
+ * {@link #createPositionAt `writer.createPositionAt()`} parameters.
1002
+ *
1003
+ * @param itemOrPosition
1004
+ * @param offset Offset or one of the flags. Used only when first parameter is a {@link module:engine/model/item~Item model item}.
1005
+ */
1006
+ setSelectionFocus(itemOrPosition, offset) {
1007
+ this._assertWriterUsedCorrectly();
1008
+ this.model.document.selection._setFocus(itemOrPosition, offset);
1009
+ }
1010
+ setSelectionAttribute(keyOrObjectOrIterable, value) {
1011
+ this._assertWriterUsedCorrectly();
1012
+ if (typeof keyOrObjectOrIterable === 'string') {
1013
+ this._setSelectionAttribute(keyOrObjectOrIterable, value);
1014
+ }
1015
+ else {
1016
+ for (const [key, value] of toMap(keyOrObjectOrIterable)) {
1017
+ this._setSelectionAttribute(key, value);
1018
+ }
1019
+ }
1020
+ }
1021
+ /**
1022
+ * Removes attribute(s) with given key(s) from the selection.
1023
+ *
1024
+ * Remove one attribute:
1025
+ *
1026
+ * ```ts
1027
+ * writer.removeSelectionAttribute( 'italic' );
1028
+ * ```
1029
+ *
1030
+ * Remove multiple attributes:
1031
+ *
1032
+ * ```ts
1033
+ * writer.removeSelectionAttribute( [ 'italic', 'bold' ] );
1034
+ * ```
1035
+ *
1036
+ * @param keyOrIterableOfKeys Key of the attribute to remove or an iterable of attribute keys to remove.
1037
+ */
1038
+ removeSelectionAttribute(keyOrIterableOfKeys) {
1039
+ this._assertWriterUsedCorrectly();
1040
+ if (typeof keyOrIterableOfKeys === 'string') {
1041
+ this._removeSelectionAttribute(keyOrIterableOfKeys);
1042
+ }
1043
+ else {
1044
+ for (const key of keyOrIterableOfKeys) {
1045
+ this._removeSelectionAttribute(key);
1046
+ }
1047
+ }
1048
+ }
1049
+ /**
1050
+ * Temporarily changes the {@link module:engine/model/documentselection~DocumentSelection#isGravityOverridden gravity}
1051
+ * of the selection from left to right.
1052
+ *
1053
+ * The gravity defines from which direction the selection inherits its attributes. If it's the default left gravity,
1054
+ * then the selection (after being moved by the user) inherits attributes from its left-hand side.
1055
+ * This method allows to temporarily override this behavior by forcing the gravity to the right.
1056
+ *
1057
+ * For the following model fragment:
1058
+ *
1059
+ * ```xml
1060
+ * <$text bold="true" linkHref="url">bar[]</$text><$text bold="true">biz</$text>
1061
+ * ```
1062
+ *
1063
+ * * Default gravity: selection will have the `bold` and `linkHref` attributes.
1064
+ * * Overridden gravity: selection will have `bold` attribute.
1065
+ *
1066
+ * **Note**: It returns an unique identifier which is required to restore the gravity. It guarantees the symmetry
1067
+ * of the process.
1068
+ *
1069
+ * @returns The unique id which allows restoring the gravity.
1070
+ */
1071
+ overrideSelectionGravity() {
1072
+ return this.model.document.selection._overrideGravity();
1073
+ }
1074
+ /**
1075
+ * Restores {@link ~Writer#overrideSelectionGravity} gravity to default.
1076
+ *
1077
+ * Restoring the gravity is only possible using the unique identifier returned by
1078
+ * {@link ~Writer#overrideSelectionGravity}. Note that the gravity remains overridden as long as won't be restored
1079
+ * the same number of times it was overridden.
1080
+ *
1081
+ * @param uid The unique id returned by {@link ~Writer#overrideSelectionGravity}.
1082
+ */
1083
+ restoreSelectionGravity(uid) {
1084
+ this.model.document.selection._restoreGravity(uid);
1085
+ }
1086
+ /**
1087
+ * @param key Key of the attribute to remove.
1088
+ * @param value Attribute value.
1089
+ */
1090
+ _setSelectionAttribute(key, value) {
1091
+ const selection = this.model.document.selection;
1092
+ // Store attribute in parent element if the selection is collapsed in an empty node.
1093
+ if (selection.isCollapsed && selection.anchor.parent.isEmpty) {
1094
+ const storeKey = DocumentSelection._getStoreAttributeKey(key);
1095
+ this.setAttribute(storeKey, value, selection.anchor.parent);
1096
+ }
1097
+ selection._setAttribute(key, value);
1098
+ }
1099
+ /**
1100
+ * @param key Key of the attribute to remove.
1101
+ */
1102
+ _removeSelectionAttribute(key) {
1103
+ const selection = this.model.document.selection;
1104
+ // Remove stored attribute from parent element if the selection is collapsed in an empty node.
1105
+ if (selection.isCollapsed && selection.anchor.parent.isEmpty) {
1106
+ const storeKey = DocumentSelection._getStoreAttributeKey(key);
1107
+ this.removeAttribute(storeKey, selection.anchor.parent);
1108
+ }
1109
+ selection._removeAttribute(key);
1110
+ }
1111
+ /**
1112
+ * Throws `writer-detached-writer-tries-to-modify-model` error when the writer is used outside of the `change()` block.
1113
+ */
1114
+ _assertWriterUsedCorrectly() {
1115
+ /**
1116
+ * Trying to use a writer outside a {@link module:engine/model/model~Model#change `change()`} or
1117
+ * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`} blocks.
1118
+ *
1119
+ * The writer can only be used inside these blocks which ensures that the model
1120
+ * can only be changed during such "sessions".
1121
+ *
1122
+ * @error writer-incorrect-use
1123
+ */
1124
+ if (this.model._currentWriter !== this) {
1125
+ throw new CKEditorError('writer-incorrect-use', this);
1126
+ }
1127
+ }
1128
+ /**
1129
+ * For given action `type` and `positionOrRange` where the action happens, this function finds all affected markers
1130
+ * and applies a marker operation with the new marker range equal to the current range. Thanks to this, the marker range
1131
+ * can be later correctly processed during undo.
1132
+ *
1133
+ * @param type Writer action type.
1134
+ * @param positionOrRange Position or range where the writer action happens.
1135
+ */
1136
+ _addOperationForAffectedMarkers(type, positionOrRange) {
1137
+ for (const marker of this.model.markers) {
1138
+ if (!marker.managedUsingOperations) {
1139
+ continue;
1140
+ }
1141
+ const markerRange = marker.getRange();
1142
+ let isAffected = false;
1143
+ if (type === 'move') {
1144
+ const range = positionOrRange;
1145
+ isAffected =
1146
+ range.containsPosition(markerRange.start) ||
1147
+ range.start.isEqual(markerRange.start) ||
1148
+ range.containsPosition(markerRange.end) ||
1149
+ range.end.isEqual(markerRange.end);
1150
+ }
1151
+ else {
1152
+ // if type === 'merge'.
1153
+ const position = positionOrRange;
1154
+ const elementBefore = position.nodeBefore;
1155
+ const elementAfter = position.nodeAfter;
1156
+ // Start: <p>Foo[</p><p>Bar]</p>
1157
+ // After merge: <p>Foo[Bar]</p>
1158
+ // After undoing split: <p>Foo</p><p>[Bar]</p> <-- incorrect, needs remembering for undo.
1159
+ //
1160
+ const affectedInLeftElement = markerRange.start.parent == elementBefore && markerRange.start.isAtEnd;
1161
+ // Start: <p>[Foo</p><p>]Bar</p>
1162
+ // After merge: <p>[Foo]Bar</p>
1163
+ // After undoing split: <p>[Foo]</p><p>Bar</p> <-- incorrect, needs remembering for undo.
1164
+ //
1165
+ const affectedInRightElement = markerRange.end.parent == elementAfter && markerRange.end.offset == 0;
1166
+ // Start: <p>[Foo</p>]<p>Bar</p>
1167
+ // After merge: <p>[Foo]Bar</p>
1168
+ // After undoing split: <p>[Foo]</p><p>Bar</p> <-- incorrect, needs remembering for undo.
1169
+ //
1170
+ const affectedAfterLeftElement = markerRange.end.nodeAfter == elementAfter;
1171
+ // Start: <p>Foo</p>[<p>Bar]</p>
1172
+ // After merge: <p>Foo[Bar]</p>
1173
+ // After undoing split: <p>Foo</p><p>[Bar]</p> <-- incorrect, needs remembering for undo.
1174
+ //
1175
+ const affectedBeforeRightElement = markerRange.start.nodeAfter == elementAfter;
1176
+ isAffected = affectedInLeftElement || affectedInRightElement || affectedAfterLeftElement || affectedBeforeRightElement;
1177
+ }
1178
+ if (isAffected) {
1179
+ this.updateMarker(marker.name, { range: markerRange });
1180
+ }
1181
+ }
1182
+ }
1183
+ }
1184
+ /**
1185
+ * Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.
1186
+ *
1187
+ * Because attribute operation needs to have the same attribute value on the whole range, this function splits
1188
+ * the range into smaller parts.
1189
+ *
1190
+ * Given `range` must be flat.
1191
+ */
1192
+ function setAttributeOnRange(writer, key, value, range) {
1193
+ const model = writer.model;
1194
+ const doc = model.document;
1195
+ // Position of the last split, the beginning of the new range.
1196
+ let lastSplitPosition = range.start;
1197
+ // Currently position in the scanning range. Because we need value after the position, it is not a current
1198
+ // position of the iterator but the previous one (we need to iterate one more time to get the value after).
1199
+ let position;
1200
+ // Value before the currently position.
1201
+ let valueBefore;
1202
+ // Value after the currently position.
1203
+ let valueAfter;
1204
+ for (const val of range.getWalker({ shallow: true })) {
1205
+ valueAfter = val.item.getAttribute(key);
1206
+ // At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
1207
+ // because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
1208
+ if (position && valueBefore != valueAfter) {
1209
+ // if valueBefore == value there is nothing to change, so we add operation only if these values are different.
1210
+ if (valueBefore != value) {
1211
+ addOperation();
1212
+ }
1213
+ lastSplitPosition = position;
1214
+ }
1215
+ position = val.nextPosition;
1216
+ valueBefore = valueAfter;
1217
+ }
1218
+ // Because position in the loop is not the iterator position (see let position comment), the last position in
1219
+ // the while loop will be last but one position in the range. We need to check the last position manually.
1220
+ if (position instanceof Position && position != lastSplitPosition && valueBefore != value) {
1221
+ addOperation();
1222
+ }
1223
+ function addOperation() {
1224
+ const range = new Range(lastSplitPosition, position);
1225
+ const version = range.root.document ? doc.version : null;
1226
+ const operation = new AttributeOperation(range, key, valueBefore, value, version);
1227
+ writer.batch.addOperation(operation);
1228
+ model.applyOperation(operation);
1229
+ }
1230
+ }
1231
+ /**
1232
+ * Sets given attribute to the given node. When attribute value is null then attribute will be removed.
1233
+ */
1234
+ function setAttributeOnItem(writer, key, value, item) {
1235
+ const model = writer.model;
1236
+ const doc = model.document;
1237
+ const previousValue = item.getAttribute(key);
1238
+ let range, operation;
1239
+ if (previousValue != value) {
1240
+ const isRootChanged = item.root === item;
1241
+ if (isRootChanged) {
1242
+ // If we change attributes of root element, we have to use `RootAttributeOperation`.
1243
+ const version = item.document ? doc.version : null;
1244
+ operation = new RootAttributeOperation(item, key, previousValue, value, version);
1245
+ }
1246
+ else {
1247
+ range = new Range(Position._createBefore(item), writer.createPositionAfter(item));
1248
+ const version = range.root.document ? doc.version : null;
1249
+ operation = new AttributeOperation(range, key, previousValue, value, version);
1250
+ }
1251
+ writer.batch.addOperation(operation);
1252
+ model.applyOperation(operation);
1253
+ }
1254
+ }
1255
+ /**
1256
+ * Creates and applies marker operation to {@link module:engine/model/operation/operation~Operation operation}.
1257
+ */
1258
+ function applyMarkerOperation(writer, name, oldRange, newRange, affectsData) {
1259
+ const model = writer.model;
1260
+ const doc = model.document;
1261
+ const operation = new MarkerOperation(name, oldRange, newRange, model.markers, !!affectsData, doc.version);
1262
+ writer.batch.addOperation(operation);
1263
+ model.applyOperation(operation);
1264
+ }
1265
+ /**
1266
+ * Creates `MoveOperation` or `DetachOperation` that removes `howMany` nodes starting from `position`.
1267
+ * The operation will be applied on given model instance and added to given operation instance.
1268
+ *
1269
+ * @param position Position from which nodes are removed.
1270
+ * @param howMany Number of nodes to remove.
1271
+ * @param batch Batch to which the operation will be added.
1272
+ * @param model Model instance on which operation will be applied.
1273
+ */
1274
+ function applyRemoveOperation(position, howMany, batch, model) {
1275
+ let operation;
1276
+ if (position.root.document) {
1277
+ const doc = model.document;
1278
+ const graveyardPosition = new Position(doc.graveyard, [0]);
1279
+ operation = new MoveOperation(position, howMany, graveyardPosition, doc.version);
1280
+ }
1281
+ else {
1282
+ operation = new DetachOperation(position, howMany);
1283
+ }
1284
+ batch.addOperation(operation);
1285
+ model.applyOperation(operation);
1286
+ }
1287
+ /**
1288
+ * Returns `true` if both root elements are the same element or both are documents root elements.
1289
+ *
1290
+ * Elements in the same tree can be moved (for instance you can move element form one documents root to another, or
1291
+ * within the same document fragment), but when element supposed to be moved from document fragment to the document, or
1292
+ * to another document it should be removed and inserted to avoid problems with OT. This is because features like undo or
1293
+ * collaboration may track changes on the document but ignore changes on detached fragments and should not get
1294
+ * unexpected `move` operation.
1295
+ */
1296
+ function isSameTree(rootA, rootB) {
1297
+ // If it is the same root this is the same tree.
1298
+ if (rootA === rootB) {
1299
+ return true;
1300
+ }
1301
+ // If both roots are documents root it is operation within the document what we still treat as the same tree.
1302
+ if (rootA instanceof RootElement && rootB instanceof RootElement) {
1303
+ return true;
1304
+ }
1305
+ return false;
1306
+ }