@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,11 +2,9 @@
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 module:engine/view/downcastwriter
8
7
  */
9
-
10
8
  import Position from './position';
11
9
  import Range from './range';
12
10
  import Selection from './selection';
@@ -21,7 +19,6 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
21
19
  import Text from './text';
22
20
  import EditableElement from './editableelement';
23
21
  import { isPlainObject } from 'lodash-es';
24
-
25
22
  /**
26
23
  * View downcast writer.
27
24
  *
@@ -38,1965 +35,1662 @@ import { isPlainObject } from 'lodash-es';
38
35
  * section of the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide.
39
36
  */
40
37
  export default class DowncastWriter {
41
- /**
42
- * @param {module:engine/view/document~Document} document The view document instance.
43
- */
44
- constructor( document ) {
45
- /**
46
- * The view document instance in which this writer operates.
47
- *
48
- * @readonly
49
- * @type {module:engine/view/document~Document}
50
- */
51
- this.document = document;
52
-
53
- /**
54
- * Holds references to the attribute groups that share the same {@link module:engine/view/attributeelement~AttributeElement#id id}.
55
- * The keys are `id`s, the values are `Set`s holding {@link module:engine/view/attributeelement~AttributeElement}s.
56
- *
57
- * @private
58
- * @type {Map.<String,Set>}
59
- */
60
- this._cloneGroups = new Map();
61
-
62
- /**
63
- * The slot factory used by the `elementToStructure` downcast helper.
64
- *
65
- * @private
66
- * @type {Function|null}
67
- */
68
- this._slotFactory = null;
69
- }
70
-
71
- /**
72
- * Sets {@link module:engine/view/documentselection~DocumentSelection selection's} ranges and direction to the
73
- * specified location based on the given {@link module:engine/view/selection~Selectable selectable}.
74
- *
75
- * Usage:
76
- *
77
- * // Sets selection to the given range.
78
- * const range = writer.createRange( start, end );
79
- * writer.setSelection( range );
80
- *
81
- * // Sets backward selection to the given range.
82
- * const range = writer.createRange( start, end );
83
- * writer.setSelection( range );
84
- *
85
- * // Sets selection to given ranges.
86
- * const ranges = [ writer.createRange( start1, end2 ), writer.createRange( start2, end2 ) ];
87
- * writer.setSelection( range );
88
- *
89
- * // Sets selection to the other selection.
90
- * const otherSelection = writer.createSelection();
91
- * writer.setSelection( otherSelection );
92
- *
93
- * // Sets collapsed selection at the given position.
94
- * const position = writer.createPositionFromPath( root, path );
95
- * writer.setSelection( position );
96
- *
97
- * // Sets collapsed selection at the position of given item and offset.
98
- * const paragraph = writer.createContainerElement( 'p' );
99
- * writer.setSelection( paragraph, offset );
100
- *
101
- * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
102
- * that element and ends after the last child of that element.
103
- *
104
- * writer.setSelection( paragraph, 'in' );
105
- *
106
- * Creates a range on the {@link module:engine/view/item~Item item} which starts before the item and ends just after the item.
107
- *
108
- * writer.setSelection( paragraph, 'on' );
109
- *
110
- * // Removes all ranges.
111
- * writer.setSelection( null );
112
- *
113
- * `DowncastWriter#setSelection()` allow passing additional options (`backward`, `fake` and `label`) as the last argument.
114
- *
115
- * // Sets selection as backward.
116
- * writer.setSelection( range, { backward: true } );
117
- *
118
- * // Sets selection as fake.
119
- * // Fake selection does not render as browser native selection over selected elements and is hidden to the user.
120
- * // This way, no native selection UI artifacts are displayed to the user and selection over elements can be
121
- * // represented in other way, for example by applying proper CSS class.
122
- * writer.setSelection( range, { fake: true } );
123
- *
124
- * // Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
125
- * // (and be properly handled by screen readers).
126
- * writer.setSelection( range, { fake: true, label: 'foo' } );
127
- *
128
- * @param {module:engine/view/selection~Selectable} selectable
129
- * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
130
- * @param {Object} [options]
131
- * @param {Boolean} [options.backward] Sets this selection instance to be backward.
132
- * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
133
- * @param {String} [options.label] Label for the fake selection.
134
- */
135
- setSelection( selectable, placeOrOffset, options ) {
136
- this.document.selection._setTo( selectable, placeOrOffset, options );
137
- }
138
-
139
- /**
140
- * Moves {@link module:engine/view/documentselection~DocumentSelection#focus selection's focus} to the specified location.
141
- *
142
- * The location can be specified in the same form as {@link module:engine/view/view~View#createPositionAt view.createPositionAt()}
143
- * parameters.
144
- *
145
- * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
146
- * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
147
- * first parameter is a {@link module:engine/view/item~Item view item}.
148
- */
149
- setSelectionFocus( itemOrPosition, offset ) {
150
- this.document.selection._setFocus( itemOrPosition, offset );
151
- }
152
-
153
- /**
154
- * Creates a new {@link module:engine/view/documentfragment~DocumentFragment} instance.
155
- *
156
- * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
157
- * A list of nodes to be inserted into the created document fragment.
158
- * @returns {module:engine/view/documentfragment~DocumentFragment} The created document fragment.
159
- */
160
- createDocumentFragment( children ) {
161
- return new DocumentFragment( this.document, children );
162
- }
163
-
164
- /**
165
- * Creates a new {@link module:engine/view/text~Text text node}.
166
- *
167
- * writer.createText( 'foo' );
168
- *
169
- * @param {String} data The text's data.
170
- * @returns {module:engine/view/text~Text} The created text node.
171
- */
172
- createText( data ) {
173
- return new Text( this.document, data );
174
- }
175
-
176
- /**
177
- * Creates a new {@link module:engine/view/attributeelement~AttributeElement}.
178
- *
179
- * writer.createAttributeElement( 'strong' );
180
- * writer.createAttributeElement( 'a', { href: 'foo.bar' } );
181
- *
182
- * // Make `<a>` element contain other attributes element so the `<a>` element is not broken.
183
- * writer.createAttributeElement( 'a', { href: 'foo.bar' }, { priority: 5 } );
184
- *
185
- * // Set `id` of a marker element so it is not joined or merged with "normal" elements.
186
- * writer.createAttributeElement( 'span', { class: 'my-marker' }, { id: 'marker:my' } );
187
- *
188
- * @param {String} name Name of the element.
189
- * @param {Object} [attributes] Element's attributes.
190
- * @param {Object} [options] Element's options.
191
- * @param {Number} [options.priority] Element's {@link module:engine/view/attributeelement~AttributeElement#priority priority}.
192
- * @param {Number|String} [options.id] Element's {@link module:engine/view/attributeelement~AttributeElement#id id}.
193
- * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
194
- * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
195
- * @returns {module:engine/view/attributeelement~AttributeElement} Created element.
196
- */
197
- createAttributeElement( name, attributes, options = {} ) {
198
- const attributeElement = new AttributeElement( this.document, name, attributes );
199
-
200
- if ( typeof options.priority === 'number' ) {
201
- attributeElement._priority = options.priority;
202
- }
203
-
204
- if ( options.id ) {
205
- attributeElement._id = options.id;
206
- }
207
-
208
- if ( options.renderUnsafeAttributes ) {
209
- attributeElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
210
- }
211
-
212
- return attributeElement;
213
- }
214
-
215
- /**
216
- * Creates a new {@link module:engine/view/containerelement~ContainerElement}.
217
- *
218
- * writer.createContainerElement( 'p' );
219
- *
220
- * // Create element with custom attributes.
221
- * writer.createContainerElement( 'div', { id: 'foo-bar', 'data-baz': '123' } );
222
- *
223
- * // Create element with custom styles.
224
- * writer.createContainerElement( 'p', { style: 'font-weight: bold; padding-bottom: 10px' } );
225
- *
226
- * // Create element with custom classes.
227
- * writer.createContainerElement( 'p', { class: 'foo bar baz' } );
228
- *
229
- * // Create element with children.
230
- * writer.createContainerElement( 'figure', { class: 'image' }, [
231
- * writer.createEmptyElement( 'img' ),
232
- * writer.createContainerElement( 'figcaption' )
233
- * ] );
234
- *
235
- * // Create element with specific options.
236
- * writer.createContainerElement( 'span', { class: 'placeholder' }, { renderUnsafeAttributes: [ 'foo' ] } );
237
- *
238
- * @param {String} name Name of the element.
239
- * @param {Object} [attributes] Elements attributes.
240
- * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>|Object} [childrenOrOptions]
241
- * A node or a list of nodes to be inserted into the created element. If no children were specified, element's `options`
242
- * can be passed in this argument.
243
- * @param {Object} [options] Element's options.
244
- * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
245
- * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
246
- * @returns {module:engine/view/containerelement~ContainerElement} Created element.
247
- */
248
- createContainerElement( name, attributes, childrenOrOptions = {}, options = {} ) {
249
- let children = null;
250
-
251
- if ( isPlainObject( childrenOrOptions ) ) {
252
- options = childrenOrOptions;
253
- } else {
254
- children = childrenOrOptions;
255
- }
256
-
257
- const containerElement = new ContainerElement( this.document, name, attributes, children );
258
-
259
- if ( options.renderUnsafeAttributes ) {
260
- containerElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
261
- }
262
-
263
- return containerElement;
264
- }
265
-
266
- /**
267
- * Creates a new {@link module:engine/view/editableelement~EditableElement}.
268
- *
269
- * writer.createEditableElement( 'div' );
270
- * writer.createEditableElement( 'div', { id: 'foo-1234' } );
271
- *
272
- * Note: The editable element is to be used in the editing pipeline. Usually, together with
273
- * {@link module:widget/utils~toWidgetEditable `toWidgetEditable()`}.
274
- *
275
- * @param {String} name Name of the element.
276
- * @param {Object} [attributes] Elements attributes.
277
- * @param {Object} [options] Element's options.
278
- * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
279
- * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
280
- * @returns {module:engine/view/editableelement~EditableElement} Created element.
281
- */
282
- createEditableElement( name, attributes, options = {} ) {
283
- const editableElement = new EditableElement( this.document, name, attributes );
284
- editableElement._document = this.document;
285
-
286
- if ( options.renderUnsafeAttributes ) {
287
- editableElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
288
- }
289
-
290
- return editableElement;
291
- }
292
-
293
- /**
294
- * Creates a new {@link module:engine/view/emptyelement~EmptyElement}.
295
- *
296
- * writer.createEmptyElement( 'img' );
297
- * writer.createEmptyElement( 'img', { id: 'foo-1234' } );
298
- *
299
- * @param {String} name Name of the element.
300
- * @param {Object} [attributes] Elements attributes.
301
- * @param {Object} [options] Element's options.
302
- * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
303
- * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
304
- * @returns {module:engine/view/emptyelement~EmptyElement} Created element.
305
- */
306
- createEmptyElement( name, attributes, options = {} ) {
307
- const emptyElement = new EmptyElement( this.document, name, attributes );
308
-
309
- if ( options.renderUnsafeAttributes ) {
310
- emptyElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
311
- }
312
-
313
- return emptyElement;
314
- }
315
-
316
- /**
317
- * Creates a new {@link module:engine/view/uielement~UIElement}.
318
- *
319
- * writer.createUIElement( 'span' );
320
- * writer.createUIElement( 'span', { id: 'foo-1234' } );
321
- *
322
- * A custom render function can be provided as the third parameter:
323
- *
324
- * writer.createUIElement( 'span', null, function( domDocument ) {
325
- * const domElement = this.toDomElement( domDocument );
326
- * domElement.innerHTML = '<b>this is ui element</b>';
327
- *
328
- * return domElement;
329
- * } );
330
- *
331
- * Unlike {@link #createRawElement raw elements}, UI elements are by no means editor content, for instance,
332
- * they are ignored by the editor selection system.
333
- *
334
- * You should not use UI elements as data containers. Check out {@link #createRawElement} instead.
335
- *
336
- * @param {String} name The name of the element.
337
- * @param {Object} [attributes] Element attributes.
338
- * @param {Function} [renderFunction] A custom render function.
339
- * @returns {module:engine/view/uielement~UIElement} The created element.
340
- */
341
- createUIElement( name, attributes, renderFunction ) {
342
- const uiElement = new UIElement( this.document, name, attributes );
343
-
344
- if ( renderFunction ) {
345
- uiElement.render = renderFunction;
346
- }
347
-
348
- return uiElement;
349
- }
350
-
351
- /**
352
- * Creates a new {@link module:engine/view/rawelement~RawElement}.
353
- *
354
- * writer.createRawElement( 'span', { id: 'foo-1234' }, function( domElement ) {
355
- * domElement.innerHTML = '<b>This is the raw content of the raw element.</b>';
356
- * } );
357
- *
358
- * Raw elements work as data containers ("wrappers", "sandboxes") but their children are not managed or
359
- * even recognized by the editor. This encapsulation allows integrations to maintain custom DOM structures
360
- * in the editor content without, for instance, worrying about compatibility with other editor features.
361
- * Raw elements are a perfect tool for integration with external frameworks and data sources.
362
- *
363
- * Unlike {@link #createUIElement UI elements}, raw elements act like "real" editor content (similar to
364
- * {@link module:engine/view/containerelement~ContainerElement} or {@link module:engine/view/emptyelement~EmptyElement}),
365
- * and they are considered by the editor selection.
366
- *
367
- * You should not use raw elements to render the UI in the editor content. Check out {@link #createUIElement `#createUIElement()`}
368
- * instead.
369
- *
370
- * @param {String} name The name of the element.
371
- * @param {Object} [attributes] Element attributes.
372
- * @param {Function} [renderFunction] A custom render function.
373
- * @param {Object} [options] Element's options.
374
- * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
375
- * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
376
- * @returns {module:engine/view/rawelement~RawElement} The created element.
377
- */
378
- createRawElement( name, attributes, renderFunction, options = {} ) {
379
- const rawElement = new RawElement( this.document, name, attributes );
380
-
381
- rawElement.render = renderFunction || ( () => {} );
382
-
383
- if ( options.renderUnsafeAttributes ) {
384
- rawElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
385
- }
386
-
387
- return rawElement;
388
- }
389
-
390
- /**
391
- * Adds or overwrites the element's attribute with a specified key and value.
392
- *
393
- * writer.setAttribute( 'href', 'http://ckeditor.com', linkElement );
394
- *
395
- * @param {String} key The attribute key.
396
- * @param {String} value The attribute value.
397
- * @param {module:engine/view/element~Element} element
398
- */
399
- setAttribute( key, value, element ) {
400
- element._setAttribute( key, value );
401
- }
402
-
403
- /**
404
- * Removes attribute from the element.
405
- *
406
- * writer.removeAttribute( 'href', linkElement );
407
- *
408
- * @param {String} key Attribute key.
409
- * @param {module:engine/view/element~Element} element
410
- */
411
- removeAttribute( key, element ) {
412
- element._removeAttribute( key );
413
- }
414
-
415
- /**
416
- * Adds specified class to the element.
417
- *
418
- * writer.addClass( 'foo', linkElement );
419
- * writer.addClass( [ 'foo', 'bar' ], linkElement );
420
- *
421
- * @param {Array.<String>|String} className
422
- * @param {module:engine/view/element~Element} element
423
- */
424
- addClass( className, element ) {
425
- element._addClass( className );
426
- }
427
-
428
- /**
429
- * Removes specified class from the element.
430
- *
431
- * writer.removeClass( 'foo', linkElement );
432
- * writer.removeClass( [ 'foo', 'bar' ], linkElement );
433
- *
434
- * @param {Array.<String>|String} className
435
- * @param {module:engine/view/element~Element} element
436
- */
437
- removeClass( className, element ) {
438
- element._removeClass( className );
439
- }
440
-
441
- /**
442
- * Adds style to the element.
443
- *
444
- * writer.setStyle( 'color', 'red', element );
445
- * writer.setStyle( {
446
- * color: 'red',
447
- * position: 'fixed'
448
- * }, element );
449
- *
450
- * **Note**: The passed style can be normalized if
451
- * {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
452
- * See {@link module:engine/view/stylesmap~StylesMap#set `StylesMap#set()`} for details.
453
- *
454
- * @param {String|Object} property Property name or object with key - value pairs.
455
- * @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter.
456
- * @param {module:engine/view/element~Element} element Element to set styles on.
457
- */
458
- setStyle( property, value, element ) {
459
- if ( isPlainObject( property ) && element === undefined ) {
460
- element = value;
461
- }
462
-
463
- element._setStyle( property, value );
464
- }
465
-
466
- /**
467
- * Removes specified style from the element.
468
- *
469
- * writer.removeStyle( 'color', element ); // Removes 'color' style.
470
- * writer.removeStyle( [ 'color', 'border-top' ], element ); // Removes both 'color' and 'border-top' styles.
471
- *
472
- * **Note**: This method can work with normalized style names if
473
- * {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
474
- * See {@link module:engine/view/stylesmap~StylesMap#remove `StylesMap#remove()`} for details.
475
- *
476
- * @param {Array.<String>|String} property
477
- * @param {module:engine/view/element~Element} element
478
- */
479
- removeStyle( property, element ) {
480
- element._removeStyle( property );
481
- }
482
-
483
- /**
484
- * Sets a custom property on element. Unlike attributes, custom properties are not rendered to the DOM,
485
- * so they can be used to add special data to elements.
486
- *
487
- * @param {String|Symbol} key
488
- * @param {*} value
489
- * @param {module:engine/view/element~Element} element
490
- */
491
- setCustomProperty( key, value, element ) {
492
- element._setCustomProperty( key, value );
493
- }
494
-
495
- /**
496
- * Removes a custom property stored under the given key.
497
- *
498
- * @param {String|Symbol} key
499
- * @param {module:engine/view/element~Element} element
500
- * @returns {Boolean} Returns true if property was removed.
501
- */
502
- removeCustomProperty( key, element ) {
503
- return element._removeCustomProperty( key );
504
- }
505
-
506
- /**
507
- * Breaks attribute elements at the provided position or at the boundaries of a provided range. It breaks attribute elements
508
- * up to their first ancestor that is a container element.
509
- *
510
- * In following examples `<p>` is a container, `<b>` and `<u>` are attribute elements:
511
- *
512
- * <p>foo<b><u>bar{}</u></b></p> -> <p>foo<b><u>bar</u></b>[]</p>
513
- * <p>foo<b><u>{}bar</u></b></p> -> <p>foo{}<b><u>bar</u></b></p>
514
- * <p>foo<b><u>b{}ar</u></b></p> -> <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
515
- * <p><b>fo{o</b><u>ba}r</u></p> -> <p><b>fo</b><b>o</b><u>ba</u><u>r</u></b></p>
516
- *
517
- * **Note:** {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
518
- *
519
- * **Note:** The difference between {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breakAttributes()} and
520
- * {@link module:engine/view/downcastwriter~DowncastWriter#breakContainer breakContainer()} is that `breakAttributes()` breaks all
521
- * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of a given `position`,
522
- * up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
523
- * `breakContainer()` assumes that a given `position` is directly in the container element and breaks that container element.
524
- *
525
- * Throws the `view-writer-invalid-range-container` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
526
- * when the {@link module:engine/view/range~Range#start start}
527
- * and {@link module:engine/view/range~Range#end end} positions of a passed range are not placed inside same parent container.
528
- *
529
- * Throws the `view-writer-cannot-break-empty-element` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
530
- * when trying to break attributes inside an {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
531
- *
532
- * Throws the `view-writer-cannot-break-ui-element` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
533
- * when trying to break attributes inside a {@link module:engine/view/uielement~UIElement UIElement}.
534
- *
535
- * @see module:engine/view/attributeelement~AttributeElement
536
- * @see module:engine/view/containerelement~ContainerElement
537
- * @see module:engine/view/downcastwriter~DowncastWriter#breakContainer
538
- * @param {module:engine/view/position~Position|module:engine/view/range~Range} positionOrRange The position where
539
- * to break attribute elements.
540
- * @returns {module:engine/view/position~Position|module:engine/view/range~Range} The new position or range, after breaking the
541
- * attribute elements.
542
- */
543
- breakAttributes( positionOrRange ) {
544
- if ( positionOrRange instanceof Position ) {
545
- return this._breakAttributes( positionOrRange );
546
- } else {
547
- return this._breakAttributesRange( positionOrRange );
548
- }
549
- }
550
-
551
- /**
552
- * Breaks a {@link module:engine/view/containerelement~ContainerElement container view element} into two, at the given position.
553
- * The position has to be directly inside the container element and cannot be in the root. It does not break the conrainer view element
554
- * if the position is at the beginning or at the end of its parent element.
555
- *
556
- * <p>foo^bar</p> -> <p>foo</p><p>bar</p>
557
- * <div><p>foo</p>^<p>bar</p></div> -> <div><p>foo</p></div><div><p>bar</p></div>
558
- * <p>^foobar</p> -> ^<p>foobar</p>
559
- * <p>foobar^</p> -> <p>foobar</p>^
560
- *
561
- * **Note:** The difference between {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breakAttributes()} and
562
- * {@link module:engine/view/downcastwriter~DowncastWriter#breakContainer breakContainer()} is that `breakAttributes()` breaks all
563
- * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of a given `position`,
564
- * up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
565
- * `breakContainer()` assumes that the given `position` is directly in the container element and breaks that container element.
566
- *
567
- * @see module:engine/view/attributeelement~AttributeElement
568
- * @see module:engine/view/containerelement~ContainerElement
569
- * @see module:engine/view/downcastwriter~DowncastWriter#breakAttributes
570
- * @param {module:engine/view/position~Position} position The position where to break the element.
571
- * @returns {module:engine/view/position~Position} The position between broken elements. If an element has not been broken,
572
- * the returned position is placed either before or after it.
573
- */
574
- breakContainer( position ) {
575
- const element = position.parent;
576
-
577
- if ( !( element.is( 'containerElement' ) ) ) {
578
- /**
579
- * Trying to break an element which is not a container element.
580
- *
581
- * @error view-writer-break-non-container-element
582
- */
583
- throw new CKEditorError( 'view-writer-break-non-container-element', this.document );
584
- }
585
-
586
- if ( !element.parent ) {
587
- /**
588
- * Trying to break root element.
589
- *
590
- * @error view-writer-break-root
591
- */
592
- throw new CKEditorError( 'view-writer-break-root', this.document );
593
- }
594
-
595
- if ( position.isAtStart ) {
596
- return Position._createBefore( element );
597
- } else if ( !position.isAtEnd ) {
598
- const newElement = element._clone( false );
599
-
600
- this.insert( Position._createAfter( element ), newElement );
601
-
602
- const sourceRange = new Range( position, Position._createAt( element, 'end' ) );
603
- const targetPosition = new Position( newElement, 0 );
604
-
605
- this.move( sourceRange, targetPosition );
606
- }
607
-
608
- return Position._createAfter( element );
609
- }
610
-
611
- /**
612
- * Merges {@link module:engine/view/attributeelement~AttributeElement attribute elements}. It also merges text nodes if needed.
613
- * Only {@link module:engine/view/attributeelement~AttributeElement#isSimilar similar} attribute elements can be merged.
614
- *
615
- * In following examples `<p>` is a container and `<b>` is an attribute element:
616
- *
617
- * <p>foo[]bar</p> -> <p>foo{}bar</p>
618
- * <p><b>foo</b>[]<b>bar</b></p> -> <p><b>foo{}bar</b></p>
619
- * <p><b foo="bar">a</b>[]<b foo="baz">b</b></p> -> <p><b foo="bar">a</b>[]<b foo="baz">b</b></p>
620
- *
621
- * It will also take care about empty attributes when merging:
622
- *
623
- * <p><b>[]</b></p> -> <p>[]</p>
624
- * <p><b>foo</b><i>[]</i><b>bar</b></p> -> <p><b>foo{}bar</b></p>
625
- *
626
- * **Note:** Difference between {@link module:engine/view/downcastwriter~DowncastWriter#mergeAttributes mergeAttributes} and
627
- * {@link module:engine/view/downcastwriter~DowncastWriter#mergeContainers mergeContainers} is that `mergeAttributes` merges two
628
- * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
629
- * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
630
- *
631
- * @see module:engine/view/attributeelement~AttributeElement
632
- * @see module:engine/view/containerelement~ContainerElement
633
- * @see module:engine/view/downcastwriter~DowncastWriter#mergeContainers
634
- * @param {module:engine/view/position~Position} position Merge position.
635
- * @returns {module:engine/view/position~Position} Position after merge.
636
- */
637
- mergeAttributes( position ) {
638
- const positionOffset = position.offset;
639
- const positionParent = position.parent;
640
-
641
- // When inside text node - nothing to merge.
642
- if ( positionParent.is( '$text' ) ) {
643
- return position;
644
- }
645
-
646
- // When inside empty attribute - remove it.
647
- if ( positionParent.is( 'attributeElement' ) && positionParent.childCount === 0 ) {
648
- const parent = positionParent.parent;
649
- const offset = positionParent.index;
650
-
651
- positionParent._remove();
652
- this._removeFromClonedElementsGroup( positionParent );
653
-
654
- return this.mergeAttributes( new Position( parent, offset ) );
655
- }
656
-
657
- const nodeBefore = positionParent.getChild( positionOffset - 1 );
658
- const nodeAfter = positionParent.getChild( positionOffset );
659
-
660
- // Position should be placed between two nodes.
661
- if ( !nodeBefore || !nodeAfter ) {
662
- return position;
663
- }
664
-
665
- // When position is between two text nodes.
666
- if ( nodeBefore.is( '$text' ) && nodeAfter.is( '$text' ) ) {
667
- return mergeTextNodes( nodeBefore, nodeAfter );
668
- }
669
- // When position is between two same attribute elements.
670
- else if ( nodeBefore.is( 'attributeElement' ) && nodeAfter.is( 'attributeElement' ) && nodeBefore.isSimilar( nodeAfter ) ) {
671
- // Move all children nodes from node placed after selection and remove that node.
672
- const count = nodeBefore.childCount;
673
- nodeBefore._appendChild( nodeAfter.getChildren() );
674
-
675
- nodeAfter._remove();
676
- this._removeFromClonedElementsGroup( nodeAfter );
677
-
678
- // New position is located inside the first node, before new nodes.
679
- // Call this method recursively to merge again if needed.
680
- return this.mergeAttributes( new Position( nodeBefore, count ) );
681
- }
682
-
683
- return position;
684
- }
685
-
686
- /**
687
- * Merges two {@link module:engine/view/containerelement~ContainerElement container elements} that are before and after given position.
688
- * Precisely, the element after the position is removed and it's contents are moved to element before the position.
689
- *
690
- * <p>foo</p>^<p>bar</p> -> <p>foo^bar</p>
691
- * <div>foo</div>^<p>bar</p> -> <div>foo^bar</div>
692
- *
693
- * **Note:** Difference between {@link module:engine/view/downcastwriter~DowncastWriter#mergeAttributes mergeAttributes} and
694
- * {@link module:engine/view/downcastwriter~DowncastWriter#mergeContainers mergeContainers} is that `mergeAttributes` merges two
695
- * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
696
- * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
697
- *
698
- * @see module:engine/view/attributeelement~AttributeElement
699
- * @see module:engine/view/containerelement~ContainerElement
700
- * @see module:engine/view/downcastwriter~DowncastWriter#mergeAttributes
701
- * @param {module:engine/view/position~Position} position Merge position.
702
- * @returns {module:engine/view/position~Position} Position after merge.
703
- */
704
- mergeContainers( position ) {
705
- const prev = position.nodeBefore;
706
- const next = position.nodeAfter;
707
-
708
- if ( !prev || !next || !prev.is( 'containerElement' ) || !next.is( 'containerElement' ) ) {
709
- /**
710
- * Element before and after given position cannot be merged.
711
- *
712
- * @error view-writer-merge-containers-invalid-position
713
- */
714
- throw new CKEditorError( 'view-writer-merge-containers-invalid-position', this.document );
715
- }
716
-
717
- const lastChild = prev.getChild( prev.childCount - 1 );
718
- const newPosition = lastChild instanceof Text ? Position._createAt( lastChild, 'end' ) : Position._createAt( prev, 'end' );
719
-
720
- this.move( Range._createIn( next ), Position._createAt( prev, 'end' ) );
721
- this.remove( Range._createOn( next ) );
722
-
723
- return newPosition;
724
- }
725
-
726
- /**
727
- * Inserts a node or nodes at specified position. Takes care about breaking attributes before insertion
728
- * and merging them afterwards.
729
- *
730
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
731
- * contains instances that are not {@link module:engine/view/text~Text Texts},
732
- * {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
733
- * {@link module:engine/view/containerelement~ContainerElement ContainerElements},
734
- * {@link module:engine/view/emptyelement~EmptyElement EmptyElements},
735
- * {@link module:engine/view/rawelement~RawElement RawElements} or
736
- * {@link module:engine/view/uielement~UIElement UIElements}.
737
- *
738
- * @param {module:engine/view/position~Position} position Insertion position.
739
- * @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
740
- * module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
741
- * module:engine/view/rawelement~RawElement|module:engine/view/uielement~UIElement|
742
- * Iterable.<module:engine/view/text~Text|
743
- * module:engine/view/attributeelement~AttributeElement|module:engine/view/containerelement~ContainerElement|
744
- * module:engine/view/emptyelement~EmptyElement|module:engine/view/rawelement~RawElement|
745
- * module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
746
- * @returns {module:engine/view/range~Range} Range around inserted nodes.
747
- */
748
- insert( position, nodes ) {
749
- nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ];
750
-
751
- // Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
752
- validateNodesToInsert( nodes, this.document );
753
-
754
- // Group nodes in batches of nodes that require or do not require breaking an AttributeElements.
755
- const nodeGroups = nodes.reduce( ( groups, node ) => {
756
- const lastGroup = groups[ groups.length - 1 ];
757
-
758
- // Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
759
- // can't have an attribute in model and won't get wrapped with an AttributeElement while down-casted.
760
- const breakAttributes = !node.is( 'uiElement' );
761
-
762
- if ( !lastGroup || lastGroup.breakAttributes != breakAttributes ) {
763
- groups.push( {
764
- breakAttributes,
765
- nodes: [ node ]
766
- } );
767
- } else {
768
- lastGroup.nodes.push( node );
769
- }
770
-
771
- return groups;
772
- }, [] );
773
-
774
- // Insert nodes in batches.
775
- let start = null;
776
- let end = position;
777
-
778
- for ( const { nodes, breakAttributes } of nodeGroups ) {
779
- const range = this._insertNodes( end, nodes, breakAttributes );
780
-
781
- if ( !start ) {
782
- start = range.start;
783
- }
784
-
785
- end = range.end;
786
- }
787
-
788
- // When no nodes were inserted - return collapsed range.
789
- if ( !start ) {
790
- return new Range( position );
791
- }
792
-
793
- return new Range( start, end );
794
- }
795
-
796
- /**
797
- * Removes provided range from the container.
798
- *
799
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
800
- * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
801
- * same parent container.
802
- *
803
- * @param {module:engine/view/range~Range|module:engine/view/item~Item} rangeOrItem Range to remove from container
804
- * or an {@link module:engine/view/item~Item item} to remove. If range is provided, after removing, it will be updated
805
- * to a collapsed range showing the new position.
806
- * @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
807
- */
808
- remove( rangeOrItem ) {
809
- const range = rangeOrItem instanceof Range ? rangeOrItem : Range._createOn( rangeOrItem );
810
-
811
- validateRangeContainer( range, this.document );
812
-
813
- // If range is collapsed - nothing to remove.
814
- if ( range.isCollapsed ) {
815
- return new DocumentFragment( this.document );
816
- }
817
-
818
- // Break attributes at range start and end.
819
- const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
820
- const parentContainer = breakStart.parent;
821
-
822
- const count = breakEnd.offset - breakStart.offset;
823
-
824
- // Remove nodes in range.
825
- const removed = parentContainer._removeChildren( breakStart.offset, count );
826
-
827
- for ( const node of removed ) {
828
- this._removeFromClonedElementsGroup( node );
829
- }
830
-
831
- // Merge after removing.
832
- const mergePosition = this.mergeAttributes( breakStart );
833
- range.start = mergePosition;
834
- range.end = mergePosition.clone();
835
-
836
- // Return removed nodes.
837
- return new DocumentFragment( this.document, removed );
838
- }
839
-
840
- /**
841
- * Removes matching elements from given range.
842
- *
843
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
844
- * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
845
- * same parent container.
846
- *
847
- * @param {module:engine/view/range~Range} range Range to clear.
848
- * @param {module:engine/view/element~Element} element Element to remove.
849
- */
850
- clear( range, element ) {
851
- validateRangeContainer( range, this.document );
852
-
853
- // Create walker on given range.
854
- // We walk backward because when we remove element during walk it modifies range end position.
855
- const walker = range.getWalker( {
856
- direction: 'backward',
857
- ignoreElementEnd: true
858
- } );
859
-
860
- // Let's walk.
861
- for ( const current of walker ) {
862
- const item = current.item;
863
- let rangeToRemove;
864
-
865
- // When current item matches to the given element.
866
- if ( item.is( 'element' ) && element.isSimilar( item ) ) {
867
- // Create range on this element.
868
- rangeToRemove = Range._createOn( item );
869
- // When range starts inside Text or TextProxy element.
870
- } else if ( !current.nextPosition.isAfter( range.start ) && item.is( '$textProxy' ) ) {
871
- // We need to check if parent of this text matches to given element.
872
- const parentElement = item.getAncestors().find( ancestor => {
873
- return ancestor.is( 'element' ) && element.isSimilar( ancestor );
874
- } );
875
-
876
- // If it is then create range inside this element.
877
- if ( parentElement ) {
878
- rangeToRemove = Range._createIn( parentElement );
879
- }
880
- }
881
-
882
- // If we have found element to remove.
883
- if ( rangeToRemove ) {
884
- // We need to check if element range stick out of the given range and truncate if it is.
885
- if ( rangeToRemove.end.isAfter( range.end ) ) {
886
- rangeToRemove.end = range.end;
887
- }
888
-
889
- if ( rangeToRemove.start.isBefore( range.start ) ) {
890
- rangeToRemove.start = range.start;
891
- }
892
-
893
- // At the end we remove range with found element.
894
- this.remove( rangeToRemove );
895
- }
896
- }
897
- }
898
-
899
- /**
900
- * Moves nodes from provided range to target position.
901
- *
902
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
903
- * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
904
- * same parent container.
905
- *
906
- * @param {module:engine/view/range~Range} sourceRange Range containing nodes to move.
907
- * @param {module:engine/view/position~Position} targetPosition Position to insert.
908
- * @returns {module:engine/view/range~Range} Range in target container. Inserted nodes are placed between
909
- * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions.
910
- */
911
- move( sourceRange, targetPosition ) {
912
- let nodes;
913
-
914
- if ( targetPosition.isAfter( sourceRange.end ) ) {
915
- targetPosition = this._breakAttributes( targetPosition, true );
916
-
917
- const parent = targetPosition.parent;
918
- const countBefore = parent.childCount;
919
-
920
- sourceRange = this._breakAttributesRange( sourceRange, true );
921
-
922
- nodes = this.remove( sourceRange );
923
-
924
- targetPosition.offset += ( parent.childCount - countBefore );
925
- } else {
926
- nodes = this.remove( sourceRange );
927
- }
928
-
929
- return this.insert( targetPosition, nodes );
930
- }
931
-
932
- /**
933
- * Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
934
- * If a collapsed range is provided, it will be wrapped only if it is equal to view selection.
935
- *
936
- * If a collapsed range was passed and is same as selection, the selection
937
- * will be moved to the inside of the wrapped attribute element.
938
- *
939
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
940
- * when {@link module:engine/view/range~Range#start}
941
- * and {@link module:engine/view/range~Range#end} positions are not placed inside same parent container.
942
- *
943
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
944
- * an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
945
- *
946
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-nonselection-collapsed-range` when passed range
947
- * is collapsed and different than view selection.
948
- *
949
- * @param {module:engine/view/range~Range} range Range to wrap.
950
- * @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
951
- * @returns {module:engine/view/range~Range} range Range after wrapping, spanning over wrapping attribute element.
952
- */
953
- wrap( range, attribute ) {
954
- if ( !( attribute instanceof AttributeElement ) ) {
955
- throw new CKEditorError(
956
- 'view-writer-wrap-invalid-attribute',
957
- this.document
958
- );
959
- }
960
-
961
- validateRangeContainer( range, this.document );
962
-
963
- if ( !range.isCollapsed ) {
964
- // Non-collapsed range. Wrap it with the attribute element.
965
- return this._wrapRange( range, attribute );
966
- } else {
967
- // Collapsed range. Wrap position.
968
- let position = range.start;
969
-
970
- if ( position.parent.is( 'element' ) && !_hasNonUiChildren( position.parent ) ) {
971
- position = position.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
972
- }
973
-
974
- position = this._wrapPosition( position, attribute );
975
- const viewSelection = this.document.selection;
976
-
977
- // If wrapping position is equal to view selection, move view selection inside wrapping attribute element.
978
- if ( viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual( range.start ) ) {
979
- this.setSelection( position );
980
- }
981
-
982
- return new Range( position );
983
- }
984
- }
985
-
986
- /**
987
- * Unwraps nodes within provided range from attribute element.
988
- *
989
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
990
- * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
991
- * same parent container.
992
- *
993
- * @param {module:engine/view/range~Range} range
994
- * @param {module:engine/view/attributeelement~AttributeElement} attribute
995
- */
996
- unwrap( range, attribute ) {
997
- if ( !( attribute instanceof AttributeElement ) ) {
998
- /**
999
- * The `attribute` passed to {@link module:engine/view/downcastwriter~DowncastWriter#unwrap `DowncastWriter#unwrap()`}
1000
- * must be an instance of {@link module:engine/view/attributeelement~AttributeElement `AttributeElement`}.
1001
- *
1002
- * @error view-writer-unwrap-invalid-attribute
1003
- */
1004
- throw new CKEditorError(
1005
- 'view-writer-unwrap-invalid-attribute',
1006
- this.document
1007
- );
1008
- }
1009
-
1010
- validateRangeContainer( range, this.document );
1011
-
1012
- // If range is collapsed - nothing to unwrap.
1013
- if ( range.isCollapsed ) {
1014
- return range;
1015
- }
1016
-
1017
- // Break attributes at range start and end.
1018
- const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
1019
- const parentContainer = breakStart.parent;
1020
-
1021
- // Unwrap children located between break points.
1022
- const newRange = this._unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
1023
-
1024
- // Merge attributes at the both ends and return a new range.
1025
- const start = this.mergeAttributes( newRange.start );
1026
-
1027
- // If start position was merged - move end position back.
1028
- if ( !start.isEqual( newRange.start ) ) {
1029
- newRange.end.offset--;
1030
- }
1031
-
1032
- const end = this.mergeAttributes( newRange.end );
1033
-
1034
- return new Range( start, end );
1035
- }
1036
-
1037
- /**
1038
- * Renames element by creating a copy of renamed element but with changed name and then moving contents of the
1039
- * old element to the new one. Keep in mind that this will invalidate all {@link module:engine/view/position~Position positions} which
1040
- * has renamed element as {@link module:engine/view/position~Position#parent a parent}.
1041
- *
1042
- * New element has to be created because `Element#tagName` property in DOM is readonly.
1043
- *
1044
- * Since this function creates a new element and removes the given one, the new element is returned to keep reference.
1045
- *
1046
- * @param {String} newName New name for element.
1047
- * @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed.
1048
- * @returns {module:engine/view/containerelement~ContainerElement} Element created due to rename.
1049
- */
1050
- rename( newName, viewElement ) {
1051
- const newElement = new ContainerElement( this.document, newName, viewElement.getAttributes() );
1052
-
1053
- this.insert( Position._createAfter( viewElement ), newElement );
1054
- this.move( Range._createIn( viewElement ), Position._createAt( newElement, 0 ) );
1055
- this.remove( Range._createOn( viewElement ) );
1056
-
1057
- return newElement;
1058
- }
1059
-
1060
- /**
1061
- * Cleans up memory by removing obsolete cloned elements group from the writer.
1062
- *
1063
- * Should be used whenever all {@link module:engine/view/attributeelement~AttributeElement attribute elements}
1064
- * with the same {@link module:engine/view/attributeelement~AttributeElement#id id} are going to be removed from the view and
1065
- * the group will no longer be needed.
1066
- *
1067
- * Cloned elements group are not removed automatically in case if the group is still needed after all its elements
1068
- * were removed from the view.
1069
- *
1070
- * Keep in mind that group names are equal to the `id` property of the attribute element.
1071
- *
1072
- * @param {String} groupName Name of the group to clear.
1073
- */
1074
- clearClonedElementsGroup( groupName ) {
1075
- this._cloneGroups.delete( groupName );
1076
- }
1077
-
1078
- /**
1079
- * Creates position at the given location. The location can be specified as:
1080
- *
1081
- * * a {@link module:engine/view/position~Position position},
1082
- * * parent element and offset (offset defaults to `0`),
1083
- * * parent element and `'end'` (sets position at the end of that element),
1084
- * * {@link module:engine/view/item~Item view item} and `'before'` or `'after'` (sets position before or after given view item).
1085
- *
1086
- * This method is a shortcut to other constructors such as:
1087
- *
1088
- * * {@link #createPositionBefore},
1089
- * * {@link #createPositionAfter},
1090
- *
1091
- * @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
1092
- * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
1093
- * first parameter is a {@link module:engine/view/item~Item view item}.
1094
- * @returns {module:engine/view/position~Position}
1095
- */
1096
- createPositionAt( itemOrPosition, offset ) {
1097
- return Position._createAt( itemOrPosition, offset );
1098
- }
1099
-
1100
- /**
1101
- * Creates a new position after given view item.
1102
- *
1103
- * @param {module:engine/view/item~Item} item View item after which the position should be located.
1104
- * @returns {module:engine/view/position~Position}
1105
- */
1106
- createPositionAfter( item ) {
1107
- return Position._createAfter( item );
1108
- }
1109
-
1110
- /**
1111
- * Creates a new position before given view item.
1112
- *
1113
- * @param {module:engine/view/item~Item} item View item before which the position should be located.
1114
- * @returns {module:engine/view/position~Position}
1115
- */
1116
- createPositionBefore( item ) {
1117
- return Position._createBefore( item );
1118
- }
1119
-
1120
- /**
1121
- * Creates a range spanning from `start` position to `end` position.
1122
- *
1123
- * **Note:** This factory method creates its own {@link module:engine/view/position~Position} instances basing on passed values.
1124
- *
1125
- * @param {module:engine/view/position~Position} start Start position.
1126
- * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
1127
- * @returns {module:engine/view/range~Range}
1128
- */
1129
- createRange( start, end ) {
1130
- return new Range( start, end );
1131
- }
1132
-
1133
- /**
1134
- * Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
1135
- *
1136
- * @param {module:engine/view/item~Item} item
1137
- * @returns {module:engine/view/range~Range}
1138
- */
1139
- createRangeOn( item ) {
1140
- return Range._createOn( item );
1141
- }
1142
-
1143
- /**
1144
- * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
1145
- * that element and ends after the last child of that element.
1146
- *
1147
- * @param {module:engine/view/element~Element} element Element which is a parent for the range.
1148
- * @returns {module:engine/view/range~Range}
1149
- */
1150
- createRangeIn( element ) {
1151
- return Range._createIn( element );
1152
- }
1153
-
1154
- /**
1155
- * Creates new {@link module:engine/view/selection~Selection} instance.
1156
- *
1157
- * // Creates empty selection without ranges.
1158
- * const selection = writer.createSelection();
1159
- *
1160
- * // Creates selection at the given range.
1161
- * const range = writer.createRange( start, end );
1162
- * const selection = writer.createSelection( range );
1163
- *
1164
- * // Creates selection at the given ranges
1165
- * const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
1166
- * const selection = writer.createSelection( ranges );
1167
- *
1168
- * // Creates selection from the other selection.
1169
- * const otherSelection = writer.createSelection();
1170
- * const selection = writer.createSelection( otherSelection );
1171
- *
1172
- * // Creates selection from the document selection.
1173
- * const selection = writer.createSelection( editor.editing.view.document.selection );
1174
- *
1175
- * // Creates selection at the given position.
1176
- * const position = writer.createPositionFromPath( root, path );
1177
- * const selection = writer.createSelection( position );
1178
- *
1179
- * // Creates collapsed selection at the position of given item and offset.
1180
- * const paragraph = writer.createContainerElement( 'p' );
1181
- * const selection = writer.createSelection( paragraph, offset );
1182
- *
1183
- * // Creates a range inside an {@link module:engine/view/element~Element element} which starts before the
1184
- * // first child of that element and ends after the last child of that element.
1185
- * const selection = writer.createSelection( paragraph, 'in' );
1186
- *
1187
- * // Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
1188
- * // just after the item.
1189
- * const selection = writer.createSelection( paragraph, 'on' );
1190
- *
1191
- * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
1192
- *
1193
- * // Creates backward selection.
1194
- * const selection = writer.createSelection( range, { backward: true } );
1195
- *
1196
- * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
1197
- * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
1198
- * represented in other way, for example by applying proper CSS class.
1199
- *
1200
- * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
1201
- * (and be properly handled by screen readers).
1202
- *
1203
- * // Creates fake selection with label.
1204
- * const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
1205
- *
1206
- * @param {module:engine/view/selection~Selectable} [selectable=null]
1207
- * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
1208
- * @param {Object} [options]
1209
- * @param {Boolean} [options.backward] Sets this selection instance to be backward.
1210
- * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
1211
- * @param {String} [options.label] Label for the fake selection.
1212
- * @returns {module:engine/view/selection~Selection}
1213
- */
1214
- createSelection( selectable, placeOrOffset, options ) {
1215
- return new Selection( selectable, placeOrOffset, options );
1216
- }
1217
-
1218
- /**
1219
- * Creates placeholders for child elements of the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
1220
- * `elementToStructure()`} conversion helper.
1221
- *
1222
- * const viewSlot = conversionApi.writer.createSlot();
1223
- * const viewPosition = conversionApi.writer.createPositionAt( viewElement, 0 );
1224
- *
1225
- * conversionApi.writer.insert( viewPosition, viewSlot );
1226
- *
1227
- * It could be filtered down to a specific subset of children (only `<foo>` model elements in this case):
1228
- *
1229
- * const viewSlot = conversionApi.writer.createSlot( node => node.is( 'element', 'foo' ) );
1230
- * const viewPosition = conversionApi.writer.createPositionAt( viewElement, 0 );
1231
- *
1232
- * conversionApi.writer.insert( viewPosition, viewSlot );
1233
- *
1234
- * While providing a filtered slot, make sure to provide slots for all child nodes. A single node can not be downcasted into
1235
- * multiple slots.
1236
- *
1237
- * **Note**: You should not change the order of nodes. View elements should be in the same order as model nodes.
1238
- *
1239
- * @param {'children'|module:engine/conversion/downcasthelpers~SlotFilter} [modeOrFilter='children'] The filter for child nodes.
1240
- * @returns {module:engine/view/element~Element} The slot element to be placed in to the view structure while processing
1241
- * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure `elementToStructure()`}.
1242
- */
1243
- createSlot( modeOrFilter ) {
1244
- if ( !this._slotFactory ) {
1245
- /**
1246
- * The `createSlot()` method is only allowed inside the `elementToStructure` downcast helper callback.
1247
- *
1248
- * @error view-writer-invalid-create-slot-context
1249
- */
1250
- throw new CKEditorError( 'view-writer-invalid-create-slot-context', this.document );
1251
- }
1252
-
1253
- return this._slotFactory( this, modeOrFilter );
1254
- }
1255
-
1256
- /**
1257
- * Registers a slot factory.
1258
- *
1259
- * @protected
1260
- * @param {Function} slotFactory The slot factory.
1261
- */
1262
- _registerSlotFactory( slotFactory ) {
1263
- this._slotFactory = slotFactory;
1264
- }
1265
-
1266
- /**
1267
- * Clears the registered slot factory.
1268
- *
1269
- * @protected
1270
- */
1271
- _clearSlotFactory() {
1272
- this._slotFactory = null;
1273
- }
1274
-
1275
- /**
1276
- * Inserts a node or nodes at the specified position. Takes care of breaking attributes before insertion
1277
- * and merging them afterwards if requested by the breakAttributes param.
1278
- *
1279
- * @private
1280
- * @param {module:engine/view/position~Position} position Insertion position.
1281
- * @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
1282
- * module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
1283
- * module:engine/view/rawelement~RawElement|module:engine/view/uielement~UIElement|
1284
- * Iterable.<module:engine/view/text~Text|
1285
- * module:engine/view/attributeelement~AttributeElement|module:engine/view/containerelement~ContainerElement|
1286
- * module:engine/view/emptyelement~EmptyElement|module:engine/view/rawelement~RawElement|
1287
- * module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
1288
- * @param {Boolean} breakAttributes Whether attributes should be broken.
1289
- * @returns {module:engine/view/range~Range} Range around inserted nodes.
1290
- */
1291
- _insertNodes( position, nodes, breakAttributes ) {
1292
- let parentElement;
1293
-
1294
- // Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
1295
- // can't have an attribute in model and won't get wrapped with an AttributeElement while down-casted.
1296
- if ( breakAttributes ) {
1297
- parentElement = getParentContainer( position );
1298
- } else {
1299
- parentElement = position.parent.is( '$text' ) ? position.parent.parent : position.parent;
1300
- }
1301
-
1302
- if ( !parentElement ) {
1303
- /**
1304
- * Position's parent container cannot be found.
1305
- *
1306
- * @error view-writer-invalid-position-container
1307
- */
1308
- throw new CKEditorError(
1309
- 'view-writer-invalid-position-container',
1310
- this.document
1311
- );
1312
- }
1313
-
1314
- let insertionPosition;
1315
-
1316
- if ( breakAttributes ) {
1317
- insertionPosition = this._breakAttributes( position, true );
1318
- } else {
1319
- insertionPosition = position.parent.is( '$text' ) ? breakTextNode( position ) : position;
1320
- }
1321
-
1322
- const length = parentElement._insertChild( insertionPosition.offset, nodes );
1323
-
1324
- for ( const node of nodes ) {
1325
- this._addToClonedElementsGroup( node );
1326
- }
1327
-
1328
- const endPosition = insertionPosition.getShiftedBy( length );
1329
- const start = this.mergeAttributes( insertionPosition );
1330
-
1331
- // If start position was merged - move end position.
1332
- if ( !start.isEqual( insertionPosition ) ) {
1333
- endPosition.offset--;
1334
- }
1335
-
1336
- const end = this.mergeAttributes( endPosition );
1337
-
1338
- return new Range( start, end );
1339
- }
1340
-
1341
- /**
1342
- * Wraps children with provided `wrapElement`. Only children contained in `parent` element between
1343
- * `startOffset` and `endOffset` will be wrapped.
1344
- *
1345
- * @private
1346
- * @param {module:engine/view/element~Element} parent
1347
- * @param {Number} startOffset
1348
- * @param {Number} endOffset
1349
- * @param {module:engine/view/element~Element} wrapElement
1350
- */
1351
- _wrapChildren( parent, startOffset, endOffset, wrapElement ) {
1352
- let i = startOffset;
1353
- const wrapPositions = [];
1354
-
1355
- while ( i < endOffset ) {
1356
- const child = parent.getChild( i );
1357
- const isText = child.is( '$text' );
1358
- const isAttribute = child.is( 'attributeElement' );
1359
-
1360
- //
1361
- // (In all examples, assume that `wrapElement` is `<span class="foo">` element.)
1362
- //
1363
- // Check if `wrapElement` can be joined with the wrapped element. One of requirements is having same name.
1364
- // If possible, join elements.
1365
- //
1366
- // <p><span class="bar">abc</span></p> --> <p><span class="foo bar">abc</span></p>
1367
- //
1368
- if ( isAttribute && this._wrapAttributeElement( wrapElement, child ) ) {
1369
- wrapPositions.push( new Position( parent, i ) );
1370
- }
1371
- //
1372
- // Wrap the child if it is not an attribute element or if it is an attribute element that should be inside
1373
- // `wrapElement` (due to priority).
1374
- //
1375
- // <p>abc</p> --> <p><span class="foo">abc</span></p>
1376
- // <p><strong>abc</strong></p> --> <p><span class="foo"><strong>abc</strong></span></p>
1377
- else if ( isText || !isAttribute || shouldABeOutsideB( wrapElement, child ) ) {
1378
- // Clone attribute.
1379
- const newAttribute = wrapElement._clone();
1380
-
1381
- // Wrap current node with new attribute.
1382
- child._remove();
1383
- newAttribute._appendChild( child );
1384
-
1385
- parent._insertChild( i, newAttribute );
1386
- this._addToClonedElementsGroup( newAttribute );
1387
-
1388
- wrapPositions.push( new Position( parent, i ) );
1389
- }
1390
- //
1391
- // If other nested attribute is found and it wasn't wrapped (see above), continue wrapping inside it.
1392
- //
1393
- // <p><a href="foo.html">abc</a></p> --> <p><a href="foo.html"><span class="foo">abc</span></a></p>
1394
- //
1395
- else /* if ( isAttribute ) */ {
1396
- this._wrapChildren( child, 0, child.childCount, wrapElement );
1397
- }
1398
-
1399
- i++;
1400
- }
1401
-
1402
- // Merge at each wrap.
1403
- let offsetChange = 0;
1404
-
1405
- for ( const position of wrapPositions ) {
1406
- position.offset -= offsetChange;
1407
-
1408
- // Do not merge with elements outside selected children.
1409
- if ( position.offset == startOffset ) {
1410
- continue;
1411
- }
1412
-
1413
- const newPosition = this.mergeAttributes( position );
1414
-
1415
- // If nodes were merged - other merge offsets will change.
1416
- if ( !newPosition.isEqual( position ) ) {
1417
- offsetChange++;
1418
- endOffset--;
1419
- }
1420
- }
1421
-
1422
- return Range._createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
1423
- }
1424
-
1425
- /**
1426
- * Unwraps children from provided `unwrapElement`. Only children contained in `parent` element between
1427
- * `startOffset` and `endOffset` will be unwrapped.
1428
- *
1429
- * @private
1430
- * @param {module:engine/view/element~Element} parent
1431
- * @param {Number} startOffset
1432
- * @param {Number} endOffset
1433
- * @param {module:engine/view/element~Element} unwrapElement
1434
- */
1435
- _unwrapChildren( parent, startOffset, endOffset, unwrapElement ) {
1436
- let i = startOffset;
1437
- const unwrapPositions = [];
1438
-
1439
- // Iterate over each element between provided offsets inside parent.
1440
- // We don't use tree walker or range iterator because we will be removing and merging potentially multiple nodes,
1441
- // so it could get messy. It is safer to it manually in this case.
1442
- while ( i < endOffset ) {
1443
- const child = parent.getChild( i );
1444
-
1445
- // Skip all text nodes. There should be no container element's here either.
1446
- if ( !child.is( 'attributeElement' ) ) {
1447
- i++;
1448
-
1449
- continue;
1450
- }
1451
-
1452
- //
1453
- // (In all examples, assume that `unwrapElement` is `<span class="foo">` element.)
1454
- //
1455
- // If the child is similar to the given attribute element, unwrap it - it will be completely removed.
1456
- //
1457
- // <p><span class="foo">abc</span>xyz</p> --> <p>abcxyz</p>
1458
- //
1459
- if ( child.isSimilar( unwrapElement ) ) {
1460
- const unwrapped = child.getChildren();
1461
- const count = child.childCount;
1462
-
1463
- // Replace wrapper element with its children
1464
- child._remove();
1465
- parent._insertChild( i, unwrapped );
1466
-
1467
- this._removeFromClonedElementsGroup( child );
1468
-
1469
- // Save start and end position of moved items.
1470
- unwrapPositions.push(
1471
- new Position( parent, i ),
1472
- new Position( parent, i + count )
1473
- );
1474
-
1475
- // Skip elements that were unwrapped. Assuming there won't be another element to unwrap in child elements.
1476
- i += count;
1477
- endOffset += count - 1;
1478
-
1479
- continue;
1480
- }
1481
-
1482
- //
1483
- // If the child is not similar but is an attribute element, try partial unwrapping - remove the same attributes/styles/classes.
1484
- // Partial unwrapping will happen only if the elements have the same name.
1485
- //
1486
- // <p><span class="foo bar">abc</span>xyz</p> --> <p><span class="bar">abc</span>xyz</p>
1487
- // <p><i class="foo">abc</i>xyz</p> --> <p><i class="foo">abc</i>xyz</p>
1488
- //
1489
- if ( this._unwrapAttributeElement( unwrapElement, child ) ) {
1490
- unwrapPositions.push(
1491
- new Position( parent, i ),
1492
- new Position( parent, i + 1 )
1493
- );
1494
-
1495
- i++;
1496
-
1497
- continue;
1498
- }
1499
-
1500
- //
1501
- // If other nested attribute is found, look through it's children for elements to unwrap.
1502
- //
1503
- // <p><i><span class="foo">abc</span></i><p> --> <p><i>abc</i><p>
1504
- //
1505
- this._unwrapChildren( child, 0, child.childCount, unwrapElement );
1506
-
1507
- i++;
1508
- }
1509
-
1510
- // Merge at each unwrap.
1511
- let offsetChange = 0;
1512
-
1513
- for ( const position of unwrapPositions ) {
1514
- position.offset -= offsetChange;
1515
-
1516
- // Do not merge with elements outside selected children.
1517
- if ( position.offset == startOffset || position.offset == endOffset ) {
1518
- continue;
1519
- }
1520
-
1521
- const newPosition = this.mergeAttributes( position );
1522
-
1523
- // If nodes were merged - other merge offsets will change.
1524
- if ( !newPosition.isEqual( position ) ) {
1525
- offsetChange++;
1526
- endOffset--;
1527
- }
1528
- }
1529
-
1530
- return Range._createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
1531
- }
1532
-
1533
- /**
1534
- * Helper function for `view.writer.wrap`. Wraps range with provided attribute element.
1535
- * This method will also merge newly added attribute element with its siblings whenever possible.
1536
- *
1537
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
1538
- * an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
1539
- *
1540
- * @private
1541
- * @param {module:engine/view/range~Range} range
1542
- * @param {module:engine/view/attributeelement~AttributeElement} attribute
1543
- * @returns {module:engine/view/range~Range} New range after wrapping, spanning over wrapping attribute element.
1544
- */
1545
- _wrapRange( range, attribute ) {
1546
- // Break attributes at range start and end.
1547
- const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
1548
- const parentContainer = breakStart.parent;
1549
-
1550
- // Wrap all children with attribute.
1551
- const newRange = this._wrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
1552
-
1553
- // Merge attributes at the both ends and return a new range.
1554
- const start = this.mergeAttributes( newRange.start );
1555
-
1556
- // If start position was merged - move end position back.
1557
- if ( !start.isEqual( newRange.start ) ) {
1558
- newRange.end.offset--;
1559
- }
1560
- const end = this.mergeAttributes( newRange.end );
1561
-
1562
- return new Range( start, end );
1563
- }
1564
-
1565
- /**
1566
- * Helper function for {@link #wrap}. Wraps position with provided attribute element.
1567
- * This method will also merge newly added attribute element with its siblings whenever possible.
1568
- *
1569
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
1570
- * an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
1571
- *
1572
- * @private
1573
- * @param {module:engine/view/position~Position} position
1574
- * @param {module:engine/view/attributeelement~AttributeElement} attribute
1575
- * @returns {module:engine/view/position~Position} New position after wrapping.
1576
- */
1577
- _wrapPosition( position, attribute ) {
1578
- // Return same position when trying to wrap with attribute similar to position parent.
1579
- if ( attribute.isSimilar( position.parent ) ) {
1580
- return movePositionToTextNode( position.clone() );
1581
- }
1582
-
1583
- // When position is inside text node - break it and place new position between two text nodes.
1584
- if ( position.parent.is( '$text' ) ) {
1585
- position = breakTextNode( position );
1586
- }
1587
-
1588
- // Create fake element that will represent position, and will not be merged with other attributes.
1589
- const fakePosition = this.createAttributeElement();
1590
- fakePosition._priority = Number.POSITIVE_INFINITY;
1591
- fakePosition.isSimilar = () => false;
1592
-
1593
- // Insert fake element in position location.
1594
- position.parent._insertChild( position.offset, fakePosition );
1595
-
1596
- // Range around inserted fake attribute element.
1597
- const wrapRange = new Range( position, position.getShiftedBy( 1 ) );
1598
-
1599
- // Wrap fake element with attribute (it will also merge if possible).
1600
- this.wrap( wrapRange, attribute );
1601
-
1602
- // Remove fake element and place new position there.
1603
- const newPosition = new Position( fakePosition.parent, fakePosition.index );
1604
- fakePosition._remove();
1605
-
1606
- // If position is placed between text nodes - merge them and return position inside.
1607
- const nodeBefore = newPosition.nodeBefore;
1608
- const nodeAfter = newPosition.nodeAfter;
1609
-
1610
- if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
1611
- return mergeTextNodes( nodeBefore, nodeAfter );
1612
- }
1613
-
1614
- // If position is next to text node - move position inside.
1615
- return movePositionToTextNode( newPosition );
1616
- }
1617
-
1618
- /**
1619
- * Wraps one {@link module:engine/view/attributeelement~AttributeElement AttributeElement} into another by
1620
- * merging them if possible. When merging is possible - all attributes, styles and classes are moved from wrapper
1621
- * element to element being wrapped.
1622
- *
1623
- * @private
1624
- * @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
1625
- * @param {module:engine/view/attributeelement~AttributeElement} toWrap AttributeElement to wrap using wrapper element.
1626
- * @returns {Boolean} Returns `true` if elements are merged.
1627
- */
1628
- _wrapAttributeElement( wrapper, toWrap ) {
1629
- if ( !canBeJoined( wrapper, toWrap ) ) {
1630
- return false;
1631
- }
1632
-
1633
- // Can't merge if name or priority differs.
1634
- if ( wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority ) {
1635
- return false;
1636
- }
1637
-
1638
- // Check if attributes can be merged.
1639
- for ( const key of wrapper.getAttributeKeys() ) {
1640
- // Classes and styles should be checked separately.
1641
- if ( key === 'class' || key === 'style' ) {
1642
- continue;
1643
- }
1644
-
1645
- // If some attributes are different we cannot wrap.
1646
- if ( toWrap.hasAttribute( key ) && toWrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
1647
- return false;
1648
- }
1649
- }
1650
-
1651
- // Check if styles can be merged.
1652
- for ( const key of wrapper.getStyleNames() ) {
1653
- if ( toWrap.hasStyle( key ) && toWrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
1654
- return false;
1655
- }
1656
- }
1657
-
1658
- // Move all attributes/classes/styles from wrapper to wrapped AttributeElement.
1659
- for ( const key of wrapper.getAttributeKeys() ) {
1660
- // Classes and styles should be checked separately.
1661
- if ( key === 'class' || key === 'style' ) {
1662
- continue;
1663
- }
1664
-
1665
- // Move only these attributes that are not present - other are similar.
1666
- if ( !toWrap.hasAttribute( key ) ) {
1667
- this.setAttribute( key, wrapper.getAttribute( key ), toWrap );
1668
- }
1669
- }
1670
-
1671
- for ( const key of wrapper.getStyleNames() ) {
1672
- if ( !toWrap.hasStyle( key ) ) {
1673
- this.setStyle( key, wrapper.getStyle( key ), toWrap );
1674
- }
1675
- }
1676
-
1677
- for ( const key of wrapper.getClassNames() ) {
1678
- if ( !toWrap.hasClass( key ) ) {
1679
- this.addClass( key, toWrap );
1680
- }
1681
- }
1682
-
1683
- return true;
1684
- }
1685
-
1686
- /**
1687
- * Unwraps {@link module:engine/view/attributeelement~AttributeElement AttributeElement} from another by removing
1688
- * corresponding attributes, classes and styles. All attributes, classes and styles from wrapper should be present
1689
- * inside element being unwrapped.
1690
- *
1691
- * @private
1692
- * @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
1693
- * @param {module:engine/view/attributeelement~AttributeElement} toUnwrap AttributeElement to unwrap using wrapper element.
1694
- * @returns {Boolean} Returns `true` if elements are unwrapped.
1695
- **/
1696
- _unwrapAttributeElement( wrapper, toUnwrap ) {
1697
- if ( !canBeJoined( wrapper, toUnwrap ) ) {
1698
- return false;
1699
- }
1700
-
1701
- // Can't unwrap if name or priority differs.
1702
- if ( wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority ) {
1703
- return false;
1704
- }
1705
-
1706
- // Check if AttributeElement has all wrapper attributes.
1707
- for ( const key of wrapper.getAttributeKeys() ) {
1708
- // Classes and styles should be checked separately.
1709
- if ( key === 'class' || key === 'style' ) {
1710
- continue;
1711
- }
1712
-
1713
- // If some attributes are missing or different we cannot unwrap.
1714
- if ( !toUnwrap.hasAttribute( key ) || toUnwrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
1715
- return false;
1716
- }
1717
- }
1718
-
1719
- // Check if AttributeElement has all wrapper classes.
1720
- if ( !toUnwrap.hasClass( ...wrapper.getClassNames() ) ) {
1721
- return false;
1722
- }
1723
-
1724
- // Check if AttributeElement has all wrapper styles.
1725
- for ( const key of wrapper.getStyleNames() ) {
1726
- // If some styles are missing or different we cannot unwrap.
1727
- if ( !toUnwrap.hasStyle( key ) || toUnwrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
1728
- return false;
1729
- }
1730
- }
1731
-
1732
- // Remove all wrapper's attributes from unwrapped element.
1733
- for ( const key of wrapper.getAttributeKeys() ) {
1734
- // Classes and styles should be checked separately.
1735
- if ( key === 'class' || key === 'style' ) {
1736
- continue;
1737
- }
1738
-
1739
- this.removeAttribute( key, toUnwrap );
1740
- }
1741
-
1742
- // Remove all wrapper's classes from unwrapped element.
1743
- this.removeClass( Array.from( wrapper.getClassNames() ), toUnwrap );
1744
-
1745
- // Remove all wrapper's styles from unwrapped element.
1746
- this.removeStyle( Array.from( wrapper.getStyleNames() ), toUnwrap );
1747
-
1748
- return true;
1749
- }
1750
-
1751
- /**
1752
- * Helper function used by other `DowncastWriter` methods. Breaks attribute elements at the boundaries of given range.
1753
- *
1754
- * @private
1755
- * @param {module:engine/view/range~Range} range Range which `start` and `end` positions will be used to break attributes.
1756
- * @param {Boolean} [forceSplitText=false] If set to `true`, will break text nodes even if they are directly in container element.
1757
- * This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
1758
- * @returns {module:engine/view/range~Range} New range with located at break positions.
1759
- */
1760
- _breakAttributesRange( range, forceSplitText = false ) {
1761
- const rangeStart = range.start;
1762
- const rangeEnd = range.end;
1763
-
1764
- validateRangeContainer( range, this.document );
1765
-
1766
- // Break at the collapsed position. Return new collapsed range.
1767
- if ( range.isCollapsed ) {
1768
- const position = this._breakAttributes( range.start, forceSplitText );
1769
-
1770
- return new Range( position, position );
1771
- }
1772
-
1773
- const breakEnd = this._breakAttributes( rangeEnd, forceSplitText );
1774
- const count = breakEnd.parent.childCount;
1775
- const breakStart = this._breakAttributes( rangeStart, forceSplitText );
1776
-
1777
- // Calculate new break end offset.
1778
- breakEnd.offset += breakEnd.parent.childCount - count;
1779
-
1780
- return new Range( breakStart, breakEnd );
1781
- }
1782
-
1783
- /**
1784
- * Helper function used by other `DowncastWriter` methods. Breaks attribute elements at given position.
1785
- *
1786
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position
1787
- * is placed inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
1788
- *
1789
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position
1790
- * is placed inside {@link module:engine/view/uielement~UIElement UIElement}.
1791
- *
1792
- * @private
1793
- * @param {module:engine/view/position~Position} position Position where to break attributes.
1794
- * @param {Boolean} [forceSplitText=false] If set to `true`, will break text nodes even if they are directly in container element.
1795
- * This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
1796
- * @returns {module:engine/view/position~Position} New position after breaking the attributes.
1797
- */
1798
- _breakAttributes( position, forceSplitText = false ) {
1799
- const positionOffset = position.offset;
1800
- const positionParent = position.parent;
1801
-
1802
- // If position is placed inside EmptyElement - throw an exception as we cannot break inside.
1803
- if ( position.parent.is( 'emptyElement' ) ) {
1804
- /**
1805
- * Cannot break an `EmptyElement` instance.
1806
- *
1807
- * This error is thrown if
1808
- * {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes `DowncastWriter#breakAttributes()`}
1809
- * was executed in an incorrect position.
1810
- *
1811
- * @error view-writer-cannot-break-empty-element
1812
- */
1813
- throw new CKEditorError( 'view-writer-cannot-break-empty-element', this.document );
1814
- }
1815
-
1816
- // If position is placed inside UIElement - throw an exception as we cannot break inside.
1817
- if ( position.parent.is( 'uiElement' ) ) {
1818
- /**
1819
- * Cannot break a `UIElement` instance.
1820
- *
1821
- * This error is thrown if
1822
- * {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes `DowncastWriter#breakAttributes()`}
1823
- * was executed in an incorrect position.
1824
- *
1825
- * @error view-writer-cannot-break-ui-element
1826
- */
1827
- throw new CKEditorError( 'view-writer-cannot-break-ui-element', this.document );
1828
- }
1829
-
1830
- // If position is placed inside RawElement - throw an exception as we cannot break inside.
1831
- if ( position.parent.is( 'rawElement' ) ) {
1832
- /**
1833
- * Cannot break a `RawElement` instance.
1834
- *
1835
- * This error is thrown if
1836
- * {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes `DowncastWriter#breakAttributes()`}
1837
- * was executed in an incorrect position.
1838
- *
1839
- * @error view-writer-cannot-break-raw-element
1840
- */
1841
- throw new CKEditorError( 'view-writer-cannot-break-raw-element', this.document );
1842
- }
1843
-
1844
- // There are no attributes to break and text nodes breaking is not forced.
1845
- if ( !forceSplitText && positionParent.is( '$text' ) && isContainerOrFragment( positionParent.parent ) ) {
1846
- return position.clone();
1847
- }
1848
-
1849
- // Position's parent is container, so no attributes to break.
1850
- if ( isContainerOrFragment( positionParent ) ) {
1851
- return position.clone();
1852
- }
1853
-
1854
- // Break text and start again in new position.
1855
- if ( positionParent.is( '$text' ) ) {
1856
- return this._breakAttributes( breakTextNode( position ), forceSplitText );
1857
- }
1858
-
1859
- const length = positionParent.childCount;
1860
-
1861
- // <p>foo<b><u>bar{}</u></b></p>
1862
- // <p>foo<b><u>bar</u>[]</b></p>
1863
- // <p>foo<b><u>bar</u></b>[]</p>
1864
- if ( positionOffset == length ) {
1865
- const newPosition = new Position( positionParent.parent, positionParent.index + 1 );
1866
-
1867
- return this._breakAttributes( newPosition, forceSplitText );
1868
- } else {
1869
- // <p>foo<b><u>{}bar</u></b></p>
1870
- // <p>foo<b>[]<u>bar</u></b></p>
1871
- // <p>foo{}<b><u>bar</u></b></p>
1872
- if ( positionOffset === 0 ) {
1873
- const newPosition = new Position( positionParent.parent, positionParent.index );
1874
-
1875
- return this._breakAttributes( newPosition, forceSplitText );
1876
- }
1877
- // <p>foo<b><u>b{}ar</u></b></p>
1878
- // <p>foo<b><u>b[]ar</u></b></p>
1879
- // <p>foo<b><u>b</u>[]<u>ar</u></b></p>
1880
- // <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
1881
- else {
1882
- const offsetAfter = positionParent.index + 1;
1883
-
1884
- // Break element.
1885
- const clonedNode = positionParent._clone();
1886
-
1887
- // Insert cloned node to position's parent node.
1888
- positionParent.parent._insertChild( offsetAfter, clonedNode );
1889
- this._addToClonedElementsGroup( clonedNode );
1890
-
1891
- // Get nodes to move.
1892
- const count = positionParent.childCount - positionOffset;
1893
- const nodesToMove = positionParent._removeChildren( positionOffset, count );
1894
-
1895
- // Move nodes to cloned node.
1896
- clonedNode._appendChild( nodesToMove );
1897
-
1898
- // Create new position to work on.
1899
- const newPosition = new Position( positionParent.parent, offsetAfter );
1900
-
1901
- return this._breakAttributes( newPosition, forceSplitText );
1902
- }
1903
- }
1904
- }
1905
-
1906
- /**
1907
- * Stores the information that an {@link module:engine/view/attributeelement~AttributeElement attribute element} was
1908
- * added to the tree. Saves the reference to the group in the given element and updates the group, so other elements
1909
- * from the group now keep a reference to the given attribute element.
1910
- *
1911
- * The clones group can be obtained using {@link module:engine/view/attributeelement~AttributeElement#getElementsWithSameId}.
1912
- *
1913
- * Does nothing if added element has no {@link module:engine/view/attributeelement~AttributeElement#id id}.
1914
- *
1915
- * @private
1916
- * @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to save.
1917
- */
1918
- _addToClonedElementsGroup( element ) {
1919
- // Add only if the element is in document tree.
1920
- if ( !element.root.is( 'rootElement' ) ) {
1921
- return;
1922
- }
1923
-
1924
- // Traverse the element's children recursively to find other attribute elements that also might got inserted.
1925
- // The loop is at the beginning so we can make fast returns later in the code.
1926
- if ( element.is( 'element' ) ) {
1927
- for ( const child of element.getChildren() ) {
1928
- this._addToClonedElementsGroup( child );
1929
- }
1930
- }
1931
-
1932
- const id = element.id;
1933
-
1934
- if ( !id ) {
1935
- return;
1936
- }
1937
-
1938
- let group = this._cloneGroups.get( id );
1939
-
1940
- if ( !group ) {
1941
- group = new Set();
1942
- this._cloneGroups.set( id, group );
1943
- }
1944
-
1945
- group.add( element );
1946
- element._clonesGroup = group;
1947
- }
1948
-
1949
- /**
1950
- * Removes all the information about the given {@link module:engine/view/attributeelement~AttributeElement attribute element}
1951
- * from its clones group.
1952
- *
1953
- * Keep in mind, that the element will still keep a reference to the group (but the group will not keep a reference to it).
1954
- * This allows to reference the whole group even if the element was already removed from the tree.
1955
- *
1956
- * Does nothing if the element has no {@link module:engine/view/attributeelement~AttributeElement#id id}.
1957
- *
1958
- * @private
1959
- * @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to remove.
1960
- */
1961
- _removeFromClonedElementsGroup( element ) {
1962
- // Traverse the element's children recursively to find other attribute elements that also got removed.
1963
- // The loop is at the beginning so we can make fast returns later in the code.
1964
- if ( element.is( 'element' ) ) {
1965
- for ( const child of element.getChildren() ) {
1966
- this._removeFromClonedElementsGroup( child );
1967
- }
1968
- }
1969
-
1970
- const id = element.id;
1971
-
1972
- if ( !id ) {
1973
- return;
1974
- }
1975
-
1976
- const group = this._cloneGroups.get( id );
1977
-
1978
- if ( !group ) {
1979
- return;
1980
- }
1981
-
1982
- group.delete( element );
1983
- // Not removing group from element on purpose!
1984
- // If other parts of code have reference to this element, they will be able to get references to other elements from the group.
1985
- }
38
+ /**
39
+ * @param {module:engine/view/document~Document} document The view document instance.
40
+ */
41
+ constructor(document) {
42
+ /**
43
+ * The view document instance in which this writer operates.
44
+ *
45
+ * @readonly
46
+ * @type {module:engine/view/document~Document}
47
+ */
48
+ this.document = document;
49
+ /**
50
+ * Holds references to the attribute groups that share the same {@link module:engine/view/attributeelement~AttributeElement#id id}.
51
+ * The keys are `id`s, the values are `Set`s holding {@link module:engine/view/attributeelement~AttributeElement}s.
52
+ *
53
+ * @private
54
+ * @type {Map.<String,Set>}
55
+ */
56
+ this._cloneGroups = new Map();
57
+ /**
58
+ * The slot factory used by the `elementToStructure` downcast helper.
59
+ *
60
+ * @private
61
+ * @type {Function|null}
62
+ */
63
+ this._slotFactory = null;
64
+ }
65
+ /**
66
+ * Sets {@link module:engine/view/documentselection~DocumentSelection selection's} ranges and direction to the
67
+ * specified location based on the given {@link module:engine/view/selection~Selectable selectable}.
68
+ *
69
+ * Usage:
70
+ *
71
+ * // Sets selection to the given range.
72
+ * const range = writer.createRange( start, end );
73
+ * writer.setSelection( range );
74
+ *
75
+ * // Sets backward selection to the given range.
76
+ * const range = writer.createRange( start, end );
77
+ * writer.setSelection( range );
78
+ *
79
+ * // Sets selection to given ranges.
80
+ * const ranges = [ writer.createRange( start1, end2 ), writer.createRange( start2, end2 ) ];
81
+ * writer.setSelection( range );
82
+ *
83
+ * // Sets selection to the other selection.
84
+ * const otherSelection = writer.createSelection();
85
+ * writer.setSelection( otherSelection );
86
+ *
87
+ * // Sets collapsed selection at the given position.
88
+ * const position = writer.createPositionFromPath( root, path );
89
+ * writer.setSelection( position );
90
+ *
91
+ * // Sets collapsed selection at the position of given item and offset.
92
+ * const paragraph = writer.createContainerElement( 'p' );
93
+ * writer.setSelection( paragraph, offset );
94
+ *
95
+ * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
96
+ * that element and ends after the last child of that element.
97
+ *
98
+ * writer.setSelection( paragraph, 'in' );
99
+ *
100
+ * Creates a range on the {@link module:engine/view/item~Item item} which starts before the item and ends just after the item.
101
+ *
102
+ * writer.setSelection( paragraph, 'on' );
103
+ *
104
+ * // Removes all ranges.
105
+ * writer.setSelection( null );
106
+ *
107
+ * `DowncastWriter#setSelection()` allow passing additional options (`backward`, `fake` and `label`) as the last argument.
108
+ *
109
+ * // Sets selection as backward.
110
+ * writer.setSelection( range, { backward: true } );
111
+ *
112
+ * // Sets selection as fake.
113
+ * // Fake selection does not render as browser native selection over selected elements and is hidden to the user.
114
+ * // This way, no native selection UI artifacts are displayed to the user and selection over elements can be
115
+ * // represented in other way, for example by applying proper CSS class.
116
+ * writer.setSelection( range, { fake: true } );
117
+ *
118
+ * // Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
119
+ * // (and be properly handled by screen readers).
120
+ * writer.setSelection( range, { fake: true, label: 'foo' } );
121
+ *
122
+ * @param {module:engine/view/selection~Selectable} selectable
123
+ * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
124
+ * @param {Object} [options]
125
+ * @param {Boolean} [options.backward] Sets this selection instance to be backward.
126
+ * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
127
+ * @param {String} [options.label] Label for the fake selection.
128
+ */
129
+ setSelection(...args) {
130
+ this.document.selection._setTo(...args);
131
+ }
132
+ /**
133
+ * Moves {@link module:engine/view/documentselection~DocumentSelection#focus selection's focus} to the specified location.
134
+ *
135
+ * The location can be specified in the same form as {@link module:engine/view/view~View#createPositionAt view.createPositionAt()}
136
+ * parameters.
137
+ *
138
+ * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
139
+ * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
140
+ * first parameter is a {@link module:engine/view/item~Item view item}.
141
+ */
142
+ setSelectionFocus(...args) {
143
+ this.document.selection._setFocus(...args);
144
+ }
145
+ /**
146
+ * Creates a new {@link module:engine/view/documentfragment~DocumentFragment} instance.
147
+ *
148
+ * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
149
+ * A list of nodes to be inserted into the created document fragment.
150
+ * @returns {module:engine/view/documentfragment~DocumentFragment} The created document fragment.
151
+ */
152
+ createDocumentFragment(children) {
153
+ return new DocumentFragment(this.document, children);
154
+ }
155
+ /**
156
+ * Creates a new {@link module:engine/view/text~Text text node}.
157
+ *
158
+ * writer.createText( 'foo' );
159
+ *
160
+ * @param {String} data The text's data.
161
+ * @returns {module:engine/view/text~Text} The created text node.
162
+ */
163
+ createText(data) {
164
+ return new Text(this.document, data);
165
+ }
166
+ /**
167
+ * Creates a new {@link module:engine/view/attributeelement~AttributeElement}.
168
+ *
169
+ * writer.createAttributeElement( 'strong' );
170
+ * writer.createAttributeElement( 'a', { href: 'foo.bar' } );
171
+ *
172
+ * // Make `<a>` element contain other attributes element so the `<a>` element is not broken.
173
+ * writer.createAttributeElement( 'a', { href: 'foo.bar' }, { priority: 5 } );
174
+ *
175
+ * // Set `id` of a marker element so it is not joined or merged with "normal" elements.
176
+ * writer.createAttributeElement( 'span', { class: 'my-marker' }, { id: 'marker:my' } );
177
+ *
178
+ * @param {String} name Name of the element.
179
+ * @param {Object} [attributes] Element's attributes.
180
+ * @param {Object} [options] Element's options.
181
+ * @param {Number} [options.priority] Element's {@link module:engine/view/attributeelement~AttributeElement#priority priority}.
182
+ * @param {Number|String} [options.id] Element's {@link module:engine/view/attributeelement~AttributeElement#id id}.
183
+ * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
184
+ * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
185
+ * @returns {module:engine/view/attributeelement~AttributeElement} Created element.
186
+ */
187
+ createAttributeElement(name, attributes, options = {}) {
188
+ const attributeElement = new AttributeElement(this.document, name, attributes);
189
+ if (typeof options.priority === 'number') {
190
+ attributeElement._priority = options.priority;
191
+ }
192
+ if (options.id) {
193
+ attributeElement._id = options.id;
194
+ }
195
+ if (options.renderUnsafeAttributes) {
196
+ attributeElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
197
+ }
198
+ return attributeElement;
199
+ }
200
+ createContainerElement(name, attributes, childrenOrOptions = {}, options = {}) {
201
+ let children = null;
202
+ if (isPlainObject(childrenOrOptions)) {
203
+ options = childrenOrOptions;
204
+ }
205
+ else {
206
+ children = childrenOrOptions;
207
+ }
208
+ const containerElement = new ContainerElement(this.document, name, attributes, children);
209
+ if (options.renderUnsafeAttributes) {
210
+ containerElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
211
+ }
212
+ return containerElement;
213
+ }
214
+ /**
215
+ * Creates a new {@link module:engine/view/editableelement~EditableElement}.
216
+ *
217
+ * writer.createEditableElement( 'div' );
218
+ * writer.createEditableElement( 'div', { id: 'foo-1234' } );
219
+ *
220
+ * Note: The editable element is to be used in the editing pipeline. Usually, together with
221
+ * {@link module:widget/utils~toWidgetEditable `toWidgetEditable()`}.
222
+ *
223
+ * @param {String} name Name of the element.
224
+ * @param {Object} [attributes] Elements attributes.
225
+ * @param {Object} [options] Element's options.
226
+ * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
227
+ * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
228
+ * @returns {module:engine/view/editableelement~EditableElement} Created element.
229
+ */
230
+ createEditableElement(name, attributes, options = {}) {
231
+ const editableElement = new EditableElement(this.document, name, attributes);
232
+ if (options.renderUnsafeAttributes) {
233
+ editableElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
234
+ }
235
+ return editableElement;
236
+ }
237
+ /**
238
+ * Creates a new {@link module:engine/view/emptyelement~EmptyElement}.
239
+ *
240
+ * writer.createEmptyElement( 'img' );
241
+ * writer.createEmptyElement( 'img', { id: 'foo-1234' } );
242
+ *
243
+ * @param {String} name Name of the element.
244
+ * @param {Object} [attributes] Elements attributes.
245
+ * @param {Object} [options] Element's options.
246
+ * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
247
+ * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
248
+ * @returns {module:engine/view/emptyelement~EmptyElement} Created element.
249
+ */
250
+ createEmptyElement(name, attributes, options = {}) {
251
+ const emptyElement = new EmptyElement(this.document, name, attributes);
252
+ if (options.renderUnsafeAttributes) {
253
+ emptyElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
254
+ }
255
+ return emptyElement;
256
+ }
257
+ /**
258
+ * Creates a new {@link module:engine/view/uielement~UIElement}.
259
+ *
260
+ * writer.createUIElement( 'span' );
261
+ * writer.createUIElement( 'span', { id: 'foo-1234' } );
262
+ *
263
+ * A custom render function can be provided as the third parameter:
264
+ *
265
+ * writer.createUIElement( 'span', null, function( domDocument ) {
266
+ * const domElement = this.toDomElement( domDocument );
267
+ * domElement.innerHTML = '<b>this is ui element</b>';
268
+ *
269
+ * return domElement;
270
+ * } );
271
+ *
272
+ * Unlike {@link #createRawElement raw elements}, UI elements are by no means editor content, for instance,
273
+ * they are ignored by the editor selection system.
274
+ *
275
+ * You should not use UI elements as data containers. Check out {@link #createRawElement} instead.
276
+ *
277
+ * @param {String} name The name of the element.
278
+ * @param {Object} [attributes] Element attributes.
279
+ * @param {Function} [renderFunction] A custom render function.
280
+ * @returns {module:engine/view/uielement~UIElement} The created element.
281
+ */
282
+ createUIElement(name, attributes, renderFunction) {
283
+ const uiElement = new UIElement(this.document, name, attributes);
284
+ if (renderFunction) {
285
+ uiElement.render = renderFunction;
286
+ }
287
+ return uiElement;
288
+ }
289
+ /**
290
+ * Creates a new {@link module:engine/view/rawelement~RawElement}.
291
+ *
292
+ * writer.createRawElement( 'span', { id: 'foo-1234' }, function( domElement ) {
293
+ * domElement.innerHTML = '<b>This is the raw content of the raw element.</b>';
294
+ * } );
295
+ *
296
+ * Raw elements work as data containers ("wrappers", "sandboxes") but their children are not managed or
297
+ * even recognized by the editor. This encapsulation allows integrations to maintain custom DOM structures
298
+ * in the editor content without, for instance, worrying about compatibility with other editor features.
299
+ * Raw elements are a perfect tool for integration with external frameworks and data sources.
300
+ *
301
+ * Unlike {@link #createUIElement UI elements}, raw elements act like "real" editor content (similar to
302
+ * {@link module:engine/view/containerelement~ContainerElement} or {@link module:engine/view/emptyelement~EmptyElement}),
303
+ * and they are considered by the editor selection.
304
+ *
305
+ * You should not use raw elements to render the UI in the editor content. Check out {@link #createUIElement `#createUIElement()`}
306
+ * instead.
307
+ *
308
+ * @param {String} name The name of the element.
309
+ * @param {Object} [attributes] Element attributes.
310
+ * @param {Function} [renderFunction] A custom render function.
311
+ * @param {Object} [options] Element's options.
312
+ * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
313
+ * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
314
+ * @returns {module:engine/view/rawelement~RawElement} The created element.
315
+ */
316
+ createRawElement(name, attributes, renderFunction, options = {}) {
317
+ const rawElement = new RawElement(this.document, name, attributes);
318
+ if (renderFunction) {
319
+ rawElement.render = renderFunction;
320
+ }
321
+ if (options.renderUnsafeAttributes) {
322
+ rawElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
323
+ }
324
+ return rawElement;
325
+ }
326
+ /**
327
+ * Adds or overwrites the element's attribute with a specified key and value.
328
+ *
329
+ * writer.setAttribute( 'href', 'http://ckeditor.com', linkElement );
330
+ *
331
+ * @param {String} key The attribute key.
332
+ * @param {String} value The attribute value.
333
+ * @param {module:engine/view/element~Element} element
334
+ */
335
+ setAttribute(key, value, element) {
336
+ element._setAttribute(key, value);
337
+ }
338
+ /**
339
+ * Removes attribute from the element.
340
+ *
341
+ * writer.removeAttribute( 'href', linkElement );
342
+ *
343
+ * @param {String} key Attribute key.
344
+ * @param {module:engine/view/element~Element} element
345
+ */
346
+ removeAttribute(key, element) {
347
+ element._removeAttribute(key);
348
+ }
349
+ /**
350
+ * Adds specified class to the element.
351
+ *
352
+ * writer.addClass( 'foo', linkElement );
353
+ * writer.addClass( [ 'foo', 'bar' ], linkElement );
354
+ *
355
+ * @param {Array.<String>|String} className
356
+ * @param {module:engine/view/element~Element} element
357
+ */
358
+ addClass(className, element) {
359
+ element._addClass(className);
360
+ }
361
+ /**
362
+ * Removes specified class from the element.
363
+ *
364
+ * writer.removeClass( 'foo', linkElement );
365
+ * writer.removeClass( [ 'foo', 'bar' ], linkElement );
366
+ *
367
+ * @param {Array.<String>|String} className
368
+ * @param {module:engine/view/element~Element} element
369
+ */
370
+ removeClass(className, element) {
371
+ element._removeClass(className);
372
+ }
373
+ setStyle(property, value, element) {
374
+ if (isPlainObject(property) && element === undefined) {
375
+ value._setStyle(property);
376
+ }
377
+ else {
378
+ element._setStyle(property, value);
379
+ }
380
+ }
381
+ /**
382
+ * Removes specified style from the element.
383
+ *
384
+ * writer.removeStyle( 'color', element ); // Removes 'color' style.
385
+ * writer.removeStyle( [ 'color', 'border-top' ], element ); // Removes both 'color' and 'border-top' styles.
386
+ *
387
+ * **Note**: This method can work with normalized style names if
388
+ * {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
389
+ * See {@link module:engine/view/stylesmap~StylesMap#remove `StylesMap#remove()`} for details.
390
+ *
391
+ * @param {Array.<String>|String} property
392
+ * @param {module:engine/view/element~Element} element
393
+ */
394
+ removeStyle(property, element) {
395
+ element._removeStyle(property);
396
+ }
397
+ /**
398
+ * Sets a custom property on element. Unlike attributes, custom properties are not rendered to the DOM,
399
+ * so they can be used to add special data to elements.
400
+ *
401
+ * @param {String|Symbol} key
402
+ * @param {*} value
403
+ * @param {module:engine/view/element~Element} element
404
+ */
405
+ setCustomProperty(key, value, element) {
406
+ element._setCustomProperty(key, value);
407
+ }
408
+ /**
409
+ * Removes a custom property stored under the given key.
410
+ *
411
+ * @param {String|Symbol} key
412
+ * @param {module:engine/view/element~Element} element
413
+ * @returns {Boolean} Returns true if property was removed.
414
+ */
415
+ removeCustomProperty(key, element) {
416
+ return element._removeCustomProperty(key);
417
+ }
418
+ /**
419
+ * Breaks attribute elements at the provided position or at the boundaries of a provided range. It breaks attribute elements
420
+ * up to their first ancestor that is a container element.
421
+ *
422
+ * In following examples `<p>` is a container, `<b>` and `<u>` are attribute elements:
423
+ *
424
+ * <p>foo<b><u>bar{}</u></b></p> -> <p>foo<b><u>bar</u></b>[]</p>
425
+ * <p>foo<b><u>{}bar</u></b></p> -> <p>foo{}<b><u>bar</u></b></p>
426
+ * <p>foo<b><u>b{}ar</u></b></p> -> <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
427
+ * <p><b>fo{o</b><u>ba}r</u></p> -> <p><b>fo</b><b>o</b><u>ba</u><u>r</u></b></p>
428
+ *
429
+ * **Note:** {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
430
+ *
431
+ * **Note:** The difference between {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breakAttributes()} and
432
+ * {@link module:engine/view/downcastwriter~DowncastWriter#breakContainer breakContainer()} is that `breakAttributes()` breaks all
433
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of a given `position`,
434
+ * up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
435
+ * `breakContainer()` assumes that a given `position` is directly in the container element and breaks that container element.
436
+ *
437
+ * Throws the `view-writer-invalid-range-container` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
438
+ * when the {@link module:engine/view/range~Range#start start}
439
+ * and {@link module:engine/view/range~Range#end end} positions of a passed range are not placed inside same parent container.
440
+ *
441
+ * Throws the `view-writer-cannot-break-empty-element` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
442
+ * when trying to break attributes inside an {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
443
+ *
444
+ * Throws the `view-writer-cannot-break-ui-element` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
445
+ * when trying to break attributes inside a {@link module:engine/view/uielement~UIElement UIElement}.
446
+ *
447
+ * @see module:engine/view/attributeelement~AttributeElement
448
+ * @see module:engine/view/containerelement~ContainerElement
449
+ * @see module:engine/view/downcastwriter~DowncastWriter#breakContainer
450
+ * @param {module:engine/view/position~Position|module:engine/view/range~Range} positionOrRange The position where
451
+ * to break attribute elements.
452
+ * @returns {module:engine/view/position~Position|module:engine/view/range~Range} The new position or range, after breaking the
453
+ * attribute elements.
454
+ */
455
+ breakAttributes(positionOrRange) {
456
+ if (positionOrRange instanceof Position) {
457
+ return this._breakAttributes(positionOrRange);
458
+ }
459
+ else {
460
+ return this._breakAttributesRange(positionOrRange);
461
+ }
462
+ }
463
+ /**
464
+ * Breaks a {@link module:engine/view/containerelement~ContainerElement container view element} into two, at the given position.
465
+ * The position has to be directly inside the container element and cannot be in the root. It does not break the conrainer view element
466
+ * if the position is at the beginning or at the end of its parent element.
467
+ *
468
+ * <p>foo^bar</p> -> <p>foo</p><p>bar</p>
469
+ * <div><p>foo</p>^<p>bar</p></div> -> <div><p>foo</p></div><div><p>bar</p></div>
470
+ * <p>^foobar</p> -> ^<p>foobar</p>
471
+ * <p>foobar^</p> -> <p>foobar</p>^
472
+ *
473
+ * **Note:** The difference between {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breakAttributes()} and
474
+ * {@link module:engine/view/downcastwriter~DowncastWriter#breakContainer breakContainer()} is that `breakAttributes()` breaks all
475
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of a given `position`,
476
+ * up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
477
+ * `breakContainer()` assumes that the given `position` is directly in the container element and breaks that container element.
478
+ *
479
+ * @see module:engine/view/attributeelement~AttributeElement
480
+ * @see module:engine/view/containerelement~ContainerElement
481
+ * @see module:engine/view/downcastwriter~DowncastWriter#breakAttributes
482
+ * @param {module:engine/view/position~Position} position The position where to break the element.
483
+ * @returns {module:engine/view/position~Position} The position between broken elements. If an element has not been broken,
484
+ * the returned position is placed either before or after it.
485
+ */
486
+ breakContainer(position) {
487
+ const element = position.parent;
488
+ if (!(element.is('containerElement'))) {
489
+ /**
490
+ * Trying to break an element which is not a container element.
491
+ *
492
+ * @error view-writer-break-non-container-element
493
+ */
494
+ throw new CKEditorError('view-writer-break-non-container-element', this.document);
495
+ }
496
+ if (!element.parent) {
497
+ /**
498
+ * Trying to break root element.
499
+ *
500
+ * @error view-writer-break-root
501
+ */
502
+ throw new CKEditorError('view-writer-break-root', this.document);
503
+ }
504
+ if (position.isAtStart) {
505
+ return Position._createBefore(element);
506
+ }
507
+ else if (!position.isAtEnd) {
508
+ const newElement = element._clone(false);
509
+ this.insert(Position._createAfter(element), newElement);
510
+ const sourceRange = new Range(position, Position._createAt(element, 'end'));
511
+ const targetPosition = new Position(newElement, 0);
512
+ this.move(sourceRange, targetPosition);
513
+ }
514
+ return Position._createAfter(element);
515
+ }
516
+ /**
517
+ * Merges {@link module:engine/view/attributeelement~AttributeElement attribute elements}. It also merges text nodes if needed.
518
+ * Only {@link module:engine/view/attributeelement~AttributeElement#isSimilar similar} attribute elements can be merged.
519
+ *
520
+ * In following examples `<p>` is a container and `<b>` is an attribute element:
521
+ *
522
+ * <p>foo[]bar</p> -> <p>foo{}bar</p>
523
+ * <p><b>foo</b>[]<b>bar</b></p> -> <p><b>foo{}bar</b></p>
524
+ * <p><b foo="bar">a</b>[]<b foo="baz">b</b></p> -> <p><b foo="bar">a</b>[]<b foo="baz">b</b></p>
525
+ *
526
+ * It will also take care about empty attributes when merging:
527
+ *
528
+ * <p><b>[]</b></p> -> <p>[]</p>
529
+ * <p><b>foo</b><i>[]</i><b>bar</b></p> -> <p><b>foo{}bar</b></p>
530
+ *
531
+ * **Note:** Difference between {@link module:engine/view/downcastwriter~DowncastWriter#mergeAttributes mergeAttributes} and
532
+ * {@link module:engine/view/downcastwriter~DowncastWriter#mergeContainers mergeContainers} is that `mergeAttributes` merges two
533
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
534
+ * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
535
+ *
536
+ * @see module:engine/view/attributeelement~AttributeElement
537
+ * @see module:engine/view/containerelement~ContainerElement
538
+ * @see module:engine/view/downcastwriter~DowncastWriter#mergeContainers
539
+ * @param {module:engine/view/position~Position} position Merge position.
540
+ * @returns {module:engine/view/position~Position} Position after merge.
541
+ */
542
+ mergeAttributes(position) {
543
+ const positionOffset = position.offset;
544
+ const positionParent = position.parent;
545
+ // When inside text node - nothing to merge.
546
+ if (positionParent.is('$text')) {
547
+ return position;
548
+ }
549
+ // When inside empty attribute - remove it.
550
+ if (positionParent.is('attributeElement') && positionParent.childCount === 0) {
551
+ const parent = positionParent.parent;
552
+ const offset = positionParent.index;
553
+ positionParent._remove();
554
+ this._removeFromClonedElementsGroup(positionParent);
555
+ return this.mergeAttributes(new Position(parent, offset));
556
+ }
557
+ const nodeBefore = positionParent.getChild(positionOffset - 1);
558
+ const nodeAfter = positionParent.getChild(positionOffset);
559
+ // Position should be placed between two nodes.
560
+ if (!nodeBefore || !nodeAfter) {
561
+ return position;
562
+ }
563
+ // When position is between two text nodes.
564
+ if (nodeBefore.is('$text') && nodeAfter.is('$text')) {
565
+ return mergeTextNodes(nodeBefore, nodeAfter);
566
+ }
567
+ // When position is between two same attribute elements.
568
+ else if (nodeBefore.is('attributeElement') && nodeAfter.is('attributeElement') && nodeBefore.isSimilar(nodeAfter)) {
569
+ // Move all children nodes from node placed after selection and remove that node.
570
+ const count = nodeBefore.childCount;
571
+ nodeBefore._appendChild(nodeAfter.getChildren());
572
+ nodeAfter._remove();
573
+ this._removeFromClonedElementsGroup(nodeAfter);
574
+ // New position is located inside the first node, before new nodes.
575
+ // Call this method recursively to merge again if needed.
576
+ return this.mergeAttributes(new Position(nodeBefore, count));
577
+ }
578
+ return position;
579
+ }
580
+ /**
581
+ * Merges two {@link module:engine/view/containerelement~ContainerElement container elements} that are before and after given position.
582
+ * Precisely, the element after the position is removed and it's contents are moved to element before the position.
583
+ *
584
+ * <p>foo</p>^<p>bar</p> -> <p>foo^bar</p>
585
+ * <div>foo</div>^<p>bar</p> -> <div>foo^bar</div>
586
+ *
587
+ * **Note:** Difference between {@link module:engine/view/downcastwriter~DowncastWriter#mergeAttributes mergeAttributes} and
588
+ * {@link module:engine/view/downcastwriter~DowncastWriter#mergeContainers mergeContainers} is that `mergeAttributes` merges two
589
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
590
+ * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
591
+ *
592
+ * @see module:engine/view/attributeelement~AttributeElement
593
+ * @see module:engine/view/containerelement~ContainerElement
594
+ * @see module:engine/view/downcastwriter~DowncastWriter#mergeAttributes
595
+ * @param {module:engine/view/position~Position} position Merge position.
596
+ * @returns {module:engine/view/position~Position} Position after merge.
597
+ */
598
+ mergeContainers(position) {
599
+ const prev = position.nodeBefore;
600
+ const next = position.nodeAfter;
601
+ if (!prev || !next || !prev.is('containerElement') || !next.is('containerElement')) {
602
+ /**
603
+ * Element before and after given position cannot be merged.
604
+ *
605
+ * @error view-writer-merge-containers-invalid-position
606
+ */
607
+ throw new CKEditorError('view-writer-merge-containers-invalid-position', this.document);
608
+ }
609
+ const lastChild = prev.getChild(prev.childCount - 1);
610
+ const newPosition = lastChild instanceof Text ? Position._createAt(lastChild, 'end') : Position._createAt(prev, 'end');
611
+ this.move(Range._createIn(next), Position._createAt(prev, 'end'));
612
+ this.remove(Range._createOn(next));
613
+ return newPosition;
614
+ }
615
+ /**
616
+ * Inserts a node or nodes at specified position. Takes care about breaking attributes before insertion
617
+ * and merging them afterwards.
618
+ *
619
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
620
+ * contains instances that are not {@link module:engine/view/text~Text Texts},
621
+ * {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
622
+ * {@link module:engine/view/containerelement~ContainerElement ContainerElements},
623
+ * {@link module:engine/view/emptyelement~EmptyElement EmptyElements},
624
+ * {@link module:engine/view/rawelement~RawElement RawElements} or
625
+ * {@link module:engine/view/uielement~UIElement UIElements}.
626
+ *
627
+ * @param {module:engine/view/position~Position} position Insertion position.
628
+ * @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
629
+ * module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
630
+ * module:engine/view/rawelement~RawElement|module:engine/view/uielement~UIElement|
631
+ * Iterable.<module:engine/view/text~Text|
632
+ * module:engine/view/attributeelement~AttributeElement|module:engine/view/containerelement~ContainerElement|
633
+ * module:engine/view/emptyelement~EmptyElement|module:engine/view/rawelement~RawElement|
634
+ * module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
635
+ * @returns {module:engine/view/range~Range} Range around inserted nodes.
636
+ */
637
+ insert(position, nodes) {
638
+ nodes = isIterable(nodes) ? [...nodes] : [nodes];
639
+ // Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
640
+ validateNodesToInsert(nodes, this.document);
641
+ // Group nodes in batches of nodes that require or do not require breaking an AttributeElements.
642
+ const nodeGroups = nodes.reduce((groups, node) => {
643
+ const lastGroup = groups[groups.length - 1];
644
+ // Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
645
+ // can't have an attribute in model and won't get wrapped with an AttributeElement while down-casted.
646
+ const breakAttributes = !node.is('uiElement');
647
+ if (!lastGroup || lastGroup.breakAttributes != breakAttributes) {
648
+ groups.push({
649
+ breakAttributes,
650
+ nodes: [node]
651
+ });
652
+ }
653
+ else {
654
+ lastGroup.nodes.push(node);
655
+ }
656
+ return groups;
657
+ }, []);
658
+ // Insert nodes in batches.
659
+ let start = null;
660
+ let end = position;
661
+ for (const { nodes, breakAttributes } of nodeGroups) {
662
+ const range = this._insertNodes(end, nodes, breakAttributes);
663
+ if (!start) {
664
+ start = range.start;
665
+ }
666
+ end = range.end;
667
+ }
668
+ // When no nodes were inserted - return collapsed range.
669
+ if (!start) {
670
+ return new Range(position);
671
+ }
672
+ return new Range(start, end);
673
+ }
674
+ /**
675
+ * Removes provided range from the container.
676
+ *
677
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
678
+ * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
679
+ * same parent container.
680
+ *
681
+ * @param {module:engine/view/range~Range|module:engine/view/item~Item} rangeOrItem Range to remove from container
682
+ * or an {@link module:engine/view/item~Item item} to remove. If range is provided, after removing, it will be updated
683
+ * to a collapsed range showing the new position.
684
+ * @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
685
+ */
686
+ remove(rangeOrItem) {
687
+ const range = rangeOrItem instanceof Range ? rangeOrItem : Range._createOn(rangeOrItem);
688
+ validateRangeContainer(range, this.document);
689
+ // If range is collapsed - nothing to remove.
690
+ if (range.isCollapsed) {
691
+ return new DocumentFragment(this.document);
692
+ }
693
+ // Break attributes at range start and end.
694
+ const { start: breakStart, end: breakEnd } = this._breakAttributesRange(range, true);
695
+ const parentContainer = breakStart.parent;
696
+ const count = breakEnd.offset - breakStart.offset;
697
+ // Remove nodes in range.
698
+ const removed = parentContainer._removeChildren(breakStart.offset, count);
699
+ for (const node of removed) {
700
+ this._removeFromClonedElementsGroup(node);
701
+ }
702
+ // Merge after removing.
703
+ const mergePosition = this.mergeAttributes(breakStart);
704
+ range.start = mergePosition;
705
+ range.end = mergePosition.clone();
706
+ // Return removed nodes.
707
+ return new DocumentFragment(this.document, removed);
708
+ }
709
+ /**
710
+ * Removes matching elements from given range.
711
+ *
712
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
713
+ * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
714
+ * same parent container.
715
+ *
716
+ * @param {module:engine/view/range~Range} range Range to clear.
717
+ * @param {module:engine/view/element~Element} element Element to remove.
718
+ */
719
+ clear(range, element) {
720
+ validateRangeContainer(range, this.document);
721
+ // Create walker on given range.
722
+ // We walk backward because when we remove element during walk it modifies range end position.
723
+ const walker = range.getWalker({
724
+ direction: 'backward',
725
+ ignoreElementEnd: true
726
+ });
727
+ // Let's walk.
728
+ for (const current of walker) {
729
+ const item = current.item;
730
+ let rangeToRemove;
731
+ // When current item matches to the given element.
732
+ if (item.is('element') && element.isSimilar(item)) {
733
+ // Create range on this element.
734
+ rangeToRemove = Range._createOn(item);
735
+ // When range starts inside Text or TextProxy element.
736
+ }
737
+ else if (!current.nextPosition.isAfter(range.start) && item.is('$textProxy')) {
738
+ // We need to check if parent of this text matches to given element.
739
+ const parentElement = item.getAncestors().find(ancestor => {
740
+ return ancestor.is('element') && element.isSimilar(ancestor);
741
+ });
742
+ // If it is then create range inside this element.
743
+ if (parentElement) {
744
+ rangeToRemove = Range._createIn(parentElement);
745
+ }
746
+ }
747
+ // If we have found element to remove.
748
+ if (rangeToRemove) {
749
+ // We need to check if element range stick out of the given range and truncate if it is.
750
+ if (rangeToRemove.end.isAfter(range.end)) {
751
+ rangeToRemove.end = range.end;
752
+ }
753
+ if (rangeToRemove.start.isBefore(range.start)) {
754
+ rangeToRemove.start = range.start;
755
+ }
756
+ // At the end we remove range with found element.
757
+ this.remove(rangeToRemove);
758
+ }
759
+ }
760
+ }
761
+ /**
762
+ * Moves nodes from provided range to target position.
763
+ *
764
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
765
+ * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
766
+ * same parent container.
767
+ *
768
+ * @param {module:engine/view/range~Range} sourceRange Range containing nodes to move.
769
+ * @param {module:engine/view/position~Position} targetPosition Position to insert.
770
+ * @returns {module:engine/view/range~Range} Range in target container. Inserted nodes are placed between
771
+ * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions.
772
+ */
773
+ move(sourceRange, targetPosition) {
774
+ let nodes;
775
+ if (targetPosition.isAfter(sourceRange.end)) {
776
+ targetPosition = this._breakAttributes(targetPosition, true);
777
+ const parent = targetPosition.parent;
778
+ const countBefore = parent.childCount;
779
+ sourceRange = this._breakAttributesRange(sourceRange, true);
780
+ nodes = this.remove(sourceRange);
781
+ targetPosition.offset += (parent.childCount - countBefore);
782
+ }
783
+ else {
784
+ nodes = this.remove(sourceRange);
785
+ }
786
+ return this.insert(targetPosition, nodes);
787
+ }
788
+ /**
789
+ * Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
790
+ * If a collapsed range is provided, it will be wrapped only if it is equal to view selection.
791
+ *
792
+ * If a collapsed range was passed and is same as selection, the selection
793
+ * will be moved to the inside of the wrapped attribute element.
794
+ *
795
+ * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
796
+ * when {@link module:engine/view/range~Range#start}
797
+ * and {@link module:engine/view/range~Range#end} positions are not placed inside same parent container.
798
+ *
799
+ * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
800
+ * an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
801
+ *
802
+ * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-nonselection-collapsed-range` when passed range
803
+ * is collapsed and different than view selection.
804
+ *
805
+ * @param {module:engine/view/range~Range} range Range to wrap.
806
+ * @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
807
+ * @returns {module:engine/view/range~Range} range Range after wrapping, spanning over wrapping attribute element.
808
+ */
809
+ wrap(range, attribute) {
810
+ if (!(attribute instanceof AttributeElement)) {
811
+ throw new CKEditorError('view-writer-wrap-invalid-attribute', this.document);
812
+ }
813
+ validateRangeContainer(range, this.document);
814
+ if (!range.isCollapsed) {
815
+ // Non-collapsed range. Wrap it with the attribute element.
816
+ return this._wrapRange(range, attribute);
817
+ }
818
+ else {
819
+ // Collapsed range. Wrap position.
820
+ let position = range.start;
821
+ if (position.parent.is('element') && !_hasNonUiChildren(position.parent)) {
822
+ position = position.getLastMatchingPosition(value => value.item.is('uiElement'));
823
+ }
824
+ position = this._wrapPosition(position, attribute);
825
+ const viewSelection = this.document.selection;
826
+ // If wrapping position is equal to view selection, move view selection inside wrapping attribute element.
827
+ if (viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual(range.start)) {
828
+ this.setSelection(position);
829
+ }
830
+ return new Range(position);
831
+ }
832
+ }
833
+ /**
834
+ * Unwraps nodes within provided range from attribute element.
835
+ *
836
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
837
+ * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
838
+ * same parent container.
839
+ *
840
+ * @param {module:engine/view/range~Range} range
841
+ * @param {module:engine/view/attributeelement~AttributeElement} attribute
842
+ */
843
+ unwrap(range, attribute) {
844
+ if (!(attribute instanceof AttributeElement)) {
845
+ /**
846
+ * The `attribute` passed to {@link module:engine/view/downcastwriter~DowncastWriter#unwrap `DowncastWriter#unwrap()`}
847
+ * must be an instance of {@link module:engine/view/attributeelement~AttributeElement `AttributeElement`}.
848
+ *
849
+ * @error view-writer-unwrap-invalid-attribute
850
+ */
851
+ throw new CKEditorError('view-writer-unwrap-invalid-attribute', this.document);
852
+ }
853
+ validateRangeContainer(range, this.document);
854
+ // If range is collapsed - nothing to unwrap.
855
+ if (range.isCollapsed) {
856
+ return range;
857
+ }
858
+ // Break attributes at range start and end.
859
+ const { start: breakStart, end: breakEnd } = this._breakAttributesRange(range, true);
860
+ const parentContainer = breakStart.parent;
861
+ // Unwrap children located between break points.
862
+ const newRange = this._unwrapChildren(parentContainer, breakStart.offset, breakEnd.offset, attribute);
863
+ // Merge attributes at the both ends and return a new range.
864
+ const start = this.mergeAttributes(newRange.start);
865
+ // If start position was merged - move end position back.
866
+ if (!start.isEqual(newRange.start)) {
867
+ newRange.end.offset--;
868
+ }
869
+ const end = this.mergeAttributes(newRange.end);
870
+ return new Range(start, end);
871
+ }
872
+ /**
873
+ * Renames element by creating a copy of renamed element but with changed name and then moving contents of the
874
+ * old element to the new one. Keep in mind that this will invalidate all {@link module:engine/view/position~Position positions} which
875
+ * has renamed element as {@link module:engine/view/position~Position#parent a parent}.
876
+ *
877
+ * New element has to be created because `Element#tagName` property in DOM is readonly.
878
+ *
879
+ * Since this function creates a new element and removes the given one, the new element is returned to keep reference.
880
+ *
881
+ * @param {String} newName New name for element.
882
+ * @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed.
883
+ * @returns {module:engine/view/containerelement~ContainerElement} Element created due to rename.
884
+ */
885
+ rename(newName, viewElement) {
886
+ const newElement = new ContainerElement(this.document, newName, viewElement.getAttributes());
887
+ this.insert(Position._createAfter(viewElement), newElement);
888
+ this.move(Range._createIn(viewElement), Position._createAt(newElement, 0));
889
+ this.remove(Range._createOn(viewElement));
890
+ return newElement;
891
+ }
892
+ /**
893
+ * Cleans up memory by removing obsolete cloned elements group from the writer.
894
+ *
895
+ * Should be used whenever all {@link module:engine/view/attributeelement~AttributeElement attribute elements}
896
+ * with the same {@link module:engine/view/attributeelement~AttributeElement#id id} are going to be removed from the view and
897
+ * the group will no longer be needed.
898
+ *
899
+ * Cloned elements group are not removed automatically in case if the group is still needed after all its elements
900
+ * were removed from the view.
901
+ *
902
+ * Keep in mind that group names are equal to the `id` property of the attribute element.
903
+ *
904
+ * @param {String} groupName Name of the group to clear.
905
+ */
906
+ clearClonedElementsGroup(groupName) {
907
+ this._cloneGroups.delete(groupName);
908
+ }
909
+ /**
910
+ * Creates position at the given location. The location can be specified as:
911
+ *
912
+ * * a {@link module:engine/view/position~Position position},
913
+ * * parent element and offset (offset defaults to `0`),
914
+ * * parent element and `'end'` (sets position at the end of that element),
915
+ * * {@link module:engine/view/item~Item view item} and `'before'` or `'after'` (sets position before or after given view item).
916
+ *
917
+ * This method is a shortcut to other constructors such as:
918
+ *
919
+ * * {@link #createPositionBefore},
920
+ * * {@link #createPositionAfter},
921
+ *
922
+ * @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
923
+ * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
924
+ * first parameter is a {@link module:engine/view/item~Item view item}.
925
+ * @returns {module:engine/view/position~Position}
926
+ */
927
+ createPositionAt(itemOrPosition, offset) {
928
+ return Position._createAt(itemOrPosition, offset);
929
+ }
930
+ /**
931
+ * Creates a new position after given view item.
932
+ *
933
+ * @param {module:engine/view/item~Item} item View item after which the position should be located.
934
+ * @returns {module:engine/view/position~Position}
935
+ */
936
+ createPositionAfter(item) {
937
+ return Position._createAfter(item);
938
+ }
939
+ /**
940
+ * Creates a new position before given view item.
941
+ *
942
+ * @param {module:engine/view/item~Item} item View item before which the position should be located.
943
+ * @returns {module:engine/view/position~Position}
944
+ */
945
+ createPositionBefore(item) {
946
+ return Position._createBefore(item);
947
+ }
948
+ /**
949
+ * Creates a range spanning from `start` position to `end` position.
950
+ *
951
+ * **Note:** This factory method creates its own {@link module:engine/view/position~Position} instances basing on passed values.
952
+ *
953
+ * @param {module:engine/view/position~Position} start Start position.
954
+ * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
955
+ * @returns {module:engine/view/range~Range}
956
+ */
957
+ createRange(...args) {
958
+ return new Range(...args);
959
+ }
960
+ /**
961
+ * Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
962
+ *
963
+ * @param {module:engine/view/item~Item} item
964
+ * @returns {module:engine/view/range~Range}
965
+ */
966
+ createRangeOn(item) {
967
+ return Range._createOn(item);
968
+ }
969
+ /**
970
+ * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
971
+ * that element and ends after the last child of that element.
972
+ *
973
+ * @param {module:engine/view/element~Element} element Element which is a parent for the range.
974
+ * @returns {module:engine/view/range~Range}
975
+ */
976
+ createRangeIn(element) {
977
+ return Range._createIn(element);
978
+ }
979
+ /**
980
+ * Creates new {@link module:engine/view/selection~Selection} instance.
981
+ *
982
+ * // Creates empty selection without ranges.
983
+ * const selection = writer.createSelection();
984
+ *
985
+ * // Creates selection at the given range.
986
+ * const range = writer.createRange( start, end );
987
+ * const selection = writer.createSelection( range );
988
+ *
989
+ * // Creates selection at the given ranges
990
+ * const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
991
+ * const selection = writer.createSelection( ranges );
992
+ *
993
+ * // Creates selection from the other selection.
994
+ * const otherSelection = writer.createSelection();
995
+ * const selection = writer.createSelection( otherSelection );
996
+ *
997
+ * // Creates selection from the document selection.
998
+ * const selection = writer.createSelection( editor.editing.view.document.selection );
999
+ *
1000
+ * // Creates selection at the given position.
1001
+ * const position = writer.createPositionFromPath( root, path );
1002
+ * const selection = writer.createSelection( position );
1003
+ *
1004
+ * // Creates collapsed selection at the position of given item and offset.
1005
+ * const paragraph = writer.createContainerElement( 'p' );
1006
+ * const selection = writer.createSelection( paragraph, offset );
1007
+ *
1008
+ * // Creates a range inside an {@link module:engine/view/element~Element element} which starts before the
1009
+ * // first child of that element and ends after the last child of that element.
1010
+ * const selection = writer.createSelection( paragraph, 'in' );
1011
+ *
1012
+ * // Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
1013
+ * // just after the item.
1014
+ * const selection = writer.createSelection( paragraph, 'on' );
1015
+ *
1016
+ * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
1017
+ *
1018
+ * // Creates backward selection.
1019
+ * const selection = writer.createSelection( range, { backward: true } );
1020
+ *
1021
+ * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
1022
+ * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
1023
+ * represented in other way, for example by applying proper CSS class.
1024
+ *
1025
+ * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
1026
+ * (and be properly handled by screen readers).
1027
+ *
1028
+ * // Creates fake selection with label.
1029
+ * const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
1030
+ *
1031
+ * @param {module:engine/view/selection~Selectable} [selectable=null]
1032
+ * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
1033
+ * @param {Object} [options]
1034
+ * @param {Boolean} [options.backward] Sets this selection instance to be backward.
1035
+ * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
1036
+ * @param {String} [options.label] Label for the fake selection.
1037
+ * @returns {module:engine/view/selection~Selection}
1038
+ */
1039
+ createSelection(...args) {
1040
+ return new Selection(...args);
1041
+ }
1042
+ /**
1043
+ * Creates placeholders for child elements of the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
1044
+ * `elementToStructure()`} conversion helper.
1045
+ *
1046
+ * const viewSlot = conversionApi.writer.createSlot();
1047
+ * const viewPosition = conversionApi.writer.createPositionAt( viewElement, 0 );
1048
+ *
1049
+ * conversionApi.writer.insert( viewPosition, viewSlot );
1050
+ *
1051
+ * It could be filtered down to a specific subset of children (only `<foo>` model elements in this case):
1052
+ *
1053
+ * const viewSlot = conversionApi.writer.createSlot( node => node.is( 'element', 'foo' ) );
1054
+ * const viewPosition = conversionApi.writer.createPositionAt( viewElement, 0 );
1055
+ *
1056
+ * conversionApi.writer.insert( viewPosition, viewSlot );
1057
+ *
1058
+ * While providing a filtered slot, make sure to provide slots for all child nodes. A single node can not be downcasted into
1059
+ * multiple slots.
1060
+ *
1061
+ * **Note**: You should not change the order of nodes. View elements should be in the same order as model nodes.
1062
+ *
1063
+ * @param {'children'|module:engine/conversion/downcasthelpers~SlotFilter} [modeOrFilter='children'] The filter for child nodes.
1064
+ * @returns {module:engine/view/element~Element} The slot element to be placed in to the view structure while processing
1065
+ * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure `elementToStructure()`}.
1066
+ */
1067
+ createSlot(modeOrFilter) {
1068
+ if (!this._slotFactory) {
1069
+ /**
1070
+ * The `createSlot()` method is only allowed inside the `elementToStructure` downcast helper callback.
1071
+ *
1072
+ * @error view-writer-invalid-create-slot-context
1073
+ */
1074
+ throw new CKEditorError('view-writer-invalid-create-slot-context', this.document);
1075
+ }
1076
+ return this._slotFactory(this, modeOrFilter);
1077
+ }
1078
+ /**
1079
+ * Registers a slot factory.
1080
+ *
1081
+ * @protected
1082
+ * @param {Function} slotFactory The slot factory.
1083
+ */
1084
+ _registerSlotFactory(slotFactory) {
1085
+ this._slotFactory = slotFactory;
1086
+ }
1087
+ /**
1088
+ * Clears the registered slot factory.
1089
+ *
1090
+ * @protected
1091
+ */
1092
+ _clearSlotFactory() {
1093
+ this._slotFactory = null;
1094
+ }
1095
+ /**
1096
+ * Inserts a node or nodes at the specified position. Takes care of breaking attributes before insertion
1097
+ * and merging them afterwards if requested by the breakAttributes param.
1098
+ *
1099
+ * @private
1100
+ * @param {module:engine/view/position~Position} position Insertion position.
1101
+ * @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
1102
+ * module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
1103
+ * module:engine/view/rawelement~RawElement|module:engine/view/uielement~UIElement|
1104
+ * Iterable.<module:engine/view/text~Text|
1105
+ * module:engine/view/attributeelement~AttributeElement|module:engine/view/containerelement~ContainerElement|
1106
+ * module:engine/view/emptyelement~EmptyElement|module:engine/view/rawelement~RawElement|
1107
+ * module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
1108
+ * @param {Boolean} breakAttributes Whether attributes should be broken.
1109
+ * @returns {module:engine/view/range~Range} Range around inserted nodes.
1110
+ */
1111
+ _insertNodes(position, nodes, breakAttributes) {
1112
+ let parentElement;
1113
+ // Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
1114
+ // can't have an attribute in model and won't get wrapped with an AttributeElement while down-casted.
1115
+ if (breakAttributes) {
1116
+ parentElement = getParentContainer(position);
1117
+ }
1118
+ else {
1119
+ parentElement = position.parent.is('$text') ? position.parent.parent : position.parent;
1120
+ }
1121
+ if (!parentElement) {
1122
+ /**
1123
+ * Position's parent container cannot be found.
1124
+ *
1125
+ * @error view-writer-invalid-position-container
1126
+ */
1127
+ throw new CKEditorError('view-writer-invalid-position-container', this.document);
1128
+ }
1129
+ let insertionPosition;
1130
+ if (breakAttributes) {
1131
+ insertionPosition = this._breakAttributes(position, true);
1132
+ }
1133
+ else {
1134
+ insertionPosition = position.parent.is('$text') ? breakTextNode(position) : position;
1135
+ }
1136
+ const length = parentElement._insertChild(insertionPosition.offset, nodes);
1137
+ for (const node of nodes) {
1138
+ this._addToClonedElementsGroup(node);
1139
+ }
1140
+ const endPosition = insertionPosition.getShiftedBy(length);
1141
+ const start = this.mergeAttributes(insertionPosition);
1142
+ // If start position was merged - move end position.
1143
+ if (!start.isEqual(insertionPosition)) {
1144
+ endPosition.offset--;
1145
+ }
1146
+ const end = this.mergeAttributes(endPosition);
1147
+ return new Range(start, end);
1148
+ }
1149
+ /**
1150
+ * Wraps children with provided `wrapElement`. Only children contained in `parent` element between
1151
+ * `startOffset` and `endOffset` will be wrapped.
1152
+ *
1153
+ * @private
1154
+ * @param {module:engine/view/element~Element} parent
1155
+ * @param {Number} startOffset
1156
+ * @param {Number} endOffset
1157
+ * @param {module:engine/view/element~Element} wrapElement
1158
+ */
1159
+ _wrapChildren(parent, startOffset, endOffset, wrapElement) {
1160
+ let i = startOffset;
1161
+ const wrapPositions = [];
1162
+ while (i < endOffset) {
1163
+ const child = parent.getChild(i);
1164
+ const isText = child.is('$text');
1165
+ const isAttribute = child.is('attributeElement');
1166
+ //
1167
+ // (In all examples, assume that `wrapElement` is `<span class="foo">` element.)
1168
+ //
1169
+ // Check if `wrapElement` can be joined with the wrapped element. One of requirements is having same name.
1170
+ // If possible, join elements.
1171
+ //
1172
+ // <p><span class="bar">abc</span></p> --> <p><span class="foo bar">abc</span></p>
1173
+ //
1174
+ if (isAttribute && this._wrapAttributeElement(wrapElement, child)) {
1175
+ wrapPositions.push(new Position(parent, i));
1176
+ }
1177
+ //
1178
+ // Wrap the child if it is not an attribute element or if it is an attribute element that should be inside
1179
+ // `wrapElement` (due to priority).
1180
+ //
1181
+ // <p>abc</p> --> <p><span class="foo">abc</span></p>
1182
+ // <p><strong>abc</strong></p> --> <p><span class="foo"><strong>abc</strong></span></p>
1183
+ else if (isText || !isAttribute || shouldABeOutsideB(wrapElement, child)) {
1184
+ // Clone attribute.
1185
+ const newAttribute = wrapElement._clone();
1186
+ // Wrap current node with new attribute.
1187
+ child._remove();
1188
+ newAttribute._appendChild(child);
1189
+ parent._insertChild(i, newAttribute);
1190
+ this._addToClonedElementsGroup(newAttribute);
1191
+ wrapPositions.push(new Position(parent, i));
1192
+ }
1193
+ //
1194
+ // If other nested attribute is found and it wasn't wrapped (see above), continue wrapping inside it.
1195
+ //
1196
+ // <p><a href="foo.html">abc</a></p> --> <p><a href="foo.html"><span class="foo">abc</span></a></p>
1197
+ //
1198
+ else /* if ( isAttribute ) */ {
1199
+ this._wrapChildren(child, 0, child.childCount, wrapElement);
1200
+ }
1201
+ i++;
1202
+ }
1203
+ // Merge at each wrap.
1204
+ let offsetChange = 0;
1205
+ for (const position of wrapPositions) {
1206
+ position.offset -= offsetChange;
1207
+ // Do not merge with elements outside selected children.
1208
+ if (position.offset == startOffset) {
1209
+ continue;
1210
+ }
1211
+ const newPosition = this.mergeAttributes(position);
1212
+ // If nodes were merged - other merge offsets will change.
1213
+ if (!newPosition.isEqual(position)) {
1214
+ offsetChange++;
1215
+ endOffset--;
1216
+ }
1217
+ }
1218
+ return Range._createFromParentsAndOffsets(parent, startOffset, parent, endOffset);
1219
+ }
1220
+ /**
1221
+ * Unwraps children from provided `unwrapElement`. Only children contained in `parent` element between
1222
+ * `startOffset` and `endOffset` will be unwrapped.
1223
+ *
1224
+ * @private
1225
+ * @param {module:engine/view/element~Element} parent
1226
+ * @param {Number} startOffset
1227
+ * @param {Number} endOffset
1228
+ * @param {module:engine/view/element~Element} unwrapElement
1229
+ */
1230
+ _unwrapChildren(parent, startOffset, endOffset, unwrapElement) {
1231
+ let i = startOffset;
1232
+ const unwrapPositions = [];
1233
+ // Iterate over each element between provided offsets inside parent.
1234
+ // We don't use tree walker or range iterator because we will be removing and merging potentially multiple nodes,
1235
+ // so it could get messy. It is safer to it manually in this case.
1236
+ while (i < endOffset) {
1237
+ const child = parent.getChild(i);
1238
+ // Skip all text nodes. There should be no container element's here either.
1239
+ if (!child.is('attributeElement')) {
1240
+ i++;
1241
+ continue;
1242
+ }
1243
+ //
1244
+ // (In all examples, assume that `unwrapElement` is `<span class="foo">` element.)
1245
+ //
1246
+ // If the child is similar to the given attribute element, unwrap it - it will be completely removed.
1247
+ //
1248
+ // <p><span class="foo">abc</span>xyz</p> --> <p>abcxyz</p>
1249
+ //
1250
+ if (child.isSimilar(unwrapElement)) {
1251
+ const unwrapped = child.getChildren();
1252
+ const count = child.childCount;
1253
+ // Replace wrapper element with its children
1254
+ child._remove();
1255
+ parent._insertChild(i, unwrapped);
1256
+ this._removeFromClonedElementsGroup(child);
1257
+ // Save start and end position of moved items.
1258
+ unwrapPositions.push(new Position(parent, i), new Position(parent, i + count));
1259
+ // Skip elements that were unwrapped. Assuming there won't be another element to unwrap in child elements.
1260
+ i += count;
1261
+ endOffset += count - 1;
1262
+ continue;
1263
+ }
1264
+ //
1265
+ // If the child is not similar but is an attribute element, try partial unwrapping - remove the same attributes/styles/classes.
1266
+ // Partial unwrapping will happen only if the elements have the same name.
1267
+ //
1268
+ // <p><span class="foo bar">abc</span>xyz</p> --> <p><span class="bar">abc</span>xyz</p>
1269
+ // <p><i class="foo">abc</i>xyz</p> --> <p><i class="foo">abc</i>xyz</p>
1270
+ //
1271
+ if (this._unwrapAttributeElement(unwrapElement, child)) {
1272
+ unwrapPositions.push(new Position(parent, i), new Position(parent, i + 1));
1273
+ i++;
1274
+ continue;
1275
+ }
1276
+ //
1277
+ // If other nested attribute is found, look through it's children for elements to unwrap.
1278
+ //
1279
+ // <p><i><span class="foo">abc</span></i><p> --> <p><i>abc</i><p>
1280
+ //
1281
+ this._unwrapChildren(child, 0, child.childCount, unwrapElement);
1282
+ i++;
1283
+ }
1284
+ // Merge at each unwrap.
1285
+ let offsetChange = 0;
1286
+ for (const position of unwrapPositions) {
1287
+ position.offset -= offsetChange;
1288
+ // Do not merge with elements outside selected children.
1289
+ if (position.offset == startOffset || position.offset == endOffset) {
1290
+ continue;
1291
+ }
1292
+ const newPosition = this.mergeAttributes(position);
1293
+ // If nodes were merged - other merge offsets will change.
1294
+ if (!newPosition.isEqual(position)) {
1295
+ offsetChange++;
1296
+ endOffset--;
1297
+ }
1298
+ }
1299
+ return Range._createFromParentsAndOffsets(parent, startOffset, parent, endOffset);
1300
+ }
1301
+ /**
1302
+ * Helper function for `view.writer.wrap`. Wraps range with provided attribute element.
1303
+ * This method will also merge newly added attribute element with its siblings whenever possible.
1304
+ *
1305
+ * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
1306
+ * an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
1307
+ *
1308
+ * @private
1309
+ * @param {module:engine/view/range~Range} range
1310
+ * @param {module:engine/view/attributeelement~AttributeElement} attribute
1311
+ * @returns {module:engine/view/range~Range} New range after wrapping, spanning over wrapping attribute element.
1312
+ */
1313
+ _wrapRange(range, attribute) {
1314
+ // Break attributes at range start and end.
1315
+ const { start: breakStart, end: breakEnd } = this._breakAttributesRange(range, true);
1316
+ const parentContainer = breakStart.parent;
1317
+ // Wrap all children with attribute.
1318
+ const newRange = this._wrapChildren(parentContainer, breakStart.offset, breakEnd.offset, attribute);
1319
+ // Merge attributes at the both ends and return a new range.
1320
+ const start = this.mergeAttributes(newRange.start);
1321
+ // If start position was merged - move end position back.
1322
+ if (!start.isEqual(newRange.start)) {
1323
+ newRange.end.offset--;
1324
+ }
1325
+ const end = this.mergeAttributes(newRange.end);
1326
+ return new Range(start, end);
1327
+ }
1328
+ /**
1329
+ * Helper function for {@link #wrap}. Wraps position with provided attribute element.
1330
+ * This method will also merge newly added attribute element with its siblings whenever possible.
1331
+ *
1332
+ * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
1333
+ * an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
1334
+ *
1335
+ * @private
1336
+ * @param {module:engine/view/position~Position} position
1337
+ * @param {module:engine/view/attributeelement~AttributeElement} attribute
1338
+ * @returns {module:engine/view/position~Position} New position after wrapping.
1339
+ */
1340
+ _wrapPosition(position, attribute) {
1341
+ // Return same position when trying to wrap with attribute similar to position parent.
1342
+ if (attribute.isSimilar(position.parent)) {
1343
+ return movePositionToTextNode(position.clone());
1344
+ }
1345
+ // When position is inside text node - break it and place new position between two text nodes.
1346
+ if (position.parent.is('$text')) {
1347
+ position = breakTextNode(position);
1348
+ }
1349
+ // Create fake element that will represent position, and will not be merged with other attributes.
1350
+ const fakeElement = this.createAttributeElement('_wrapPosition-fake-element');
1351
+ fakeElement._priority = Number.POSITIVE_INFINITY;
1352
+ fakeElement.isSimilar = () => false;
1353
+ // Insert fake element in position location.
1354
+ position.parent._insertChild(position.offset, fakeElement);
1355
+ // Range around inserted fake attribute element.
1356
+ const wrapRange = new Range(position, position.getShiftedBy(1));
1357
+ // Wrap fake element with attribute (it will also merge if possible).
1358
+ this.wrap(wrapRange, attribute);
1359
+ // Remove fake element and place new position there.
1360
+ const newPosition = new Position(fakeElement.parent, fakeElement.index);
1361
+ fakeElement._remove();
1362
+ // If position is placed between text nodes - merge them and return position inside.
1363
+ const nodeBefore = newPosition.nodeBefore;
1364
+ const nodeAfter = newPosition.nodeAfter;
1365
+ if (nodeBefore instanceof Text && nodeAfter instanceof Text) {
1366
+ return mergeTextNodes(nodeBefore, nodeAfter);
1367
+ }
1368
+ // If position is next to text node - move position inside.
1369
+ return movePositionToTextNode(newPosition);
1370
+ }
1371
+ /**
1372
+ * Wraps one {@link module:engine/view/attributeelement~AttributeElement AttributeElement} into another by
1373
+ * merging them if possible. When merging is possible - all attributes, styles and classes are moved from wrapper
1374
+ * element to element being wrapped.
1375
+ *
1376
+ * @private
1377
+ * @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
1378
+ * @param {module:engine/view/attributeelement~AttributeElement} toWrap AttributeElement to wrap using wrapper element.
1379
+ * @returns {Boolean} Returns `true` if elements are merged.
1380
+ */
1381
+ _wrapAttributeElement(wrapper, toWrap) {
1382
+ if (!canBeJoined(wrapper, toWrap)) {
1383
+ return false;
1384
+ }
1385
+ // Can't merge if name or priority differs.
1386
+ if (wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority) {
1387
+ return false;
1388
+ }
1389
+ // Check if attributes can be merged.
1390
+ for (const key of wrapper.getAttributeKeys()) {
1391
+ // Classes and styles should be checked separately.
1392
+ if (key === 'class' || key === 'style') {
1393
+ continue;
1394
+ }
1395
+ // If some attributes are different we cannot wrap.
1396
+ if (toWrap.hasAttribute(key) && toWrap.getAttribute(key) !== wrapper.getAttribute(key)) {
1397
+ return false;
1398
+ }
1399
+ }
1400
+ // Check if styles can be merged.
1401
+ for (const key of wrapper.getStyleNames()) {
1402
+ if (toWrap.hasStyle(key) && toWrap.getStyle(key) !== wrapper.getStyle(key)) {
1403
+ return false;
1404
+ }
1405
+ }
1406
+ // Move all attributes/classes/styles from wrapper to wrapped AttributeElement.
1407
+ for (const key of wrapper.getAttributeKeys()) {
1408
+ // Classes and styles should be checked separately.
1409
+ if (key === 'class' || key === 'style') {
1410
+ continue;
1411
+ }
1412
+ // Move only these attributes that are not present - other are similar.
1413
+ if (!toWrap.hasAttribute(key)) {
1414
+ this.setAttribute(key, wrapper.getAttribute(key), toWrap);
1415
+ }
1416
+ }
1417
+ for (const key of wrapper.getStyleNames()) {
1418
+ if (!toWrap.hasStyle(key)) {
1419
+ this.setStyle(key, wrapper.getStyle(key), toWrap);
1420
+ }
1421
+ }
1422
+ for (const key of wrapper.getClassNames()) {
1423
+ if (!toWrap.hasClass(key)) {
1424
+ this.addClass(key, toWrap);
1425
+ }
1426
+ }
1427
+ return true;
1428
+ }
1429
+ /**
1430
+ * Unwraps {@link module:engine/view/attributeelement~AttributeElement AttributeElement} from another by removing
1431
+ * corresponding attributes, classes and styles. All attributes, classes and styles from wrapper should be present
1432
+ * inside element being unwrapped.
1433
+ *
1434
+ * @private
1435
+ * @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
1436
+ * @param {module:engine/view/attributeelement~AttributeElement} toUnwrap AttributeElement to unwrap using wrapper element.
1437
+ * @returns {Boolean} Returns `true` if elements are unwrapped.
1438
+ **/
1439
+ _unwrapAttributeElement(wrapper, toUnwrap) {
1440
+ if (!canBeJoined(wrapper, toUnwrap)) {
1441
+ return false;
1442
+ }
1443
+ // Can't unwrap if name or priority differs.
1444
+ if (wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority) {
1445
+ return false;
1446
+ }
1447
+ // Check if AttributeElement has all wrapper attributes.
1448
+ for (const key of wrapper.getAttributeKeys()) {
1449
+ // Classes and styles should be checked separately.
1450
+ if (key === 'class' || key === 'style') {
1451
+ continue;
1452
+ }
1453
+ // If some attributes are missing or different we cannot unwrap.
1454
+ if (!toUnwrap.hasAttribute(key) || toUnwrap.getAttribute(key) !== wrapper.getAttribute(key)) {
1455
+ return false;
1456
+ }
1457
+ }
1458
+ // Check if AttributeElement has all wrapper classes.
1459
+ if (!toUnwrap.hasClass(...wrapper.getClassNames())) {
1460
+ return false;
1461
+ }
1462
+ // Check if AttributeElement has all wrapper styles.
1463
+ for (const key of wrapper.getStyleNames()) {
1464
+ // If some styles are missing or different we cannot unwrap.
1465
+ if (!toUnwrap.hasStyle(key) || toUnwrap.getStyle(key) !== wrapper.getStyle(key)) {
1466
+ return false;
1467
+ }
1468
+ }
1469
+ // Remove all wrapper's attributes from unwrapped element.
1470
+ for (const key of wrapper.getAttributeKeys()) {
1471
+ // Classes and styles should be checked separately.
1472
+ if (key === 'class' || key === 'style') {
1473
+ continue;
1474
+ }
1475
+ this.removeAttribute(key, toUnwrap);
1476
+ }
1477
+ // Remove all wrapper's classes from unwrapped element.
1478
+ this.removeClass(Array.from(wrapper.getClassNames()), toUnwrap);
1479
+ // Remove all wrapper's styles from unwrapped element.
1480
+ this.removeStyle(Array.from(wrapper.getStyleNames()), toUnwrap);
1481
+ return true;
1482
+ }
1483
+ /**
1484
+ * Helper function used by other `DowncastWriter` methods. Breaks attribute elements at the boundaries of given range.
1485
+ *
1486
+ * @private
1487
+ * @param {module:engine/view/range~Range} range Range which `start` and `end` positions will be used to break attributes.
1488
+ * @param {Boolean} [forceSplitText=false] If set to `true`, will break text nodes even if they are directly in container element.
1489
+ * This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
1490
+ * @returns {module:engine/view/range~Range} New range with located at break positions.
1491
+ */
1492
+ _breakAttributesRange(range, forceSplitText = false) {
1493
+ const rangeStart = range.start;
1494
+ const rangeEnd = range.end;
1495
+ validateRangeContainer(range, this.document);
1496
+ // Break at the collapsed position. Return new collapsed range.
1497
+ if (range.isCollapsed) {
1498
+ const position = this._breakAttributes(range.start, forceSplitText);
1499
+ return new Range(position, position);
1500
+ }
1501
+ const breakEnd = this._breakAttributes(rangeEnd, forceSplitText);
1502
+ const count = breakEnd.parent.childCount;
1503
+ const breakStart = this._breakAttributes(rangeStart, forceSplitText);
1504
+ // Calculate new break end offset.
1505
+ breakEnd.offset += breakEnd.parent.childCount - count;
1506
+ return new Range(breakStart, breakEnd);
1507
+ }
1508
+ /**
1509
+ * Helper function used by other `DowncastWriter` methods. Breaks attribute elements at given position.
1510
+ *
1511
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position
1512
+ * is placed inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
1513
+ *
1514
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position
1515
+ * is placed inside {@link module:engine/view/uielement~UIElement UIElement}.
1516
+ *
1517
+ * @private
1518
+ * @param {module:engine/view/position~Position} position Position where to break attributes.
1519
+ * @param {Boolean} [forceSplitText=false] If set to `true`, will break text nodes even if they are directly in container element.
1520
+ * This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
1521
+ * @returns {module:engine/view/position~Position} New position after breaking the attributes.
1522
+ */
1523
+ _breakAttributes(position, forceSplitText = false) {
1524
+ const positionOffset = position.offset;
1525
+ const positionParent = position.parent;
1526
+ // If position is placed inside EmptyElement - throw an exception as we cannot break inside.
1527
+ if (position.parent.is('emptyElement')) {
1528
+ /**
1529
+ * Cannot break an `EmptyElement` instance.
1530
+ *
1531
+ * This error is thrown if
1532
+ * {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes `DowncastWriter#breakAttributes()`}
1533
+ * was executed in an incorrect position.
1534
+ *
1535
+ * @error view-writer-cannot-break-empty-element
1536
+ */
1537
+ throw new CKEditorError('view-writer-cannot-break-empty-element', this.document);
1538
+ }
1539
+ // If position is placed inside UIElement - throw an exception as we cannot break inside.
1540
+ if (position.parent.is('uiElement')) {
1541
+ /**
1542
+ * Cannot break a `UIElement` instance.
1543
+ *
1544
+ * This error is thrown if
1545
+ * {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes `DowncastWriter#breakAttributes()`}
1546
+ * was executed in an incorrect position.
1547
+ *
1548
+ * @error view-writer-cannot-break-ui-element
1549
+ */
1550
+ throw new CKEditorError('view-writer-cannot-break-ui-element', this.document);
1551
+ }
1552
+ // If position is placed inside RawElement - throw an exception as we cannot break inside.
1553
+ if (position.parent.is('rawElement')) {
1554
+ /**
1555
+ * Cannot break a `RawElement` instance.
1556
+ *
1557
+ * This error is thrown if
1558
+ * {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes `DowncastWriter#breakAttributes()`}
1559
+ * was executed in an incorrect position.
1560
+ *
1561
+ * @error view-writer-cannot-break-raw-element
1562
+ */
1563
+ throw new CKEditorError('view-writer-cannot-break-raw-element', this.document);
1564
+ }
1565
+ // There are no attributes to break and text nodes breaking is not forced.
1566
+ if (!forceSplitText && positionParent.is('$text') && isContainerOrFragment(positionParent.parent)) {
1567
+ return position.clone();
1568
+ }
1569
+ // Position's parent is container, so no attributes to break.
1570
+ if (isContainerOrFragment(positionParent)) {
1571
+ return position.clone();
1572
+ }
1573
+ // Break text and start again in new position.
1574
+ if (positionParent.is('$text')) {
1575
+ return this._breakAttributes(breakTextNode(position), forceSplitText);
1576
+ }
1577
+ const length = positionParent.childCount;
1578
+ // <p>foo<b><u>bar{}</u></b></p>
1579
+ // <p>foo<b><u>bar</u>[]</b></p>
1580
+ // <p>foo<b><u>bar</u></b>[]</p>
1581
+ if (positionOffset == length) {
1582
+ const newPosition = new Position(positionParent.parent, positionParent.index + 1);
1583
+ return this._breakAttributes(newPosition, forceSplitText);
1584
+ }
1585
+ else {
1586
+ // <p>foo<b><u>{}bar</u></b></p>
1587
+ // <p>foo<b>[]<u>bar</u></b></p>
1588
+ // <p>foo{}<b><u>bar</u></b></p>
1589
+ if (positionOffset === 0) {
1590
+ const newPosition = new Position(positionParent.parent, positionParent.index);
1591
+ return this._breakAttributes(newPosition, forceSplitText);
1592
+ }
1593
+ // <p>foo<b><u>b{}ar</u></b></p>
1594
+ // <p>foo<b><u>b[]ar</u></b></p>
1595
+ // <p>foo<b><u>b</u>[]<u>ar</u></b></p>
1596
+ // <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
1597
+ else {
1598
+ const offsetAfter = positionParent.index + 1;
1599
+ // Break element.
1600
+ const clonedNode = positionParent._clone();
1601
+ // Insert cloned node to position's parent node.
1602
+ positionParent.parent._insertChild(offsetAfter, clonedNode);
1603
+ this._addToClonedElementsGroup(clonedNode);
1604
+ // Get nodes to move.
1605
+ const count = positionParent.childCount - positionOffset;
1606
+ const nodesToMove = positionParent._removeChildren(positionOffset, count);
1607
+ // Move nodes to cloned node.
1608
+ clonedNode._appendChild(nodesToMove);
1609
+ // Create new position to work on.
1610
+ const newPosition = new Position(positionParent.parent, offsetAfter);
1611
+ return this._breakAttributes(newPosition, forceSplitText);
1612
+ }
1613
+ }
1614
+ }
1615
+ /**
1616
+ * Stores the information that an {@link module:engine/view/attributeelement~AttributeElement attribute element} was
1617
+ * added to the tree. Saves the reference to the group in the given element and updates the group, so other elements
1618
+ * from the group now keep a reference to the given attribute element.
1619
+ *
1620
+ * The clones group can be obtained using {@link module:engine/view/attributeelement~AttributeElement#getElementsWithSameId}.
1621
+ *
1622
+ * Does nothing if added element has no {@link module:engine/view/attributeelement~AttributeElement#id id}.
1623
+ *
1624
+ * @private
1625
+ * @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to save.
1626
+ */
1627
+ _addToClonedElementsGroup(element) {
1628
+ // Add only if the element is in document tree.
1629
+ if (!element.root.is('rootElement')) {
1630
+ return;
1631
+ }
1632
+ // Traverse the element's children recursively to find other attribute elements that also might got inserted.
1633
+ // The loop is at the beginning so we can make fast returns later in the code.
1634
+ if (element.is('element')) {
1635
+ for (const child of element.getChildren()) {
1636
+ this._addToClonedElementsGroup(child);
1637
+ }
1638
+ }
1639
+ const id = element.id;
1640
+ if (!id) {
1641
+ return;
1642
+ }
1643
+ let group = this._cloneGroups.get(id);
1644
+ if (!group) {
1645
+ group = new Set();
1646
+ this._cloneGroups.set(id, group);
1647
+ }
1648
+ group.add(element);
1649
+ element._clonesGroup = group;
1650
+ }
1651
+ /**
1652
+ * Removes all the information about the given {@link module:engine/view/attributeelement~AttributeElement attribute element}
1653
+ * from its clones group.
1654
+ *
1655
+ * Keep in mind, that the element will still keep a reference to the group (but the group will not keep a reference to it).
1656
+ * This allows to reference the whole group even if the element was already removed from the tree.
1657
+ *
1658
+ * Does nothing if the element has no {@link module:engine/view/attributeelement~AttributeElement#id id}.
1659
+ *
1660
+ * @private
1661
+ * @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to remove.
1662
+ */
1663
+ _removeFromClonedElementsGroup(element) {
1664
+ // Traverse the element's children recursively to find other attribute elements that also got removed.
1665
+ // The loop is at the beginning so we can make fast returns later in the code.
1666
+ if (element.is('element')) {
1667
+ for (const child of element.getChildren()) {
1668
+ this._removeFromClonedElementsGroup(child);
1669
+ }
1670
+ }
1671
+ const id = element.id;
1672
+ if (!id) {
1673
+ return;
1674
+ }
1675
+ const group = this._cloneGroups.get(id);
1676
+ if (!group) {
1677
+ return;
1678
+ }
1679
+ group.delete(element);
1680
+ // Not removing group from element on purpose!
1681
+ // If other parts of code have reference to this element, they will be able to get references to other elements from the group.
1682
+ }
1986
1683
  }
1987
-
1988
1684
  // Helper function for `view.writer.wrap`. Checks if given element has any children that are not ui elements.
1989
- function _hasNonUiChildren( parent ) {
1990
- return Array.from( parent.getChildren() ).some( child => !child.is( 'uiElement' ) );
1685
+ function _hasNonUiChildren(parent) {
1686
+ return Array.from(parent.getChildren()).some(child => !child.is('uiElement'));
1991
1687
  }
1992
-
1993
1688
  /**
1994
1689
  * The `attribute` passed to {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#wrap()`}
1995
1690
  * must be an instance of {@link module:engine/view/attributeelement~AttributeElement `AttributeElement`}.
1996
1691
  *
1997
1692
  * @error view-writer-wrap-invalid-attribute
1998
1693
  */
1999
-
2000
1694
  // Returns first parent container of specified {@link module:engine/view/position~Position Position}.
2001
1695
  // Position's parent node is checked as first, then next parents are checked.
2002
1696
  // Note that {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
@@ -2004,19 +1698,16 @@ function _hasNonUiChildren( parent ) {
2004
1698
  // @param {module:engine/view/position~Position} position Position used as a start point to locate parent container.
2005
1699
  // @returns {module:engine/view/containerelement~ContainerElement|module:engine/view/documentfragment~DocumentFragment|undefined}
2006
1700
  // Parent container element or `undefined` if container is not found.
2007
- function getParentContainer( position ) {
2008
- let parent = position.parent;
2009
-
2010
- while ( !isContainerOrFragment( parent ) ) {
2011
- if ( !parent ) {
2012
- return undefined;
2013
- }
2014
- parent = parent.parent;
2015
- }
2016
-
2017
- return parent;
1701
+ function getParentContainer(position) {
1702
+ let parent = position.parent;
1703
+ while (!isContainerOrFragment(parent)) {
1704
+ if (!parent) {
1705
+ return undefined;
1706
+ }
1707
+ parent = parent.parent;
1708
+ }
1709
+ return parent;
2018
1710
  }
2019
-
2020
1711
  // Checks if first {@link module:engine/view/attributeelement~AttributeElement AttributeElement} provided to the function
2021
1712
  // can be wrapped outside second element. It is done by comparing elements'
2022
1713
  // {@link module:engine/view/attributeelement~AttributeElement#priority priorities}, if both have same priority
@@ -2025,17 +1716,16 @@ function getParentContainer( position ) {
2025
1716
  // @param {module:engine/view/attributeelement~AttributeElement} a
2026
1717
  // @param {module:engine/view/attributeelement~AttributeElement} b
2027
1718
  // @returns {Boolean}
2028
- function shouldABeOutsideB( a, b ) {
2029
- if ( a.priority < b.priority ) {
2030
- return true;
2031
- } else if ( a.priority > b.priority ) {
2032
- return false;
2033
- }
2034
-
2035
- // When priorities are equal and names are different - use identities.
2036
- return a.getIdentity() < b.getIdentity();
1719
+ function shouldABeOutsideB(a, b) {
1720
+ if (a.priority < b.priority) {
1721
+ return true;
1722
+ }
1723
+ else if (a.priority > b.priority) {
1724
+ return false;
1725
+ }
1726
+ // When priorities are equal and names are different - use identities.
1727
+ return a.getIdentity() < b.getIdentity();
2037
1728
  }
2038
-
2039
1729
  // Returns new position that is moved to near text node. Returns same position if there is no text node before of after
2040
1730
  // specified position.
2041
1731
  //
@@ -2045,22 +1735,17 @@ function shouldABeOutsideB( a, b ) {
2045
1735
  // @param {module:engine/view/position~Position} position
2046
1736
  // @returns {module:engine/view/position~Position} Position located inside text node or same position if there is no text nodes
2047
1737
  // before or after position location.
2048
- function movePositionToTextNode( position ) {
2049
- const nodeBefore = position.nodeBefore;
2050
-
2051
- if ( nodeBefore && nodeBefore.is( '$text' ) ) {
2052
- return new Position( nodeBefore, nodeBefore.data.length );
2053
- }
2054
-
2055
- const nodeAfter = position.nodeAfter;
2056
-
2057
- if ( nodeAfter && nodeAfter.is( '$text' ) ) {
2058
- return new Position( nodeAfter, 0 );
2059
- }
2060
-
2061
- return position;
1738
+ function movePositionToTextNode(position) {
1739
+ const nodeBefore = position.nodeBefore;
1740
+ if (nodeBefore && nodeBefore.is('$text')) {
1741
+ return new Position(nodeBefore, nodeBefore.data.length);
1742
+ }
1743
+ const nodeAfter = position.nodeAfter;
1744
+ if (nodeAfter && nodeAfter.is('$text')) {
1745
+ return new Position(nodeAfter, 0);
1746
+ }
1747
+ return position;
2062
1748
  }
2063
-
2064
1749
  // Breaks text node into two text nodes when possible.
2065
1750
  //
2066
1751
  // <p>foo{}bar</p> -> <p>foo[]bar</p>
@@ -2069,43 +1754,36 @@ function movePositionToTextNode( position ) {
2069
1754
  //
2070
1755
  // @param {module:engine/view/position~Position} position Position that need to be placed inside text node.
2071
1756
  // @returns {module:engine/view/position~Position} New position after breaking text node.
2072
- function breakTextNode( position ) {
2073
- if ( position.offset == position.parent.data.length ) {
2074
- return new Position( position.parent.parent, position.parent.index + 1 );
2075
- }
2076
-
2077
- if ( position.offset === 0 ) {
2078
- return new Position( position.parent.parent, position.parent.index );
2079
- }
2080
-
2081
- // Get part of the text that need to be moved.
2082
- const textToMove = position.parent.data.slice( position.offset );
2083
-
2084
- // Leave rest of the text in position's parent.
2085
- position.parent._data = position.parent.data.slice( 0, position.offset );
2086
-
2087
- // Insert new text node after position's parent text node.
2088
- position.parent.parent._insertChild( position.parent.index + 1, new Text( position.root.document, textToMove ) );
2089
-
2090
- // Return new position between two newly created text nodes.
2091
- return new Position( position.parent.parent, position.parent.index + 1 );
1757
+ function breakTextNode(position) {
1758
+ if (position.offset == position.parent.data.length) {
1759
+ return new Position(position.parent.parent, position.parent.index + 1);
1760
+ }
1761
+ if (position.offset === 0) {
1762
+ return new Position(position.parent.parent, position.parent.index);
1763
+ }
1764
+ // Get part of the text that need to be moved.
1765
+ const textToMove = position.parent.data.slice(position.offset);
1766
+ // Leave rest of the text in position's parent.
1767
+ position.parent._data = position.parent.data.slice(0, position.offset);
1768
+ // Insert new text node after position's parent text node.
1769
+ position.parent.parent._insertChild(position.parent.index + 1, new Text(position.root.document, textToMove));
1770
+ // Return new position between two newly created text nodes.
1771
+ return new Position(position.parent.parent, position.parent.index + 1);
2092
1772
  }
2093
-
2094
1773
  // Merges two text nodes into first node. Removes second node and returns merge position.
2095
1774
  //
2096
1775
  // @param {module:engine/view/text~Text} t1 First text node to merge. Data from second text node will be moved at the end of
2097
1776
  // this text node.
2098
1777
  // @param {module:engine/view/text~Text} t2 Second text node to merge. This node will be removed after merging.
2099
1778
  // @returns {module:engine/view/position~Position} Position after merging text nodes.
2100
- function mergeTextNodes( t1, t2 ) {
2101
- // Merge text data into first text node and remove second one.
2102
- const nodeBeforeLength = t1.data.length;
2103
- t1._data += t2.data;
2104
- t2._remove();
2105
-
2106
- return new Position( t1, nodeBeforeLength );
1779
+ function mergeTextNodes(t1, t2) {
1780
+ // Merge text data into first text node and remove second one.
1781
+ const nodeBeforeLength = t1.data.length;
1782
+ t1._data += t2.data;
1783
+ t2._remove();
1784
+ return new Position(t1, nodeBeforeLength);
2107
1785
  }
2108
-
1786
+ const validNodesToInsert = [Text, AttributeElement, ContainerElement, EmptyElement, RawElement, UIElement];
2109
1787
  // Checks if provided nodes are valid to insert.
2110
1788
  //
2111
1789
  // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
@@ -2113,74 +1791,67 @@ function mergeTextNodes( t1, t2 ) {
2113
1791
  //
2114
1792
  // @param Iterable.<module:engine/view/text~Text|module:engine/view/element~Element> nodes
2115
1793
  // @param {Object} errorContext
2116
- function validateNodesToInsert( nodes, errorContext ) {
2117
- for ( const node of nodes ) {
2118
- if ( !validNodesToInsert.some( ( validNode => node instanceof validNode ) ) ) { // eslint-disable-line no-use-before-define
2119
- /**
2120
- * One of the nodes to be inserted is of an invalid type.
2121
- *
2122
- * Nodes to be inserted with {@link module:engine/view/downcastwriter~DowncastWriter#insert `DowncastWriter#insert()`} should be
2123
- * of the following types:
2124
- *
2125
- * * {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
2126
- * * {@link module:engine/view/containerelement~ContainerElement ContainerElement},
2127
- * * {@link module:engine/view/emptyelement~EmptyElement EmptyElement},
2128
- * * {@link module:engine/view/uielement~UIElement UIElement},
2129
- * * {@link module:engine/view/rawelement~RawElement RawElement},
2130
- * * {@link module:engine/view/text~Text Text}.
2131
- *
2132
- * @error view-writer-insert-invalid-node-type
2133
- */
2134
- throw new CKEditorError( 'view-writer-insert-invalid-node-type', errorContext );
2135
- }
2136
-
2137
- if ( !node.is( '$text' ) ) {
2138
- validateNodesToInsert( node.getChildren(), errorContext );
2139
- }
2140
- }
1794
+ function validateNodesToInsert(nodes, errorContext) {
1795
+ for (const node of nodes) {
1796
+ if (!validNodesToInsert.some((validNode => node instanceof validNode))) { // eslint-disable-line no-use-before-define
1797
+ /**
1798
+ * One of the nodes to be inserted is of an invalid type.
1799
+ *
1800
+ * Nodes to be inserted with {@link module:engine/view/downcastwriter~DowncastWriter#insert `DowncastWriter#insert()`} should be
1801
+ * of the following types:
1802
+ *
1803
+ * * {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
1804
+ * * {@link module:engine/view/containerelement~ContainerElement ContainerElement},
1805
+ * * {@link module:engine/view/emptyelement~EmptyElement EmptyElement},
1806
+ * * {@link module:engine/view/uielement~UIElement UIElement},
1807
+ * * {@link module:engine/view/rawelement~RawElement RawElement},
1808
+ * * {@link module:engine/view/text~Text Text}.
1809
+ *
1810
+ * @error view-writer-insert-invalid-node-type
1811
+ */
1812
+ throw new CKEditorError('view-writer-insert-invalid-node-type', errorContext);
1813
+ }
1814
+ if (!node.is('$text')) {
1815
+ validateNodesToInsert(node.getChildren(), errorContext);
1816
+ }
1817
+ }
2141
1818
  }
2142
-
2143
- const validNodesToInsert = [ Text, AttributeElement, ContainerElement, EmptyElement, RawElement, UIElement ];
2144
-
2145
1819
  // Checks if node is ContainerElement or DocumentFragment, because in most cases they should be treated the same way.
2146
1820
  //
2147
1821
  // @param {module:engine/view/node~Node} node
2148
1822
  // @returns {Boolean} Returns `true` if node is instance of ContainerElement or DocumentFragment.
2149
- function isContainerOrFragment( node ) {
2150
- return node && ( node.is( 'containerElement' ) || node.is( 'documentFragment' ) );
1823
+ function isContainerOrFragment(node) {
1824
+ return node && (node.is('containerElement') || node.is('documentFragment'));
2151
1825
  }
2152
-
2153
1826
  // Checks if {@link module:engine/view/range~Range#start range start} and {@link module:engine/view/range~Range#end range end} are placed
2154
1827
  // inside same {@link module:engine/view/containerelement~ContainerElement container element}.
2155
1828
  // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails.
2156
1829
  //
2157
1830
  // @param {module:engine/view/range~Range} range
2158
1831
  // @param {Object} errorContext
2159
- function validateRangeContainer( range, errorContext ) {
2160
- const startContainer = getParentContainer( range.start );
2161
- const endContainer = getParentContainer( range.end );
2162
-
2163
- if ( !startContainer || !endContainer || startContainer !== endContainer ) {
2164
- /**
2165
- * The container of the given range is invalid.
2166
- *
2167
- * This may happen if {@link module:engine/view/range~Range#start range start} and
2168
- * {@link module:engine/view/range~Range#end range end} positions are not placed inside the same container element or
2169
- * a parent container for these positions cannot be found.
2170
- *
2171
- * Methods like {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#remove()`},
2172
- * {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#clean()`},
2173
- * {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#wrap()`},
2174
- * {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#unwrap()`} need to be called
2175
- * on a range that has its start and end positions located in the same container element. Both positions can be
2176
- * nested within other elements (e.g. an attribute element) but the closest container ancestor must be the same.
2177
- *
2178
- * @error view-writer-invalid-range-container
2179
- */
2180
- throw new CKEditorError( 'view-writer-invalid-range-container', errorContext );
2181
- }
1832
+ function validateRangeContainer(range, errorContext) {
1833
+ const startContainer = getParentContainer(range.start);
1834
+ const endContainer = getParentContainer(range.end);
1835
+ if (!startContainer || !endContainer || startContainer !== endContainer) {
1836
+ /**
1837
+ * The container of the given range is invalid.
1838
+ *
1839
+ * This may happen if {@link module:engine/view/range~Range#start range start} and
1840
+ * {@link module:engine/view/range~Range#end range end} positions are not placed inside the same container element or
1841
+ * a parent container for these positions cannot be found.
1842
+ *
1843
+ * Methods like {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#remove()`},
1844
+ * {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#clean()`},
1845
+ * {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#wrap()`},
1846
+ * {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#unwrap()`} need to be called
1847
+ * on a range that has its start and end positions located in the same container element. Both positions can be
1848
+ * nested within other elements (e.g. an attribute element) but the closest container ancestor must be the same.
1849
+ *
1850
+ * @error view-writer-invalid-range-container
1851
+ */
1852
+ throw new CKEditorError('view-writer-invalid-range-container', errorContext);
1853
+ }
2182
1854
  }
2183
-
2184
1855
  // Checks if two attribute elements can be joined together. Elements can be joined together if, and only if
2185
1856
  // they do not have ids specified.
2186
1857
  //
@@ -2188,6 +1859,6 @@ function validateRangeContainer( range, errorContext ) {
2188
1859
  // @param {module:engine/view/element~Element} a
2189
1860
  // @param {module:engine/view/element~Element} b
2190
1861
  // @returns {Boolean}
2191
- function canBeJoined( a, b ) {
2192
- return a.id === null && b.id === null;
1862
+ function canBeJoined(a, b) {
1863
+ return a.id === null && b.id === null;
2193
1864
  }