@ckeditor/ckeditor5-engine 32.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/package.json +22 -22
- package/src/controller/datacontroller.js +58 -66
- package/src/controller/editingcontroller.js +82 -5
- package/src/conversion/conversion.js +14 -13
- package/src/conversion/downcastdispatcher.js +297 -366
- package/src/conversion/downcasthelpers.js +770 -62
- package/src/conversion/mapper.js +104 -59
- package/src/conversion/modelconsumable.js +84 -34
- package/src/conversion/upcastdispatcher.js +2 -5
- package/src/conversion/upcasthelpers.js +3 -1
- package/src/dataprocessor/htmldataprocessor.js +1 -1
- package/src/dev-utils/model.js +13 -11
- package/src/index.js +1 -0
- package/src/model/batch.js +12 -12
- package/src/model/differ.js +86 -58
- package/src/model/document.js +12 -3
- package/src/model/markercollection.js +28 -4
- package/src/model/model.js +2 -1
- package/src/model/utils/modifyselection.js +14 -7
- package/src/model/writer.js +16 -26
- package/src/view/document.js +2 -1
- package/src/view/domconverter.js +31 -10
- package/src/view/downcastwriter.js +88 -3
- package/theme/placeholder.css +9 -0
package/src/conversion/mapper.js
CHANGED
|
@@ -15,11 +15,12 @@ import ViewRange from '../view/range';
|
|
|
15
15
|
import ViewText from '../view/text';
|
|
16
16
|
|
|
17
17
|
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
|
|
18
|
+
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
|
|
18
19
|
import mix from '@ckeditor/ckeditor5-utils/src/mix';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
|
-
* Maps elements, positions and markers between {@link module:engine/view/document~Document
|
|
22
|
-
* {@link module:engine/model/model
|
|
22
|
+
* Maps elements, positions and markers between the {@link module:engine/view/document~Document view} and
|
|
23
|
+
* the {@link module:engine/model/model model}.
|
|
23
24
|
*
|
|
24
25
|
* The instance of the Mapper used for the editing pipeline is available in
|
|
25
26
|
* {@link module:engine/controller/editingcontroller~EditingController#mapper `editor.editing.mapper`}.
|
|
@@ -27,12 +28,12 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
|
|
|
27
28
|
* Mapper uses bound elements to find corresponding elements and positions, so, to get proper results,
|
|
28
29
|
* all model elements should be {@link module:engine/conversion/mapper~Mapper#bindElements bound}.
|
|
29
30
|
*
|
|
30
|
-
* To map complex model to/from view relations, you may provide custom callbacks for
|
|
31
|
+
* To map the complex model to/from view relations, you may provide custom callbacks for the
|
|
31
32
|
* {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition modelToViewPosition event} and
|
|
32
33
|
* {@link module:engine/conversion/mapper~Mapper#event:viewToModelPosition viewToModelPosition event} that are fired whenever
|
|
33
34
|
* a position mapping request occurs.
|
|
34
|
-
* Those events are fired by {@link module:engine/conversion/mapper~Mapper#toViewPosition toViewPosition}
|
|
35
|
-
* and {@link module:engine/conversion/mapper~Mapper#toModelPosition toModelPosition} methods. `Mapper` adds
|
|
35
|
+
* Those events are fired by the {@link module:engine/conversion/mapper~Mapper#toViewPosition toViewPosition}
|
|
36
|
+
* and {@link module:engine/conversion/mapper~Mapper#toModelPosition toModelPosition} methods. `Mapper` adds its own default callbacks
|
|
36
37
|
* with `'lowest'` priority. To override default `Mapper` mapping, add custom callback with higher priority and
|
|
37
38
|
* stop the event.
|
|
38
39
|
* @mixes module:utils/emittermixin~EmitterMixin
|
|
@@ -89,7 +90,15 @@ export default class Mapper {
|
|
|
89
90
|
this._elementToMarkerNames = new Map();
|
|
90
91
|
|
|
91
92
|
/**
|
|
92
|
-
*
|
|
93
|
+
* The map of removed view elements with their current root (used for deferred unbinding).
|
|
94
|
+
*
|
|
95
|
+
* @private
|
|
96
|
+
* @member {Map.<module:engine/view/element~Element,module:engine/view/documentfragment~DocumentFragment>}
|
|
97
|
+
*/
|
|
98
|
+
this._deferredBindingRemovals = new Map();
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Stores marker names of markers which have changed due to unbinding a view element (so it is assumed that the view element
|
|
93
102
|
* has been removed, moved or renamed).
|
|
94
103
|
*
|
|
95
104
|
* @private
|
|
@@ -105,6 +114,18 @@ export default class Mapper {
|
|
|
105
114
|
|
|
106
115
|
const viewContainer = this._modelToViewMapping.get( data.modelPosition.parent );
|
|
107
116
|
|
|
117
|
+
if ( !viewContainer ) {
|
|
118
|
+
/**
|
|
119
|
+
* A model position could not be mapped to the view because the parent of the model position
|
|
120
|
+
* does not have a mapped view element (might have not been converted yet or it has no converter).
|
|
121
|
+
*
|
|
122
|
+
* Make sure that the model element is correctly converted to the view.
|
|
123
|
+
*
|
|
124
|
+
* @error mapping-view-position-parent-not-found
|
|
125
|
+
*/
|
|
126
|
+
throw new CKEditorError( 'mapping-view-position-parent-not-found', this, { modelPosition: data.modelPosition } );
|
|
127
|
+
}
|
|
128
|
+
|
|
108
129
|
data.viewPosition = this.findPositionIn( viewContainer, data.modelPosition.offset );
|
|
109
130
|
}, { priority: 'low' } );
|
|
110
131
|
|
|
@@ -137,37 +158,44 @@ export default class Mapper {
|
|
|
137
158
|
}
|
|
138
159
|
|
|
139
160
|
/**
|
|
140
|
-
* Unbinds given {@link module:engine/view/element~Element view element} from the map.
|
|
161
|
+
* Unbinds the given {@link module:engine/view/element~Element view element} from the map.
|
|
141
162
|
*
|
|
142
163
|
* **Note:** view-to-model binding will be removed, if it existed. However, corresponding model-to-view binding
|
|
143
|
-
* will be removed only if model element is still bound to passed `viewElement`.
|
|
164
|
+
* will be removed only if model element is still bound to the passed `viewElement`.
|
|
144
165
|
*
|
|
145
|
-
* This behavior
|
|
166
|
+
* This behavior allows for re-binding model element to another view element without fear of losing the new binding
|
|
146
167
|
* when the previously bound view element is unbound.
|
|
147
168
|
*
|
|
148
169
|
* @param {module:engine/view/element~Element} viewElement View element to unbind.
|
|
170
|
+
* @param {Object} [options={}] The options object.
|
|
171
|
+
* @param {Boolean} [options.defer=false] Controls whether the binding should be removed immediately or deferred until a
|
|
172
|
+
* {@link #flushDeferredBindings `flushDeferredBindings()`} call.
|
|
149
173
|
*/
|
|
150
|
-
unbindViewElement( viewElement ) {
|
|
174
|
+
unbindViewElement( viewElement, options = {} ) {
|
|
151
175
|
const modelElement = this.toModelElement( viewElement );
|
|
152
176
|
|
|
153
|
-
this._viewToModelMapping.delete( viewElement );
|
|
154
|
-
|
|
155
177
|
if ( this._elementToMarkerNames.has( viewElement ) ) {
|
|
156
178
|
for ( const markerName of this._elementToMarkerNames.get( viewElement ) ) {
|
|
157
179
|
this._unboundMarkerNames.add( markerName );
|
|
158
180
|
}
|
|
159
181
|
}
|
|
160
182
|
|
|
161
|
-
if (
|
|
162
|
-
this.
|
|
183
|
+
if ( options.defer ) {
|
|
184
|
+
this._deferredBindingRemovals.set( viewElement, viewElement.root );
|
|
185
|
+
} else {
|
|
186
|
+
this._viewToModelMapping.delete( viewElement );
|
|
187
|
+
|
|
188
|
+
if ( this._modelToViewMapping.get( modelElement ) == viewElement ) {
|
|
189
|
+
this._modelToViewMapping.delete( modelElement );
|
|
190
|
+
}
|
|
163
191
|
}
|
|
164
192
|
}
|
|
165
193
|
|
|
166
194
|
/**
|
|
167
|
-
* Unbinds given {@link module:engine/model/element~Element model element} from the map.
|
|
195
|
+
* Unbinds the given {@link module:engine/model/element~Element model element} from the map.
|
|
168
196
|
*
|
|
169
|
-
* **Note:** model-to-view binding will be removed, if it existed. However, corresponding view-to-model binding
|
|
170
|
-
* will be removed only if view element is still bound to passed `modelElement`.
|
|
197
|
+
* **Note:** the model-to-view binding will be removed, if it existed. However, the corresponding view-to-model binding
|
|
198
|
+
* will be removed only if the view element is still bound to the passed `modelElement`.
|
|
171
199
|
*
|
|
172
200
|
* This behavior lets for re-binding view element to another model element without fear of losing the new binding
|
|
173
201
|
* when the previously bound model element is unbound.
|
|
@@ -185,8 +213,8 @@ export default class Mapper {
|
|
|
185
213
|
}
|
|
186
214
|
|
|
187
215
|
/**
|
|
188
|
-
* Binds given marker name with given {@link module:engine/view/element~Element view element}. The element
|
|
189
|
-
* will be added to the current set of elements bound with given marker name.
|
|
216
|
+
* Binds the given marker name with the given {@link module:engine/view/element~Element view element}. The element
|
|
217
|
+
* will be added to the current set of elements bound with the given marker name.
|
|
190
218
|
*
|
|
191
219
|
* @param {module:engine/view/element~Element} element Element to bind.
|
|
192
220
|
* @param {String} name Marker name.
|
|
@@ -231,7 +259,7 @@ export default class Mapper {
|
|
|
231
259
|
}
|
|
232
260
|
|
|
233
261
|
/**
|
|
234
|
-
* Returns all marker names of markers which
|
|
262
|
+
* Returns all marker names of markers which have changed due to unbinding a view element (so it is assumed that the view element
|
|
235
263
|
* has been removed, moved or renamed) since the last flush. After returning, the marker names list is cleared.
|
|
236
264
|
*
|
|
237
265
|
* @returns {Array.<String>}
|
|
@@ -244,6 +272,22 @@ export default class Mapper {
|
|
|
244
272
|
return markerNames;
|
|
245
273
|
}
|
|
246
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Unbinds all deferred binding removals of view elements that in the meantime were not re-attached to some root or document fragment.
|
|
277
|
+
*
|
|
278
|
+
* See: {@link #unbindViewElement `unbindViewElement()`}.
|
|
279
|
+
*/
|
|
280
|
+
flushDeferredBindings() {
|
|
281
|
+
for ( const [ viewElement, root ] of this._deferredBindingRemovals ) {
|
|
282
|
+
// Unbind it only if it wasn't re-attached to some root or document fragment.
|
|
283
|
+
if ( viewElement.root == root ) {
|
|
284
|
+
this.unbindViewElement( viewElement );
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this._deferredBindingRemovals = new Map();
|
|
289
|
+
}
|
|
290
|
+
|
|
247
291
|
/**
|
|
248
292
|
* Removes all model to view and view to model bindings.
|
|
249
293
|
*/
|
|
@@ -253,6 +297,7 @@ export default class Mapper {
|
|
|
253
297
|
this._markerNameToElements = new Map();
|
|
254
298
|
this._elementToMarkerNames = new Map();
|
|
255
299
|
this._unboundMarkerNames = new Set();
|
|
300
|
+
this._deferredBindingRemovals = new Map();
|
|
256
301
|
}
|
|
257
302
|
|
|
258
303
|
/**
|
|
@@ -341,8 +386,8 @@ export default class Mapper {
|
|
|
341
386
|
* Gets all view elements bound to the given marker name.
|
|
342
387
|
*
|
|
343
388
|
* @param {String} name Marker name.
|
|
344
|
-
* @returns {Set.<module:engine/view/element~Element>|null} View elements bound with given marker name or `null`
|
|
345
|
-
* if no elements are bound to given marker name.
|
|
389
|
+
* @returns {Set.<module:engine/view/element~Element>|null} View elements bound with the given marker name or `null`
|
|
390
|
+
* if no elements are bound to the given marker name.
|
|
346
391
|
*/
|
|
347
392
|
markerNameToElements( name ) {
|
|
348
393
|
const boundElements = this._markerNameToElements.get( name );
|
|
@@ -367,10 +412,10 @@ export default class Mapper {
|
|
|
367
412
|
}
|
|
368
413
|
|
|
369
414
|
/**
|
|
370
|
-
* Registers a callback that evaluates the length in the model of a view element with given name.
|
|
415
|
+
* Registers a callback that evaluates the length in the model of a view element with the given name.
|
|
371
416
|
*
|
|
372
417
|
* The callback is fired with one argument, which is a view element instance. The callback is expected to return
|
|
373
|
-
* a number representing the length of view element in model.
|
|
418
|
+
* a number representing the length of the view element in the model.
|
|
374
419
|
*
|
|
375
420
|
* // List item in view may contain nested list, which have other list items. In model though,
|
|
376
421
|
* // the lists are represented by flat structure. Because of those differences, length of list view element
|
|
@@ -400,10 +445,10 @@ export default class Mapper {
|
|
|
400
445
|
}
|
|
401
446
|
|
|
402
447
|
/**
|
|
403
|
-
* For given `viewPosition`, finds and returns the closest ancestor of this position that has a mapping to
|
|
448
|
+
* For the given `viewPosition`, finds and returns the closest ancestor of this position that has a mapping to
|
|
404
449
|
* the model.
|
|
405
450
|
*
|
|
406
|
-
* @param {module:engine/view/position~Position} viewPosition Position for which mapped ancestor should be found.
|
|
451
|
+
* @param {module:engine/view/position~Position} viewPosition Position for which a mapped ancestor should be found.
|
|
407
452
|
* @returns {module:engine/view/element~Element}
|
|
408
453
|
*/
|
|
409
454
|
findMappedViewAncestor( viewPosition ) {
|
|
@@ -464,17 +509,17 @@ export default class Mapper {
|
|
|
464
509
|
* Gets the length of the view element in the model.
|
|
465
510
|
*
|
|
466
511
|
* The length is calculated as follows:
|
|
467
|
-
* * if {@link #registerViewToModelLength length mapping callback} is provided for given `viewNode
|
|
468
|
-
* evaluate model length (`viewNode` is used as first and only parameter passed to the callback),
|
|
469
|
-
* * length of a {@link module:engine/view/text~Text text node} is equal to the length of
|
|
512
|
+
* * if a {@link #registerViewToModelLength length mapping callback} is provided for the given `viewNode`, it is used to
|
|
513
|
+
* evaluate the model length (`viewNode` is used as first and only parameter passed to the callback),
|
|
514
|
+
* * length of a {@link module:engine/view/text~Text text node} is equal to the length of its
|
|
470
515
|
* {@link module:engine/view/text~Text#data data},
|
|
471
516
|
* * length of a {@link module:engine/view/uielement~UIElement ui element} is equal to 0,
|
|
472
517
|
* * length of a mapped {@link module:engine/view/element~Element element} is equal to 1,
|
|
473
|
-
* * length of a
|
|
518
|
+
* * length of a non-mapped {@link module:engine/view/element~Element element} is equal to the length of its children.
|
|
474
519
|
*
|
|
475
520
|
* Examples:
|
|
476
521
|
*
|
|
477
|
-
* foo -> 3 // Text length is equal to
|
|
522
|
+
* foo -> 3 // Text length is equal to its data length.
|
|
478
523
|
* <p>foo</p> -> 1 // Length of an element which is mapped is by default equal to 1.
|
|
479
524
|
* <b>foo</b> -> 3 // Length of an element which is not mapped is a length of its children.
|
|
480
525
|
* <div><p>x</p><p>y</p></div> -> 2 // Assuming that <div> is not mapped and <p> are mapped.
|
|
@@ -505,7 +550,7 @@ export default class Mapper {
|
|
|
505
550
|
}
|
|
506
551
|
|
|
507
552
|
/**
|
|
508
|
-
* Finds the position in the view node (or its children) with the expected model offset.
|
|
553
|
+
* Finds the position in the view node (or in its children) with the expected model offset.
|
|
509
554
|
*
|
|
510
555
|
* Example:
|
|
511
556
|
*
|
|
@@ -537,7 +582,7 @@ export default class Mapper {
|
|
|
537
582
|
let modelOffset = 0;
|
|
538
583
|
let viewOffset = 0;
|
|
539
584
|
|
|
540
|
-
// In the text node it is simple: offset in the model equals offset in the text.
|
|
585
|
+
// In the text node it is simple: the offset in the model equals the offset in the text.
|
|
541
586
|
if ( viewParent.is( '$text' ) ) {
|
|
542
587
|
return new ViewPosition( viewParent, expectedOffset );
|
|
543
588
|
}
|
|
@@ -565,20 +610,20 @@ export default class Mapper {
|
|
|
565
610
|
}
|
|
566
611
|
|
|
567
612
|
/**
|
|
568
|
-
* Because we prefer positions in text nodes over positions next to text
|
|
569
|
-
*
|
|
613
|
+
* Because we prefer positions in the text nodes over positions next to text nodes, if the view position was next to a text node,
|
|
614
|
+
* it moves it into the text node instead.
|
|
570
615
|
*
|
|
571
616
|
* <p>[]<b>foo</b></p> -> <p>[]<b>foo</b></p> // do not touch if position is not directly next to text
|
|
572
617
|
* <p>foo[]<b>foo</b></p> -> <p>foo{}<b>foo</b></p> // move to text node
|
|
573
618
|
* <p><b>[]foo</b></p> -> <p><b>{}foo</b></p> // move to text node
|
|
574
619
|
*
|
|
575
620
|
* @private
|
|
576
|
-
* @param {module:engine/view/position~Position} viewPosition Position potentially next to text node.
|
|
577
|
-
* @returns {module:engine/view/position~Position} Position in text node if possible.
|
|
621
|
+
* @param {module:engine/view/position~Position} viewPosition Position potentially next to the text node.
|
|
622
|
+
* @returns {module:engine/view/position~Position} Position in the text node if possible.
|
|
578
623
|
*/
|
|
579
624
|
_moveViewPositionToTextNode( viewPosition ) {
|
|
580
|
-
// If the position is just after text node, put it at the end of that text node.
|
|
581
|
-
// If the position is just before text node, put it at the beginning of that text node.
|
|
625
|
+
// If the position is just after a text node, put it at the end of that text node.
|
|
626
|
+
// If the position is just before a text node, put it at the beginning of that text node.
|
|
582
627
|
const nodeBefore = viewPosition.nodeBefore;
|
|
583
628
|
const nodeAfter = viewPosition.nodeAfter;
|
|
584
629
|
|
|
@@ -595,8 +640,8 @@ export default class Mapper {
|
|
|
595
640
|
/**
|
|
596
641
|
* Fired for each model-to-view position mapping request. The purpose of this event is to enable custom model-to-view position
|
|
597
642
|
* mapping. Callbacks added to this event take {@link module:engine/model/position~Position model position} and are expected to
|
|
598
|
-
* calculate {@link module:engine/view/position~Position view position}.
|
|
599
|
-
* value in `data` object that is passed as one of parameters to the event callback.
|
|
643
|
+
* calculate the {@link module:engine/view/position~Position view position}. The calculated view position should be added as
|
|
644
|
+
* a `viewPosition` value in the `data` object that is passed as one of parameters to the event callback.
|
|
600
645
|
*
|
|
601
646
|
* // Assume that "captionedImage" model element is converted to <img> and following <span> elements in view,
|
|
602
647
|
* // and the model element is bound to <img> element. Force mapping model positions inside "captionedImage" to that
|
|
@@ -615,14 +660,14 @@ export default class Mapper {
|
|
|
615
660
|
* }
|
|
616
661
|
* } );
|
|
617
662
|
*
|
|
618
|
-
* **Note:** keep in mind that sometimes a "phantom" model position is being converted. "
|
|
619
|
-
* a position that points to a
|
|
620
|
-
* (it would point to a correct place in view when converted). One example of such situation is when a range is
|
|
621
|
-
* removed from model, there may be a need to map the range's end (which is no longer valid model position). To
|
|
622
|
-
* handle such
|
|
663
|
+
* **Note:** keep in mind that sometimes a "phantom" model position is being converted. A "phantom" model position is
|
|
664
|
+
* a position that points to a nonexistent place in model. Such a position might still be valid for conversion, though
|
|
665
|
+
* (it would point to a correct place in the view when converted). One example of such a situation is when a range is
|
|
666
|
+
* removed from the model, there may be a need to map the range's end (which is no longer a valid model position). To
|
|
667
|
+
* handle such situations, check the `data.isPhantom` flag:
|
|
623
668
|
*
|
|
624
|
-
* // Assume that there is "customElement" model element and whenever position is before it,
|
|
625
|
-
* // to the inside of the view element bound to "customElement".
|
|
669
|
+
* // Assume that there is a "customElement" model element and whenever the position is before it,
|
|
670
|
+
* // we want to move it to the inside of the view element bound to "customElement".
|
|
626
671
|
* mapper.on( 'modelToViewPosition', ( evt, data ) => {
|
|
627
672
|
* if ( data.isPhantom ) {
|
|
628
673
|
* return;
|
|
@@ -643,19 +688,19 @@ export default class Mapper {
|
|
|
643
688
|
* evt.stop();
|
|
644
689
|
* } );
|
|
645
690
|
*
|
|
646
|
-
* **Note:** default mapping callback is provided with `low` priority setting and does not cancel the event, so it is possible to
|
|
647
|
-
* attach a custom callback after default callback and also use `data.viewPosition` calculated by default callback
|
|
691
|
+
* **Note:** the default mapping callback is provided with a `low` priority setting and does not cancel the event, so it is possible to
|
|
692
|
+
* attach a custom callback after a default callback and also use `data.viewPosition` calculated by the default callback
|
|
648
693
|
* (for example to fix it).
|
|
649
694
|
*
|
|
650
|
-
* **Note:** default mapping callback will not fire if `data.viewPosition` is already set.
|
|
695
|
+
* **Note:** the default mapping callback will not fire if `data.viewPosition` is already set.
|
|
651
696
|
*
|
|
652
697
|
* **Note:** these callbacks are called **very often**. For efficiency reasons, it is advised to use them only when position
|
|
653
|
-
* mapping between given model and view elements is unsolvable using just elements mapping and default algorithm.
|
|
654
|
-
* the condition that checks if special case scenario happened should be as simple as possible.
|
|
698
|
+
* mapping between the given model and view elements is unsolvable by using just elements mapping and default algorithm.
|
|
699
|
+
* Also, the condition that checks if a special case scenario happened should be as simple as possible.
|
|
655
700
|
*
|
|
656
701
|
* @event modelToViewPosition
|
|
657
702
|
* @param {Object} data Data pipeline object that can store and pass data between callbacks. The callback should add
|
|
658
|
-
* `viewPosition` value to that object with calculated {@link module:engine/view/position~Position view position}.
|
|
703
|
+
* the `viewPosition` value to that object with calculated the {@link module:engine/view/position~Position view position}.
|
|
659
704
|
* @param {module:engine/conversion/mapper~Mapper} data.mapper Mapper instance that fired the event.
|
|
660
705
|
*/
|
|
661
706
|
|
|
@@ -676,15 +721,15 @@ export default class Mapper {
|
|
|
676
721
|
* }
|
|
677
722
|
* } );
|
|
678
723
|
*
|
|
679
|
-
* **Note:** default mapping callback is provided with `low` priority setting and does not cancel the event, so it is possible to
|
|
680
|
-
* attach a custom callback after default callback and also use `data.modelPosition` calculated by default callback
|
|
724
|
+
* **Note:** the default mapping callback is provided with a `low` priority setting and does not cancel the event, so it is possible to
|
|
725
|
+
* attach a custom callback after the default callback and also use `data.modelPosition` calculated by the default callback
|
|
681
726
|
* (for example to fix it).
|
|
682
727
|
*
|
|
683
|
-
* **Note:** default mapping callback will not fire if `data.modelPosition` is already set.
|
|
728
|
+
* **Note:** the default mapping callback will not fire if `data.modelPosition` is already set.
|
|
684
729
|
*
|
|
685
730
|
* **Note:** these callbacks are called **very often**. For efficiency reasons, it is advised to use them only when position
|
|
686
|
-
* mapping between given model and view elements is unsolvable using just elements mapping and default algorithm.
|
|
687
|
-
* the condition that checks if special case scenario happened should be as simple as possible.
|
|
731
|
+
* mapping between the given model and view elements is unsolvable by using just elements mapping and default algorithm.
|
|
732
|
+
* Also, the condition that checks if special case scenario happened should be as simple as possible.
|
|
688
733
|
*
|
|
689
734
|
* @event viewToModelPosition
|
|
690
735
|
* @param {Object} data Data pipeline object that can store and pass data between callbacks. The callback should add
|
|
@@ -8,33 +8,34 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import TextProxy from '../model/textproxy';
|
|
11
|
+
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* Manages a list of consumable values for {@link module:engine/model/item~Item model items}.
|
|
14
|
+
* Manages a list of consumable values for the {@link module:engine/model/item~Item model items}.
|
|
14
15
|
*
|
|
15
|
-
* Consumables are various aspects of the model. A model item can be broken down into
|
|
16
|
+
* Consumables are various aspects of the model. A model item can be broken down into separate, single properties that might be
|
|
16
17
|
* taken into consideration when converting that item.
|
|
17
18
|
*
|
|
18
|
-
* `ModelConsumable` is used by {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} while analyzing changed
|
|
19
|
+
* `ModelConsumable` is used by {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} while analyzing the changed
|
|
19
20
|
* parts of {@link module:engine/model/document~Document the document}. The added / changed / removed model items are broken down
|
|
20
|
-
* into singular properties (the item itself and
|
|
21
|
-
* during conversion, when given part of model item is converted (i.e. the view element has been inserted into the view,
|
|
22
|
-
* but without attributes), consumable value is removed from `ModelConsumable`.
|
|
21
|
+
* into singular properties (the item itself and its attributes). All those parts are saved in `ModelConsumable`. Then,
|
|
22
|
+
* during conversion, when the given part of a model item is converted (i.e. the view element has been inserted into the view,
|
|
23
|
+
* but without attributes), the consumable value is removed from `ModelConsumable`.
|
|
23
24
|
*
|
|
24
25
|
* For model items, `ModelConsumable` stores consumable values of one of following types: `insert`, `addattribute:<attributeKey>`,
|
|
25
26
|
* `changeattributes:<attributeKey>`, `removeattributes:<attributeKey>`.
|
|
26
27
|
*
|
|
27
|
-
* In most cases, it is enough to let {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
|
|
28
|
+
* In most cases, it is enough to let th {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
|
|
28
29
|
* gather consumable values, so there is no need to use
|
|
29
|
-
* {@link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly.
|
|
30
|
+
* the {@link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly.
|
|
30
31
|
* However, it is important to understand how consumable values can be
|
|
31
32
|
* {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}.
|
|
32
33
|
* See {@link module:engine/conversion/downcasthelpers default downcast converters} for more information.
|
|
33
34
|
*
|
|
34
|
-
* Keep in mind
|
|
35
|
+
* Keep in mind that one conversion event may have multiple callbacks (converters) attached to it. Each of those is
|
|
35
36
|
* able to convert one or more parts of the model. However, when one of those callbacks actually converts
|
|
36
|
-
* something,
|
|
37
|
-
* this situation, because callbacks should only convert
|
|
37
|
+
* something, the others should not, because they would duplicate the results. Using `ModelConsumable` helps to avoid
|
|
38
|
+
* this situation, because callbacks should only convert these values that were not yet consumed from `ModelConsumable`.
|
|
38
39
|
*
|
|
39
40
|
* Consuming multiple values in a single callback:
|
|
40
41
|
*
|
|
@@ -101,12 +102,12 @@ export default class ModelConsumable {
|
|
|
101
102
|
this._consumable = new Map();
|
|
102
103
|
|
|
103
104
|
/**
|
|
104
|
-
* For each {@link module:engine/model/textproxy~TextProxy} added to `ModelConsumable`, this registry holds parent
|
|
105
|
-
* of that `TextProxy` and start and end indices of that `TextProxy`. This allows identification of `TextProxy`
|
|
106
|
-
* instances that
|
|
107
|
-
* is given unique `Symbol` which is then registered as consumable. This process is transparent for `ModelConsumable`
|
|
108
|
-
* API user because whenever `TextProxy` is added, tested, consumed or reverted, internal mechanisms of
|
|
109
|
-
* `ModelConsumable`
|
|
105
|
+
* For each {@link module:engine/model/textproxy~TextProxy} added to `ModelConsumable`, this registry holds a parent
|
|
106
|
+
* of that `TextProxy` and the start and end indices of that `TextProxy`. This allows identification of the `TextProxy`
|
|
107
|
+
* instances that point to the same part of the model but are different instances. Each distinct `TextProxy`
|
|
108
|
+
* is given a unique `Symbol` which is then registered as consumable. This process is transparent for the `ModelConsumable`
|
|
109
|
+
* API user because whenever `TextProxy` is added, tested, consumed or reverted, the internal mechanisms of
|
|
110
|
+
* `ModelConsumable` translate `TextProxy` to that unique `Symbol`.
|
|
110
111
|
*
|
|
111
112
|
* @private
|
|
112
113
|
* @member {Map} module:engine/conversion/modelconsumable~ModelConsumable#_textProxyRegistry
|
|
@@ -115,7 +116,7 @@ export default class ModelConsumable {
|
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
/**
|
|
118
|
-
* Adds a consumable value to the consumables list and links it with given model item.
|
|
119
|
+
* Adds a consumable value to the consumables list and links it with a given model item.
|
|
119
120
|
*
|
|
120
121
|
* modelConsumable.add( modelElement, 'insert' ); // Add `modelElement` insertion change to consumable values.
|
|
121
122
|
* modelConsumable.add( modelElement, 'addAttribute:bold' ); // Add `bold` attribute insertion on `modelElement` change.
|
|
@@ -143,7 +144,7 @@ export default class ModelConsumable {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
/**
|
|
146
|
-
* Removes given consumable value from given model item.
|
|
147
|
+
* Removes a given consumable value from a given model item.
|
|
147
148
|
*
|
|
148
149
|
* modelConsumable.consume( modelElement, 'insert' ); // Remove `modelElement` insertion change from consumable values.
|
|
149
150
|
* modelConsumable.consume( modelElement, 'addAttribute:bold' ); // Remove `bold` attribute insertion on `modelElement` change.
|
|
@@ -174,7 +175,7 @@ export default class ModelConsumable {
|
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
/**
|
|
177
|
-
* Tests whether there is a consumable value of given type connected with given model item.
|
|
178
|
+
* Tests whether there is a consumable value of a given type connected with a given model item.
|
|
178
179
|
*
|
|
179
180
|
* modelConsumable.test( modelElement, 'insert' ); // Check for `modelElement` insertion change.
|
|
180
181
|
* modelConsumable.test( modelElement, 'addAttribute:bold' ); // Check for `bold` attribute insertion on `modelElement` change.
|
|
@@ -212,7 +213,7 @@ export default class ModelConsumable {
|
|
|
212
213
|
}
|
|
213
214
|
|
|
214
215
|
/**
|
|
215
|
-
* Reverts consuming of consumable value.
|
|
216
|
+
* Reverts consuming of a consumable value.
|
|
216
217
|
*
|
|
217
218
|
* modelConsumable.revert( modelElement, 'insert' ); // Revert consuming `modelElement` insertion change.
|
|
218
219
|
* modelConsumable.revert( modelElement, 'addAttribute:bold' ); // Revert consuming `bold` attribute insert from `modelElement`.
|
|
@@ -247,12 +248,54 @@ export default class ModelConsumable {
|
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
/**
|
|
250
|
-
*
|
|
251
|
+
* Verifies if all events from the specified group were consumed.
|
|
252
|
+
*
|
|
253
|
+
* @param {String} eventGroup The events group to verify.
|
|
254
|
+
*/
|
|
255
|
+
verifyAllConsumed( eventGroup ) {
|
|
256
|
+
const items = [];
|
|
257
|
+
|
|
258
|
+
for ( const [ item, consumables ] of this._consumable ) {
|
|
259
|
+
for ( const [ event, canConsume ] of consumables ) {
|
|
260
|
+
const eventPrefix = event.split( ':' )[ 0 ];
|
|
261
|
+
|
|
262
|
+
if ( canConsume && eventGroup == eventPrefix ) {
|
|
263
|
+
items.push( {
|
|
264
|
+
event,
|
|
265
|
+
item: item.name || item.description
|
|
266
|
+
} );
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if ( items.length ) {
|
|
272
|
+
/**
|
|
273
|
+
* Some of the {@link module:engine/model/item~Item model items} were not consumed while downcasting the model to view.
|
|
274
|
+
*
|
|
275
|
+
* This might be the effect of:
|
|
276
|
+
*
|
|
277
|
+
* * A missing converter for some model elements. Make sure that you registered downcast converters for all model elements.
|
|
278
|
+
* * A custom converter that does not consume converted items. Make sure that you
|
|
279
|
+
* {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} all model elements that you converted
|
|
280
|
+
* from the model to the view.
|
|
281
|
+
* * A custom converter that called `event.stop()`. When providing a custom converter, keep in mind that you should not stop
|
|
282
|
+
* the event. If you stop it then the default converter at the `lowest` priority will not trigger the conversion of this node's
|
|
283
|
+
* attributes and child nodes.
|
|
284
|
+
*
|
|
285
|
+
* @error conversion-model-consumable-not-consumed
|
|
286
|
+
* @param {Array.<module:engine/model/item~Item>} items Items that were not consumed.
|
|
287
|
+
*/
|
|
288
|
+
throw new CKEditorError( 'conversion-model-consumable-not-consumed', null, { items } );
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Gets a unique symbol for the passed {@link module:engine/model/textproxy~TextProxy} instance. All `TextProxy` instances that
|
|
251
294
|
* have same parent, same start index and same end index will get the same symbol.
|
|
252
295
|
*
|
|
253
296
|
* Used internally to correctly consume `TextProxy` instances.
|
|
254
297
|
*
|
|
255
|
-
* @
|
|
298
|
+
* @protected
|
|
256
299
|
* @param {module:engine/model/textproxy~TextProxy} textProxy `TextProxy` instance to get a symbol for.
|
|
257
300
|
* @returns {Symbol} Symbol representing all equal instances of `TextProxy`.
|
|
258
301
|
*/
|
|
@@ -270,25 +313,27 @@ export default class ModelConsumable {
|
|
|
270
313
|
}
|
|
271
314
|
|
|
272
315
|
if ( !symbol ) {
|
|
273
|
-
symbol = this._addSymbolForTextProxy( textProxy
|
|
316
|
+
symbol = this._addSymbolForTextProxy( textProxy );
|
|
274
317
|
}
|
|
275
318
|
|
|
276
319
|
return symbol;
|
|
277
320
|
}
|
|
278
321
|
|
|
279
322
|
/**
|
|
280
|
-
* Adds a symbol for given
|
|
323
|
+
* Adds a symbol for the given {@link module:engine/model/textproxy~TextProxy} instance.
|
|
281
324
|
*
|
|
282
325
|
* Used internally to correctly consume `TextProxy` instances.
|
|
283
326
|
*
|
|
284
327
|
* @private
|
|
285
|
-
* @param {
|
|
286
|
-
* @
|
|
287
|
-
* @param {module:engine/model/element~Element} parent Text proxy parent.
|
|
288
|
-
* @returns {Symbol} Symbol generated for given properties.
|
|
328
|
+
* @param {module:engine/model/textproxy~TextProxy} textProxy Text proxy instance.
|
|
329
|
+
* @returns {Symbol} Symbol generated for given `TextProxy`.
|
|
289
330
|
*/
|
|
290
|
-
_addSymbolForTextProxy(
|
|
291
|
-
const
|
|
331
|
+
_addSymbolForTextProxy( textProxy ) {
|
|
332
|
+
const start = textProxy.startOffset;
|
|
333
|
+
const end = textProxy.endOffset;
|
|
334
|
+
const parent = textProxy.parent;
|
|
335
|
+
|
|
336
|
+
const symbol = Symbol( '$textProxy:' + textProxy.data );
|
|
292
337
|
let startMap, endMap;
|
|
293
338
|
|
|
294
339
|
startMap = this._textProxyRegistry.get( start );
|
|
@@ -311,15 +356,20 @@ export default class ModelConsumable {
|
|
|
311
356
|
}
|
|
312
357
|
}
|
|
313
358
|
|
|
314
|
-
// Returns a normalized consumable type name from given string. A normalized consumable type name is a string that has
|
|
315
|
-
// at most one colon, for example: `insert` or `addMarker:highlight`. If string to normalize has more "parts" (more colons),
|
|
316
|
-
// the
|
|
359
|
+
// Returns a normalized consumable type name from the given string. A normalized consumable type name is a string that has
|
|
360
|
+
// at most one colon, for example: `insert` or `addMarker:highlight`. If a string to normalize has more "parts" (more colons),
|
|
361
|
+
// the further parts are dropped, for example: `addattribute:bold:$text` -> `addattributes:bold`.
|
|
317
362
|
//
|
|
318
363
|
// @param {String} type Consumable type.
|
|
319
364
|
// @returns {String} Normalized consumable type.
|
|
320
365
|
function _normalizeConsumableType( type ) {
|
|
321
366
|
const parts = type.split( ':' );
|
|
322
367
|
|
|
368
|
+
// For inserts allow passing event name, it's stored in the context of a specified element so the element name is not needed.
|
|
369
|
+
if ( parts[ 0 ] == 'insert' ) {
|
|
370
|
+
return parts[ 0 ];
|
|
371
|
+
}
|
|
372
|
+
|
|
323
373
|
// Markers are identified by the whole name (otherwise we would consume the whole markers group).
|
|
324
374
|
if ( parts[ 0 ] == 'addMarker' || parts[ 0 ] == 'removeMarker' ) {
|
|
325
375
|
return type;
|
|
@@ -40,10 +40,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
|
|
|
40
40
|
* The third parameter of the callback is an instance of {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi}
|
|
41
41
|
* which provides additional tools for converters.
|
|
42
42
|
*
|
|
43
|
-
* You can read more about conversion in the
|
|
44
|
-
*
|
|
45
|
-
* * {@glink framework/guides/deep-dive/conversion/conversion-introduction Advanced conversion concepts — attributes}
|
|
46
|
-
* * {@glink framework/guides/deep-dive/conversion/custom-element-conversion Custom element conversion}
|
|
43
|
+
* You can read more about conversion in the {@glink framework/guides/deep-dive/conversion/upcast Upcast conversion} guide.
|
|
47
44
|
*
|
|
48
45
|
* Examples of event-based converters:
|
|
49
46
|
*
|
|
@@ -127,7 +124,7 @@ export default class UpcastDispatcher {
|
|
|
127
124
|
/**
|
|
128
125
|
* The list of elements that were created during splitting.
|
|
129
126
|
*
|
|
130
|
-
* After the conversion process the list is cleared.
|
|
127
|
+
* After the conversion process, the list is cleared.
|
|
131
128
|
*
|
|
132
129
|
* @private
|
|
133
130
|
* @type {Map.<module:engine/model/element~Element,Array.<module:engine/model/element~Element>>}
|
|
@@ -12,7 +12,7 @@ import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
|
|
|
12
12
|
import { isParagraphable, wrapInParagraph } from '../model/utils/autoparagraphing';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
|
|
15
|
+
* Contains the {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
|
|
16
16
|
* {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}.
|
|
17
17
|
*
|
|
18
18
|
* @module engine/conversion/upcasthelpers
|
|
@@ -21,6 +21,8 @@ import { isParagraphable, wrapInParagraph } from '../model/utils/autoparagraphin
|
|
|
21
21
|
/**
|
|
22
22
|
* Upcast conversion helper functions.
|
|
23
23
|
*
|
|
24
|
+
* Learn more about {@glink framework/guides/deep-dive/conversion/upcast upcast helpers}.
|
|
25
|
+
*
|
|
24
26
|
* @extends module:engine/conversion/conversionhelpers~ConversionHelpers
|
|
25
27
|
*/
|
|
26
28
|
export default class UpcastHelpers extends ConversionHelpers {
|
|
@@ -114,7 +114,7 @@ export default class HtmlDataProcessor {
|
|
|
114
114
|
* @returns {DocumentFragment}
|
|
115
115
|
*/
|
|
116
116
|
_toDom( data ) {
|
|
117
|
-
// Wrap data with a <body> so leading non-layout nodes (like <script>, <style>, HTML comment)
|
|
117
|
+
// Wrap data with a <body> tag so leading non-layout nodes (like <script>, <style>, HTML comment)
|
|
118
118
|
// will be preserved in the body collection.
|
|
119
119
|
// Do it only for data that is not a full HTML document.
|
|
120
120
|
if ( !data.match( /<(?:html|body|head|meta)(?:\s[^>]*)?>/i ) ) {
|