Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit af34f31

Browse files
author
Piotr Jasiun
authored
Merge pull request #1040 from ckeditor/t/1015
Feature: Introduce Virtual selection feature. BREAKING CHANGE: ModelConverterBuilder#toStamp() functionality is renamed to ModelConverterBuilder#toElement. Introduced ModelConverterBuilder#toVirtualSelection which replaces current marker to element conversion. BREAKING CHANGE: Parameter change for convertSelectionMarker() function from model-selection-to-view-converters.js. BREAKING CHANGE: Removed wrapRange() and unwrapRange() functions from model-to-view-converters.js as they're no longer used. BREAKING CHANGE: Renamed marker stamps to marker elements in code and docs.
2 parents 87da91c + 5201602 commit af34f31

18 files changed

+1104
-599
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@ckeditor/ckeditor5-paragraph": "^0.8.0",
2020
"@ckeditor/ckeditor5-typing": "^0.9.1",
2121
"@ckeditor/ckeditor5-undo": "^0.8.1",
22+
"@ckeditor/ckeditor5-widget": "^0.1.1",
2223
"eslint-config-ckeditor5": "^1.0.5",
2324
"gulp": "^3.9.1",
2425
"guppy-pre-commit": "^0.4.0"

src/controller/datacontroller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ export default class DataController {
225225
* {@link module:engine/view/documentfragment~DocumentFragment view document fragment} converted by the
226226
* {@link #viewToModel view to model converters}.
227227
*
228-
* When marker stamps were converted during conversion process then will be set as DocumentFragment's
228+
* When marker elements were converted during conversion process then will be set as DocumentFragment's
229229
* {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
230230
*
231231
* @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment

src/conversion/buildmodelconverter.js

Lines changed: 103 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -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

2222
import { 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+
*/

src/conversion/model-selection-to-view-converters.js

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import ViewElement from '../view/element';
77
import ViewRange from '../view/range';
88
import viewWriter from '../view/writer';
9+
import { virtualSelectionDescriptorToAttributeElement } from './model-to-view-converters';
910

1011
/**
1112
* Contains {@link module:engine/model/selection~Selection model selection} to
@@ -112,11 +113,6 @@ export function convertCollapsedSelection() {
112113
* }
113114
* }
114115
* modelDispatcher.on( 'selectionAttribute:style', convertSelectionAttribute( styleCreator ) );
115-
*
116-
* **Note:** You can use the same `elementCreator` function for this converter factory
117-
* and {@link module:engine/conversion/model-to-view-converters~wrapRange}
118-
* model to view converter, as long as the `elementCreator` function uses only the first parameter (attribute value).
119-
*
120116
* modelDispatcher.on( 'selection', convertCollapsedSelection() );
121117
* modelDispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
122118
* modelDispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'strong' ) ) );
@@ -163,27 +159,25 @@ export function convertSelectionAttribute( elementCreator ) {
163159
* Performs similar conversion as {@link ~convertSelectionAttribute}, but depends on a marker name of a marker in which
164160
* collapsed selection is placed.
165161
*
166-
* modelDispatcher.on( 'selectionMarker:searchResult', wrapRange( new ViewAttributeElement( 'span', { class: 'searchResult' } ) ) );
167-
*
168-
* **Note:** You can use the same `elementCreator` function for this converter factory
169-
* and {@link module:engine/conversion/model-to-view-converters~wrapRange}.
162+
* modelDispatcher.on( 'selectionMarker:searchResult', convertSelectionMarker( { class: 'search' } ) );
170163
*
171164
* @see module:engine/conversion/model-selection-to-view-converters~convertSelectionAttribute
172-
* @param {module:engine/view/attributeelement~AttributeElement|Function} elementCreator View element,
173-
* or function returning a view element, which will be used for wrapping.
165+
* @param {module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor|Function} selectionDescriptor Virtual
166+
* selection descriptor object or function returning a descriptor object.
174167
* @returns {Function} Selection converter.
175168
*/
176-
export function convertSelectionMarker( elementCreator ) {
169+
export function convertSelectionMarker( selectionDescriptor ) {
177170
return ( evt, data, consumable, conversionApi ) => {
178-
const viewElement = elementCreator instanceof ViewElement ?
179-
elementCreator.clone( true ) :
180-
elementCreator( data, consumable, conversionApi );
171+
const descriptor = typeof selectionDescriptor == 'function' ?
172+
selectionDescriptor( data, consumable, conversionApi ) :
173+
selectionDescriptor;
181174

182-
if ( !viewElement ) {
175+
if ( !descriptor ) {
183176
return;
184177
}
185178

186-
const consumableName = 'selectionMarker:' + data.name;
179+
const viewElement = virtualSelectionDescriptorToAttributeElement( descriptor );
180+
const consumableName = 'selectionMarker:' + data.markerName;
187181

188182
wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName );
189183
};

0 commit comments

Comments
 (0)