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

Introduced markers serialization #845

Merged
merged 45 commits into from
Mar 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
6b43114
Improved insert and remove UIElement to handle collapsed and non-coll…
oskarwrobel Feb 21, 2017
c0c9632
Added test for wrap / unwrap collpased range.
oskarwrobel Feb 21, 2017
8f11347
Introduced model marker to view stapm conversion.
oskarwrobel Feb 21, 2017
2832c0b
Improved docs.
oskarwrobel Feb 23, 2017
345b9e6
Added toMarker view -> model converter.
oskarwrobel Feb 23, 2017
7fc0dfe
Added parsing for temporary $marker elements.
oskarwrobel Feb 26, 2017
4007a74
Added docs.
oskarwrobel Feb 26, 2017
0b53657
Changed path of writer import to relative.
oskarwrobel Feb 26, 2017
2b7fb41
Changed attribute `marker-name` to `data-name`.
oskarwrobel Feb 26, 2017
5ff7e1c
Improved docs.
oskarwrobel Feb 26, 2017
ca1cdd5
Increased cc to 100%.
oskarwrobel Feb 26, 2017
d77d177
Renamed helper function.
oskarwrobel Feb 26, 2017
3ce2d1f
Simplified removing temporary markers elements from document fragment.
oskarwrobel Feb 27, 2017
9bb8453
Fixed docs.
oskarwrobel Feb 27, 2017
369e8db
Simplified adding marker to the document.
oskarwrobel Feb 27, 2017
5088e71
Replaced reduce by iterator.
oskarwrobel Feb 28, 2017
59bc054
Moved extracting of marker stamps to ViewConversiondispatcher.
oskarwrobel Feb 28, 2017
d92d0ce
Aligned engine files to latest ViewConversionDispatcher#convert() API.
oskarwrobel Feb 28, 2017
d2ef220
Improved docs.
oskarwrobel Feb 28, 2017
ad66a93
Added Markers Collection to DocumentFragment.
oskarwrobel Mar 2, 2017
b7132ca
Changed format of static markers on DocumentFragment.
oskarwrobel Mar 3, 2017
9ed723b
Improved writer to transfer markers from DocumentFragment to Document.
oskarwrobel Mar 3, 2017
d74c2f5
Changed format of returned by a converter markers.
oskarwrobel Mar 3, 2017
9ca9fd1
Changed format of data returned by DataController.
oskarwrobel Mar 3, 2017
179d5a6
Improved docs.
oskarwrobel Mar 3, 2017
7fe28b6
Changed failing circular import.
oskarwrobel Mar 3, 2017
1083da0
Adjusted test to new data format.
oskarwrobel Mar 3, 2017
3de977d
Removed default confitg option.
oskarwrobel Mar 3, 2017
704225d
Changed anonymous function to named.
oskarwrobel Mar 3, 2017
76dca9f
Improved docs and code style.
oskarwrobel Mar 3, 2017
f833850
Simplified creationg position.
oskarwrobel Mar 3, 2017
b19e9f1
Docs.
oskarwrobel Mar 3, 2017
3ab1002
Docs.
oskarwrobel Mar 3, 2017
e4963aa
Added option to create different opening and closing marker stamp.
oskarwrobel Mar 3, 2017
04e6ebf
Added warning about losing markers.
oskarwrobel Mar 6, 2017
a9e0311
Improved condition checking type of conversion.
oskarwrobel Mar 6, 2017
93db8f0
Moved isOpening parameter to data object.
oskarwrobel Mar 6, 2017
70ba353
Fixed transfering mrkers to work for documentFragment -> documentFrag…
oskarwrobel Mar 6, 2017
8437adc
Improved tests of transfering markers.
oskarwrobel Mar 6, 2017
aa0daed
Changed format of view -> model converter result.
oskarwrobel Mar 6, 2017
9b268df
Aligned DataController to new format of view -> model conversion result.
oskarwrobel Mar 6, 2017
ce35560
Improved output returned by the converter.
oskarwrobel Mar 6, 2017
e7b6c3d
Aligned tests to new data format returned by the view -> model conver…
oskarwrobel Mar 6, 2017
f9abd15
Improved docs.
oskarwrobel Mar 7, 2017
e72ab66
Merge branch 'master' into t/787
oskarwrobel Mar 7, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/controller/datacontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,13 @@ export default class DataController {
}

/**
* Returns the content of the given {@link module:engine/view/element~Element view element} or
* Returns wrapped by {module:engine/model/documentfragment~DocumentFragment} result of the given
* {@link module:engine/view/element~Element view element} or
* {@link module:engine/view/documentfragment~DocumentFragment view document fragment} converted by the
* {@link #viewToModel view to model converters} to a
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
* {@link #viewToModel view to model converters}.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could add a note to toModel and viewToModel.convert that the document fragment may contain markers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

*
* When marker stamps were converted during conversion process then will be set as DocumentFragment
* {@link module:engine/view/documentfragment~DocumentFragment#markers static markers map}.
*
* @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
* Element or document fragment which content will be converted.
Expand Down Expand Up @@ -281,7 +284,7 @@ export default class DataController {
* See {@link module:engine/controller/modifyselection~modifySelection}.
*
* @fires modifySelection
* @param {module:engine/model/selection~Selection} The selection to modify.
* @param {module:engine/model/selection~Selection} selection The selection to modify.
* @param {Object} options See {@link module:engine/controller/modifyselection~modifySelection}'s options.
*/
modifySelection( selection, options ) {
Expand Down
114 changes: 78 additions & 36 deletions src/conversion/buildmodelconverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,7 @@ class ModelConverterBuilder {
}

/**
* Registers what type of non-collapsed marker should be converted. For collapsed markers conversion, see
* {@link #fromCollapsedMarker}.
* Registers what type of marker should be converted.
*
* @chainable
* @param {String} markerName Name of marker to convert.
Expand All @@ -163,27 +162,7 @@ class ModelConverterBuilder {
this._from = {
type: 'marker',
name: markerName,
priority: null,
collapsed: false
};

return this;
}

/**
* Registers what type of collapsed marker should be converted. For non-collapsed markers conversion,
* see {@link #fromMarker}.
*
* @chainable
* @param {String} markerName Name of marker to convert.
* @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
*/
fromCollapsedMarker( markerName ) {
this._from = {
type: 'marker',
name: markerName,
priority: null,
collapsed: true
priority: null
};

return this;
Expand Down Expand Up @@ -265,22 +244,76 @@ class ModelConverterBuilder {

dispatcher.on( 'selectionAttribute:' + this._from.key, convertSelectionAttribute( element ), { priority } );
} else {
if ( this._from.collapsed ) {
// From collapsed marker to view element -> insertUIElement, removeUIElement.
element = typeof element == 'string' ? new ViewUIElement( element ) : element;
element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;

dispatcher.on( 'addMarker:' + this._from.name, insertUIElement( element ), { priority } );
dispatcher.on( 'removeMarker:' + this._from.name, removeUIElement( element ), { priority } );
} else {
// From non-collapsed marker to view element -> wrapRange and unwrapRange.
element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
dispatcher.on( 'addMarker:' + this._from.name, wrapRange( element ), { priority } );
dispatcher.on( 'removeMarker:' + this._from.name, unwrapRange( element ), { priority } );

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

dispatcher.on( 'selectionMarker:' + this._from.name, convertSelectionMarker( 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.
*
* 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}.
*
* buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toStamp( 'span' );
*
* buildModelConverter().for( dispatcher )
* .fromMarker( 'search' )
* .toStamp( new UIElement( 'span', { 'data-name': 'search' } ) );
*
* buildModelConverter().for( dispatcher )
* .fromMarker( 'search' )
* .toStamp( ( data ) => new UIElement( 'span', { 'data-name': data.marker.getName() ) );
*
* 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.marker.getName(), 'data-start': true ) );
* }
*
* return new UIElement( 'span', { 'data-name': data.marker.getName(), 'data-end': true ) );
* }
*
* Creator function provides
* {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#StampCreatorData} parameters.
*
* 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.
*
* @param {String|module:engine/view/element~UIElement|Function} element UIElement created by converter or
* a function that returns view element.
*/
toStamp( element ) {
for ( let 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.'
);
}

const priority = this._from.priority === null ? 'normal' : this._from.priority;

element = typeof element == 'string' ? new ViewUIElement( element ) : element;

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

Expand Down Expand Up @@ -321,7 +354,6 @@ class ModelConverterBuilder {
* To-attribute conversion is supported only for model attributes.
*
* @error build-model-converter-element-to-attribute
* @param {module:engine/model/range~Range} range
*/
throw new CKEditorError( 'build-model-converter-non-attribute-to-attribute: ' +
'To-attribute conversion is supported only from model attributes.' );
Expand Down Expand Up @@ -370,3 +402,13 @@ class ModelConverterBuilder {
export default function buildModelConverter() {
return new ModelConverterBuilder();
}

/**
* @typedef {StampCreatorData} {module:engine/conversion/buildmodelconverter~ModelConverterBuilder#StampCreatorData}
* @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 {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.
*/
89 changes: 85 additions & 4 deletions src/conversion/buildviewconverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Matcher from '../view/matcher';
import ModelElement from '../model/element';
import ModelPosition from '../model/position';
import modelWriter from '../model/writer';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';

/**
Expand Down Expand Up @@ -254,7 +255,7 @@ class ViewConverterBuilder {
* @param {String|Function} element Model element name or model element creator function.
*/
toElement( element ) {
const eventCallbackGen = function( from ) {
function eventCallbackGen( from ) {
return ( evt, data, consumable, conversionApi ) => {
// There is one callback for all patterns in the matcher.
// This will be usually just one pattern but we support matchers with many patterns too.
Expand Down Expand Up @@ -301,7 +302,7 @@ class ViewConverterBuilder {
break;
}
};
};
}

this._setCallback( eventCallbackGen, 'normal' );
}
Expand All @@ -322,7 +323,7 @@ class ViewConverterBuilder {
* @param {String} [value] Attribute value. Required if `keyOrCreator` is a `string`. Ignored otherwise.
*/
toAttribute( keyOrCreator, value ) {
const eventCallbackGen = function( from ) {
function eventCallbackGen( from ) {
return ( evt, data, consumable, conversionApi ) => {
// There is one callback for all patterns in the matcher.
// This will be usually just one pattern but we support matchers with many patterns too.
Expand Down Expand Up @@ -356,11 +357,91 @@ class ViewConverterBuilder {
break;
}
};
};
}

this._setCallback( eventCallbackGen, 'low' );
}

/**
* Registers how model element marking marker range will be created by converter.
*
* Created element has to match the following pattern:
*
* { name: '$marker', attribute: { data-name: /^\w/ } }
*
* There are two ways of creating this element:
*
* 1. Makes sure that converted view element will have property `data-name` then converter will
* automatically take this property value. In this case there is no need to provide creator function.
* For the following view:
*
* <marker data-name="search"></marker>foo<marker data-name="search"></marker>
*
* converter should look like this:
*
* buildViewConverter().for( dispatcher ).fromElement( 'marker' ).toMarker();
*
* 2. Creates element by creator:
*
* For the following view:
*
* <span foo="search"></span>foo<span foo="search"></span>
*
* converter should look like this:
*
* buildViewConverter().for( dispatcher ).from( { name: 'span', { attribute: foo: /^\w/ } } ).toMarker( ( data ) => {
* return new Element( '$marker', { 'data-name': data.getAttribute( 'foo' ) } );
* } );
*
* @param {Function} [creator] Creator function.
*/
toMarker( creator ) {
function eventCallbackGen( from ) {
return ( evt, data, consumable ) => {
// There is one callback for all patterns in the matcher.
// This will be usually just one pattern but we support matchers with many patterns too.
const matchAll = from.matcher.matchAll( data.input );

// If there is no match, this callback should not do anything.
if ( !matchAll ) {
return;
}

let modelElement;

// When creator is provided then create model element basing on creator function.
if ( creator instanceof Function ) {
modelElement = creator( data.input );
// When there is no creator then create model element basing on data from view element.
} else {
modelElement = new ModelElement( '$marker', { 'data-name': data.input.getAttribute( 'data-name' ) } );
}

// Check if model element is correct (has proper name and property).
if ( modelElement.name != '$marker' || typeof modelElement.getAttribute( 'data-name' ) != 'string' ) {
throw new CKEditorError(
'build-view-converter-invalid-marker: Invalid model element to mark marker range.'
);
}

// Now, for every match between matcher and actual element, we will try to consume the match.
for ( const match of matchAll ) {
// Try to consume appropriate values from consumable values list.
if ( !consumable.consume( data.input, from.consume || match.match ) ) {
continue;
}

data.output = modelElement;

// Prevent multiple conversion if there are other correct matches.
break;
}
};
}

this._setCallback( eventCallbackGen, 'normal' );
}

/**
* Helper function that uses given callback generator to created callback function and sets it on registered dispatchers.
*
Expand Down
52 changes: 41 additions & 11 deletions src/conversion/model-to-view-converters.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export function insertText() {
/**
* Function factory, creates a converter that converts marker adding change to the view ui element.
* The view ui element that will be added to the view depends on passed parameter. See {@link ~insertElement}.
* In a case of collapsed range element will not wrap range but separate elements will be placed at the beginning
* and at the end of the range.
*
* **Note:** unlike {@link ~insertElement}, the converter does not bind view element to model, because this converter
* uses marker as "model source of data". This means that view ui element does not have corresponding model element.
Expand All @@ -101,21 +103,34 @@ export function insertText() {
*/
export function insertUIElement( elementCreator ) {
return ( evt, data, consumable, conversionApi ) => {
const viewElement = ( elementCreator instanceof ViewElement ) ?
elementCreator.clone( true ) :
elementCreator( data, consumable, conversionApi );
let viewStartElement, viewEndElement;

if ( !viewElement ) {
if ( elementCreator instanceof ViewElement ) {
viewStartElement = elementCreator.clone( true );
viewEndElement = elementCreator.clone( true );
} else {
data.isOpening = true;
viewStartElement = elementCreator( data, consumable, conversionApi );

data.isOpening = false;
viewEndElement = elementCreator( data, consumable, conversionApi );
}

if ( !viewStartElement || !viewEndElement ) {
return;
}

if ( !consumable.consume( data.range, 'addMarker' ) ) {
return;
}

const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
const mapper = conversionApi.mapper;

viewWriter.insert( viewPosition, viewElement );
viewWriter.insert( mapper.toViewPosition( data.range.start ), viewStartElement );

if ( !data.range.isCollapsed ) {
viewWriter.insert( mapper.toViewPosition( data.range.end ), viewEndElement );
}
};
}

Expand Down Expand Up @@ -439,11 +454,20 @@ export function remove() {
*/
export function removeUIElement( elementCreator ) {
return ( evt, data, consumable, conversionApi ) => {
const viewElement = ( elementCreator instanceof ViewElement ) ?
elementCreator.clone( true ) :
elementCreator( data, consumable, conversionApi );
let viewStartElement, viewEndElement;

if ( !viewElement ) {
if ( elementCreator instanceof ViewElement ) {
viewStartElement = elementCreator.clone( true );
viewEndElement = elementCreator.clone( true );
} else {
data.isOpening = true;
viewStartElement = elementCreator( data, consumable, conversionApi );

data.isOpening = false;
viewEndElement = elementCreator( data, consumable, conversionApi );
}

if ( !viewStartElement || !viewEndElement ) {
return;
}

Expand All @@ -453,7 +477,13 @@ export function removeUIElement( elementCreator ) {

const viewRange = conversionApi.mapper.toViewRange( data.range );

viewWriter.clear( viewRange.getEnlarged(), viewElement );
// First remove closing element.
viewWriter.clear( viewRange.getEnlarged(), viewEndElement );

// If closing and opening elements are not the same then remove opening element.
if ( !viewStartElement.isSimilar( viewEndElement ) ) {
viewWriter.clear( viewRange.getEnlarged(), viewStartElement );
}
};
}

Expand Down
Loading