8
0
Quellcode durchsuchen

Changed: treeController.ModelConverterBuilder now adds selection converter when needed.

Szymon Cofalik vor 9 Jahren
Ursprung
Commit
14743c36cf

+ 8 - 3
packages/ckeditor5-engine/src/treecontroller/model-converter-builder.js

@@ -13,6 +13,8 @@ import {
 	unwrap
 } from '/ckeditor5/engine/treecontroller/model-to-view-converters.js';
 
+import { convertSelectionAttribute } from '/ckeditor5/engine/treecontroller/selection-to-view-converters.js';
+
 import ViewAttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import ViewContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
 
@@ -165,9 +167,10 @@ class ModelConverterBuilder {
 	 * Creator function will be passed different values depending whether conversion is from element or from attribute:
 	 *
 	 * * from element: dispatcher's {@link engine.treeController.ModelConversionDispatcher#event:insert insert event} parameters
-	 * will be passed
-	 * * from attribute: first parameter is attribute value, then the rest of parameters are dispatcher's
-	 * {@link engine.treeController.ModelConversionDispatcher#event:changeAttribute changeAttribute event} parameters.
+	 * will be passed,
+	 * * from attribute: there is one parameter and it is attribute value.
+	 *
+	 * This method also registers model selection to view selection converter, if conversion is from attribute.
 	 *
 	 * This method creates the converter and adds it as a callback to a proper
 	 * {@link engine.treeController.ModelConversionDispatcher conversion dispatcher} event.
@@ -190,6 +193,8 @@ class ModelConverterBuilder {
 				dispatcher.on( 'addAttribute:' + this._from.key, wrap( element ), null, priority );
 				dispatcher.on( 'changeAttribute:' + this._from.key, wrap( element ), null, priority );
 				dispatcher.on( 'removeAttribute:' + this._from.key, unwrap( element ), null, priority );
+
+				dispatcher.on( 'selectionAttribute:' + this._from.key, convertSelectionAttribute( element ), null, priority );
 			}
 		}
 	}

+ 85 - 2
packages/ckeditor5-engine/tests/treecontroller/model-converter-builder.js

@@ -13,12 +13,15 @@ import ModelDocument from '/ckeditor5/engine/treemodel/document.js';
 import ModelElement from '/ckeditor5/engine/treemodel/element.js';
 import ModelText from '/ckeditor5/engine/treemodel/text.js';
 import ModelRange from '/ckeditor5/engine/treemodel/range.js';
+import ModelPosition from '/ckeditor5/engine/treemodel/position.js';
 
 import ViewElement from '/ckeditor5/engine/treeview/element.js';
 import ViewContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
 import ViewAttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import ViewText from '/ckeditor5/engine/treeview/text.js';
 import ViewWriter from '/ckeditor5/engine/treeview/writer.js';
+import ViewSelection from '/ckeditor5/engine/treeview/selection.js';
+import ViewRange from '/ckeditor5/engine/treeview/range.js';
 
 import Mapper from '/ckeditor5/engine/treecontroller/mapper.js';
 import ModelConversionDispatcher from '/ckeditor5/engine/treecontroller/modelconversiondispatcher.js';
@@ -63,7 +66,7 @@ function viewToString( item ) {
 }
 
 describe( 'Model converter builder', () => {
-	let dispatcher, modelDoc, modelRoot, viewRoot, mapper, writer;
+	let dispatcher, modelDoc, modelRoot, viewRoot, mapper, writer, viewSelection;
 
 	beforeEach( () => {
 		modelDoc = new ModelDocument();
@@ -75,8 +78,9 @@ describe( 'Model converter builder', () => {
 		mapper.bindElements( modelRoot, viewRoot );
 
 		writer = new ViewWriter();
+		viewSelection = new ViewSelection();
 
-		dispatcher = new ModelConversionDispatcher( { writer, mapper } );
+		dispatcher = new ModelConversionDispatcher( { writer, mapper, viewSelection } );
 
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'move', move() );
@@ -189,6 +193,85 @@ describe( 'Model converter builder', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
 		} );
+
+		it( 'selection conversion', () => {
+			// This test requires collapsed range selection converter (breaking attributes), clearing view selection
+			// and clearing "artifacts" (empty nodes) before new conversion happens.
+			dispatcher.on( 'selection', ( evt, selection, consumable, conversionApi ) => {
+				// Clear artifacts.
+				for ( let range of conversionApi.viewSelection.getRanges() ) {
+					const parentNode = range.start.parent;
+
+					if ( parentNode instanceof ViewElement && parentNode.getChildCount() === 0 ) {
+						parentNode.parent.removeChildren( parentNode.getIndex() );
+					}
+				}
+
+				// Break attributes.
+				const viewPosition = conversionApi.mapper.toViewPosition( selection.getFirstPosition() );
+				const brokenPosition = conversionApi.writer.breakAttributes( viewPosition );
+
+				// Set the new range as the only range of selection (clear old ranges).
+				conversionApi.viewSelection.setRanges( [ new ViewRange( brokenPosition, brokenPosition ) ], selection.isBackward );
+			} );
+
+			// Model converter builder should add selection converter.
+			BuildModelConverterFor( dispatcher ).fromAttribute( 'italic' ).toElement( ( value ) => new ViewAttributeElement( value ) );
+
+			modelRoot.appendChildren( new ModelText( 'foo', { italic: 'em' } ) );
+
+			// Set collapsed selection after "f".
+			const position = new ModelPosition( modelRoot, [ 1 ] );
+			modelDoc.selection.setRanges( [ new ModelRange( position, position ) ] );
+			modelDoc.selection._updateAttributes();
+
+			// Convert stuff.
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+			dispatcher.convertSelection( modelDoc.selection );
+
+			// Check if view structure is ok.
+			expect( viewToString( viewRoot ) ).to.equal( '<div><em>foo</em></div>' );
+
+			// Check if view selection is collapsed after "f" letter.
+			let ranges = Array.from( viewSelection.getRanges() );
+			expect( ranges.length ).to.equal( 1 );
+			expect( ranges[ 0 ].start.isEqual( ranges[ 0 ].end ) ).to.be.true;
+			expect( ranges[ 0 ].start.parent ).to.be.instanceof( ViewText ); // "foo".
+			expect( ranges[ 0 ].start.offset ).to.equal( 1 );
+
+			// Change selection attribute, convert it.
+			modelDoc.selection.setAttribute( 'italic', 'i' );
+			dispatcher.convertSelection( modelDoc.selection );
+
+			// Check if view structure has changed.
+			expect( viewToString( viewRoot ) ).to.equal( '<div><em>f</em><i></i><em>oo</em></div>' );
+
+			// Check if view selection is inside new <em> element.
+			ranges = Array.from( viewSelection.getRanges() );
+			expect( ranges.length ).to.equal( 1 );
+			expect( ranges[ 0 ].start.isEqual( ranges[ 0 ].end ) ).to.be.true;
+			expect( ranges[ 0 ].start.parent.name ).to.equal( 'i' );
+			expect( ranges[ 0 ].start.offset ).to.equal( 0 );
+
+			// Some more tests checking how selection attributes changes are converted:
+			modelDoc.selection.removeAttribute( 'italic' );
+			dispatcher.convertSelection( modelDoc.selection );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><em>f</em><em>oo</em></div>' );
+			ranges = Array.from( viewSelection.getRanges() );
+			expect( ranges[ 0 ].start.parent.name ).to.equal( 'div' );
+			expect( ranges[ 0 ].start.offset ).to.equal( 1 );
+
+			modelDoc.selection.setAttribute( 'italic', 'em' );
+			dispatcher.convertSelection( modelDoc.selection );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><em>foo</em></div>' );
+			ranges = Array.from( viewSelection.getRanges() );
+			expect( ranges.length ).to.equal( 1 );
+			expect( ranges[ 0 ].start.isEqual( ranges[ 0 ].end ) ).to.be.true;
+			expect( ranges[ 0 ].start.parent ).to.be.instanceof( ViewText ); // "foo".
+			expect( ranges[ 0 ].start.offset ).to.equal( 1 );
+		} );
 	} );
 
 	describe( 'model attribute to view attribute conversion', () => {