Parcourir la source

Merge pull request #1545 from ckeditor/t/ckeditor5/1243

Other: The `model.insertContent()` accepts range and position. Closes ckeditor/ckeditor5#1243.
Piotr Jasiun il y a 7 ans
Parent
commit
85560a8543

+ 38 - 7
packages/ckeditor5-engine/src/model/model.js

@@ -275,12 +275,12 @@ export default class Model {
 	 *
 	 *		// insertContent() doesn't have to be used in a change() block. It can, though,
 	 *		// so this code could be moved to the callback defined above.
-	 *		editor.model.insertContent( docFrag, editor.model.document.selection );
+	 *		editor.model.insertContent( docFrag );
 	 *
 	 * Using `insertContent()` with HTML string converted to a model document fragment (similar to the pasting mechanism):
 	 *
 	 *		// You can create your own HtmlDataProcessor instance or use editor.data.processor
-	 *		// if you haven't overriden the default one (which is HtmlDataProcessor instance).
+	 *		// if you haven't overridden the default one (which is HtmlDataProcessor instance).
 	 *		const htmlDP = new HtmlDataProcessor();
 	 *
 	 *		// Convert an HTML string to a view document fragment.
@@ -296,15 +296,43 @@ export default class Model {
 	 *		// which has a loosened schema.
 	 *		const modelFragment = editor.data.toModel( viewFragment );
 	 *
-	 *		editor.model.insertContent( modelFragment, editor.model.document.selection );
+	 *		editor.model.insertContent( modelFragment );
+	 *
+	 * By default this method will use the document selection but it can also be used with a position, range or selection instance.
+	 *
+	 *		// Insert text at the current document selection position.
+	 *		editor.model.change( writer => {
+	 *			editor.model.insertContent( writer.createText( 'x' ) );
+	 *		} );
+	 *
+	 *		// Insert text at given position - document selection will not be modified.
+	 *		editor.model.change( writer => {
+	 *			editor.model.insertContent( writer.createText( 'x' ), Position.createAt( doc.getRoot(), 2 ) );
+	 *		} );
+	 *
+	 * If an instance of {module:engine/model/selection~Selection} is passed as `selectable`
+	 * it will be moved to the target position (where the document selection should be moved after the insertion).
+	 *
+	 *		// Insert text replacing given selection instance.
+	 *		const selection = new Selection( paragraph, 'in' );
+	 *
+	 *		editor.model.change( writer => {
+	 *			editor.model.insertContent( writer.createText( 'x' ), selection );
+	 *
+	 *			// insertContent() modifies the passed selection instance so it can be used to set the document selection.
+	 *			// Note: This is not necessary when you passed document selection to insertContent().
+	 *			writer.setSelection( selection );
+	 *		} );
 	 *
 	 * @fires insertContent
 	 * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
-	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
-	 * Selection into which the content should be inserted.
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+	 * module:engine/model/position~Position|module:engine/model/element~Element|
+	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} [selectable=model.document.selection]
+	 * Selection into which the content should be inserted. If not provided the current model document selection will be used.
 	 */
-	insertContent( content, selection ) {
-		insertContent( this, content, selection );
+	insertContent( content, selectable ) {
+		insertContent( this, content, selectable );
 	}
 
 	/**
@@ -534,6 +562,9 @@ export default class Model {
 	 * The {@link #insertContent default action of that method} is implemented as a
 	 * listener to this event so it can be fully customized by the features.
 	 *
+	 * **Note** The `selectable` parameter for the {@link #insertContent} is optional. When `undefined` value is passed the method uses
+	 * `model.document.selection`.
+	 *
 	 * @event insertContent
 	 * @param {Array} args The arguments passed to the original method.
 	 */

+ 16 - 16
packages/ckeditor5-engine/src/model/selection.js

@@ -35,7 +35,7 @@ export default class Selection {
 	 * or on the given {@link module:engine/model/element~Element element},
 	 * or creates an empty selection if no arguments were passed.
 	 *
-	 * 		// Creates empty selection without ranges.
+	 *		// Creates empty selection without ranges.
 	 *		const selection = new Selection();
 	 *
 	 *		// Creates selection at the given range.
@@ -43,7 +43,7 @@ export default class Selection {
 	 *		const selection = new Selection( range );
 	 *
 	 *		// Creates selection at the given ranges
-	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
 	 *		const selection = new Selection( ranges );
 	 *
 	 *		// Creates selection from the other selection.
@@ -51,30 +51,30 @@ export default class Selection {
 	 *		const otherSelection = new Selection();
 	 *		const selection = new Selection( otherSelection );
 	 *
-	 * 		// Creates selection from the given document selection.
+	 *		// Creates selection from the given document selection.
 	 *		// Note: It doesn't copies selection attributes.
 	 *		const documentSelection = new DocumentSelection( doc );
 	 *		const selection = new Selection( documentSelection );
 	 *
-	 * 		// Creates selection at the given position.
+	 *		// Creates selection at the given position.
 	 *		const position = new Position( root, path );
 	 *		const selection = new Selection( position );
 	 *
-	 * 		// Creates selection at the start position of the given element.
+	 *		// Creates selection at the start position of the given element.
 	 *		const paragraph = writer.createElement( 'paragraph' );
 	 *		const selection = new Selection( paragraph, offset );
 	 *
-	 * 		// Creates a range inside an {@link module:engine/model/element~Element element} which starts before the
-	 * 		// first child of that element and ends after the last child of that element.
+	 *		// Creates a range inside an {@link module:engine/model/element~Element element} which starts before the
+	 *		// first child of that element and ends after the last child of that element.
 	 *		const selection = new Selection( paragraph, 'in' );
 	 *
-	 * 		// Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends
-	 * 		// just after the item.
+	 *		// Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends
+	 *		// just after the item.
 	 *		const selection = new Selection( paragraph, 'on' );
 	 *
 	 * Selection's constructor allow passing additional options (`'backward'`) as the last argument.
 	 *
-	 * 		// Creates backward selection.
+	 *		// Creates backward selection.
 	 *		const selection = new Selection( range, { backward: true } );
 	 *
 	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
@@ -326,7 +326,7 @@ export default class Selection {
 	 * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
 	 * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
 	 *
-	 * 		// Removes all selection's ranges.
+	 *		// Removes all selection's ranges.
 	 *		selection.setTo( null );
 	 *
 	 *		// Sets selection to the given range.
@@ -334,7 +334,7 @@ export default class Selection {
 	 *		selection.setTo( range );
 	 *
 	 *		// Sets selection to given ranges.
-	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
 	 *		selection.setTo( ranges );
 	 *
 	 *		// Sets selection to other selection.
@@ -342,16 +342,16 @@ export default class Selection {
 	 *		const otherSelection = new Selection();
 	 *		selection.setTo( otherSelection );
 	 *
-	 * 		// Sets selection to the given document selection.
+	 *		// Sets selection to the given document selection.
 	 *		// Note: It doesn't copies selection attributes.
 	 *		const documentSelection = new DocumentSelection( doc );
 	 *		selection.setTo( documentSelection );
 	 *
-	 * 		// Sets collapsed selection at the given position.
+	 *		// Sets collapsed selection at the given position.
 	 *		const position = new Position( root, path );
 	 *		selection.setTo( position );
 	 *
-	 * 		// Sets collapsed selection at the position of the given node and an offset.
+	 *		// Sets collapsed selection at the position of the given node and an offset.
 	 *		selection.setTo( paragraph, offset );
 	 *
 	 * Creates a range inside an {@link module:engine/model/element~Element element} which starts before the first child of
@@ -365,7 +365,7 @@ export default class Selection {
 	 *
 	 * `Selection#setTo()`' method allow passing additional options (`backward`) as the last argument.
 	 *
-	 * 		// Sets backward selection.
+	 *		// Sets backward selection.
 	 *		const selection = new Selection( range, { backward: true } );
 	 *
 	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|

+ 19 - 4
packages/ckeditor5-engine/src/model/utils/insertcontent.js

@@ -13,24 +13,39 @@ import Element from '../element';
 import Range from '../range';
 import log from '@ckeditor/ckeditor5-utils/src/log';
 import DocumentSelection from '../documentselection';
+import Selection from '../selection';
 
 /**
  * Inserts content into the editor (specified selection) as one would expect the paste
  * functionality to work.
  *
+ * If an instance of {module:engine/model/selection~Selection} is passed as `selectable` it will be modified
+ * to the insertion selection (equal to a range to be selected after insertion).
+ *
  * **Note:** Use {@link module:engine/model/model~Model#insertContent} instead of this function.
- * This function is only exposed to be reusable in algorithms
- * which change the {@link module:engine/model/model~Model#insertContent}
+ * This function is only exposed to be reusable in algorithms which change the {@link module:engine/model/model~Model#insertContent}
  * method's behavior.
  *
  * @param {module:engine/model/model~Model} model The model in context of which the insertion
  * should be performed.
  * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
- * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
+ * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+ * module:engine/model/position~Position|module:engine/model/element~Element|
+ * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} [selectable=model.document.selection]
  * Selection into which the content should be inserted.
  */
-export default function insertContent( model, content, selection ) {
+export default function insertContent( model, content, selectable ) {
 	model.change( writer => {
+		let selection;
+
+		if ( !selectable ) {
+			selection = model.document.selection;
+		} else if ( selectable instanceof Selection || selectable instanceof DocumentSelection ) {
+			selection = selectable;
+		} else {
+			selection = new Selection( selectable );
+		}
+
 		if ( !selection.isCollapsed ) {
 			model.deleteContent( selection );
 		}

+ 14 - 4
packages/ckeditor5-engine/tests/model/model.js

@@ -347,7 +347,7 @@ describe( 'Model', () => {
 
 			model.on( 'insertContent', spy );
 
-			model.insertContent( new ModelText( 'a' ), model.document.selection );
+			model.insertContent( new ModelText( 'a' ) );
 
 			expect( spy.calledOnce ).to.be.true;
 		} );
@@ -357,7 +357,7 @@ describe( 'Model', () => {
 
 			setData( model, '<paragraph>fo[]ar</paragraph>' );
 
-			model.insertContent( new ModelText( 'ob' ), model.document.selection );
+			model.insertContent( new ModelText( 'ob' ) );
 
 			expect( getData( model ) ).to.equal( '<paragraph>foob[]ar</paragraph>' );
 		} );
@@ -367,7 +367,17 @@ describe( 'Model', () => {
 
 			setData( model, '<paragraph>fo[]ar</paragraph>' );
 
-			model.insertContent( new ModelDocumentFragment( [ new ModelText( 'ob' ) ] ), model.document.selection );
+			model.insertContent( new ModelDocumentFragment( [ new ModelText( 'ob' ) ] ) );
+
+			expect( getData( model ) ).to.equal( '<paragraph>foob[]ar</paragraph>' );
+		} );
+
+		it( 'should use current model selection if no selectable passed', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+
+			setData( model, '<paragraph>fo[]ar</paragraph>' );
+
+			model.insertContent( new ModelText( 'ob' ) );
 
 			expect( getData( model ) ).to.equal( '<paragraph>foob[]ar</paragraph>' );
 		} );
@@ -377,7 +387,7 @@ describe( 'Model', () => {
 			setData( model, '<paragraph>[]</paragraph>' );
 
 			model.change( writer => {
-				model.insertContent( new ModelText( 'abc' ), model.document.selection );
+				model.insertContent( new ModelText( 'abc' ) );
 				expect( writer.batch.operations ).to.length( 1 );
 			} );
 		} );

+ 92 - 8
packages/ckeditor5-engine/tests/model/utils/insertcontent.js

@@ -12,6 +12,7 @@ import Selection from '../../../src/model/selection';
 import Position from '../../../src/model/position';
 
 import { setData, getData, parse } from '../../../src/dev-utils/model';
+import Range from '../../../src/model/range';
 
 describe( 'DataController utils', () => {
 	let model, doc;
@@ -26,7 +27,7 @@ describe( 'DataController utils', () => {
 			setData( model, 'x[]x' );
 
 			model.change( writer => {
-				insertContent( model, new Text( 'a' ), doc.selection );
+				insertContent( model, writer.createText( 'a' ) );
 				expect( writer.batch.operations ).to.length( 1 );
 			} );
 		} );
@@ -41,12 +42,95 @@ describe( 'DataController utils', () => {
 
 			const selection = new Selection( new Position( doc.getRoot(), [ 2 ] ) );
 
-			model.change( () => {
-				insertContent( model, new Text( 'x' ), selection );
+			model.change( writer => {
+				insertContent( model, writer.createText( 'x' ), selection );
+				expect( getData( model ) ).to.equal( 'a[]bxc' );
+			} );
+		} );
+
+		it( 'should modify passed selection instance', () => {
+			model = new Model();
+			doc = model.document;
+			doc.createRoot();
+
+			model.schema.extend( '$text', { allowIn: '$root' } );
+			setData( model, 'a[]bc' );
+
+			const selection = new Selection( new Position( doc.getRoot(), [ 2 ] ) );
+			const selectionCopy = new Selection( new Position( doc.getRoot(), [ 2 ] ) );
+
+			expect( selection.isEqual( selectionCopy ) ).to.be.true;
+
+			model.change( writer => {
+				insertContent( model, writer.createText( 'x' ), selection );
+			} );
+
+			expect( selection.isEqual( selectionCopy ) ).to.be.false;
+
+			const insertionSelection = new Selection( new Position( doc.getRoot(), [ 3 ] ) );
+			expect( selection.isEqual( insertionSelection ) ).to.be.true;
+		} );
+
+		it( 'should be able to insert content at custom position', () => {
+			model = new Model();
+			doc = model.document;
+			doc.createRoot();
+
+			model.schema.extend( '$text', { allowIn: '$root' } );
+			setData( model, 'a[]bc' );
+
+			const position = new Position( doc.getRoot(), [ 2 ] );
+
+			model.change( writer => {
+				insertContent( model, writer.createText( 'x' ), position );
 				expect( getData( model ) ).to.equal( 'a[]bxc' );
 			} );
 		} );
 
+		it( 'should be able to insert content at custom range', () => {
+			model = new Model();
+			doc = model.document;
+			doc.createRoot();
+
+			model.schema.extend( '$text', { allowIn: '$root' } );
+			setData( model, 'a[]bc' );
+
+			const range = new Range( new Position( doc.getRoot(), [ 2 ] ), new Position( doc.getRoot(), [ 3 ] ) );
+
+			model.change( writer => {
+				insertContent( model, writer.createText( 'x' ), range );
+				expect( getData( model ) ).to.equal( 'a[]bx' );
+			} );
+		} );
+
+		it( 'should be able to insert content at model selection if document selection is passed', () => {
+			model = new Model();
+			doc = model.document;
+			doc.createRoot();
+
+			model.schema.extend( '$text', { allowIn: '$root' } );
+			setData( model, 'a[]bc' );
+
+			model.change( writer => {
+				insertContent( model, writer.createText( 'x' ), model.document.selection );
+				expect( getData( model ) ).to.equal( 'ax[]bc' );
+			} );
+		} );
+
+		it( 'should be able to insert content at model selection if none passed', () => {
+			model = new Model();
+			doc = model.document;
+			doc.createRoot();
+
+			model.schema.extend( '$text', { allowIn: '$root' } );
+			setData( model, 'a[]bc' );
+
+			model.change( writer => {
+				insertContent( model, writer.createText( 'x' ) );
+				expect( getData( model ) ).to.equal( 'ax[]bc' );
+			} );
+		} );
+
 		it( 'accepts DocumentFragment', () => {
 			model = new Model();
 			doc = model.document;
@@ -56,7 +140,7 @@ describe( 'DataController utils', () => {
 
 			setData( model, 'x[]x' );
 
-			insertContent( model, new DocumentFragment( [ new Text( 'a' ) ] ), doc.selection );
+			insertContent( model, new DocumentFragment( [ new Text( 'a' ) ] ) );
 
 			expect( getData( model ) ).to.equal( 'xa[]x' );
 		} );
@@ -70,7 +154,7 @@ describe( 'DataController utils', () => {
 
 			setData( model, 'x[]x' );
 
-			insertContent( model, new Text( 'a' ), doc.selection );
+			insertContent( model, new Text( 'a' ) );
 
 			expect( getData( model ) ).to.equal( 'xa[]x' );
 		} );
@@ -90,7 +174,7 @@ describe( 'DataController utils', () => {
 
 			setData( model, '<paragraph>foo[]</paragraph>' );
 
-			insertContent( model, content, doc.selection );
+			insertContent( model, content );
 
 			expect( doc.getRoot().getChild( 0 ).getChild( 1 ) ).to.equal( content );
 		} );
@@ -303,7 +387,7 @@ describe( 'DataController utils', () => {
 				] );
 
 				setData( model, '[<heading2>foo</heading2>]' );
-				insertContent( model, content, doc.selection );
+				insertContent( model, content );
 				expect( getData( model ) ).to.equal( '<heading1>bar[]</heading1>' );
 			} );
 
@@ -854,6 +938,6 @@ describe( 'DataController utils', () => {
 			} );
 		}
 
-		insertContent( model, content, doc.selection );
+		insertContent( model, content );
 	}
 } );