Преглед на файлове

Adjusting view writer to support UIElement.

Oskar Wróbel преди 8 години
родител
ревизия
809e9e9c2b

+ 48 - 17
packages/ckeditor5-engine/src/view/writer.js

@@ -11,6 +11,7 @@ import Position from './position';
 import ContainerElement from './containerelement';
 import AttributeElement from './attributeelement';
 import EmptyElement from './emptyelement';
+import UIElement from './uielement';
 import Element from './element';
 import Text from './text';
 import TextProxy from './textproxy';
@@ -19,7 +20,6 @@ import TreeWalker from './treewalker';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import DocumentFragment from './documentfragment';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
-import { stringify, parse } from '../dev-utils/view';
 
 /**
  * Contains functions used for composing view tree.
@@ -72,6 +72,10 @@ export default writer;
  * when trying to break attributes
  * inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
  *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element`
+ * when trying to break attributes
+ * inside {@link module:engine/view/uielement~UIElement UIElement}.
+ *
  * @see module:engine/view/attributeelement~AttributeElement
  * @see module:engine/view/containerelement~ContainerElement
  * @see module:engine/view/writer~writer.breakContainer
@@ -213,6 +217,11 @@ export function mergeAttributes( position ) {
 		return position;
 	}
 
+	// When one or both nodes are UIElements - no attributes to merge.
+	if ( ( nodeBefore instanceof UIElement ) || ( nodeAfter instanceof UIElement ) ) {
+		return position;
+	}
+
 	// When position is between two text nodes.
 	if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
 		return mergeTextNodes( nodeBefore, nodeAfter );
@@ -313,22 +322,23 @@ export function breakViewRangePerContainer( range ) {
  * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
  * contains instances that are not {@link module:engine/view/text~Text Texts},
  * {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
- * {@link module:engine/view/containerelement~ContainerElement ContainerElements} or
- * {@link module:engine/view/emptyelement~EmptyElement EmptyElements}.
+ * {@link module:engine/view/containerelement~ContainerElement ContainerElements},
+ * {@link module:engine/view/emptyelement~EmptyElement EmptyElements} or
+ * {@link module:engine/view/uielement~UIElement UIElements}.
  *
  * @function insert
  * @param {module:engine/view/position~Position} position Insertion position.
  * @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
- * module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement
- * |Iterable.<module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
- * module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement>} nodes Node or
- * nodes to insert.
+ * module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
+ * module:engine/view/uielement~UIElement|Iterable.<module:engine/view/text~Text|
+ * module:engine/view/attributeelement~AttributeElement|module:engine/view/containerelement~ContainerElement|
+ * module:engine/view/emptyelement~EmptyElement|module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
  * @returns {module:engine/view/range~Range} Range around inserted nodes.
  */
 export function insert( position, nodes ) {
 	nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ];
 
-	// Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements or Text.
+	// Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
 	validateNodesToInsert( nodes );
 
 	const container = getParentContainer( position );
@@ -439,7 +449,7 @@ export function clear( range, element ) {
 			} );
 
 			// If it is then create a range inside this element.
-			if ( parentElement && parentElement) {
+			if ( parentElement && parentElement ) {
 				rangeToRemove = Range.createIn( parentElement );
 			}
 		}
@@ -760,8 +770,11 @@ function _breakAttributesRange( range, forceSplitText = false ) {
 // Function used by public breakAttributes (without splitting text nodes) and by other methods (with
 // splitting text nodes).
 //
-// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` break position is placed
-// inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
+// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position
+// is placed inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
+//
+// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position
+// is placed inside {@link module:engine/view/uielement~UIElement UIElement}.
 //
 // @param {module:engine/view/position~Position} position Position where to break attributes.
 // @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in
@@ -782,6 +795,16 @@ function _breakAttributes( position, forceSplitText = false ) {
 		throw new CKEditorError( 'view-writer-cannot-break-empty-element' );
 	}
 
+	// If position is placed inside UIElement - throw an exception as we cannot break inside.
+	if ( position.parent instanceof UIElement ) {
+		/**
+		 * Cannot break inside UIElement instance.
+		 *
+		 * @error view-writer-cannot-break-ui-element
+		 */
+		throw new CKEditorError( 'view-writer-cannot-break-ui-element' );
+	}
+
 	// There are no attributes to break and text nodes breaking is not forced.
 	if ( !forceSplitText && positionParent instanceof Text && isContainerOrFragment( positionParent.parent ) ) {
 		return Position.createFromPosition( position );
@@ -925,9 +948,10 @@ function wrapChildren( parent, startOffset, endOffset, attribute ) {
 		const isText = child instanceof Text;
 		const isAttribute = child instanceof AttributeElement;
 		const isEmpty = child instanceof EmptyElement;
+		const isUI = child instanceof UIElement;
 
-		// Wrap text, empty elements or attributes with higher or equal priority.
-		if ( isText || isEmpty || ( isAttribute && attribute.priority <= child.priority ) ) {
+		// Wrap text, empty elements, ui elements or attributes with higher or equal priority.
+		if ( isText || isEmpty || isUI || ( isAttribute && attribute.priority <= child.priority ) ) {
 			// Clone attribute.
 			const newAttribute = attribute.clone();
 
@@ -1172,12 +1196,14 @@ function rangeSpansOnAllChildren( range ) {
 
 // Checks if provided nodes are valid to insert. Checks if each node is an instance of
 // {@link module:engine/view/text~Text Text} or {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
-// {@link module:engine/view/containerelement~ContainerElement ContainerElement} or
-// {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
+// {@link module:engine/view/containerelement~ContainerElement ContainerElement},
+// {@link module:engine/view/emptyelement~EmptyElement EmptyElement} or
+// {@link module:engine/view/uielement~UIElement UIElement}.
 //
 // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
 // contains instances that are not {@link module:engine/view/text~Text Texts},
 // {@link module:engine/view/emptyelement~EmptyElement EmptyElements},
+// {@link module:engine/view/uielement~UIElement UIElements},
 // {@link module:engine/view/attributeelement~AttributeElement AttributeElements} or
 // {@link module:engine/view/containerelement~ContainerElement ContainerElements}.
 //
@@ -1185,10 +1211,15 @@ function rangeSpansOnAllChildren( range ) {
 // |module:engine/view/containerelement~ContainerElement> nodes
 function validateNodesToInsert( nodes ) {
 	for ( let node of nodes ) {
-		if ( !( node instanceof Text || node instanceof AttributeElement || node instanceof ContainerElement || node instanceof EmptyElement ) ) {
+		if (
+			!( node instanceof Text || node instanceof AttributeElement || node instanceof ContainerElement ||
+			node instanceof EmptyElement || node instanceof UIElement )
+		) {
 			/**
 			 * Inserted nodes should be instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
-			 * {@link module:engine/view/containerelement~ContainerElement ContainerElement} or {@link module:engine/view/text~Text Text}.
+			 * {@link module:engine/view/containerelement~ContainerElement ContainerElement},
+			 * {@link module:engine/view/emptyerelement~EmptyElement EmptyElement},
+			 * {@link module:engine/view/uierelement~UIElement UIElement}, {@link module:engine/view/text~Text Text}.
 			 *
 			 * @error view-writer-insert-invalid-node
 			 */

+ 22 - 0
packages/ckeditor5-engine/tests/view/writer/breakAttributes.js

@@ -8,6 +8,7 @@ import { stringify, parse } from '../../../src/dev-utils/view';
 import ContainerElement from '../../../src/view/containerelement';
 import AttributeElement from '../../../src/view/attributeelement';
 import EmptyElement from '../../../src/view/emptyelement';
+import UIElement from '../../../src/view/uielement';
 import Range from '../../../src/view/range';
 import Position from '../../../src/view/position';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
@@ -248,6 +249,27 @@ describe( 'writer', () => {
 					breakAttributes( range );
 				} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
 			} );
+
+			it( 'should throw if breaking inside UIElement #1', () => {
+				const span = new UIElement( 'span' );
+				new ContainerElement( 'p', null, span );
+				const position = new Position( span, 0 );
+
+				expect( () => {
+					breakAttributes( position );
+				} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+			} );
+
+			it( 'should throw if breaking inside UIElement #2', () => {
+				const span = new UIElement( 'span' );
+				const b = new AttributeElement( 'b' );
+				new ContainerElement( 'p', null, [ span, b ] );
+				const range = Range.createFromParentsAndOffsets( span, 0, b, 0 );
+
+				expect( () => {
+					breakAttributes( range );
+				} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+			} );
 		} );
 	} );
 } );

+ 11 - 0
packages/ckeditor5-engine/tests/view/writer/clear.js

@@ -9,6 +9,7 @@ import { stringify, parse } from '../../../src/dev-utils/view';
 import ContainerElement from '../../../src/view/containerelement';
 import AttributeElement from '../../../src/view/attributeelement';
 import EmptyElement from '../../../src/view/emptyelement';
+import UIElement from '../../../src/view/uielement';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 describe( 'writer', () => {
@@ -144,5 +145,15 @@ describe( 'writer', () => {
 				'<container:p>foobar</container:p>'
 			);
 		} );
+
+		it( 'should remove UIElement', () => {
+			const elementToRemove = new UIElement( 'span' );
+
+			test(
+				elementToRemove,
+				'<container:p>f{oo<ui:span></ui:span>ba}r</container:p>',
+				'<container:p>foobar</container:p>'
+			);
+		} );
 	} );
 } );

+ 12 - 0
packages/ckeditor5-engine/tests/view/writer/insert.js

@@ -7,6 +7,7 @@ import { insert } from '../../../src/view/writer';
 import ContainerElement from '../../../src/view/containerelement';
 import Element from '../../../src/view/element';
 import EmptyElement from '../../../src/view/emptyelement';
+import UIElement from '../../../src/view/uielement';
 import Position from '../../../src/view/position';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import { stringify, parse } from '../../../src/dev-utils/view';
@@ -193,5 +194,16 @@ describe( 'writer', () => {
 				insert( position, attributeElement );
 			} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
 		} );
+
+		it( 'should throw if trying to insert inside UIElement', () => {
+			const uiElement = new UIElement( 'span' );
+			new ContainerElement( 'p', null, uiElement );
+			const position = new Position( uiElement, 0 );
+			const attributeElement = new AttributeElement( 'i' );
+
+			expect( () => {
+				insert( position, attributeElement );
+			} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+		} );
 	} );
 } );

+ 7 - 0
packages/ckeditor5-engine/tests/view/writer/mergeAttributes.js

@@ -134,5 +134,12 @@ describe( 'writer', () => {
 				'<container:p><empty:img></empty:img>[]<empty:img></empty:img></container:p>'
 			);
 		} );
+
+		it( 'should not merge when placed between UIElements', () => {
+			test(
+				'<container:p><ui:span></ui:span>[]<ui:span></ui:span></container:p>',
+				'<container:p><ui:span></ui:span>[]<ui:span></ui:span></container:p>'
+			);
+		} );
 	} );
 } );

+ 24 - 0
packages/ckeditor5-engine/tests/view/writer/move.js

@@ -9,6 +9,7 @@ import { stringify, parse } from '../../../src/dev-utils/view';
 import ContainerElement from '../../../src/view/containerelement';
 import AttributeElement from '../../../src/view/attributeelement';
 import EmptyElement from '../../../src/view/emptyelement';
+import UIElement from '../../../src/view/uielement';
 import Range from '../../../src/view/range';
 import Position from '../../../src/view/position';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
@@ -150,5 +151,28 @@ describe( 'writer', () => {
 				move( srcRange, dstPosition );
 			} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
 		} );
+
+		it( 'should move UIElement', () => {
+			test(
+				'<container:p>foo[<ui:span></ui:span>]bar</container:p>',
+				'<container:div>baz{}quix</container:div>',
+				'<container:p>foobar</container:p>',
+				'<container:div>baz[<ui:span></ui:span>]quix</container:div>'
+			);
+		} );
+
+		it( 'should throw if trying to move to UIElement', () => {
+			const srcAttribute = new AttributeElement( 'b' );
+			const srcContainer = new ContainerElement( 'p', null, srcAttribute );
+			const srcRange = Range.createFromParentsAndOffsets( srcContainer, 0, srcContainer, 1 );
+
+			const dstUI = new UIElement( 'span' );
+			new ContainerElement( 'p', null, dstUI );
+			const dstPosition = new Position( dstUI, 0 );
+
+			expect( () => {
+				move( srcRange, dstPosition );
+			} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+		} );
 	} );
 } );

+ 20 - 0
packages/ckeditor5-engine/tests/view/writer/remove.js

@@ -10,6 +10,7 @@ import DocumentFragment from '../../../src/view/documentfragment';
 import { stringify, parse } from '../../../src/dev-utils/view';
 import AttributeElement from '../../../src/view/attributeelement';
 import EmptyElement from '../../../src/view/emptyelement';
+import UIElement from '../../../src/view/uielement';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 describe( 'writer', () => {
@@ -128,5 +129,24 @@ describe( 'writer', () => {
 				remove( range );
 			} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
 		} );
+
+		it( 'should remove UIElement', () => {
+			test(
+				'<container:p>foo[<ui:span></ui:span>]bar</container:p>',
+				'<container:p>foo{}bar</container:p>',
+				'<ui:span></ui:span>'
+			);
+		} );
+
+		it( 'should throw if range is placed inside UIElement', () => {
+			const uiElement = new UIElement( 'span' );
+			const attributeElement = new AttributeElement( 'b' );
+			new ContainerElement( 'p', null, [ uiElement, attributeElement ] );
+			const range = Range.createFromParentsAndOffsets( uiElement, 0, attributeElement, 0 );
+
+			expect( () => {
+				remove( range );
+			} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+		} );
 	} );
 } );

+ 20 - 0
packages/ckeditor5-engine/tests/view/writer/unwrap.js

@@ -8,6 +8,7 @@ import Element from '../../../src/view/element';
 import ContainerElement from '../../../src/view/containerelement';
 import AttributeElement from '../../../src/view/attributeelement';
 import EmptyElement from '../../../src/view/emptyelement';
+import UIElement from '../../../src/view/uielement';
 import Position from '../../../src/view/position';
 import Range from '../../../src/view/range';
 import Text from '../../../src/view/text';
@@ -316,5 +317,24 @@ describe( 'writer', () => {
 				unwrap( range, attribute );
 			} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
 		} );
+
+		it( 'should unwrap UIElement', () => {
+			test(
+				'<container:p>[<attribute:b><ui:span></ui:span></attribute:b>]</container:p>',
+				'<attribute:b></attribute:b>',
+				'<container:p>[<ui:span></ui:span>]</container:p>'
+			);
+		} );
+
+		it( 'should throw if range is placed inside UIElement', () => {
+			const uiElement = new UIElement( 'span' );
+			const attribute = new AttributeElement( 'b' );
+			const container = new ContainerElement( 'p', null, [ uiElement, attribute ] );
+			const range = Range.createFromParentsAndOffsets( uiElement, 0, container, 2 );
+
+			expect( () => {
+				unwrap( range, attribute );
+			} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+		} );
 	} );
 } );

+ 19 - 0
packages/ckeditor5-engine/tests/view/writer/wrap.js

@@ -8,6 +8,7 @@ import Element from '../../../src/view/element';
 import ContainerElement from '../../../src/view/containerelement';
 import AttributeElement from '../../../src/view/attributeelement';
 import EmptyElement from '../../../src/view/emptyelement';
+import UIElement from '../../../src/view/uielement';
 import Position from '../../../src/view/position';
 import Range from '../../../src/view/range';
 import Text from '../../../src/view/text';
@@ -299,5 +300,23 @@ describe( 'writer', () => {
 				wrap( range, new AttributeElement( 'b' ) );
 			} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
 		} );
+
+		it( 'should wrap UIElement', () => {
+			test(
+				'<container:p>[<ui:span></ui:span>]</container:p>',
+				'<attribute:b></attribute:b>',
+				'<container:p>[<attribute:b view-priority="10"><ui:span></ui:span></attribute:b>]</container:p>'
+			);
+		} );
+
+		it( 'should throw if range is inside UIElement', () => {
+			const uiElement = new UIElement( 'span' );
+			const container = new ContainerElement( 'p', null, uiElement );
+			const range = Range.createFromParentsAndOffsets( uiElement, 0, container, 1 );
+
+			expect( () => {
+				wrap( range, new AttributeElement( 'b' ) );
+			} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
+		} );
 	} );
 } );

+ 12 - 0
packages/ckeditor5-engine/tests/view/writer/wrapposition.js

@@ -9,6 +9,7 @@ import Element from '../../../src/view/element';
 import ContainerElement from '../../../src/view/containerelement';
 import AttributeElement from '../../../src/view/attributeelement';
 import EmptyElement from '../../../src/view/emptyelement';
+import UIElement from '../../../src/view/uielement';
 import Position from '../../../src/view/position';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import { stringify, parse } from '../../../src/dev-utils/view';
@@ -133,4 +134,15 @@ describe( 'wrapPosition', () => {
 			wrapPosition( position, attributeElement );
 		} ).to.throw( CKEditorError, 'view-emptyelement-cannot-add' );
 	} );
+
+	it( 'should throw if position is set inside UIElement', () => {
+		const uiElement = new UIElement( 'span' );
+		new ContainerElement( 'p', null, uiElement );
+		const attributeElement = new AttributeElement( 'b' );
+		const position = new Position( uiElement, 0 );
+
+		expect( () => {
+			wrapPosition( position, attributeElement );
+		} ).to.throw( CKEditorError, 'view-uielement-cannot-add' );
+	} );
 } );