@ckeditor/ckeditor5-engine 31.0.0 → 33.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE.md +2 -2
  2. package/package.json +25 -25
  3. package/src/controller/datacontroller.js +71 -80
  4. package/src/controller/editingcontroller.js +83 -6
  5. package/src/conversion/conversion.js +15 -14
  6. package/src/conversion/conversionhelpers.js +1 -1
  7. package/src/conversion/downcastdispatcher.js +298 -367
  8. package/src/conversion/downcasthelpers.js +771 -63
  9. package/src/conversion/mapper.js +105 -60
  10. package/src/conversion/modelconsumable.js +85 -35
  11. package/src/conversion/upcastdispatcher.js +3 -6
  12. package/src/conversion/upcasthelpers.js +4 -2
  13. package/src/conversion/viewconsumable.js +1 -1
  14. package/src/dataprocessor/basichtmlwriter.js +1 -1
  15. package/src/dataprocessor/dataprocessor.jsdoc +1 -1
  16. package/src/dataprocessor/htmldataprocessor.js +9 -31
  17. package/src/dataprocessor/htmlwriter.js +1 -1
  18. package/src/dataprocessor/xmldataprocessor.js +1 -1
  19. package/src/dev-utils/model.js +16 -14
  20. package/src/dev-utils/operationreplayer.js +1 -1
  21. package/src/dev-utils/utils.js +1 -1
  22. package/src/dev-utils/view.js +7 -7
  23. package/src/index.js +2 -1
  24. package/src/model/batch.js +77 -10
  25. package/src/model/differ.js +87 -59
  26. package/src/model/document.js +13 -4
  27. package/src/model/documentfragment.js +1 -1
  28. package/src/model/documentselection.js +1 -1
  29. package/src/model/element.js +1 -1
  30. package/src/model/history.js +1 -1
  31. package/src/model/item.jsdoc +1 -1
  32. package/src/model/liveposition.js +1 -1
  33. package/src/model/liverange.js +1 -1
  34. package/src/model/markercollection.js +29 -5
  35. package/src/model/model.js +18 -9
  36. package/src/model/node.js +1 -1
  37. package/src/model/nodelist.js +1 -1
  38. package/src/model/operation/attributeoperation.js +1 -1
  39. package/src/model/operation/detachoperation.js +1 -1
  40. package/src/model/operation/insertoperation.js +1 -1
  41. package/src/model/operation/markeroperation.js +1 -1
  42. package/src/model/operation/mergeoperation.js +1 -1
  43. package/src/model/operation/moveoperation.js +1 -1
  44. package/src/model/operation/nooperation.js +1 -1
  45. package/src/model/operation/operation.js +1 -1
  46. package/src/model/operation/operationfactory.js +1 -1
  47. package/src/model/operation/renameoperation.js +1 -1
  48. package/src/model/operation/rootattributeoperation.js +1 -1
  49. package/src/model/operation/splitoperation.js +1 -1
  50. package/src/model/operation/transform.js +1 -1
  51. package/src/model/operation/utils.js +1 -1
  52. package/src/model/position.js +1 -1
  53. package/src/model/range.js +1 -1
  54. package/src/model/rootelement.js +1 -1
  55. package/src/model/schema.js +1 -1
  56. package/src/model/selection.js +1 -1
  57. package/src/model/text.js +1 -1
  58. package/src/model/textproxy.js +1 -1
  59. package/src/model/treewalker.js +1 -1
  60. package/src/model/utils/autoparagraphing.js +1 -1
  61. package/src/model/utils/deletecontent.js +1 -1
  62. package/src/model/utils/getselectedcontent.js +1 -1
  63. package/src/model/utils/insertcontent.js +1 -1
  64. package/src/model/utils/modifyselection.js +15 -8
  65. package/src/model/utils/selection-post-fixer.js +37 -30
  66. package/src/model/writer.js +17 -27
  67. package/src/view/attributeelement.js +1 -1
  68. package/src/view/containerelement.js +1 -1
  69. package/src/view/document.js +3 -2
  70. package/src/view/documentfragment.js +1 -1
  71. package/src/view/documentselection.js +1 -1
  72. package/src/view/domconverter.js +169 -47
  73. package/src/view/downcastwriter.js +121 -5
  74. package/src/view/editableelement.js +1 -1
  75. package/src/view/element.js +29 -1
  76. package/src/view/elementdefinition.jsdoc +1 -1
  77. package/src/view/emptyelement.js +1 -1
  78. package/src/view/filler.js +1 -1
  79. package/src/view/item.jsdoc +1 -1
  80. package/src/view/matcher.js +15 -14
  81. package/src/view/node.js +1 -1
  82. package/src/view/observer/arrowkeysobserver.js +1 -1
  83. package/src/view/observer/bubblingemittermixin.js +1 -1
  84. package/src/view/observer/bubblingeventinfo.js +1 -1
  85. package/src/view/observer/clickobserver.js +1 -1
  86. package/src/view/observer/compositionobserver.js +1 -1
  87. package/src/view/observer/domeventdata.js +1 -1
  88. package/src/view/observer/domeventobserver.js +1 -1
  89. package/src/view/observer/fakeselectionobserver.js +1 -1
  90. package/src/view/observer/focusobserver.js +1 -1
  91. package/src/view/observer/inputobserver.js +1 -1
  92. package/src/view/observer/keyobserver.js +1 -1
  93. package/src/view/observer/mouseobserver.js +1 -1
  94. package/src/view/observer/mutationobserver.js +1 -1
  95. package/src/view/observer/observer.js +1 -1
  96. package/src/view/observer/selectionobserver.js +3 -3
  97. package/src/view/placeholder.js +1 -1
  98. package/src/view/position.js +1 -1
  99. package/src/view/range.js +1 -1
  100. package/src/view/rawelement.js +1 -1
  101. package/src/view/renderer.js +7 -17
  102. package/src/view/rooteditableelement.js +1 -1
  103. package/src/view/selection.js +1 -1
  104. package/src/view/styles/background.js +1 -1
  105. package/src/view/styles/border.js +1 -1
  106. package/src/view/styles/margin.js +1 -1
  107. package/src/view/styles/padding.js +1 -1
  108. package/src/view/styles/utils.js +1 -1
  109. package/src/view/stylesmap.js +1 -1
  110. package/src/view/text.js +1 -1
  111. package/src/view/textproxy.js +1 -1
  112. package/src/view/treewalker.js +1 -1
  113. package/src/view/uielement.js +1 -1
  114. package/src/view/upcastwriter.js +1 -1
  115. package/src/view/view.js +1 -1
  116. package/theme/placeholder.css +10 -1
  117. package/theme/renderer.css +2 -2
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
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
5
 
@@ -12,6 +12,7 @@
12
12
  import ModelRange from '../model/range';
13
13
  import ModelSelection from '../model/selection';
14
14
  import ModelElement from '../model/element';
15
+ import ModelPosition from '../model/position';
15
16
 
16
17
  import ViewAttributeElement from '../view/attributeelement';
17
18
  import DocumentSelection from '../model/documentselection';
@@ -24,6 +25,8 @@ import toArray from '@ckeditor/ckeditor5-utils/src/toarray';
24
25
  /**
25
26
  * Downcast conversion helper functions.
26
27
  *
28
+ * Learn more about {@glink framework/guides/deep-dive/conversion/downcast downcast helpers}.
29
+ *
27
30
  * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
28
31
  */
29
32
  export default class DowncastHelpers extends ConversionHelpers {
@@ -60,42 +63,242 @@ export default class DowncastHelpers extends ConversionHelpers {
60
63
  * }
61
64
  * } );
62
65
  *
63
- * The element-to-element conversion supports the reconversion mechanism. This is helpful in the conversion to complex view structures
64
- * where multiple atomic element-to-element and attribute-to-attribute or attribute-to-element could be used. By specifying
65
- * `triggerBy()` events you can trigger reconverting the model to full view tree structures at once.
66
+ * The element-to-element conversion supports the reconversion mechanism. It can be enabled by using either the `attributes` or
67
+ * the `children` props on a model description. You will find a couple examples below.
68
+ *
69
+ * In order to reconvert an element if any of its direct children have been added or removed, use the `children` property on a `model`
70
+ * description. For example, this model:
71
+ *
72
+ * <box>
73
+ * <paragraph>Some text.</paragraph>
74
+ * </box>
75
+ *
76
+ * will be converted into this structure in the view:
77
+ *
78
+ * <div class="box" data-type="single">
79
+ * <p>Some text.</p>
80
+ * </div>
81
+ *
82
+ * But if more items were inserted in the model:
83
+ *
84
+ * <box>
85
+ * <paragraph>Some text.</paragraph>
86
+ * <paragraph>Other item.</paragraph>
87
+ * </box>
88
+ *
89
+ * it will be converted into this structure in the view (note the element `data-type` change):
90
+ *
91
+ * <div class="box" data-type="multiple">
92
+ * <p>Some text.</p>
93
+ * <p>Other item.</p>
94
+ * </div>
95
+ *
96
+ * Such a converter would look like this (note that the `paragraph` elements are converted separately):
66
97
  *
67
98
  * editor.conversion.for( 'downcast' ).elementToElement( {
68
- * model: 'complex',
69
- * view: ( modelElement, conversionApi ) => createComplexViewFromModel( modelElement, conversionApi ),
70
- * triggerBy: {
71
- * attributes: [ 'foo', 'bar' ],
72
- * children: [ 'slot' ]
99
+ * model: {
100
+ * name: 'box',
101
+ * children: true
102
+ * },
103
+ * view: ( modelElement, conversionApi ) => {
104
+ * const { writer } = conversionApi;
105
+ *
106
+ * return writer.createContainerElement( 'div', {
107
+ * class: 'box',
108
+ * 'data-type': modelElement.childCount == 1 ? 'single' : 'multiple'
109
+ * } );
110
+ * }
111
+ * } );
112
+ *
113
+ * In order to reconvert element if any of its attributes have been updated, use the `attributes` property on a `model`
114
+ * description. For example, this model:
115
+ *
116
+ * <heading level="2">Some text.</heading>
117
+ *
118
+ * will be converted into this structure in the view:
119
+ *
120
+ * <h2>Some text.</h2>
121
+ *
122
+ * But if the `heading` element's `level` attribute has been updated to `3` for example, then
123
+ * it will be converted into this structure in the view:
124
+ *
125
+ * <h3>Some text.</h3>
126
+ *
127
+ * Such a converter would look as follows:
128
+ *
129
+ * editor.conversion.for( 'downcast' ).elementToElement( {
130
+ * model: {
131
+ * name: 'heading',
132
+ * attributes: 'level'
133
+ * },
134
+ * view: ( modelElement, conversionApi ) => {
135
+ * const { writer } = conversionApi;
136
+ *
137
+ * return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
73
138
  * }
74
139
  * } );
75
140
  *
76
141
  * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
77
142
  * to the conversion process.
78
143
  *
79
- * You can read more about element-to-element conversion in the
80
- * {@glink framework/guides/deep-dive/conversion/custom-element-conversion Custom element conversion} guide.
144
+ * You can read more about the element-to-element conversion in the
145
+ * {@glink framework/guides/deep-dive/conversion/downcast downcast conversion} guide.
81
146
  *
82
147
  * @method #elementToElement
83
148
  * @param {Object} config Conversion configuration.
84
- * @param {String} config.model The name of the model element to convert.
149
+ * @param {String|Object} config.model The description or a name of the model element to convert.
150
+ * @param {String|Array.<String>} [config.model.attributes] The list of attribute names that should be consumed while creating
151
+ * the view element. Note that the view will be reconverted if any of the listed attributes changes.
152
+ * @param {Boolean} [config.model.children] Specifies whether the view element requires reconversion if the list
153
+ * of the model child nodes changed.
85
154
  * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
86
155
  * that takes the model element and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
87
156
  * as parameters and returns a view container element.
88
- * @param {Object} [config.triggerBy] Reconversion triggers. At least one trigger must be defined.
89
- * @param {Array.<String>} config.triggerBy.attributes The name of the element's attributes whose change will trigger element
90
- * reconversion.
91
- * @param {Array.<String>} config.triggerBy.children The name of direct children whose adding or removing will trigger element
92
- * reconversion.
93
157
  * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
94
158
  */
95
159
  elementToElement( config ) {
96
160
  return this.add( downcastElementToElement( config ) );
97
161
  }
98
162
 
163
+ /**
164
+ * The model element to view structure (several elements) conversion helper.
165
+ *
166
+ * This conversion results in creating a view structure with one or more slots defined for the child nodes.
167
+ * For example, a model `<table>` may become this structure in the view:
168
+ *
169
+ * <figure class="table">
170
+ * <table>
171
+ * <tbody>${ slot for table rows }</tbody>
172
+ * </table>
173
+ * </figure>
174
+ *
175
+ * The children of the model's `<table>` element will be inserted into the `<tbody>` element.
176
+ * If the `elementToElement()` helper was used, the children would be inserted into the `<figure>`.
177
+ *
178
+ * An example converter that converts the following model structure:
179
+ *
180
+ * <wrappedParagraph>Some text.</wrappedParagraph>
181
+ *
182
+ * into this structure in the view:
183
+ *
184
+ * <div class="wrapper">
185
+ * <p>Some text.</p>
186
+ * </div>
187
+ *
188
+ * would look like this:
189
+ *
190
+ * editor.conversion.for( 'downcast' ).elementToStructure( {
191
+ * model: 'wrappedParagraph',
192
+ * view: ( modelElement, conversionApi ) => {
193
+ * const { writer } = conversionApi;
194
+ *
195
+ * const wrapperViewElement = writer.createContainerElement( 'div', { class: 'wrapper' } );
196
+ * const paragraphViewElement = writer.createContainerElement( 'p' );
197
+ *
198
+ * writer.insert( writer.createPositionAt( wrapperViewElement, 0 ), paragraphViewElement );
199
+ * writer.insert( writer.createPositionAt( paragraphViewElement, 0 ), writer.createSlot() );
200
+ *
201
+ * return wrapperViewElement;
202
+ * }
203
+ * } );
204
+ *
205
+ * The `slorFor()` function can also take a callback that allows filtering which children of the model element
206
+ * should be converted into this slot.
207
+ *
208
+ * Imagine a table feature where for this model structure:
209
+ *
210
+ * <table headingRows="1">
211
+ * <tableRow> ... table cells 1 ... </tableRow>
212
+ * <tableRow> ... table cells 2 ... </tableRow>
213
+ * <tableRow> ... table cells 3 ... </tableRow>
214
+ * <caption>Caption text</caption>
215
+ * </table>
216
+ *
217
+ * we want to generate this view structure:
218
+ *
219
+ * <figure class="table">
220
+ * <table>
221
+ * <thead>
222
+ * <tr> ... table cells 1 ... </tr>
223
+ * </thead>
224
+ * <tbody>
225
+ * <tr> ... table cells 2 ... </tr>
226
+ * <tr> ... table cells 3 ... </tr>
227
+ * </tbody>
228
+ * </table>
229
+ * <figcaption>Caption text</figcaption>
230
+ * </figure>
231
+ *
232
+ * The converter has to take the `headingRows` attribute into consideration when allocating the `<tableRow>` elements
233
+ * into the `<tbody>` and `<thead>` elements. Hence, we need two slots and need to define proper filter callbacks for them.
234
+ *
235
+ * Additionally, all elements other than `<tableRow>` should be placed outside the `<table>` tag.
236
+ * In the example above, this will handle the table caption.
237
+ *
238
+ * Such a converter would look like this:
239
+ *
240
+ * editor.conversion.for( 'downcast' ).elementToStructure( {
241
+ * model: {
242
+ * name: 'table',
243
+ * attributes: [ 'headingRows' ]
244
+ * },
245
+ * view: ( modelElement, conversionApi ) => {
246
+ * const { writer } = conversionApi;
247
+ *
248
+ * const figureElement = writer.createContainerElement( 'figure', { class: 'table' } );
249
+ * const tableElement = writer.createContainerElement( 'table' );
250
+ *
251
+ * writer.insert( writer.createPositionAt( figureElement, 0 ), tableElement );
252
+ *
253
+ * const headingRows = modelElement.getAttribute( 'headingRows' ) || 0;
254
+ *
255
+ * if ( headingRows > 0 ) {
256
+ * const tableHead = writer.createContainerElement( 'thead' );
257
+ *
258
+ * const headSlot = writer.createSlot( node => node.is( 'element', 'tableRow' ) && node.index < headingRows );
259
+ *
260
+ * writer.insert( writer.createPositionAt( tableElement, 'end' ), tableHead );
261
+ * writer.insert( writer.createPositionAt( tableHead, 0 ), headSlot );
262
+ * }
263
+ *
264
+ * if ( headingRows < tableUtils.getRows( table ) ) {
265
+ * const tableBody = writer.createContainerElement( 'tbody' );
266
+ *
267
+ * const bodySlot = writer.createSlot( node => node.is( 'element', 'tableRow' ) && node.index >= headingRows );
268
+ *
269
+ * writer.insert( writer.createPositionAt( tableElement, 'end' ), tableBody );
270
+ * writer.insert( writer.createPositionAt( tableBody, 0 ), bodySlot );
271
+ * }
272
+ *
273
+ * const restSlot = writer.createSlot( node => !node.is( 'element', 'tableRow' ) );
274
+ *
275
+ * writer.insert( writer.createPositionAt( figureElement, 'end' ), restSlot );
276
+ *
277
+ * return figureElement;
278
+ * }
279
+ * } );
280
+ *
281
+ * Note: The children of a model element that's being converted must be allocated in the same order in the view
282
+ * in which they are placed in the model.
283
+ *
284
+ * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
285
+ * to the conversion process.
286
+ *
287
+ * @method #elementToStructure
288
+ * @param {Object} config Conversion configuration.
289
+ * @param {String|Object} config.model The description or a name of the model element to convert.
290
+ * @param {String} [config.model.name] The name of the model element to convert.
291
+ * @param {String|Array.<String>} [config.model.attributes] The list of attribute names that should be consumed while creating
292
+ * the view structure. Note that the view will be reconverted if any of the listed attributes will change.
293
+ * @param {module:engine/conversion/downcasthelpers~StructureCreatorFunction} config.view A function
294
+ * that takes the model element and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast
295
+ * conversion API} as parameters and returns a view container element with slots for model child nodes to be converted into.
296
+ * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
297
+ */
298
+ elementToStructure( config ) {
299
+ return this.add( downcastElementToStructure( config ) );
300
+ }
301
+
99
302
  /**
100
303
  * Model attribute to view element conversion helper.
101
304
  *
@@ -258,8 +461,8 @@ export default class DowncastHelpers extends ConversionHelpers {
258
461
  * the attribute key, possible values and, optionally, an element name to convert from.
259
462
  * @param {String|Object|Function} config.view A view attribute key, or a `{ key, value }` object or a function that takes
260
463
  * the model attribute value and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
261
- * as parameters and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an
262
- * array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`.
464
+ * as parameters and returns a `{ key, value }` object. If the `key` is `'class'`, the `value` can be a `String` or an
465
+ * array of `String`s. If the `key` is `'style'`, the `value` is an object with key-value pairs. In other cases, `value` is a `String`.
263
466
  * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
264
467
  * `{ key, value }` objects or a functions.
265
468
  * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
@@ -272,14 +475,14 @@ export default class DowncastHelpers extends ConversionHelpers {
272
475
  /**
273
476
  * Model marker to view element conversion helper.
274
477
  *
275
- * **Note**: This method should be used mainly for editing downcast and it is recommended
276
- * to use {@link #markerToData `#markerToData()`} helper instead.
478
+ * **Note**: This method should be used mainly for editing the downcast and it is recommended
479
+ * to use the {@link #markerToData `#markerToData()`} helper instead.
277
480
  *
278
481
  * This helper may produce invalid HTML code (e.g. a span between table cells).
279
- * It should be used only when you are sure that the produced HTML will be semantically correct.
482
+ * It should only be used when you are sure that the produced HTML will be semantically correct.
280
483
  *
281
484
  * This conversion results in creating a view element on the boundaries of the converted marker. If the converted marker
282
- * is collapsed, only one element is created. For example, model marker set like this: `<paragraph>F[oo b]ar</paragraph>`
485
+ * is collapsed, only one element is created. For example, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>`
283
486
  * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
284
487
  *
285
488
  * editor.conversion.for( 'editingDowncast' ).markerToElement( {
@@ -321,7 +524,7 @@ export default class DowncastHelpers extends ConversionHelpers {
321
524
  * {@link module:engine/view/uielement~UIElement view UI element}. The `data` object and
322
525
  * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi `conversionApi`} are passed from
323
526
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
324
- * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` to
527
+ * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` for
325
528
  * the marker end boundary element.
326
529
  *
327
530
  * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
@@ -344,7 +547,7 @@ export default class DowncastHelpers extends ConversionHelpers {
344
547
  * Model marker to highlight conversion helper.
345
548
  *
346
549
  * This conversion results in creating a highlight on view nodes. For this kind of conversion,
347
- * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor} should be provided.
550
+ * the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor} should be provided.
348
551
  *
349
552
  * For text nodes, a `<span>` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes
350
553
  * in the converted marker range. For example, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>` becomes
@@ -382,7 +585,7 @@ export default class DowncastHelpers extends ConversionHelpers {
382
585
  *
383
586
  * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function
384
587
  * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
385
- * as a parameters and should return a
588
+ * as the parameters and should return a
386
589
  * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor}.
387
590
  * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
388
591
  *
@@ -464,7 +667,7 @@ export default class DowncastHelpers extends ConversionHelpers {
464
667
  * // Model:
465
668
  * <blockQuote>[]<paragraph>Foo</paragraph></blockQuote>
466
669
  *
467
- * // View:
670
+ * // View:
468
671
  * <blockquote><p data-group-end-before="name" data-group-start-before="name">Foo</p></blockquote>
469
672
  *
470
673
  * Similarly, when a marker is collapsed after the last element:
@@ -530,7 +733,7 @@ export default class DowncastHelpers extends ConversionHelpers {
530
733
  */
531
734
  export function insertText() {
532
735
  return ( evt, data, conversionApi ) => {
533
- if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
736
+ if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
534
737
  return;
535
738
  }
536
739
 
@@ -542,6 +745,23 @@ export function insertText() {
542
745
  };
543
746
  }
544
747
 
748
+ /**
749
+ * Function factory that creates a default downcast converter for triggering attributes and children conversion.
750
+ *
751
+ * @returns {Function} The converter.
752
+ */
753
+ export function insertAttributesAndChildren() {
754
+ return ( evt, data, conversionApi ) => {
755
+ conversionApi.convertAttributes( data.item );
756
+
757
+ // Start converting children of the current item.
758
+ // In case of reconversion children were already re-inserted or converted separately.
759
+ if ( !data.reconversion && data.item.is( 'element' ) && !data.item.isEmpty ) {
760
+ conversionApi.convertChildren( data.item );
761
+ }
762
+ };
763
+ }
764
+
545
765
  /**
546
766
  * Function factory that creates a default downcast converter for node remove changes.
547
767
  *
@@ -565,7 +785,7 @@ export function remove() {
565
785
  // After the range is removed, unbind all view elements from the model.
566
786
  // Range inside view document fragment is used to unbind deeply.
567
787
  for ( const child of conversionApi.writer.createRangeIn( removed ).getItems() ) {
568
- conversionApi.mapper.unbindViewElement( child );
788
+ conversionApi.mapper.unbindViewElement( child, { defer: true } );
569
789
  }
570
790
  };
571
791
  }
@@ -713,7 +933,7 @@ export function clearAttributes() {
713
933
  }
714
934
 
715
935
  /**
716
- * Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view.
936
+ * Function factory that creates a converter which converts the set/change/remove attribute changes from the model to the view.
717
937
  * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the
718
938
  * selection will be put inside it.
719
939
  *
@@ -745,6 +965,10 @@ export function clearAttributes() {
745
965
  */
746
966
  export function wrap( elementCreator ) {
747
967
  return ( evt, data, conversionApi ) => {
968
+ if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
969
+ return;
970
+ }
971
+
748
972
  // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
749
973
  // or the attribute was removed.
750
974
  const oldViewElement = elementCreator( data.attributeOldValue, conversionApi );
@@ -756,9 +980,7 @@ export function wrap( elementCreator ) {
756
980
  return;
757
981
  }
758
982
 
759
- if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
760
- return;
761
- }
983
+ conversionApi.consumable.consume( data.item, evt.name );
762
984
 
763
985
  const viewWriter = conversionApi.writer;
764
986
  const viewSelection = viewWriter.document.selection;
@@ -789,8 +1011,7 @@ export function wrap( elementCreator ) {
789
1011
  * It is expected that the function returns an {@link module:engine/view/element~Element}.
790
1012
  * The result of the function will be inserted into the view.
791
1013
  *
792
- * The converter automatically consumes the corresponding value from the consumables list, stops the event (see
793
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and binds the model and view elements.
1014
+ * The converter automatically consumes the corresponding value from the consumables list and binds the model and view elements.
794
1015
  *
795
1016
  * downcastDispatcher.on(
796
1017
  * 'insert:myElem',
@@ -806,24 +1027,88 @@ export function wrap( elementCreator ) {
806
1027
  *
807
1028
  * @protected
808
1029
  * @param {Function} elementCreator Function returning a view element, which will be inserted.
1030
+ * @param {module:engine/conversion/downcasthelpers~ConsumerFunction} [consumer] Function defining element consumption process.
1031
+ * By default this function just consume passed item insertion.
809
1032
  * @returns {Function} Insert element event converter.
810
1033
  */
811
- export function insertElement( elementCreator ) {
1034
+ export function insertElement( elementCreator, consumer = defaultConsumer ) {
812
1035
  return ( evt, data, conversionApi ) => {
1036
+ if ( !consumer( data.item, conversionApi.consumable, { preflight: true } ) ) {
1037
+ return;
1038
+ }
1039
+
813
1040
  const viewElement = elementCreator( data.item, conversionApi );
814
1041
 
815
1042
  if ( !viewElement ) {
816
1043
  return;
817
1044
  }
818
1045
 
819
- if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
1046
+ // Consume an element insertion and all present attributes that are specified as a reconversion triggers.
1047
+ consumer( data.item, conversionApi.consumable );
1048
+
1049
+ const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
1050
+
1051
+ conversionApi.mapper.bindElements( data.item, viewElement );
1052
+ conversionApi.writer.insert( viewPosition, viewElement );
1053
+
1054
+ // Convert attributes before converting children.
1055
+ conversionApi.convertAttributes( data.item );
1056
+
1057
+ // Convert children or reinsert previous view elements.
1058
+ reinsertOrConvertNodes( viewElement, data.item.getChildren(), conversionApi, { reconversion: data.reconversion } );
1059
+ };
1060
+ }
1061
+
1062
+ /**
1063
+ * Function factory that creates a converter which converts a single model node insertion to a view structure.
1064
+ *
1065
+ * It is expected that the passed element creator function returns an {@link module:engine/view/element~Element} with attached slots
1066
+ * created with `writer.createSlot()` to indicate where child nodes should be converted.
1067
+ *
1068
+ * @see module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
1069
+ *
1070
+ * @protected
1071
+ * @param {module:engine/conversion/downcasthelpers~StructureCreatorFunction} elementCreator Function returning a view structure,
1072
+ * which will be inserted.
1073
+ * @param {module:engine/conversion/downcasthelpers~ConsumerFunction} consumer A callback that is expected to consume all the consumables
1074
+ * that were used by the element creator.
1075
+ * @returns {Function} Insert element event converter.
1076
+ */
1077
+ export function insertStructure( elementCreator, consumer ) {
1078
+ return ( evt, data, conversionApi ) => {
1079
+ if ( !consumer( data.item, conversionApi.consumable, { preflight: true } ) ) {
1080
+ return;
1081
+ }
1082
+
1083
+ const slotsMap = new Map();
1084
+
1085
+ conversionApi.writer._registerSlotFactory( createSlotFactory( data.item, slotsMap, conversionApi ) );
1086
+
1087
+ // View creation.
1088
+ const viewElement = elementCreator( data.item, conversionApi );
1089
+
1090
+ conversionApi.writer._clearSlotFactory();
1091
+
1092
+ if ( !viewElement ) {
820
1093
  return;
821
1094
  }
822
1095
 
1096
+ // Check if all children are covered by slots and there is no child that landed in multiple slots.
1097
+ validateSlotsChildren( data.item, slotsMap, conversionApi );
1098
+
1099
+ // Consume an element insertion and all present attributes that are specified as a reconversion triggers.
1100
+ consumer( data.item, conversionApi.consumable );
1101
+
823
1102
  const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
824
1103
 
825
1104
  conversionApi.mapper.bindElements( data.item, viewElement );
826
1105
  conversionApi.writer.insert( viewPosition, viewElement );
1106
+
1107
+ // Convert attributes before converting children.
1108
+ conversionApi.convertAttributes( data.item );
1109
+
1110
+ // Fill view slots with previous view elements or create new ones.
1111
+ fillSlots( viewElement, slotsMap, conversionApi, { reconversion: data.reconversion } );
827
1112
  };
828
1113
  }
829
1114
 
@@ -1056,7 +1341,7 @@ function removeMarkerData( viewCreator ) {
1056
1341
  };
1057
1342
  }
1058
1343
 
1059
- // Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view.
1344
+ // Function factory that creates a converter which converts the set/change/remove attribute changes from the model to the view.
1060
1345
  //
1061
1346
  // Attributes from the model are converted to the view element attributes in the view. You may provide a custom function to generate
1062
1347
  // a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view element
@@ -1087,6 +1372,10 @@ function removeMarkerData( viewCreator ) {
1087
1372
  // @returns {Function} Set/change attribute converter.
1088
1373
  function changeAttribute( attributeCreator ) {
1089
1374
  return ( evt, data, conversionApi ) => {
1375
+ if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
1376
+ return;
1377
+ }
1378
+
1090
1379
  const oldAttribute = attributeCreator( data.attributeOldValue, conversionApi );
1091
1380
  const newAttribute = attributeCreator( data.attributeNewValue, conversionApi );
1092
1381
 
@@ -1094,9 +1383,7 @@ function changeAttribute( attributeCreator ) {
1094
1383
  return;
1095
1384
  }
1096
1385
 
1097
- if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
1098
- return;
1099
- }
1386
+ conversionApi.consumable.consume( data.item, evt.name );
1100
1387
 
1101
1388
  const viewElement = conversionApi.mapper.toViewElement( data.item );
1102
1389
  const viewWriter = conversionApi.writer;
@@ -1106,7 +1393,7 @@ function changeAttribute( attributeCreator ) {
1106
1393
  if ( !viewElement ) {
1107
1394
  /**
1108
1395
  * This error occurs when a {@link module:engine/model/textproxy~TextProxy text node's} attribute is to be downcasted
1109
- * by {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `Attribute to Attribute converter`}.
1396
+ * by an {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `Attribute to Attribute converter`}.
1110
1397
  * In most cases it is caused by converters misconfiguration when only "generic" converter is defined:
1111
1398
  *
1112
1399
  * editor.conversion.for( 'downcast' ).attributeToAttribute( {
@@ -1138,10 +1425,7 @@ function changeAttribute( attributeCreator ) {
1138
1425
  *
1139
1426
  * @error conversion-attribute-to-attribute-on-text
1140
1427
  */
1141
- throw new CKEditorError(
1142
- 'conversion-attribute-to-attribute-on-text',
1143
- [ data, conversionApi ]
1144
- );
1428
+ throw new CKEditorError( 'conversion-attribute-to-attribute-on-text', conversionApi.dispatcher, data );
1145
1429
  }
1146
1430
 
1147
1431
  // First remove the old attribute if there was one.
@@ -1366,34 +1650,106 @@ function removeHighlight( highlightDescriptor ) {
1366
1650
  // See {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper} for examples and config params description.
1367
1651
  //
1368
1652
  // @param {Object} config Conversion configuration.
1369
- // @param {String} config.model
1653
+ // @param {String|Object} config.model The description or a name of the model element to convert.
1654
+ // @param {String|Array.<String>} [config.model.attributes] List of attributes triggering element reconversion.
1655
+ // @param {Boolean} [config.model.children] Should reconvert element if the list of model child nodes changed.
1370
1656
  // @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view
1371
- // @param {Object} [config.triggerBy]
1372
- // @param {Array.<String>} [config.triggerBy.attributes]
1373
- // @param {Array.<String>} [config.triggerBy.children]
1374
1657
  // @returns {Function} Conversion helper.
1375
1658
  function downcastElementToElement( config ) {
1376
1659
  config = cloneDeep( config );
1377
1660
 
1661
+ config.model = normalizeModelElementConfig( config.model );
1378
1662
  config.view = normalizeToElementConfig( config.view, 'container' );
1379
1663
 
1664
+ // Trigger reconversion on children list change if element is a subject to any reconversion.
1665
+ // This is required to be able to trigger Differ#refreshItem() on a direct child of the reconverted element.
1666
+ if ( config.model.attributes.length ) {
1667
+ config.model.children = true;
1668
+ }
1669
+
1380
1670
  return dispatcher => {
1381
- dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.converterPriority || 'normal' } );
1671
+ dispatcher.on(
1672
+ 'insert:' + config.model.name,
1673
+ insertElement( config.view, createConsumer( config.model ) ),
1674
+ { priority: config.converterPriority || 'normal' }
1675
+ );
1676
+
1677
+ if ( config.model.children || config.model.attributes.length ) {
1678
+ dispatcher.on( 'reduceChanges', createChangeReducer( config.model ), { priority: 'low' } );
1679
+ }
1680
+ };
1681
+ }
1382
1682
 
1383
- if ( config.triggerBy ) {
1384
- if ( config.triggerBy.attributes ) {
1385
- for ( const attributeKey of config.triggerBy.attributes ) {
1386
- dispatcher._mapReconversionTriggerEvent( config.model, `attribute:${ attributeKey }:${ config.model }` );
1387
- }
1388
- }
1683
+ // Model element to view structure conversion helper.
1684
+ //
1685
+ // See {@link ~DowncastHelpers#elementToStructure `.elementToStructure()` downcast helper} for examples and config params description.
1686
+ //
1687
+ // @param {Object} config Conversion configuration.
1688
+ // @param {String|Object} config.model
1689
+ // @param {String} [config.model.name]
1690
+ // @param {Array.<String>} [config.model.attributes]
1691
+ // @param {module:engine/conversion/downcasthelpers~StructureCreatorFunction} config.view
1692
+ // @returns {Function} Conversion helper.
1693
+ function downcastElementToStructure( config ) {
1694
+ config = cloneDeep( config );
1389
1695
 
1390
- if ( config.triggerBy.children ) {
1391
- for ( const childName of config.triggerBy.children ) {
1392
- dispatcher._mapReconversionTriggerEvent( config.model, `insert:${ childName }` );
1393
- dispatcher._mapReconversionTriggerEvent( config.model, `remove:${ childName }` );
1394
- }
1395
- }
1696
+ config.model = normalizeModelElementConfig( config.model );
1697
+ config.view = normalizeToElementConfig( config.view, 'container' );
1698
+
1699
+ // Trigger reconversion on children list change because it always needs to use slots to put children in proper places.
1700
+ // This is required to be able to trigger Differ#refreshItem() on a direct child of the reconverted element.
1701
+ config.model.children = true;
1702
+
1703
+ return dispatcher => {
1704
+ if ( dispatcher._conversionApi.schema.checkChild( config.model.name, '$text' ) ) {
1705
+ /**
1706
+ * This error occurs when a {@link module:engine/model/element~Element model element} is downcasted
1707
+ * via {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure} helper but the element was
1708
+ * allowed to host `$text` by the {@link module:engine/model/schema~Schema model schema}.
1709
+ *
1710
+ * For instance, this may be the result of `myElement` allowing the content of
1711
+ * {@glink framework/guides/deep-dive/schema#generic-items `$block`} in its schema definition:
1712
+ *
1713
+ * // Element definition in schema.
1714
+ * schema.register( 'myElement', {
1715
+ * allowContentOf: '$block',
1716
+ *
1717
+ * // ...
1718
+ * } );
1719
+ *
1720
+ * // ...
1721
+ *
1722
+ * // Conversion of myElement with the use of elementToStructure().
1723
+ * editor.conversion.for( 'downcast' ).elementToStructure( {
1724
+ * model: 'myElement',
1725
+ * view: ( modelElement, { writer } ) => {
1726
+ * // ...
1727
+ * }
1728
+ * } );
1729
+ *
1730
+ * In such case, {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} helper
1731
+ * can be used instead to get around this problem:
1732
+ *
1733
+ * editor.conversion.for( 'downcast' ).elementToElement( {
1734
+ * model: 'myElement',
1735
+ * view: ( modelElement, { writer } ) => {
1736
+ * // ...
1737
+ * }
1738
+ * } );
1739
+ *
1740
+ * @error conversion-element-to-structure-disallowed-text
1741
+ * @param {String} elementName The name of the element the structure is to be created for.
1742
+ */
1743
+ throw new CKEditorError( 'conversion-element-to-structure-disallowed-text', dispatcher, { elementName: config.model.name } );
1396
1744
  }
1745
+
1746
+ dispatcher.on(
1747
+ 'insert:' + config.model.name,
1748
+ insertStructure( config.view, createConsumer( config.model ) ),
1749
+ { priority: config.converterPriority || 'normal' }
1750
+ );
1751
+
1752
+ dispatcher.on( 'reduceChanges', createChangeReducer( config.model ), { priority: 'low' } );
1397
1753
  };
1398
1754
  }
1399
1755
 
@@ -1541,6 +1897,31 @@ function downcastMarkerToHighlight( config ) {
1541
1897
  };
1542
1898
  }
1543
1899
 
1900
+ // Takes `config.model`, and converts it to an object with normalized structure.
1901
+ //
1902
+ // @param {String|Object} model Model configuration or element name.
1903
+ // @param {String} model.name
1904
+ // @param {Array.<String>} [model.attributes]
1905
+ // @param {Boolean} [model.children]
1906
+ // @returns {Object}
1907
+ function normalizeModelElementConfig( model ) {
1908
+ if ( typeof model == 'string' ) {
1909
+ model = { name: model };
1910
+ }
1911
+
1912
+ // List of attributes that should trigger reconversion.
1913
+ if ( !model.attributes ) {
1914
+ model.attributes = [];
1915
+ } else if ( !Array.isArray( model.attributes ) ) {
1916
+ model.attributes = [ model.attributes ];
1917
+ }
1918
+
1919
+ // Whether a children insertion/deletion should trigger reconversion.
1920
+ model.children = !!model.children;
1921
+
1922
+ return model;
1923
+ }
1924
+
1544
1925
  // Takes `config.view`, and if it is an {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
1545
1926
  // to a function (because lower level converters accept only element creator functions).
1546
1927
  //
@@ -1670,6 +2051,291 @@ function prepareDescriptor( highlightDescriptor, data, conversionApi ) {
1670
2051
  return descriptor;
1671
2052
  }
1672
2053
 
2054
+ // Creates a function that checks a single differ diff item whether it should trigger reconversion.
2055
+ //
2056
+ // @param {Object} model A normalized `config.model` converter configuration.
2057
+ // @param {String} model.name The name of element.
2058
+ // @param {Array.<String>} model.attributes The list of attribute names that should trigger reconversion.
2059
+ // @param {Boolean} [model.children] Whether the child list change should trigger reconversion.
2060
+ // @returns {Function}
2061
+ function createChangeReducerCallback( model ) {
2062
+ return ( node, change ) => {
2063
+ if ( !node.is( 'element', model.name ) ) {
2064
+ return false;
2065
+ }
2066
+
2067
+ if ( change.type == 'attribute' ) {
2068
+ if ( model.attributes.includes( change.attributeKey ) ) {
2069
+ return true;
2070
+ }
2071
+ } else {
2072
+ /* istanbul ignore else: This is always true because otherwise it would not register a reducer callback. */
2073
+ if ( model.children ) {
2074
+ return true;
2075
+ }
2076
+ }
2077
+
2078
+ return false;
2079
+ };
2080
+ }
2081
+
2082
+ // Creates a `reduceChanges` event handler for reconversion.
2083
+ //
2084
+ // @param {Object} model A normalized `config.model` converter configuration.
2085
+ // @param {String} model.name The name of element.
2086
+ // @param {Array.<String>} model.attributes The list of attribute names that should trigger reconversion.
2087
+ // @param {Boolean} [model.children] Whether the child list change should trigger reconversion.
2088
+ // @returns {Function}
2089
+ function createChangeReducer( model ) {
2090
+ const shouldReplace = createChangeReducerCallback( model );
2091
+
2092
+ return ( evt, data ) => {
2093
+ const reducedChanges = [];
2094
+
2095
+ if ( !data.reconvertedElements ) {
2096
+ data.reconvertedElements = new Set();
2097
+ }
2098
+
2099
+ for ( const change of data.changes ) {
2100
+ // For attribute use node affected by the change.
2101
+ // For insert or remove use parent element because we need to check if it's added/removed child.
2102
+ const node = change.position ? change.position.parent : change.range.start.nodeAfter;
2103
+
2104
+ if ( !node || !shouldReplace( node, change ) ) {
2105
+ reducedChanges.push( change );
2106
+
2107
+ continue;
2108
+ }
2109
+
2110
+ // If it's already marked for reconversion, so skip this change, otherwise add the diff items.
2111
+ if ( !data.reconvertedElements.has( node ) ) {
2112
+ data.reconvertedElements.add( node );
2113
+
2114
+ const position = ModelPosition._createBefore( node );
2115
+
2116
+ reducedChanges.push( {
2117
+ type: 'remove',
2118
+ name: node.name,
2119
+ position,
2120
+ length: 1
2121
+ }, {
2122
+ type: 'reinsert',
2123
+ name: node.name,
2124
+ position,
2125
+ length: 1
2126
+ } );
2127
+ }
2128
+ }
2129
+
2130
+ data.changes = reducedChanges;
2131
+ };
2132
+ }
2133
+
2134
+ // Creates a function that checks if an element and its watched attributes can be consumed and consumes them.
2135
+ //
2136
+ // @param {Object} model A normalized `config.model` converter configuration.
2137
+ // @param {String} model.name The name of element.
2138
+ // @param {Array.<String>} model.attributes The list of attribute names that should trigger reconversion.
2139
+ // @param {Boolean} [model.children] Whether the child list change should trigger reconversion.
2140
+ // @returns {module:engine/conversion/downcasthelpers~ConsumerFunction}
2141
+ function createConsumer( model ) {
2142
+ return ( node, consumable, options = {} ) => {
2143
+ const events = [ 'insert' ];
2144
+
2145
+ // Collect all set attributes that are triggering conversion.
2146
+ for ( const attributeName of model.attributes ) {
2147
+ if ( node.hasAttribute( attributeName ) ) {
2148
+ events.push( `attribute:${ attributeName }` );
2149
+ }
2150
+ }
2151
+
2152
+ if ( !events.every( event => consumable.test( node, event ) ) ) {
2153
+ return false;
2154
+ }
2155
+
2156
+ if ( !options.preflight ) {
2157
+ events.forEach( event => consumable.consume( node, event ) );
2158
+ }
2159
+
2160
+ return true;
2161
+ };
2162
+ }
2163
+
2164
+ // Creates a function that create view slots.
2165
+ //
2166
+ // @param {module:engine/model/element~Element} element
2167
+ // @param {Map.<module:engine/view/element~Element,Array.<module:engine/model/node~Node>>} slotsMap
2168
+ // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
2169
+ // @returns {Function} Exposed by writer as createSlot().
2170
+ function createSlotFactory( element, slotsMap, conversionApi ) {
2171
+ return ( writer, modeOrFilter = 'children' ) => {
2172
+ const slot = writer.createContainerElement( '$slot' );
2173
+
2174
+ let children = null;
2175
+
2176
+ if ( modeOrFilter === 'children' ) {
2177
+ children = Array.from( element.getChildren() );
2178
+ } else if ( typeof modeOrFilter == 'function' ) {
2179
+ children = Array.from( element.getChildren() ).filter( element => modeOrFilter( element ) );
2180
+ } else {
2181
+ /**
2182
+ * Unknown slot mode was provided to `writer.createSlot()` in downcast converter.
2183
+ *
2184
+ * @error conversion-slot-mode-unknown
2185
+ */
2186
+ throw new CKEditorError( 'conversion-slot-mode-unknown', conversionApi.dispatcher, { modeOrFilter } );
2187
+ }
2188
+
2189
+ slotsMap.set( slot, children );
2190
+
2191
+ return slot;
2192
+ };
2193
+ }
2194
+
2195
+ // Checks if all children are covered by slots and there is no child that landed in multiple slots.
2196
+ //
2197
+ // @param {module:engine/model/element~Element}
2198
+ // @param {Map.<module:engine/view/element~Element,Array.<module:engine/model/node~Node>>} slotsMap
2199
+ // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
2200
+ function validateSlotsChildren( element, slotsMap, conversionApi ) {
2201
+ const childrenInSlots = Array.from( slotsMap.values() ).flat();
2202
+ const uniqueChildrenInSlots = new Set( childrenInSlots );
2203
+
2204
+ if ( uniqueChildrenInSlots.size != childrenInSlots.length ) {
2205
+ /**
2206
+ * Filters provided to `writer.createSlot()` overlap (at least two filters accept the same child element).
2207
+ *
2208
+ * @error conversion-slot-filter-overlap
2209
+ * @param {module:engine/model/element~Element} element The element of which children would not be properly
2210
+ * allocated to multiple slots.
2211
+ */
2212
+ throw new CKEditorError( 'conversion-slot-filter-overlap', conversionApi.dispatcher, { element } );
2213
+ }
2214
+
2215
+ if ( uniqueChildrenInSlots.size != element.childCount ) {
2216
+ /**
2217
+ * Filters provided to `writer.createSlot()` are incomplete and exclude at least one children element (one of
2218
+ * the children elements would not be assigned to any of the slots).
2219
+ *
2220
+ * @error conversion-slot-filter-incomplete
2221
+ * @param {module:engine/model/element~Element} element The element of which children would not be properly
2222
+ * allocated to multiple slots.
2223
+ */
2224
+ throw new CKEditorError( 'conversion-slot-filter-incomplete', conversionApi.dispatcher, { element } );
2225
+ }
2226
+ }
2227
+
2228
+ // Fill slots with appropriate view elements.
2229
+ //
2230
+ // @param {module:engine/view/element~Element} viewElement
2231
+ // @param {Map.<module:engine/view/element~Element,Array.<module:engine/model/node~Node>>} slotsMap
2232
+ // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
2233
+ // @param {Object} options
2234
+ // @param {Boolean} [options.reconversion]
2235
+ function fillSlots( viewElement, slotsMap, conversionApi, options ) {
2236
+ // Set temporary position mapping to redirect child view elements into a proper slots.
2237
+ conversionApi.mapper.on( 'modelToViewPosition', toViewPositionMapping, { priority: 'highest' } );
2238
+
2239
+ let currentSlot = null;
2240
+ let currentSlotNodes = null;
2241
+
2242
+ // Fill slots with nested view nodes.
2243
+ for ( [ currentSlot, currentSlotNodes ] of slotsMap ) {
2244
+ reinsertOrConvertNodes( viewElement, currentSlotNodes, conversionApi, options );
2245
+
2246
+ conversionApi.writer.move(
2247
+ conversionApi.writer.createRangeIn( currentSlot ),
2248
+ conversionApi.writer.createPositionBefore( currentSlot )
2249
+ );
2250
+ conversionApi.writer.remove( currentSlot );
2251
+ }
2252
+
2253
+ conversionApi.mapper.off( 'modelToViewPosition', toViewPositionMapping );
2254
+
2255
+ function toViewPositionMapping( evt, data ) {
2256
+ const element = data.modelPosition.nodeAfter;
2257
+
2258
+ // Find the proper offset within the slot.
2259
+ const index = currentSlotNodes.indexOf( element );
2260
+
2261
+ if ( index < 0 ) {
2262
+ return;
2263
+ }
2264
+
2265
+ data.viewPosition = data.mapper.findPositionIn( currentSlot, index );
2266
+ }
2267
+ }
2268
+
2269
+ // Inserts view representation of `nodes` into the `viewElement` either by bringing back just removed view nodes
2270
+ // or by triggering conversion for them.
2271
+ //
2272
+ // @param {module:engine/view/element~Element} viewElement
2273
+ // @param {Iterable.<module:engine/model/element~Element>} modelNodes
2274
+ // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
2275
+ // @param {Object} options
2276
+ // @param {Boolean} [options.reconversion]
2277
+ function reinsertOrConvertNodes( viewElement, modelNodes, conversionApi, options ) {
2278
+ // Fill with nested view nodes.
2279
+ for ( const modelChildNode of modelNodes ) {
2280
+ // Try reinserting the view node for the specified model node...
2281
+ if ( !reinsertNode( viewElement.root, modelChildNode, conversionApi, options ) ) {
2282
+ // ...or else convert the model element to the view.
2283
+ conversionApi.convertItem( modelChildNode );
2284
+ }
2285
+ }
2286
+ }
2287
+
2288
+ // Checks if the view for the given model element could be reused and reinserts it to the view.
2289
+ //
2290
+ // @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} viewRoot
2291
+ // @param {module:engine/model/element~Element} modelElement
2292
+ // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
2293
+ // @param {Object} options
2294
+ // @param {Boolean} [options.reconversion]
2295
+ // @returns {Boolean} `false` if view element can't be reused.
2296
+ function reinsertNode( viewRoot, modelElement, conversionApi, options ) {
2297
+ const { writer, mapper } = conversionApi;
2298
+
2299
+ // Don't reinsert if this is not a reconversion...
2300
+ if ( !options.reconversion ) {
2301
+ return false;
2302
+ }
2303
+
2304
+ const viewChildNode = mapper.toViewElement( modelElement );
2305
+
2306
+ // ...or there is no view to reinsert or it was already inserted to the view structure...
2307
+ if ( !viewChildNode || viewChildNode.root == viewRoot ) {
2308
+ return false;
2309
+ }
2310
+
2311
+ // ...or it was strictly marked as not to be reused.
2312
+ if ( !conversionApi.canReuseView( viewChildNode ) ) {
2313
+ return false;
2314
+ }
2315
+
2316
+ // Otherwise reinsert the view node.
2317
+ writer.move(
2318
+ writer.createRangeOn( viewChildNode ),
2319
+ mapper.toViewPosition( ModelPosition._createBefore( modelElement ) )
2320
+ );
2321
+
2322
+ return true;
2323
+ }
2324
+
2325
+ // The default consumer for insert events.
2326
+ // @param {module:engine/model/item~Item} item Model item.
2327
+ // @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable The model consumable.
2328
+ // @param {Object} [options]
2329
+ // @param {Boolean} [options.preflight=false] Whether should consume or just check if can be consumed.
2330
+ // @returns {Boolean}
2331
+ function defaultConsumer( item, consumable, { preflight } = {} ) {
2332
+ if ( preflight ) {
2333
+ return consumable.test( item, 'insert' );
2334
+ } else {
2335
+ return consumable.consume( item, 'insert' );
2336
+ }
2337
+ }
2338
+
1673
2339
  /**
1674
2340
  * An object describing how the marker highlight should be represented in the view.
1675
2341
  *
@@ -1704,3 +2370,45 @@ function prepareDescriptor( highlightDescriptor, data, conversionApi ) {
1704
2370
  * attribute element. If the descriptor is applied to an element, usually these attributes will be set on that element, however,
1705
2371
  * this depends on how the element converts the descriptor.
1706
2372
  */
2373
+
2374
+ /**
2375
+ * A filtering function used to choose model child nodes to be downcasted into the specific view
2376
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createSlot "slot"} while executing the
2377
+ * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure `elementToStructure()`} converter.
2378
+ *
2379
+ * @callback module:engine/conversion/downcasthelpers~SlotFilter
2380
+ *
2381
+ * @param {module:engine/model/node~Node} node A model node.
2382
+ * @returns {Boolean} Whether the provided model node should be downcasted into this slot.
2383
+ *
2384
+ * @see module:engine/view/downcastwriter~DowncastWriter#createSlot
2385
+ * @see module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
2386
+ * @see module:engine/conversion/downcasthelpers~insertStructure
2387
+ */
2388
+
2389
+ /**
2390
+ * A function that takes the model element and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast
2391
+ * conversion API} as parameters and returns a view container element with slots for model child nodes to be converted into.
2392
+ *
2393
+ * @callback module:engine/conversion/downcasthelpers~StructureCreatorFunction
2394
+ * @param {module:engine/model/element~Element} element The model element to be converted to the view structure.
2395
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion interface.
2396
+ * @returns {module:engine/view/element~Element} The view structure with slots for model child nodes.
2397
+ *
2398
+ * @see module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
2399
+ * @see module:engine/conversion/downcasthelpers~insertStructure
2400
+ */
2401
+
2402
+ /**
2403
+ * A function that is expected to consume all the consumables that were used by the element creator.
2404
+ *
2405
+ * @callback module:engine/conversion/downcasthelpers~ConsumerFunction
2406
+ * @param {module:engine/model/element~Element} element The model element to be converted to the view structure.
2407
+ * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable The `ModelConsumable` same as in
2408
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi#consumable `DowncastConversionApi.consumable`}.
2409
+ * @param {Object} [options]
2410
+ * @param {Boolean} [options.preflight=false] Whether should consume or just check if can be consumed.
2411
+ * @returns {Boolean} `true` if all consumable values were available and were consumed, `false` otherwise.
2412
+ *
2413
+ * @see module:engine/conversion/downcasthelpers~insertStructure
2414
+ */