8
0
Quellcode durchsuchen

Enhanced model/Writer#split to split more than one element at once.

Oskar Wróbel vor 7 Jahren
Ursprung
Commit
69085efaeb

+ 65 - 25
packages/ckeditor5-engine/src/model/writer.js

@@ -573,20 +573,20 @@ export default class Writer {
 	}
 
 	/**
-	 * Splits an element at the given position.
+	 * Splits elements start from the given position and goes to the top of the model tree as long as given
+	 * `limitElement` won't be reached. When limitElement is not defined then only a parent of given position will be split.
 	 *
 	 * The element needs to have a parent. It cannot be a root element nor document fragment.
 	 * The `writer-split-element-no-parent` error will be thrown if you try to split an element with no parent.
 	 *
 	 * @param {module:engine/model/position~Position} position Position of split.
+	 * @param {module:engine/model/node~Node} [limitElement] Stop splitting when this element will be reached.
+	 * @returns {~SplitResult}
 	 */
-	split( position ) {
+	split( position, limitElement ) {
 		this._assertWriterUsedCorrectly();
 
-		const delta = new SplitDelta();
-		this.batch.addDelta( delta );
-
-		const splitElement = position.parent;
+		let splitElement = position.parent;
 
 		if ( !splitElement.parent ) {
 			/**
@@ -597,30 +597,63 @@ export default class Writer {
 			throw new CKEditorError( 'writer-split-element-no-parent: Element with no parent can not be split.' );
 		}
 
-		const copy = new Element( splitElement.name, splitElement.getAttributes() );
-		const insertVersion = splitElement.root.document ? splitElement.root.document.version : null;
+		// When limit element is not defined lets set splitElement parent as limit.
+		if ( !limitElement ) {
+			limitElement = splitElement.parent;
+		}
 
-		const insert = new InsertOperation(
-			Position.createAfter( splitElement ),
-			copy,
-			insertVersion
-		);
+		if ( !position.parent.getAncestors( { includeSelf: true } ).includes( limitElement ) ) {
+			throw new CKEditorError( 'writer-split-invalid-limit-element: Limit element is not a position ancestor.' );
+		}
 
-		delta.addOperation( insert );
-		this.model.applyOperation( insert );
+		// We need to cache elements that will be created as a result of the first split because
+		// we need to create a range from the end of the first split element to the beginning of the
+		// first copy element. This should be handled by LiveRange but it doesn't work on detached nodes.
+		let firstSplitElement, firstCopyElement;
 
-		const moveVersion = insertVersion !== null ? insertVersion + 1 : null;
+		do {
+			const delta = new SplitDelta();
+			this.batch.addDelta( delta );
 
-		const move = new MoveOperation(
-			position,
-			splitElement.maxOffset - position.offset,
-			Position.createFromParentAndOffset( copy, 0 ),
-			moveVersion
-		);
-		move.isSticky = true;
+			const copy = new Element( splitElement.name, splitElement.getAttributes() );
+			const insertVersion = splitElement.root.document ? splitElement.root.document.version : null;
 
-		delta.addOperation( move );
-		this.model.applyOperation( move );
+			const insert = new InsertOperation(
+				Position.createAfter( splitElement ),
+				copy,
+				insertVersion
+			);
+
+			delta.addOperation( insert );
+			this.model.applyOperation( insert );
+
+			const moveVersion = insertVersion !== null ? insertVersion + 1 : null;
+
+			const move = new MoveOperation(
+				position,
+				splitElement.maxOffset - position.offset,
+				Position.createFromParentAndOffset( copy, 0 ),
+				moveVersion
+			);
+			move.isSticky = true;
+
+			delta.addOperation( move );
+			this.model.applyOperation( move );
+
+			// Cache result of the first split.
+			if ( !firstSplitElement && !firstCopyElement ) {
+				firstSplitElement = splitElement;
+				firstCopyElement = copy;
+			}
+
+			position = Position.createBefore( copy );
+			splitElement = position.parent;
+		} while ( splitElement !== limitElement );
+
+		return {
+			position,
+			range: new Range( Position.createAt( firstSplitElement, 'end' ), Position.createAt( firstCopyElement ) )
+		};
 	}
 
 	/**
@@ -1141,3 +1174,10 @@ function isSameTree( rootA, rootB ) {
 
 	return false;
 }
+
+/**
+ * @typedef {Object} SplitResult
+ * @property {module:engine/model/position~Position} position between split elements.
+ * @property {module:engine/model/range~Range} range Range that stars from the end of the first split element and ands
+ * at the beginning of the first copy element.
+ */

+ 38 - 2
packages/ckeditor5-engine/tests/model/writer.js

@@ -1790,6 +1790,42 @@ describe( 'Writer', () => {
 			} ).to.throw( CKEditorError, /^writer-split-element-no-parent/ );
 		} );
 
+		it( 'should split elements to limitElement', () => {
+			const div = new Element( 'div', null, p );
+			const section = new Element( 'section', null, div );
+
+			root.insertChildren( 0, section );
+
+			split( new Position( p, [ 3 ] ), section );
+
+			expect( root.maxOffset ).to.equal( 1 );
+			expect( section.maxOffset ).to.equal( 2 );
+
+			expect( section.getChild( 0 ).name ).to.equal( 'div' );
+			expect( section.getChild( 0 ).getChild( 0 ).name ).to.equal( 'p' );
+			expect( section.getChild( 0 ).getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
+			expect( count( section.getChild( 0 ).getChild( 0 ).getAttributes() ) ).to.equal( 1 );
+			expect( section.getChild( 0 ).getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
+
+			expect( section.getChild( 1 ).name ).to.equal( 'div' );
+			expect( section.getChild( 1 ).getChild( 0 ).name ).to.equal( 'p' );
+			expect( section.getChild( 1 ).getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
+			expect( count( section.getChild( 1 ).getChild( 0 ).getAttributes() ) ).to.equal( 1 );
+			expect( section.getChild( 1 ).getChild( 0 ).getChild( 0 ).data ).to.equal( 'bar' );
+		} );
+
+		it( 'should throw when limitElement is not a position ancestor', () => {
+			const div = new Element( 'div', null, p );
+			const section = new Element( 'section', null, div );
+
+			root.insertChildren( 0, div );
+			root.insertChildren( 1, section );
+
+			expect( () => {
+				split( new Position( p, [ 3 ] ), section );
+			} ).to.throw( CKEditorError, /^writer-split-invalid-limit-element/ );
+		} );
+
 		it( 'should throw when trying to use detached writer', () => {
 			const writer = new Writer( model, batch );
 
@@ -2299,9 +2335,9 @@ describe( 'Writer', () => {
 		} );
 	}
 
-	function split( position ) {
+	function split( position, limitElement ) {
 		model.enqueueChange( batch, writer => {
-			writer.split( position );
+			writer.split( position, limitElement );
 		} );
 	}