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

Commit

Permalink
Merge pull request #1040 from ckeditor/t/1015
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Piotr Jasiun authored Aug 18, 2017
2 parents 87da91c + 5201602 commit af34f31
Show file tree
Hide file tree
Showing 18 changed files with 1,104 additions and 599 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@ckeditor/ckeditor5-paragraph": "^0.8.0",
"@ckeditor/ckeditor5-typing": "^0.9.1",
"@ckeditor/ckeditor5-undo": "^0.8.1",
"@ckeditor/ckeditor5-widget": "^0.1.1",
"eslint-config-ckeditor5": "^1.0.5",
"gulp": "^3.9.1",
"guppy-pre-commit": "^0.4.0"
Expand Down
2 changes: 1 addition & 1 deletion src/controller/datacontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export default class DataController {
* {@link module:engine/view/documentfragment~DocumentFragment view document fragment} converted by the
* {@link #viewToModel view to model converters}.
*
* When marker stamps were converted during conversion process then will be set as DocumentFragment's
* When marker elements were converted during conversion process then will be set as DocumentFragment's
* {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
*
* @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
Expand Down
171 changes: 103 additions & 68 deletions src/conversion/buildmodelconverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
removeUIElement,
wrapItem,
unwrapItem,
wrapRange,
unwrapRange
convertTextsInsideMarker,
convertElementsInsideMarker
} from './model-to-view-converters';

import { convertSelectionAttribute, convertSelectionMarker } from './model-selection-to-view-converters';
Expand All @@ -37,7 +37,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
* {@link module:engine/conversion/model-to-view-converters}, {@link module:engine/conversion/modelconsumable~ModelConsumable},
* {@link module:engine/conversion/mapper~Mapper}.
*
* Using this API it is possible to create four kinds of converters:
* Using this API it is possible to create five kinds of converters:
*
* 1. Model element to view element converter. This is a converter that takes the model element and represents it
* in the view.
Expand All @@ -58,17 +58,25 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
*
* buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
*
* 4. Model marker to view element converter. This is a converter that converts markers from given group to view attribute element.
* Markers, basically, are {@link module:engine/model/liverange~LiveRange} instances, that are named. In this conversion, model range is
* converted to view range, then that view range is wrapped (or unwrapped, if range is removed) in a view attribute element.
* To learn more about markers, see {@link module:engine/model/markercollection~MarkerCollection}.
* 4. Model marker to virtual selection converter. This is a converter that converts model markers to virtual
* selection described by {@link module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor} object passed to
* {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toVirtualSelection} method.
*
* const viewSpanSearchResult = new ViewAttributeElement( 'span', { class: 'search-result' } );
* buildModelConverter().for( dispatcher ).fromMarker( 'searchResult' ).toElement( viewSpanSearchResult );
* buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toVirtualSelection( {
* class: 'search',
* priority: 20
* } );
*
* 5. Model marker to element converter. This is a converter that takes model marker and creates separate elements at
* the beginning and at the end of the marker's range. For more information see
* {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement} method.
*
* buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
*
* It is possible to provide various different parameters for
* {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement}
* and {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toAttribute} methods.
* {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement},
* {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toAttribute} and
* {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toVirtualSelection} methods.
* See their descriptions to learn more.
*
* It is also possible to {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority change default priority}
Expand Down Expand Up @@ -198,7 +206,13 @@ class ModelConverterBuilder {
* `string`, view element instance which will be cloned and used, or creator function which returns view element that
* will be used. Keep in mind that when you view element instance or creator function, it has to be/return a
* proper type of view element: {@link module:engine/view/containerelement~ContainerElement ViewContainerElement} if you convert
* from element or {@link module:engine/view/attributeelement~AttributeElement ViewAttributeElement} if you convert from attribute.
* from element, {@link module:engine/view/attributeelement~AttributeElement ViewAttributeElement} if you convert
* from attribute and {@link module:engine/view/uielement~UIElement ViewUIElement} if you convert from marker.
*
* NOTE: When converting from model's marker, separate elements will be created at the beginning and at the end of the
* marker's range. If range is collapsed then only one element will be created. See how markers
* {module:engine/model/buildviewconverter~ViewConverterBuilder#toMarker view -> model serialization}
* works to find out what view element format is the best for you.
*
* buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
*
Expand All @@ -210,12 +224,17 @@ class ModelConverterBuilder {
*
* buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( new ViewAttributeElement( 'strong' ) );
*
* buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
*
* buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( new ViewUIElement( 'span' ) );
*
* Creator function will be passed different values depending whether conversion is from element or from attribute:
*
* * from element: dispatcher's
* {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event}
* parameters will be passed,
* * from attribute: there is one parameter and it is attribute value.
* * from attribute: there is one parameter and it is attribute value,
* * from marker: {@link module:engine/conversion/buildmodelconverter~MarkerViewElementCreatorData}.
*
* This method also registers model selection to view selection converter, if conversion is from attribute.
*
Expand Down Expand Up @@ -243,77 +262,78 @@ class ModelConverterBuilder {
dispatcher.on( 'removeAttribute:' + this._from.key, unwrapItem( element ), { priority } );

dispatcher.on( 'selectionAttribute:' + this._from.key, convertSelectionAttribute( element ), { priority } );
} else {
element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
} else { // From marker to element.
const priority = this._from.priority === null ? 'normal' : this._from.priority;

dispatcher.on( 'addMarker:' + this._from.name, wrapRange( element ), { priority } );
dispatcher.on( 'removeMarker:' + this._from.name, unwrapRange( element ), { priority } );
element = typeof element == 'string' ? new ViewUIElement( element ) : element;

dispatcher.on( 'selectionMarker:' + this._from.name, convertSelectionMarker( element ), { priority } );
dispatcher.on( 'addMarker:' + this._from.name, insertUIElement( element ), { priority } );
dispatcher.on( 'removeMarker:' + this._from.name, removeUIElement( element ), { priority } );
}
}
}

/**
* Registers what view stamp will be created by converter to mark marker range bounds. Separate elements will be
* created at the beginning and at the end of the range. If range is collapsed then only one element will be created.
* Registers that marker should be converted to virtual selection. Markers, basically,
* are {@link module:engine/model/liverange~LiveRange} instances, that are named. Virtual selection is
* a representation of the model marker in the view:
* * each {@link module:engine/view/text~Text view text node} in the marker's range will be wrapped with `span`
* {@link module:engine/view/attributeelement~AttributeElement},
* * each {@link module:engine/view/containerelement~ContainerElement container view element} in the marker's
* range can handle the virtual selection individually by providing `setVirtualSelection` and `removeVirtualSelection`
* custom properties:
*
* Method accepts various ways of providing how the view element will be created. You can pass view element name as
* `string`, view element instance which will be cloned and used, or creator function which returns view element that
* will be used. Keep in mind that when you provide view element instance or creator function, it has to be/return a
* proper type of view element: {@link module:engine/view/uielement~UIElement UIElement}.
* viewElement.setCustomProperty( 'setVirtualSelection', ( element, descriptor ) => {} );
* viewElement.setCustomProperty( 'removeVirtualSelection', ( element, descriptor ) => {} );
*
* buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toStamp( 'span' );
* {@link module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor Descriptor} will be used to create
* spans over text nodes and also will be provided to `setVirtualSelection` and `removeVirtualSelection` methods
* each time virtual selection should be set or removed from view elements.
* NOTE: When `setVirtualSelection` and `removeVirtualSelection` custom properties are present, converter assumes
* that element itself is taking care of presenting virtual selection on its child nodes, so it won't convert virtual
* selection on them.
*
* buildModelConverter().for( dispatcher )
* .fromMarker( 'search' )
* .toStamp( new UIElement( 'span', { 'data-name': 'search' } ) );
* Virtual selection descriptor can be provided as plain object:
*
* buildModelConverter().for( dispatcher )
* .fromMarker( 'search' )
* .toStamp( ( data ) => new UIElement( 'span', { 'data-name': data.name ) );
*
* Creator function provides additional `data.isOpening` parameter which defined if currently converted element is
* a beginning or end of the marker range. This makes possible to create different opening and closing stamp.
*
* buildModelConverter().for( dispatcher )
* .fromMarker( 'search' )
* .toStamp( ( data ) => {
* if ( data.isOpening ) {
* return new UIElement( 'span', { 'data-name': data.name, 'data-start': true ) );
* }
* buildModelConverter.for( dispatcher ).fromMarker( 'search' ).toVirtualSelection( { class: 'search-mark' } );
*
* Also, descriptor creator function can be provided:
*
* return new UIElement( 'span', { 'data-name': data.name, 'data-end': true ) );
* }
* buildModelConverter.for( dispatcher ).fromMarker( 'search:blue' ).toVirtualSelection( data => {
* const color = data.markerName.split( ':' )[ 1 ];
*
* Creator function provides
* {@link module:engine/conversion/buildmodelconverter~StampCreatorData} parameters.
* return { class: 'search-' + color };
* } );
*
* See how markers {module:engine/model/buildviewconverter~ViewConverterBuilder#toMarker view -> model serialization}
* works to find out what view element format is the best for you.
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
* `build-model-converter-non-marker-to-virtual-selection` when trying to convert not from marker.
*
* @param {String|module:engine/view/uielement~UIElement|Function} element UIElement created by converter or
* a function that returns view element.
* @param {function|module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor} selectionDescriptor
*/
toStamp( element ) {
for ( const dispatcher of this._dispatchers ) {
if ( this._from.type != 'marker' ) {
/**
* To-stamp conversion is supported only for model markers.
*
* @error build-model-converter-element-to-stamp
*/
throw new CKEditorError(
'build-model-converter-non-marker-to-stamp: To-stamp conversion is supported only from model markers.'
);
}
toVirtualSelection( selectionDescriptor ) {
const priority = this._from.priority === null ? 'normal' : this._from.priority;

const priority = this._from.priority === null ? 'normal' : this._from.priority;
if ( this._from.type != 'marker' ) {
/**
* To virtual selection conversion is supported only for model markers.
*
* @error build-model-converter-non-marker-to-virtual-selection
*/
throw new CKEditorError(
'build-model-converter-non-marker-to-virtual-selection: Conversion to virtual selection is supported ' +
'only from model markers.'
);
}

for ( const dispatcher of this._dispatchers ) {
// Separate converters for converting texts and elements inside marker's range.
dispatcher.on( 'addMarker:' + this._from.name, convertTextsInsideMarker( selectionDescriptor ), { priority } );
dispatcher.on( 'addMarker:' + this._from.name, convertElementsInsideMarker( selectionDescriptor ), { priority } );

element = typeof element == 'string' ? new ViewUIElement( element ) : element;
dispatcher.on( 'removeMarker:' + this._from.name, convertTextsInsideMarker( selectionDescriptor ), { priority } );
dispatcher.on( 'removeMarker:' + this._from.name, convertElementsInsideMarker( selectionDescriptor ), { priority } );

dispatcher.on( 'addMarker:' + this._from.name, insertUIElement( element ), { priority } );
dispatcher.on( 'removeMarker:' + this._from.name, removeUIElement( element ), { priority } );
dispatcher.on( 'selectionMarker:' + this._from.name, convertSelectionMarker( selectionDescriptor ), { priority } );
}
}

Expand Down Expand Up @@ -404,11 +424,26 @@ export default function buildModelConverter() {
}

/**
* @typedef StampCreatorData
* @typedef MarkerViewElementCreatorData
* @param {Object} data Additional information about the change.
* @param {String} data.name Marker name.
* @param {module:engine/model/range~Range} data.range Marker range.
* @param {String} data.markerName Marker name.
* @param {module:engine/model/range~Range} data.markerRange Marker range.
* @param {Boolean} data.isOpening Defines if currently converted element is a beginning or end of the marker range.
* @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
* @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
*/

/**
* @typedef VirtualSelectionDescriptor
* Object describing how virtual selection should be created in the view. Each text node in virtual selection
* will be wrapped with `span` element with CSS class, attributes and priority described by this object. Each element
* can handle virtual selection separately by providing `setVirtualSelection` and `removeVirtualSelection` custom
* properties.
*
* @property {String} class CSS class that will be added to `span`
* {@link module:engine/view/attributeelement~AttributeElement} wrapping each text node in the virtual selection.
* @property {Number} [priority] {@link module:engine/view/attributeelement~AttributeElement#priority} of the `span`
* wrapping each text node in the virtual selection. If not provided, default 10 priority will be used.
* @property {Object} [attributes] Attributes that will be added to `span`
* {@link module:engine/view/attributeelement~AttributeElement} wrapping each text node it the virtual selection.
*/
28 changes: 11 additions & 17 deletions src/conversion/model-selection-to-view-converters.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import ViewElement from '../view/element';
import ViewRange from '../view/range';
import viewWriter from '../view/writer';
import { virtualSelectionDescriptorToAttributeElement } from './model-to-view-converters';

/**
* Contains {@link module:engine/model/selection~Selection model selection} to
Expand Down Expand Up @@ -112,11 +113,6 @@ export function convertCollapsedSelection() {
* }
* }
* modelDispatcher.on( 'selectionAttribute:style', convertSelectionAttribute( styleCreator ) );
*
* **Note:** You can use the same `elementCreator` function for this converter factory
* and {@link module:engine/conversion/model-to-view-converters~wrapRange}
* model to view converter, as long as the `elementCreator` function uses only the first parameter (attribute value).
*
* modelDispatcher.on( 'selection', convertCollapsedSelection() );
* modelDispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
* modelDispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'strong' ) ) );
Expand Down Expand Up @@ -163,27 +159,25 @@ export function convertSelectionAttribute( elementCreator ) {
* Performs similar conversion as {@link ~convertSelectionAttribute}, but depends on a marker name of a marker in which
* collapsed selection is placed.
*
* modelDispatcher.on( 'selectionMarker:searchResult', wrapRange( new ViewAttributeElement( 'span', { class: 'searchResult' } ) ) );
*
* **Note:** You can use the same `elementCreator` function for this converter factory
* and {@link module:engine/conversion/model-to-view-converters~wrapRange}.
* modelDispatcher.on( 'selectionMarker:searchResult', convertSelectionMarker( { class: 'search' } ) );
*
* @see module:engine/conversion/model-selection-to-view-converters~convertSelectionAttribute
* @param {module:engine/view/attributeelement~AttributeElement|Function} elementCreator View element,
* or function returning a view element, which will be used for wrapping.
* @param {module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor|Function} selectionDescriptor Virtual
* selection descriptor object or function returning a descriptor object.
* @returns {Function} Selection converter.
*/
export function convertSelectionMarker( elementCreator ) {
export function convertSelectionMarker( selectionDescriptor ) {
return ( evt, data, consumable, conversionApi ) => {
const viewElement = elementCreator instanceof ViewElement ?
elementCreator.clone( true ) :
elementCreator( data, consumable, conversionApi );
const descriptor = typeof selectionDescriptor == 'function' ?
selectionDescriptor( data, consumable, conversionApi ) :
selectionDescriptor;

if ( !viewElement ) {
if ( !descriptor ) {
return;
}

const consumableName = 'selectionMarker:' + data.name;
const viewElement = virtualSelectionDescriptorToAttributeElement( descriptor );
const consumableName = 'selectionMarker:' + data.markerName;

wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName );
};
Expand Down
Loading

0 comments on commit af34f31

Please sign in to comment.