8
0
Просмотр исходного кода

Thread DocumentFragment as ContainerElement in Writer methods.

Piotr Jasiun 9 лет назад
Родитель
Сommit
5de3e84419

+ 16 - 4
packages/ckeditor5-engine/src/view/writer.js

@@ -25,13 +25,16 @@ import isIterable from '../../utils/isiterable.js';
 	 * Returns first parent container of specified {@link engine.view.Position Position}.
 	 * Position's parent node is checked as first, then next parents are checked.
 	 *
+	 * Note that {@link engine.view.DocumentFragment DocumentFragment} is thread like a container.
+	 *
 	 * @param {engine.view.Position} position Position used as a start point to locate parent container.
-	 * @returns {engine.view.Element|undefined} Parent container element or `undefined` if container is not found.
+	 * @returns {engine.view.ContainerElement|engine.view.DocumentFragment|undefined} Parent container element or
+	 * `undefined` if container is not found.
 	 */
 	getParentContainer( position ) {
 		let parent = position.parent;
 
-		while ( !( parent instanceof ContainerElement ) ) {
+		while ( !isContainerOrFragment( parent ) ) {
 			if ( !parent ) {
 				return undefined;
 			}
@@ -50,6 +53,8 @@ import isIterable from '../../utils/isiterable.js';
 	 *		<p>foo<b><u>{}bar</u></b></p> -> <p>foo{}<b><u>bar</u></b></p>
 	 *		<p>foo<b><u>b{}ar</u></b></p> -> <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
 	 *
+	 * Note that {@link engine.view.DocumentFragment DocumentFragment} is thread like a container.
+	 *
 	 * @see engine.view.AttributeElement
 	 * @see engine.view.ContainerElement
 	 * @param {engine.view.Position} position Position where to break attributes.
@@ -75,12 +80,12 @@ import isIterable from '../../utils/isiterable.js';
 		const positionParent = position.parent;
 
 		// There are no attributes to break and text nodes breaking is not forced.
-		if ( !forceSplitText && positionParent instanceof Text && positionParent.parent instanceof ContainerElement ) {
+		if ( !forceSplitText && positionParent instanceof Text && isContainerOrFragment( positionParent.parent ) ) {
 			return Position.createFromPosition( position );
 		}
 
 		// Position's parent is container, so no attributes to break.
-		if ( positionParent instanceof ContainerElement ) {
+		if ( isContainerOrFragment( positionParent ) ) {
 			return Position.createFromPosition( position );
 		}
 
@@ -143,6 +148,8 @@ import isIterable from '../../utils/isiterable.js';
 	 * {@link engine.view.Range#start start} and {@link engine.view.Range#end end} positions are not placed inside
 	 * same parent container.
 	 *
+	 * Note that {@link engine.view.DocumentFragment DocumentFragment} is thread like a container.
+	 *
 	 * @see engine.view.Writer#breakAttribute
 	 * @param {engine.view.Range} range Range which `start` and `end` positions will be used to break attributes.
 	 * @returns {engine.view.Range} New range with located at break positions.
@@ -937,3 +944,8 @@ function validateNodesToInsert( nodes ) {
 		}
 	}
 }
+
+// Checks if node is ContainerElement or DocumentFragment, because in most cases they should be thread the same way.
+function isContainerOrFragment( node ) {
+	return node instanceof ContainerElement || node instanceof DocumentFragment;
+}

+ 21 - 1
packages/ckeditor5-engine/tests/view/writer/breakattributes.js

@@ -8,6 +8,7 @@
 'use strict';
 
 import Writer from '/ckeditor5/engine/view/writer.js';
+import DocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
 import { stringify, parse } from '/tests/engine/_utils/view.js';
 
 describe( 'Writer', () => {
@@ -21,7 +22,12 @@ describe( 'Writer', () => {
 	 * @param {String} expected
 	 */
 	function test( input, expected ) {
-		const { view, selection } = parse( input );
+		let { view, selection } = parse( input );
+
+		if ( !( view instanceof DocumentFragment ) ) {
+			view = new DocumentFragment( view );
+		}
+
 		const newPosition = writer.breakAttributes( selection.getFirstPosition() );
 		expect( stringify( view, newPosition, { showType: true, showPriority: true } ) ).to.equal( expected );
 	}
@@ -98,5 +104,19 @@ describe( 'Writer', () => {
 				'<container:p><attribute:b:1><attribute:u:1>foobar</attribute:u:1></attribute:b:1>[]</container:p>'
 			);
 		} );
+
+		it( 'should split attribute element directly in document fragment', () => {
+			test(
+				'<attribute:b:1>foo{}bar</attribute:b:1>',
+				'<attribute:b:1>foo</attribute:b:1>[]<attribute:b:1>bar</attribute:b:1>'
+			);
+		} );
+
+		it( 'should not split text directly in document fragment', () => {
+			test(
+				'foo{}bar',
+				'foo{}bar'
+			);
+		} );
 	} );
 } );

+ 21 - 1
packages/ckeditor5-engine/tests/view/writer/breakrange.js

@@ -8,6 +8,7 @@
 'use strict';
 
 import Writer from '/ckeditor5/engine/view/writer.js';
+import DocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
 import ContainerElement from '/ckeditor5/engine/view/containerelement.js';
 import Range from '/ckeditor5/engine/view/range.js';
 import { stringify, parse } from '/tests/engine/_utils/view.js';
@@ -22,7 +23,12 @@ describe( 'Writer', () => {
 	 * @param {String} expected
 	 */
 	function test( input, expected ) {
-		const { view, selection } = parse( input );
+		let { view, selection } = parse( input );
+
+		if ( !( view instanceof DocumentFragment ) ) {
+			view = new DocumentFragment( view );
+		}
+
 		const newRange = writer.breakRange( selection.getFirstRange() );
 		expect( stringify( view, newRange, { showType: true } ) ).to.equal( expected );
 	}
@@ -96,5 +102,19 @@ describe( 'Writer', () => {
 				'<container:p>foo{bar<attribute:b>baz</attribute:b>]<attribute:b>qux</attribute:b></container:p>'
 			);
 		} );
+
+		it( 'should split attribute element directly in document fragment', () => {
+			test(
+				'<attribute:b>fo{ob}ar</attribute:b>',
+				'<attribute:b>fo</attribute:b>[<attribute:b>ob</attribute:b>]<attribute:b>ar</attribute:b>'
+			);
+		} );
+
+		it( 'should not split text directly in document fragment', () => {
+			test(
+				'foo{}bar',
+				'foo{}bar'
+			);
+		} );
 	} );
 } );

+ 23 - 1
packages/ckeditor5-engine/tests/view/writer/insert.js

@@ -8,6 +8,7 @@
 'use strict';
 
 import Writer from '/ckeditor5/engine/view/writer.js';
+import DocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
 import ContainerElement from '/ckeditor5/engine/view/containerelement.js';
 import Element from '/ckeditor5/engine/view/element.js';
 import Position from '/ckeditor5/engine/view/position.js';
@@ -26,7 +27,12 @@ describe( 'Writer', () => {
 	 */
 	function test( input, nodesToInsert, expected ) {
 		nodesToInsert = nodesToInsert.map( node => parse( node ) );
-		const { view, selection } = parse( input );
+		let { view, selection } = parse( input );
+
+		if ( !( view instanceof DocumentFragment ) ) {
+			view = new DocumentFragment( view );
+		}
+
 		const newRange = writer.insert( selection.getFirstPosition(), nodesToInsert );
 		expect( stringify( view, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
 	}
@@ -128,6 +134,22 @@ describe( 'Writer', () => {
 			);
 		} );
 
+		it( 'should insert text into in document fragment', () => {
+			test(
+				'foo{}bar',
+				[ 'baz' ],
+				'foo{baz}bar'
+			);
+		} );
+
+		it( 'should merge same attribute nodes in document fragment', () => {
+			test(
+				'<attribute:b:2>foo</attribute:b:2>[]',
+				[ '<attribute:b:1>bar</attribute:b:1>' ],
+				'<attribute:b:2>foo</attribute:b:2>[<attribute:b:1>bar</attribute:b:1>]'
+			);
+		} );
+
 		it( 'should throw when inserting Element', () => {
 			const element = new Element( 'b' );
 			const container = new ContainerElement( 'p' );

+ 10 - 1
packages/ckeditor5-engine/tests/view/writer/remove.js

@@ -25,7 +25,12 @@ describe( 'Writer', () => {
 	 * @param {String} expectedRemoved
 	 */
 	function test( input, expectedResult, expectedRemoved ) {
-		const { view, selection } = parse( input );
+		let { view, selection } = parse( input );
+
+		if ( !( view instanceof DocumentFragment ) ) {
+			view = new DocumentFragment( view );
+		}
+
 		const range = selection.getFirstRange();
 		const removed = writer.remove( range );
 		expect( stringify( view, range, { showType: true, showPriority: true } ) ).to.equal( expectedResult );
@@ -91,5 +96,9 @@ describe( 'Writer', () => {
 				'<attribute:b:1>o</attribute:b:1>bar<attribute:b:1>ba</attribute:b:1>'
 			);
 		} );
+
+		it( 'should remove part of the text node in document fragment', () => {
+			test( 'fo{ob}ar', 'fo{}ar', 'ob' );
+		} );
 	} );
 } );

+ 15 - 1
packages/ckeditor5-engine/tests/view/writer/unwrap.js

@@ -11,6 +11,7 @@ import Writer from '/ckeditor5/engine/view/writer.js';
 import Element from '/ckeditor5/engine/view/element.js';
 import ContainerElement from '/ckeditor5/engine/view/containerelement.js';
 import AttributeElement from '/ckeditor5/engine/view/attributeelement.js';
+import DocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
 import Position from '/ckeditor5/engine/view/position.js';
 import Range from '/ckeditor5/engine/view/range.js';
 import Text from '/ckeditor5/engine/view/text.js';
@@ -28,7 +29,12 @@ describe( 'Writer', () => {
 	 * @param {String} expected
 	 */
 	function test( input, unwrapAttribute, expected ) {
-		const { view, selection } = parse( input );
+		let { view, selection } = parse( input );
+
+		if ( !( view instanceof DocumentFragment ) ) {
+			view = new DocumentFragment( view );
+		}
+
 		const newRange = writer.unwrap( selection.getFirstRange(), parse( unwrapAttribute ) );
 		expect( stringify( view, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
 	}
@@ -274,5 +280,13 @@ describe( 'Writer', () => {
 				'<container:p>[<attribute:b:1 style="color:red;position:absolute;top:10px;">test</attribute:b:1>]</container:p>'
 			);
 		} );
+
+		it( 'should unwrap single node in document fragment', () => {
+			test(
+				'<container:p>[<attribute:b:1>foobar</attribute:b:1>]</container:p>',
+				'<attribute:b:1></attribute:b:1>',
+				'<container:p>[foobar]</container:p>'
+			);
+		} );
 	} );
 } );

+ 15 - 1
packages/ckeditor5-engine/tests/view/writer/wrap.js

@@ -11,6 +11,7 @@ import Writer from '/ckeditor5/engine/view/writer.js';
 import Element from '/ckeditor5/engine/view/element.js';
 import ContainerElement from '/ckeditor5/engine/view/containerelement.js';
 import AttributeElement from '/ckeditor5/engine/view/attributeelement.js';
+import DocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
 import Position from '/ckeditor5/engine/view/position.js';
 import Range from '/ckeditor5/engine/view/range.js';
 import Text from '/ckeditor5/engine/view/text.js';
@@ -28,7 +29,12 @@ describe( 'Writer', () => {
 	 * @param {String} expected
 	 */
 	function test( input, unwrapAttribute, expected ) {
-		const { view, selection } = parse( input );
+		let { view, selection } = parse( input );
+
+		if ( !( view instanceof DocumentFragment ) ) {
+			view = new DocumentFragment( view );
+		}
+
 		const newRange = writer.wrap( selection.getFirstRange(), parse( unwrapAttribute ) );
 		expect( stringify( view, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
 	}
@@ -54,6 +60,14 @@ describe( 'Writer', () => {
 			);
 		} );
 
+		it( 'wraps single text node in document fragment', () => {
+			test(
+				'{foobar}',
+				'<attribute:b:1></attribute:b:1>',
+				'[<attribute:b:1>foobar</attribute:b:1>]'
+			);
+		} );
+
 		it( 'should throw error when element is not instance of AttributeElement', () => {
 			const container = new ContainerElement( 'p', null, new Text( 'foo' ) );
 			const range = new Range(

+ 15 - 1
packages/ckeditor5-engine/tests/view/writer/wrapposition.js

@@ -11,6 +11,7 @@ import Writer from '/ckeditor5/engine/view/writer.js';
 import Text from '/ckeditor5/engine/view/text.js';
 import Element from '/ckeditor5/engine/view/element.js';
 import ContainerElement from '/ckeditor5/engine/view/containerelement.js';
+import DocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
 import Position from '/ckeditor5/engine/view/position.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import { stringify, parse } from '/tests/engine/_utils/view.js';
@@ -26,7 +27,12 @@ describe( 'wrapPosition', () => {
 	 * @param {String} expected
 	 */
 	function test( input, unwrapAttribute, expected ) {
-		const { view, selection } = parse( input );
+		let { view, selection } = parse( input );
+
+		if ( !( view instanceof DocumentFragment ) ) {
+			view = new DocumentFragment( view );
+		}
+
 		const newPosition = writer.wrapPosition( selection.getFirstPosition(), parse( unwrapAttribute ) );
 		expect( stringify( view, newPosition, { showType: true, showPriority: true } ) ).to.equal( expected );
 	}
@@ -61,6 +67,14 @@ describe( 'wrapPosition', () => {
 		);
 	} );
 
+	it( 'should wrap position inside document fragment', () => {
+		test(
+			'<attribute:b:1>foo</attribute:b:1>[]<attribute:b:3>bar</attribute:b:3>',
+			'<attribute:b:2></attribute:b:2>',
+			'<attribute:b:1>foo</attribute:b:1><attribute:b:2>[]</attribute:b:2><attribute:b:3>bar</attribute:b:3>'
+		);
+	} );
+
 	it( 'should wrap position at the end of text node', () => {
 		test(
 			'<container:p>foobar{}</container:p>',