Selaa lähdekoodia

Added: converters for Selection markers.

Szymon Cofalik 9 vuotta sitten
vanhempi
commit
e32792f55f

+ 3 - 1
packages/ckeditor5-engine/src/conversion/buildmodelconverter.js

@@ -17,7 +17,7 @@ import {
 	unwrapRange
 } from './model-to-view-converters';
 
-import { convertSelectionAttribute } from './model-selection-to-view-converters';
+import { convertSelectionAttribute, convertSelectionMarker } from './model-selection-to-view-converters';
 
 import ViewAttributeElement from '../view/attributeelement';
 import ViewContainerElement from '../view/containerelement';
@@ -246,6 +246,8 @@ class ModelConverterBuilder {
 
 				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 } );
 			}
 		}
 	}

+ 42 - 15
packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js

@@ -145,27 +145,54 @@ export function convertCollapsedSelection() {
  */
 export function convertSelectionAttribute( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
-		const selection = data.selection;
+		const viewElement = elementCreator instanceof ViewElement ?
+			elementCreator.clone( true ) :
+			elementCreator( data.value, data, data.selection, consumable, conversionApi );
 
-		if ( !selection.isCollapsed ) {
-			return;
-		}
+		convertSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, 'selectionAttribute:' + data.key );
+	};
+}
 
-		if ( !consumable.consume( selection, 'selectionAttribute:' + data.key ) ) {
-			return;
-		}
+/**
+ * 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}.
+ *
+ * @see {~convertSelectionAttribute}
+ * @param {module:engine/view/attributeelement~AttributeElement|Function} elementCreator View element,
+ * or function returning a view element, which will be used for wrapping.
+ * @returns {Function} Selection converter.
+ */
+export function convertSelectionMarker( elementCreator ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		const viewElement = elementCreator instanceof ViewElement ?
+			elementCreator.clone( true ) :
+			elementCreator( data, consumable, conversionApi );
 
-		let viewPosition = conversionApi.viewSelection.getFirstPosition();
-		conversionApi.viewSelection.removeAllRanges();
+		convertSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, 'selectionMarker:' + data.name );
+	};
+}
 
-		const viewElement = elementCreator instanceof ViewElement ?
-				elementCreator.clone( true ) :
-				elementCreator( data.value, data, selection, consumable, conversionApi );
+// Helper function for `convertSelectionAttribute` and `convertSelectionMarker`, which perform similar task.
+function convertSelectionPosition( modelSelection, viewSelection, viewElement, consumable, consumableName ) {
+	if ( !modelSelection.isCollapsed ) {
+		return;
+	}
 
-		viewPosition = viewWriter.wrapPosition( viewPosition, viewElement );
+	if ( !consumable.consume( modelSelection, consumableName ) ) {
+		return;
+	}
 
-		conversionApi.viewSelection.addRange( new ViewRange( viewPosition, viewPosition ) );
-	};
+	let viewPosition = viewSelection.getFirstPosition();
+	viewSelection.removeAllRanges();
+
+	viewPosition = viewWriter.wrapPosition( viewPosition, viewElement );
+
+	viewSelection.addRange( new ViewRange( viewPosition, viewPosition ) );
 }
 
 /**

+ 21 - 6
packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js

@@ -311,15 +311,26 @@ export default class ModelConversionDispatcher {
 	 */
 	convertSelection( selection ) {
 		const consumable = this._createSelectionConsumable( selection );
-		const data = {
-			selection: selection
-		};
 
-		this.fire( 'selection', data, consumable, this.conversionApi );
+		this.fire( 'selection', { selection }, consumable, this.conversionApi );
+
+		for ( let markerName of selection.getMarkers() ) {
+			const data = {
+				selection: selection,
+				name: markerName
+			};
+
+			if ( consumable.test( selection, 'selectionMarker:' + markerName ) ) {
+				this.fire( 'selectionMarker:' + markerName, data, consumable, this.conversionApi );
+			}
+		}
 
 		for ( let key of selection.getAttributeKeys() ) {
-			data.key = key;
-			data.value = selection.getAttribute( key );
+			const data = {
+				selection: selection,
+				key: key,
+				value: selection.getAttribute( key )
+			};
 
 			// Do not fire event if the attribute has been consumed.
 			if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) {
@@ -404,6 +415,10 @@ export default class ModelConversionDispatcher {
 
 		consumable.add( selection, 'selection' );
 
+		for ( let markerName of selection.getMarkers() ) {
+			consumable.add( selection, 'selectionMarker:' + markerName );
+		}
+
 		for ( let key of selection.getAttributeKeys() ) {
 			consumable.add( selection, 'selectionAttribute:' + key );
 		}

+ 85 - 5
packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js

@@ -19,6 +19,7 @@ import {
 	convertRangeSelection,
 	convertCollapsedSelection,
 	convertSelectionAttribute,
+	convertSelectionMarker,
 	clearAttributes,
 	clearFakeSelection
 } from '../../src/conversion/model-selection-to-view-converters';
@@ -26,12 +27,17 @@ import {
 import {
 	insertElement,
 	insertText,
-	wrapItem
+	wrapItem,
+	wrapRange,
+	unwrapRange
 } from '../../src/conversion/model-to-view-converters';
 
 import { stringify as stringifyView } from '../../src/dev-utils/view';
 import { setData as setModelData } from '../../src/dev-utils/model';
 
+const spanViewElementForMarker = new ViewAttributeElement( 'span' );
+spanViewElementForMarker.priority = 1;
+
 describe( 'model-selection-to-view-converters', () => {
 	let dispatcher, mapper;
 	let modelDoc, modelRoot, modelSelection;
@@ -55,6 +61,8 @@ describe( 'model-selection-to-view-converters', () => {
 
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
+		dispatcher.on( 'addMarker:marker', wrapRange( spanViewElementForMarker ) );
+		dispatcher.on( 'removeMarker:marker', unwrapRange( spanViewElementForMarker ) );
 
 		// Default selection converters.
 		dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
@@ -71,6 +79,7 @@ describe( 'model-selection-to-view-converters', () => {
 			// Selection converters for selection attributes.
 			dispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'strong' ) ) );
 			dispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
+			dispatcher.on( 'selectionMarker:marker', convertSelectionMarker( spanViewElementForMarker ) );
 		} );
 
 		describe( 'range selection', () => {
@@ -207,6 +216,75 @@ describe( 'model-selection-to-view-converters', () => {
 				);
 			} );
 
+			it( 'in attribute and marker', () => {
+				setModelData( modelDoc, 'fo<$text bold="true">ob</$text>ar' );
+				const marker = modelDoc.markers.set( 'marker', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
+
+				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
+
+				// Update selection attributes according to model.
+				modelSelection.refresh();
+
+				// Remove view children manually (without firing additional conversion).
+				viewRoot.removeChildren( 0, viewRoot.childCount );
+
+				// Convert model to view.
+				dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
+				dispatcher.convertMarker( 'addMarker', marker.name, marker.getRange() );
+				dispatcher.convertSelection( modelSelection );
+
+				// Stringify view and check if it is same as expected.
+				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
+					.to.equal( '<div>f<span>o<strong>o{}b</strong>a</span>r</div>' );
+			} );
+
+			it( 'in attribute and marker - no attribute', () => {
+				setModelData( modelDoc, 'fo<$text bold="true">ob</$text>ar' );
+				const marker = modelDoc.markers.set( 'marker', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
+
+				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
+
+				// Update selection attributes according to model.
+				modelSelection.refresh();
+
+				modelSelection.removeAttribute( 'bold' );
+
+				// Remove view children manually (without firing additional conversion).
+				viewRoot.removeChildren( 0, viewRoot.childCount );
+
+				// Convert model to view.
+				dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
+				dispatcher.convertMarker( 'addMarker', marker.name, marker.getRange() );
+				dispatcher.convertSelection( modelSelection );
+
+				// Stringify view and check if it is same as expected.
+				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
+					.to.equal( '<div>f<span>o<strong>o</strong>[]<strong>b</strong>a</span>r</div>' );
+			} );
+
+			it( 'in marker - using view element as element creator function for selection marker conversion', () => {
+				dispatcher.on( 'selectionMarker:marker2', convertSelectionMarker(
+					( data ) => new ViewAttributeElement( 'span', { 'class': data.name } )
+				) );
+
+				setModelData( modelDoc, 'foobar' );
+				const marker = modelDoc.markers.set( 'marker2', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
+
+				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
+
+				// Remove view children manually (without firing additional conversion).
+				viewRoot.removeChildren( 0, viewRoot.childCount );
+
+				// Convert model to view.
+				dispatcher.convertInsertion( ModelRange.createIn( modelRoot ) );
+				dispatcher.convertMarker( 'addMarker', marker.name, marker.getRange() );
+				dispatcher.convertSelection( modelSelection );
+
+				// Stringify view and check if it is same as expected.
+				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
+					.to.equal( '<div>foo<span class="marker2">[]</span>bar</div>' );
+			} );
+
 			it( 'consumes consumable values properly', () => {
 				// Add callbacks that will fire before default ones.
 				// This should prevent default callbacks doing anything.
@@ -218,7 +296,7 @@ describe( 'model-selection-to-view-converters', () => {
 					expect( consumable.consume( data.selection, 'selectionAttribute:bold' ) ).to.be.true;
 				}, { priority: 'high' } );
 
-				// Similar test case as above
+				// Similar test case as above.
 				test(
 					[ 3, 3 ],
 					'f<$text bold="true">ooba</$text>r',
@@ -499,13 +577,15 @@ describe( 'model-selection-to-view-converters', () => {
 		// Manually set selection ranges using passed `selectionPaths`.
 		const startPath = typeof selectionPaths[ 0 ] == 'number' ? [ selectionPaths[ 0 ] ] : selectionPaths[ 0 ];
 		const endPath = typeof selectionPaths[ 1 ] == 'number' ? [ selectionPaths[ 1 ] ] : selectionPaths[ 1 ];
+
 		const startPos = new ModelPosition( modelRoot, startPath );
 		const endPos = new ModelPosition( modelRoot, endPath );
+
 		const isBackward = selectionPaths[ 2 ] === 'backward';
 		modelSelection.setRanges( [ new ModelRange( startPos, endPos ) ], isBackward );
 
-		// Updated selection attributes according to model.
-		modelSelection._updateAttributes();
+		// Update selection attributes according to model.
+		modelSelection.refresh();
 
 		// And add or remove passed attributes.
 		for ( let key in selectionAttributes ) {
@@ -518,7 +598,7 @@ describe( 'model-selection-to-view-converters', () => {
 			}
 		}
 
-		// Remove view children since we do not want to convert deletion.
+		// Remove view children manually (without firing additional conversion).
 		viewRoot.removeChildren( 0, viewRoot.childCount );
 
 		// Convert model to view.

+ 25 - 0
packages/ckeditor5-engine/tests/conversion/modelconversiondispatcher.js

@@ -503,6 +503,31 @@ describe( 'ModelConversionDispatcher', () => {
 
 			expect( dispatcher.fire.calledWith( 'selectionAttribute:bold' ) ).to.be.false;
 		} );
+
+		it( 'should fire events for each marker which contains selection', () => {
+			doc.markers.set( 'name', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
+
+			sinon.spy( dispatcher, 'fire' );
+
+			dispatcher.convertSelection( doc.selection );
+
+			expect( dispatcher.fire.calledWith( 'selectionMarker:name' ) ).to.be.true;
+		} );
+
+		it( 'should not fire events if information about marker has been consumed', () => {
+			doc.markers.set( 'foo', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
+			doc.markers.set( 'bar', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
+
+			sinon.spy( dispatcher, 'fire' );
+
+			dispatcher.on( 'selectionMarker:foo', ( evt, data, consumable ) => {
+				consumable.consume( data.selection, 'selectionMarker:bar' );
+			} );
+
+			dispatcher.convertSelection( doc.selection );
+
+			expect( dispatcher.fire.calledWith( 'selectionMarker:bar' ) ).to.be.false;
+		} );
 	} );
 
 	describe( 'convertMarker', () => {