Browse Source

Added parsing for temporary $marker elements.

Oskar Wróbel 8 years ago
parent
commit
26dc193a7d

+ 85 - 5
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -22,12 +22,15 @@ import ViewDocumentFragment from '../view/documentfragment';
 
 import ModelRange from '../model/range';
 import ModelPosition from '../model/position';
+import ModelTreeWalker from '../model/treewalker';
 
 import insertContent from './insertcontent';
 import deleteContent from './deletecontent';
 import modifySelection from './modifyselection';
 import getSelectedContent from './getselectedcontent';
 
+import { remove } from '@ckeditor/ckeditor5-engine/src/model/writer';
+
 /**
  * Controller for the data pipeline. The data pipeline controls how data is retrieved from the document
  * and set inside it. Hence, the controller features two methods which allow to {@link ~DataController#get get}
@@ -180,7 +183,10 @@ export default class DataController {
 
 	/**
 	 * Sets input data parsed by the {@link #processor data processor} and
-	 * converted by the {@link #viewToModel view to model converters}.
+	 * converted by the {@link #viewToModel view to model converters}. When markers where converted
+	 * from view to model as temporary {@link module:engine/model/element/~Element model elements} then those element
+	 * will be removed from parsed {@link module:engine/model/element/~DocumentFragment} and added to the
+	 * {@link module:engine/model/document~Document#markers markers collection}.
 	 *
 	 * This method also creates a batch with all the changes applied. If all you need is to parse data use
 	 * the {@link #parse} method.
@@ -198,10 +204,28 @@ export default class DataController {
 			this.model.selection.removeAllRanges();
 			this.model.selection.clearAttributes();
 
+			// Parse data to model and extract markers from parsed document fragment.
+			const { documentFragment, markersData } = extractMarkersDataFromModelElement( this.parse( data ) );
+
 			// Initial batch should be ignored by features like undo, etc.
-			this.model.batch( 'transparent' )
+			const batch = this.model.batch( 'transparent' );
+
+			// Replace current editor data by the new one.
+			batch
 				.remove( ModelRange.createIn( modelRoot ) )
-				.insert( ModelPosition.createAt( modelRoot, 0 ), this.parse( data ) );
+				.insert( ModelPosition.createAt( modelRoot, 0 ), documentFragment );
+
+			// Add markers to the document.
+			for ( const marker of markersData ) {
+				const markerName = marker[ 0 ];
+				const markerData = marker[ 1 ];
+				const range = new ModelRange(
+					new ModelPosition( modelRoot, markerData.startPath ),
+					markerData.endPath ? new ModelPosition( modelRoot, markerData.endPath ) : null
+				);
+
+				batch.setMarker( this.model.markers.set( markerName, range ) );
+			}
 		} );
 	}
 
@@ -247,6 +271,10 @@ export default class DataController {
 	/**
 	 * See {@link module:engine/controller/insertcontent~insertContent}.
 	 *
+	 * Note that data inserted by a data pipeline might contain temporary {@link module:engine/model/element/~Element elements}
+	 * marking {@link module:engine/model/document~Document#markers markers} ranges. We need to remove them because
+	 * data pipeline allows to set markers only by a {@link #set set method}.
+	 *
 	 * @fires insertContent
 	 * @param {module:engine/model/documentfragment~DocumentFragment} content The content to insert.
 	 * @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted.
@@ -254,7 +282,8 @@ export default class DataController {
 	 * changes will be added to a new batch.
 	 */
 	insertContent( content, selection, batch ) {
-		this.fire( 'insertContent', { content, selection, batch } );
+		const { documentFragment } = extractMarkersDataFromModelElement( content );
+		this.fire( 'insertContent', { content: documentFragment, selection, batch } );
 	}
 
 	/**
@@ -281,7 +310,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 ) {
@@ -306,6 +335,57 @@ export default class DataController {
 
 mix( DataController, EmitterMixin );
 
+// Traverses given DocumentFragment and searches elements which marks marker range. Founded element is removed from
+// DocumentFragment but path of this element is stored in Map.
+//
+// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Model DocumentFragment.
+// @returns {Object} Object with markers data and cleaned up document fragment.
+function extractMarkersDataFromModelElement( documentFragment ) {
+	const markersData = new Map();
+
+	// Creates ModelTreeWalker with given start position.
+	function walkFrom( position ) {
+		const walker = new ModelTreeWalker( {
+			startPosition: position,
+			ignoreElementEnd: true,
+			shallow: false
+		} );
+
+		// Walk through DocumentFragment.
+		for ( const value of walker ) {
+			// Check if current element is a marker stamp.
+			if ( value.item.name == '$marker' ) {
+				const markerName = value.item.getAttribute( 'marker-name' );
+				const currentPosition = ModelPosition.createBefore( value.item );
+
+				// When marker of given name is not stored it means that we have found the beginning of the range.
+				if ( !markersData.has( markerName ) ) {
+					markersData.set( markerName, { startPath: currentPosition.path } );
+				// Otherwise is means that we have found end of the marker range.
+				} else {
+					markersData.get( markerName ).endPath = currentPosition.path;
+				}
+
+				// Remove marker stamp element from DocumentFragment.
+				remove( ModelRange.createOn( value.item ) );
+
+				// Keep walking using new instance of TreeWalker but starting from last visited position.
+				// This is because after removing marker stamp element DocumentFragment structure might change
+				// and TreeWalker might omit some node.
+				walkFrom( currentPosition );
+
+				// Stop this walker, we have continued walk using new TreeWalker instance.
+				break;
+			}
+		}
+	}
+
+	// Start traversing.
+	walkFrom( ModelPosition.createAt( documentFragment, 0 ) );
+
+	return { markersData, documentFragment };
+}
+
 /**
  * Event fired when {@link #insertContent} method is called.
  * The {@link #insertContent default action of that method} is implemented as a

+ 65 - 0
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -14,6 +14,7 @@ import ModelDocumentFragment from '../../src/model/documentfragment';
 import ModelElement from '../../src/model/element';
 import ModelText from '../../src/model/text';
 import ModelSelection from '../../src/model/selection';
+import ModelRange from '../../src/model/range';
 
 import ViewDocumentFragment from '../../src/view/documentfragment';
 
@@ -222,6 +223,54 @@ describe( 'DataController', () => {
 			expect( getData( modelDocument, { withoutSelection: true } ) ).to.equal( 'foo' );
 		} );
 
+		it( 'should extract markers stamps from converted data and set to `modelDocument#markers` collection', () => {
+			modelDocument.schema.registerItem( 'paragraph', '$block' );
+			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
+			buildViewConverter().for( data.viewToModel ).fromElement( 'm' ).toMarker();
+
+			data.set(
+				'<p>' +
+					'F' +
+					'<m marker-name="comment"></m>' +
+					'o' +
+					'<m marker-name="search"></m>' +
+					'o ba' +
+					'<m marker-name="comment"></m>' +
+					'r bi' +
+					'<m marker-name="search"></m>' +
+					'z' +
+				'</p>'
+			);
+
+			expect( getData( modelDocument, { withoutSelection: true } ) ).to.equal( '<paragraph>Foo bar biz</paragraph>' );
+			expect( Array.from( modelDocument.markers ).length ).to.equal( 2 );
+
+			const paragraph = modelDocument.getRoot().getChild( 0 );
+			const commentMarkerRange = ModelRange.createFromParentsAndOffsets( paragraph, 1, paragraph, 6 );
+			const searchMarkerRange = ModelRange.createFromParentsAndOffsets( paragraph, 2, paragraph, 10 );
+
+			expect( modelDocument.markers.get( 'comment' ).getRange().isEqual( commentMarkerRange ) ).to.true;
+			expect( modelDocument.markers.get( 'search' ).getRange().isEqual( searchMarkerRange ) ).to.true;
+		} );
+
+		it( 'should extract collapsed markers stamps from converted data and set to `modelDocument#markers` collection', () => {
+			modelDocument.schema.registerItem( 'paragraph', '$block' );
+			buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' );
+			buildViewConverter().for( data.viewToModel ).fromElement( 'm' ).toMarker();
+
+			data.set( '<p>F<m marker-name="comment"></m>o<m marker-name="search"></m>o ba</m>r biz</p>' );
+
+			expect( getData( modelDocument, { withoutSelection: true } ) ).to.equal( '<paragraph>Foo bar biz</paragraph>' );
+			expect( Array.from( modelDocument.markers ).length ).to.equal( 2 );
+
+			const paragraph = modelDocument.getRoot().getChild( 0 );
+			const commentMarkerRange = ModelRange.createFromParentsAndOffsets( paragraph, 1, paragraph, 1 );
+			const searchMarkerRange = ModelRange.createFromParentsAndOffsets( paragraph, 2, paragraph, 2 );
+
+			expect( modelDocument.markers.get( 'comment' ).getRange().isEqual( commentMarkerRange ) ).to.true;
+			expect( modelDocument.markers.get( 'search' ).getRange().isEqual( searchMarkerRange ) ).to.true;
+		} );
+
 		it( 'should create a batch', () => {
 			schema.allow( { name: '$text', inside: '$root' } );
 			data.set( 'foo' );
@@ -427,6 +476,22 @@ describe( 'DataController', () => {
 				content: content
 			} );
 		} );
+
+		it( 'should remove markers stamps from content', () => {
+			const spy = sinon.spy();
+			const content = new ModelDocumentFragment( [
+				new ModelText( 'x' ),
+				new ModelElement( '$marker', { 'marker-name': 'search' } ),
+				new ModelText( 'y' ),
+				new ModelElement( '$marker', { 'marker-name': 'search' } ),
+				new ModelText( 'z' )
+			] );
+
+			data.on( 'insertContent', spy );
+			data.insertContent( content, modelDocument.selection );
+
+			expect( stringify( spy.args[ 0 ][ 1 ].content ) ).to.equal( 'xyz' );
+		} );
 	} );
 
 	describe( 'deleteContent', () => {