@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.
- package/LICENSE.md +2 -2
- package/package.json +25 -25
- package/src/controller/datacontroller.js +71 -80
- package/src/controller/editingcontroller.js +83 -6
- package/src/conversion/conversion.js +15 -14
- package/src/conversion/conversionhelpers.js +1 -1
- package/src/conversion/downcastdispatcher.js +298 -367
- package/src/conversion/downcasthelpers.js +771 -63
- package/src/conversion/mapper.js +105 -60
- package/src/conversion/modelconsumable.js +85 -35
- package/src/conversion/upcastdispatcher.js +3 -6
- package/src/conversion/upcasthelpers.js +4 -2
- package/src/conversion/viewconsumable.js +1 -1
- package/src/dataprocessor/basichtmlwriter.js +1 -1
- package/src/dataprocessor/dataprocessor.jsdoc +1 -1
- package/src/dataprocessor/htmldataprocessor.js +9 -31
- package/src/dataprocessor/htmlwriter.js +1 -1
- package/src/dataprocessor/xmldataprocessor.js +1 -1
- package/src/dev-utils/model.js +16 -14
- package/src/dev-utils/operationreplayer.js +1 -1
- package/src/dev-utils/utils.js +1 -1
- package/src/dev-utils/view.js +7 -7
- package/src/index.js +2 -1
- package/src/model/batch.js +77 -10
- package/src/model/differ.js +87 -59
- package/src/model/document.js +13 -4
- package/src/model/documentfragment.js +1 -1
- package/src/model/documentselection.js +1 -1
- package/src/model/element.js +1 -1
- package/src/model/history.js +1 -1
- package/src/model/item.jsdoc +1 -1
- package/src/model/liveposition.js +1 -1
- package/src/model/liverange.js +1 -1
- package/src/model/markercollection.js +29 -5
- package/src/model/model.js +18 -9
- package/src/model/node.js +1 -1
- package/src/model/nodelist.js +1 -1
- package/src/model/operation/attributeoperation.js +1 -1
- package/src/model/operation/detachoperation.js +1 -1
- package/src/model/operation/insertoperation.js +1 -1
- package/src/model/operation/markeroperation.js +1 -1
- package/src/model/operation/mergeoperation.js +1 -1
- package/src/model/operation/moveoperation.js +1 -1
- package/src/model/operation/nooperation.js +1 -1
- package/src/model/operation/operation.js +1 -1
- package/src/model/operation/operationfactory.js +1 -1
- package/src/model/operation/renameoperation.js +1 -1
- package/src/model/operation/rootattributeoperation.js +1 -1
- package/src/model/operation/splitoperation.js +1 -1
- package/src/model/operation/transform.js +1 -1
- package/src/model/operation/utils.js +1 -1
- package/src/model/position.js +1 -1
- package/src/model/range.js +1 -1
- package/src/model/rootelement.js +1 -1
- package/src/model/schema.js +1 -1
- package/src/model/selection.js +1 -1
- package/src/model/text.js +1 -1
- package/src/model/textproxy.js +1 -1
- package/src/model/treewalker.js +1 -1
- package/src/model/utils/autoparagraphing.js +1 -1
- package/src/model/utils/deletecontent.js +1 -1
- package/src/model/utils/getselectedcontent.js +1 -1
- package/src/model/utils/insertcontent.js +1 -1
- package/src/model/utils/modifyselection.js +15 -8
- package/src/model/utils/selection-post-fixer.js +37 -30
- package/src/model/writer.js +17 -27
- package/src/view/attributeelement.js +1 -1
- package/src/view/containerelement.js +1 -1
- package/src/view/document.js +3 -2
- package/src/view/documentfragment.js +1 -1
- package/src/view/documentselection.js +1 -1
- package/src/view/domconverter.js +169 -47
- package/src/view/downcastwriter.js +121 -5
- package/src/view/editableelement.js +1 -1
- package/src/view/element.js +29 -1
- package/src/view/elementdefinition.jsdoc +1 -1
- package/src/view/emptyelement.js +1 -1
- package/src/view/filler.js +1 -1
- package/src/view/item.jsdoc +1 -1
- package/src/view/matcher.js +15 -14
- package/src/view/node.js +1 -1
- package/src/view/observer/arrowkeysobserver.js +1 -1
- package/src/view/observer/bubblingemittermixin.js +1 -1
- package/src/view/observer/bubblingeventinfo.js +1 -1
- package/src/view/observer/clickobserver.js +1 -1
- package/src/view/observer/compositionobserver.js +1 -1
- package/src/view/observer/domeventdata.js +1 -1
- package/src/view/observer/domeventobserver.js +1 -1
- package/src/view/observer/fakeselectionobserver.js +1 -1
- package/src/view/observer/focusobserver.js +1 -1
- package/src/view/observer/inputobserver.js +1 -1
- package/src/view/observer/keyobserver.js +1 -1
- package/src/view/observer/mouseobserver.js +1 -1
- package/src/view/observer/mutationobserver.js +1 -1
- package/src/view/observer/observer.js +1 -1
- package/src/view/observer/selectionobserver.js +3 -3
- package/src/view/placeholder.js +1 -1
- package/src/view/position.js +1 -1
- package/src/view/range.js +1 -1
- package/src/view/rawelement.js +1 -1
- package/src/view/renderer.js +7 -17
- package/src/view/rooteditableelement.js +1 -1
- package/src/view/selection.js +1 -1
- package/src/view/styles/background.js +1 -1
- package/src/view/styles/border.js +1 -1
- package/src/view/styles/margin.js +1 -1
- package/src/view/styles/padding.js +1 -1
- package/src/view/styles/utils.js +1 -1
- package/src/view/stylesmap.js +1 -1
- package/src/view/text.js +1 -1
- package/src/view/textproxy.js +1 -1
- package/src/view/treewalker.js +1 -1
- package/src/view/uielement.js +1 -1
- package/src/view/upcastwriter.js +1 -1
- package/src/view/view.js +1 -1
- package/theme/placeholder.css +10 -1
- package/theme/renderer.css +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @license Copyright (c) 2003-
|
|
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.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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:
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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/
|
|
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
|
|
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`
|
|
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
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
+
*/
|