@ckeditor/ckeditor5-engine 34.2.0 → 35.1.0

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