@ckeditor/ckeditor5-engine 40.0.0 → 40.2.0

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