Преглед изворни кода

Add: view.Writer#breakContainer and view.Writer#mergeContainers.

Szymon Cofalik пре 9 година
родитељ
комит
2262f27222

+ 85 - 0
packages/ckeditor5-engine/src/view/writer.js

@@ -21,7 +21,9 @@ import isIterable from '../../utils/isiterable.js';
 export default {
 export default {
 	breakAt,
 	breakAt,
 	breakRange,
 	breakRange,
+	breakContainer,
 	mergeAt,
 	mergeAt,
+	mergeContainers,
 	insert,
 	insert,
 	remove,
 	remove,
 	move,
 	move,
@@ -71,6 +73,57 @@ export function breakRange( range ) {
 	return _breakRange( range );
 	return _breakRange( range );
 }
 }
 
 
+/**
+ * Breaks {@link engine.view.ContainerElement container view element} into two, at the given position. Position
+ * has to be directly inside container element and cannot be in root. Does not break if position is at the beginning
+ * or at the end of it's parent element.
+ *
+ *		<p>foo^bar</p> -> <p>foo</p><p>bar</p>
+ *		<div><p>foo</p>^<p>bar</p></div> -> <div><p>foo</p></div><div><p>bar</p></div>
+ *		<p>^foobar</p> -> ^<p>foobar</p>
+ *		<p>foobar^</p> -> <p>foobar</p>^
+ *
+ * @param {engine.view.Position} position Position where to break element.
+ * @returns {engine.view.Position} Position between broken elements. If element has not been broken, the returned position
+ * is placed either before it or after it.
+ */
+export function breakContainer( position ) {
+	const element = position.parent;
+
+	if ( !( element instanceof ContainerElement ) ) {
+		/**
+		 * Trying to break an element which is not a container element.
+		 *
+		 * @error view-writer-break-non-container-element
+		 */
+		throw new CKEditorError( 'view-writer-break-non-container-element: Trying to break an element which is not a container element.' );
+	}
+
+	if ( !element.parent ) {
+		/**
+		 * Trying to break root element.
+		 *
+		 * @error view-writer-break-root
+		 */
+		throw new CKEditorError( 'view-writer-break-root: Trying to break root element.' );
+	}
+
+	if ( position.isAtStart ) {
+		return Position.createBefore( element );
+	} else if ( !position.isAtEnd ) {
+		const newElement = element.clone( false );
+
+		insert( Position.createAfter( element ), newElement );
+
+		const sourceRange = new Range( position, Position.createAt( element, 'end' ) );
+		const targetPosition = new Position( newElement, 0 );
+
+		move( sourceRange, targetPosition );
+	}
+
+	return Position.createAfter( element );
+}
+
 /**
 /**
  * Merges attribute nodes. It also merges text nodes if needed.
  * Merges attribute nodes. It also merges text nodes if needed.
  * Only {@link engine.view.AttributeElement#isSimilar similar} `attribute` nodes can be merged.
  * Only {@link engine.view.AttributeElement#isSimilar similar} `attribute` nodes can be merged.
@@ -143,6 +196,38 @@ export function mergeAt( position ) {
 	return position;
 	return position;
 }
 }
 
 
+/**
+ * Merges two {@link engine.view.ContainerElement container view elements} that are before and after given position.
+ * Precisly, the element after the position is removed and it's contents are moved to element before the position.
+ *
+ *		<p>foo</p>^<p>bar</p> -> <p>foo^bar</p>
+ *		<div>foo</div>^<p>bar</p> -> <div>foo^bar</div>
+ *
+ * @param position
+ * @returns {Position}
+ */
+export function mergeContainers( position ) {
+	const prev = position.nodeBefore;
+	const next = position.nodeAfter;
+
+	if ( !prev || !next || !( prev instanceof ContainerElement ) || !( next instanceof ContainerElement ) ) {
+		/**
+		 * Element before and after given position cannot be merged.
+		 *
+		 * @error view-writer-merge-containers-invalid-position
+		 */
+		throw new CKEditorError( 'view-writer-merge-containers-invalid-position: Element before and after given position cannot be merged.' );
+	}
+
+	const lastChild = prev.getChild( prev.childCount - 1 );
+	const newPosition = lastChild instanceof Text ? Position.createAt( lastChild, 'end' ) : Position.createAt( prev, 'end' );
+
+	move( Range.createIn( next ), Position.createAt( prev, 'end' ) );
+	remove( Range.createOn( next ) );
+
+	return newPosition;
+}
+
 /**
 /**
  * Insert node or nodes at specified position. Takes care about breaking attributes before insertion
  * Insert node or nodes at specified position. Takes care about breaking attributes before insertion
  * and merging them afterwards.
  * and merging them afterwards.

+ 80 - 0
packages/ckeditor5-engine/tests/view/writer/breakContainer.js

@@ -0,0 +1,80 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: view, browser-only */
+
+import { breakContainer } from '/ckeditor5/engine/view/writer.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import ContainerElement from '/ckeditor5/engine/view/containerelement.js';
+import Position from '/ckeditor5/engine/view/position.js';
+
+describe( 'writer', () => {
+	/**
+	 * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
+	 * test break position.
+	 *
+	 * @param {String} input
+	 * @param {String} expected
+	 */
+	function test( input, expected ) {
+		let { view, selection } = parse( input );
+
+		const newPosition = breakContainer( selection.getFirstPosition() );
+		expect( stringify( view.root, newPosition, { showType: true, showPriority: false } ) ).to.equal( expected );
+	}
+
+	describe( 'breakContainer', () => {
+		it( 'break inside element - should break container element at given position', () => {
+			test(
+				'<container:div>' +
+					'<container:p>' +
+						'<attribute:b>foo</attribute:b>[]<attribute:u>bar</attribute:u>' +
+					'</container:p>' +
+				'</container:div>',
+
+				'<container:div>' +
+					'<container:p>' +
+						'<attribute:b>foo</attribute:b>' +
+					'</container:p>' +
+					'[]<container:p>' +
+						'<attribute:u>bar</attribute:u>' +
+					'</container:p>' +
+				'</container:div>'
+			);
+		} );
+
+		it( 'break at start of element - should not break container and place returned position before element', () => {
+			test(
+				'<container:div><container:p>[]foobar</container:p></container:div>',
+				'<container:div>[]<container:p>foobar</container:p></container:div>'
+			);
+		} );
+
+		it( 'break at the end of element - should not break container and place returned position after element', () => {
+			test(
+				'<container:div><container:p>foobar[]</container:p></container:div>',
+				'<container:div><container:p>foobar</container:p>[]</container:div>'
+			);
+		} );
+
+		it( 'should throw if position parent is not container', () => {
+			let { selection } = parse( '<container:div>foo{}bar</container:div>' );
+
+			expect( () => {
+				breakContainer( selection.getFirstPosition() );
+			} ).to.throw( CKEditorError, /view-writer-break-non-container-element/ );
+		} );
+
+		it( 'should throw if position parent is root', () => {
+			const element = new ContainerElement( 'div' );
+			const position = Position.createAt( element, 0 );
+
+			expect( () => {
+				breakContainer( position );
+			} ).to.throw( CKEditorError, /view-writer-break-root/ );
+		} );
+	} );
+} );

+ 87 - 0
packages/ckeditor5-engine/tests/view/writer/mergeContainers.js

@@ -0,0 +1,87 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: view, browser-only */
+
+import { mergeContainers } from '/ckeditor5/engine/view/writer.js';
+import { stringify, parse } from '/tests/engine/_utils/view.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+
+describe( 'writer', () => {
+	/**
+	 * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
+	 * test break position.
+	 *
+	 * @param {String} input
+	 * @param {String} expected
+	 */
+	function test( input, expected ) {
+		let { view, selection } = parse( input );
+
+		const newPosition = mergeContainers( selection.getFirstPosition() );
+		expect( stringify( view.root, newPosition, { showType: true, showPriority: false } ) ).to.equal( expected );
+	}
+
+	describe( 'mergeContainers', () => {
+		it( 'should merge two container elements - position between elements', () => {
+			test(
+				'<container:div>' +
+					'<attribute:b>foo</attribute:b>' +
+				'</container:div>' +
+				'[]<container:div>' +
+					'<attribute:u>bar</attribute:u>' +
+				'</container:div>',
+
+				'<container:div><attribute:b>foo</attribute:b>[]<attribute:u>bar</attribute:u></container:div>'
+			);
+		} );
+
+		it( 'should merge two container elements - position in text', () => {
+			test(
+				'<container:div>foo</container:div>[]<container:div>bar</container:div>',
+				'<container:div>foo{}bar</container:div>'
+			);
+		} );
+
+		it( 'should merge two different container elements', () => {
+			test(
+				'<container:div>foo</container:div>[]<container:p>bar</container:p>',
+				'<container:div>foo{}bar</container:div>'
+			);
+		} );
+
+		it( 'should throw if there is no element before position', () => {
+			let { selection } = parse( '[]<container:div>foobar</container:div>' );
+
+			expect( () => {
+				mergeContainers( selection.getFirstPosition() );
+			} ).to.throw( CKEditorError, /view-writer-merge-containers-invalid-position/ );
+		} );
+
+		it( 'should throw if there is no element after position', () => {
+			let { selection } = parse( '<container:div>foobar</container:div>[]' );
+
+			expect( () => {
+				mergeContainers( selection.getFirstPosition() );
+			} ).to.throw( CKEditorError, /view-writer-merge-containers-invalid-position/ );
+		} );
+
+		it( 'should throw if element before position is not a container element', () => {
+			let { selection } = parse( '<attribute:u>foo</attribute:u>[]<container:div>bar</container:div>' );
+
+			expect( () => {
+				mergeContainers( selection.getFirstPosition() );
+			} ).to.throw( CKEditorError, /view-writer-merge-containers-invalid-position/ );
+		} );
+
+		it( 'should throw if element after position is not a container element', () => {
+			let { selection } = parse( '<container:div>foo</container:div>[]<attribute:u>bar</attribute:u>' );
+
+			expect( () => {
+				mergeContainers( selection.getFirstPosition() );
+			} ).to.throw( CKEditorError, /view-writer-merge-containers-invalid-position/ );
+		} );
+	} );
+} );