@@ -15,8 +15,8 @@ import {
1515 removeUIElement ,
1616 wrapItem ,
1717 unwrapItem ,
18- wrapRange ,
19- unwrapRange
18+ convertTextsInsideMarker ,
19+ convertElementsInsideMarker
2020} from './model-to-view-converters' ;
2121
2222import { convertSelectionAttribute , convertSelectionMarker } from './model-selection-to-view-converters' ;
@@ -37,7 +37,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
3737 * {@link module:engine/conversion/model-to-view-converters}, {@link module:engine/conversion/modelconsumable~ModelConsumable},
3838 * {@link module:engine/conversion/mapper~Mapper}.
3939 *
40- * Using this API it is possible to create four kinds of converters:
40+ * Using this API it is possible to create five kinds of converters:
4141 *
4242 * 1. Model element to view element converter. This is a converter that takes the model element and represents it
4343 * in the view.
@@ -58,17 +58,25 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
5858 *
5959 * buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
6060 *
61- * 4. Model marker to view element converter. This is a converter that converts markers from given group to view attribute element.
62- * Markers, basically, are {@link module:engine/model/liverange~LiveRange} instances, that are named. In this conversion, model range is
63- * converted to view range, then that view range is wrapped (or unwrapped, if range is removed) in a view attribute element.
64- * To learn more about markers, see {@link module:engine/model/markercollection~MarkerCollection}.
61+ * 4. Model marker to virtual selection converter. This is a converter that converts model markers to virtual
62+ * selection described by {@link module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor} object passed to
63+ * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toVirtualSelection} method.
6564 *
66- * const viewSpanSearchResult = new ViewAttributeElement( 'span', { class: 'search-result' } );
67- * buildModelConverter().for( dispatcher ).fromMarker( 'searchResult' ).toElement( viewSpanSearchResult );
65+ * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toVirtualSelection( {
66+ * class: 'search',
67+ * priority: 20
68+ * } );
69+ *
70+ * 5. Model marker to element converter. This is a converter that takes model marker and creates separate elements at
71+ * the beginning and at the end of the marker's range. For more information see
72+ * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement} method.
73+ *
74+ * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
6875 *
6976 * It is possible to provide various different parameters for
70- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement}
71- * and {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toAttribute} methods.
77+ * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement},
78+ * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toAttribute} and
79+ * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toVirtualSelection} methods.
7280 * See their descriptions to learn more.
7381 *
7482 * It is also possible to {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority change default priority}
@@ -198,7 +206,13 @@ class ModelConverterBuilder {
198206 * `string`, view element instance which will be cloned and used, or creator function which returns view element that
199207 * will be used. Keep in mind that when you view element instance or creator function, it has to be/return a
200208 * proper type of view element: {@link module:engine/view/containerelement~ContainerElement ViewContainerElement} if you convert
201- * from element or {@link module:engine/view/attributeelement~AttributeElement ViewAttributeElement} if you convert from attribute.
209+ * from element, {@link module:engine/view/attributeelement~AttributeElement ViewAttributeElement} if you convert
210+ * from attribute and {@link module:engine/view/uielement~UIElement ViewUIElement} if you convert from marker.
211+ *
212+ * NOTE: When converting from model's marker, separate elements will be created at the beginning and at the end of the
213+ * marker's range. If range is collapsed then only one element will be created. See how markers
214+ * {module:engine/model/buildviewconverter~ViewConverterBuilder#toMarker view -> model serialization}
215+ * works to find out what view element format is the best for you.
202216 *
203217 * buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
204218 *
@@ -210,12 +224,17 @@ class ModelConverterBuilder {
210224 *
211225 * buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( new ViewAttributeElement( 'strong' ) );
212226 *
227+ * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
228+ *
229+ * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( new ViewUIElement( 'span' ) );
230+ *
213231 * Creator function will be passed different values depending whether conversion is from element or from attribute:
214232 *
215233 * * from element: dispatcher's
216234 * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event}
217235 * parameters will be passed,
218- * * from attribute: there is one parameter and it is attribute value.
236+ * * from attribute: there is one parameter and it is attribute value,
237+ * * from marker: {@link module:engine/conversion/buildmodelconverter~MarkerViewElementCreatorData}.
219238 *
220239 * This method also registers model selection to view selection converter, if conversion is from attribute.
221240 *
@@ -243,77 +262,78 @@ class ModelConverterBuilder {
243262 dispatcher . on ( 'removeAttribute:' + this . _from . key , unwrapItem ( element ) , { priority } ) ;
244263
245264 dispatcher . on ( 'selectionAttribute:' + this . _from . key , convertSelectionAttribute ( element ) , { priority } ) ;
246- } else {
247- element = typeof element == 'string' ? new ViewAttributeElement ( element ) : element ;
265+ } else { // From marker to element.
266+ const priority = this . _from . priority === null ? 'normal' : this . _from . priority ;
248267
249- dispatcher . on ( 'addMarker:' + this . _from . name , wrapRange ( element ) , { priority } ) ;
250- dispatcher . on ( 'removeMarker:' + this . _from . name , unwrapRange ( element ) , { priority } ) ;
268+ element = typeof element == 'string' ? new ViewUIElement ( element ) : element ;
251269
252- dispatcher . on ( 'selectionMarker:' + this . _from . name , convertSelectionMarker ( element ) , { priority } ) ;
270+ dispatcher . on ( 'addMarker:' + this . _from . name , insertUIElement ( element ) , { priority } ) ;
271+ dispatcher . on ( 'removeMarker:' + this . _from . name , removeUIElement ( element ) , { priority } ) ;
253272 }
254273 }
255274 }
256275
257276 /**
258- * Registers what view stamp will be created by converter to mark marker range bounds. Separate elements will be
259- * created at the beginning and at the end of the range. If range is collapsed then only one element will be created.
277+ * Registers that marker should be converted to virtual selection. Markers, basically,
278+ * are {@link module:engine/model/liverange~LiveRange} instances, that are named. Virtual selection is
279+ * a representation of the model marker in the view:
280+ * * each {@link module:engine/view/text~Text view text node} in the marker's range will be wrapped with `span`
281+ * {@link module:engine/view/attributeelement~AttributeElement},
282+ * * each {@link module:engine/view/containerelement~ContainerElement container view element} in the marker's
283+ * range can handle the virtual selection individually by providing `setVirtualSelection` and `removeVirtualSelection`
284+ * custom properties:
260285 *
261- * Method accepts various ways of providing how the view element will be created. You can pass view element name as
262- * `string`, view element instance which will be cloned and used, or creator function which returns view element that
263- * will be used. Keep in mind that when you provide view element instance or creator function, it has to be/return a
264- * proper type of view element: {@link module:engine/view/uielement~UIElement UIElement}.
286+ * viewElement.setCustomProperty( 'setVirtualSelection', ( element, descriptor ) => {} );
287+ * viewElement.setCustomProperty( 'removeVirtualSelection', ( element, descriptor ) => {} );
265288 *
266- * buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toStamp( 'span' );
289+ * {@link module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor Descriptor} will be used to create
290+ * spans over text nodes and also will be provided to `setVirtualSelection` and `removeVirtualSelection` methods
291+ * each time virtual selection should be set or removed from view elements.
292+ * NOTE: When `setVirtualSelection` and `removeVirtualSelection` custom properties are present, converter assumes
293+ * that element itself is taking care of presenting virtual selection on its child nodes, so it won't convert virtual
294+ * selection on them.
267295 *
268- * buildModelConverter().for( dispatcher )
269- * .fromMarker( 'search' )
270- * .toStamp( new UIElement( 'span', { 'data-name': 'search' } ) );
296+ * Virtual selection descriptor can be provided as plain object:
271297 *
272- * buildModelConverter().for( dispatcher )
273- * .fromMarker( 'search' )
274- * .toStamp( ( data ) => new UIElement( 'span', { 'data-name': data.name ) );
275- *
276- * Creator function provides additional `data.isOpening` parameter which defined if currently converted element is
277- * a beginning or end of the marker range. This makes possible to create different opening and closing stamp.
278- *
279- * buildModelConverter().for( dispatcher )
280- * .fromMarker( 'search' )
281- * .toStamp( ( data ) => {
282- * if ( data.isOpening ) {
283- * return new UIElement( 'span', { 'data-name': data.name, 'data-start': true ) );
284- * }
298+ * buildModelConverter.for( dispatcher ).fromMarker( 'search' ).toVirtualSelection( { class: 'search-mark' } );
299+ *
300+ * Also, descriptor creator function can be provided:
285301 *
286- * return new UIElement ( 'span', { 'data-name': data.name, ' data-end': true ) );
287- * }
302+ * buildModelConverter.for( dispatcher ).fromMarker ( 'search:blue' ).toVirtualSelection( data => {
303+ * const color = data.markerName.split( ':' )[ 1 ];
288304 *
289- * Creator function provides
290- * { @link module:engine/conversion/buildmodelconverter~StampCreatorData} parameters.
305+ * return { class: 'search-' + color };
306+ * } );
291307 *
292- * See how markers { module:engine/model/buildviewconverter~ViewConverterBuilder#toMarker view -> model serialization }
293- * works to find out what view element format is the best for you .
308+ * Throws { @link module:utils/ckeditorerror~CKEditorError CKEditorError }
309+ * `build-model-converter-non-marker-to-virtual-selection` when trying to convert not from marker .
294310 *
295- * @param {String|module:engine/view/uielement~UIElement|Function } element UIElement created by converter or
296- * a function that returns view element.
311+ * @param {function|module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor } selectionDescriptor
297312 */
298- toStamp ( element ) {
299- for ( const dispatcher of this . _dispatchers ) {
300- if ( this . _from . type != 'marker' ) {
301- /**
302- * To-stamp conversion is supported only for model markers.
303- *
304- * @error build-model-converter-element-to-stamp
305- */
306- throw new CKEditorError (
307- 'build-model-converter-non-marker-to-stamp: To-stamp conversion is supported only from model markers.'
308- ) ;
309- }
313+ toVirtualSelection ( selectionDescriptor ) {
314+ const priority = this . _from . priority === null ? 'normal' : this . _from . priority ;
310315
311- const priority = this . _from . priority === null ? 'normal' : this . _from . priority ;
316+ if ( this . _from . type != 'marker' ) {
317+ /**
318+ * To virtual selection conversion is supported only for model markers.
319+ *
320+ * @error build-model-converter-non-marker-to-virtual-selection
321+ */
322+ throw new CKEditorError (
323+ 'build-model-converter-non-marker-to-virtual-selection: Conversion to virtual selection is supported ' +
324+ 'only from model markers.'
325+ ) ;
326+ }
327+
328+ for ( const dispatcher of this . _dispatchers ) {
329+ // Separate converters for converting texts and elements inside marker's range.
330+ dispatcher . on ( 'addMarker:' + this . _from . name , convertTextsInsideMarker ( selectionDescriptor ) , { priority } ) ;
331+ dispatcher . on ( 'addMarker:' + this . _from . name , convertElementsInsideMarker ( selectionDescriptor ) , { priority } ) ;
312332
313- element = typeof element == 'string' ? new ViewUIElement ( element ) : element ;
333+ dispatcher . on ( 'removeMarker:' + this . _from . name , convertTextsInsideMarker ( selectionDescriptor ) , { priority } ) ;
334+ dispatcher . on ( 'removeMarker:' + this . _from . name , convertElementsInsideMarker ( selectionDescriptor ) , { priority } ) ;
314335
315- dispatcher . on ( 'addMarker:' + this . _from . name , insertUIElement ( element ) , { priority } ) ;
316- dispatcher . on ( 'removeMarker:' + this . _from . name , removeUIElement ( element ) , { priority } ) ;
336+ dispatcher . on ( 'selectionMarker:' + this . _from . name , convertSelectionMarker ( selectionDescriptor ) , { priority } ) ;
317337 }
318338 }
319339
@@ -404,11 +424,26 @@ export default function buildModelConverter() {
404424}
405425
406426/**
407- * @typedef StampCreatorData
427+ * @typedef MarkerViewElementCreatorData
408428 * @param {Object } data Additional information about the change.
409- * @param {String } data.name Marker name.
410- * @param {module:engine/model/range~Range } data.range Marker range.
429+ * @param {String } data.markerName Marker name.
430+ * @param {module:engine/model/range~Range } data.markerRange Marker range.
411431 * @param {Boolean } data.isOpening Defines if currently converted element is a beginning or end of the marker range.
412432 * @param {module:engine/conversion/modelconsumable~ModelConsumable } consumable Values to consume.
413433 * @param {Object } conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
414434 */
435+
436+ /**
437+ * @typedef VirtualSelectionDescriptor
438+ * Object describing how virtual selection should be created in the view. Each text node in virtual selection
439+ * will be wrapped with `span` element with CSS class, attributes and priority described by this object. Each element
440+ * can handle virtual selection separately by providing `setVirtualSelection` and `removeVirtualSelection` custom
441+ * properties.
442+ *
443+ * @property {String } class CSS class that will be added to `span`
444+ * {@link module:engine/view/attributeelement~AttributeElement} wrapping each text node in the virtual selection.
445+ * @property {Number } [priority] {@link module:engine/view/attributeelement~AttributeElement#priority } of the `span`
446+ * wrapping each text node in the virtual selection. If not provided, default 10 priority will be used.
447+ * @property {Object } [attributes] Attributes that will be added to `span`
448+ * {@link module:engine/view/attributeelement~AttributeElement} wrapping each text node it the virtual selection.
449+ */
0 commit comments