Jelajahi Sumber

Refactored text proxy.

Oskar Wrobel 9 tahun lalu
induk
melakukan
616d117dfa

+ 2 - 2
packages/ckeditor5-engine/src/view/position.js

@@ -206,7 +206,7 @@ export default class Position {
 
 		// {@link engine.view.TextProxy} is not a instance of {@link engine.view.Node} so we need do handle it in specific way.
 		if ( node instanceof TextProxy ) {
-			return new Position( node._textNodeParent, node._index + 1 );
+			return new Position( node._textNode, node._index + 1 );
 		}
 
 		return new Position( node.parent, node.getIndex() + 1 );
@@ -231,7 +231,7 @@ export default class Position {
 
 		// {@link engine.view.TextProxy} is not a instance of {@link engine.view.Node} so we need do handle it in specific way.
 		if ( node instanceof TextProxy ) {
-			return new Position( node._textNodeParent, node._index );
+			return new Position( node._textNode, node._index );
 		}
 
 		return new Position( node.parent, node.getIndex() );

+ 67 - 11
packages/ckeditor5-engine/src/view/textproxy.js

@@ -5,6 +5,8 @@
 
 'use strict';
 
+import Node from './node.js';
+
 /**
  * Tree view text proxy.
  * It is a wrapper for substring of {@link engine.view.Text}.
@@ -15,28 +17,27 @@ export default class TextProxy {
 	/**
 	 * Creates a tree view text proxy.
 	 *
-	 * @param {String} text Substring of {#_textNodeParent}.
-	 * @param {engine.view.Node} parent Parent of {#_textNodeParent}.
-	 * @param {engine.view.Text} textNodeParent Text node which text proxy is a substring.
-     * @param {Number} index Offset from beginning of {#_textNodeParent} and first character of {#_data}.
-     */
-	constructor( text, parent, textNodeParent, index ) {
+	 * @param {engine.view.Text} textNode Text node which text proxy is a substring.
+	 * @param {Number} startOffset Offset from beginning of {#_textNodeParent} and first character of {#_data}.
+	 * @param {Number} [length] Length of substring. If is not set the end offset is at the end of {#_textNodeParent}.
+	 */
+	constructor( textNode, startOffset, length ) {
 		/**
 		 * Element that is a parent of this node.
 		 *
 		 * @readonly
 		 * @member {engine.view.Element|engine.view.DocumentFragment|null} engine.view.Node#parent
 		 */
-		this.parent = parent;
+		this.parent = textNode.parent;
 
 		/**
 		 * Reference to the {@link engine.view.Text} element which TextProxy is a substring.
 		 *
 		 * @protected
 		 * @readonly
-		 * @member {engine.view.Text} engine.view.TextProxy#_textNodeParent
+		 * @member {engine.view.Text} engine.view.TextProxy#_textNode
 		 */
-		this._textNodeParent = textNodeParent;
+		this._textNode = textNode;
 
 		/**
 		 * Index of the substring in the `textParent`.
@@ -45,7 +46,7 @@ export default class TextProxy {
 		 * @readonly
 		 * @member {Number} engine.view.TextProxy#_index
 		 */
-		this._index = index;
+		this._index = startOffset;
 
 		/**
 		 * The text content.
@@ -54,6 +55,61 @@ export default class TextProxy {
 		 * @readonly
 		 * @member {String} engine.view.TextProxy#_data
 		 */
-		this._data = text;
+
+		this._data = textNode._data.substring(
+			startOffset,
+			startOffset + ( length || textNode._data.length - startOffset )
+		);
+	}
+
+	/**
+	 * Gets {@link engine.view.Document} reference, from the {@link engine.view.Node#getRoot root} or
+	 * returns null if the root has no reference to the {@link engine.view.Document}.
+	 *
+	 * @returns {engine.view.Document|null} View Document of the node or null.
+	 */
+	getDocument() {
+		// Parent might be Node, null or DocumentFragment.
+		if ( this.parent instanceof Node ) {
+			return this.parent.getDocument();
+		} else {
+			return null;
+		}
+	}
+
+	/**
+	 * Gets the top parent for the node. If node has no parent it is the root itself.
+	 *
+	 * @returns {engine.view.Node}
+	 */
+	getRoot() {
+		let root = this;
+
+		while ( root.parent ) {
+			root = root.parent;
+		}
+
+		return root;
+	}
+
+	/**
+	 * Returns ancestors array of this node.
+	 *
+	 * @param {Object} options Options object.
+	 * @param {Boolean} [options.includeNode=false] When set to `true` this node will be also included in parent's array.
+	 * @param {Boolean} [options.parentFirst=false] When set to `true`, array will be sorted from node's parent to root element,
+	 * otherwise root element will be the first item in the array.
+	 * @returns {Array} Array with ancestors.
+	 */
+	getAncestors( options = { includeNode: false, parentFirst: false } ) {
+		const ancestors = [];
+		let parent = options.includeNode ? this : this.parent;
+
+		while ( parent !== null ) {
+			ancestors[ options.parentFirst ? 'push' : 'unshift' ]( parent );
+			parent = parent.parent;
+		}
+
+		return ancestors;
 	}
 }

+ 15 - 16
packages/ckeditor5-engine/src/view/treewalker.js

@@ -204,16 +204,15 @@ export default class TreeWalker {
 				return this._next();
 			} else {
 				let charactersCount = node._data.length;
+				let prevPosition = previousPosition;
 				let item = node;
 
-				// If text stick out of walker range, we need to cut it.
+				// If text stick out of walker range, we need to cut it and wrap by TextProxy.
 				if ( node == this._boundaryEndParent ) {
-					const offset = this.boundaries.end.offset;
-					const textFragment = node._data.substring( 0, offset );
-
-					charactersCount = textFragment.length;
-					item = new TextProxy( textFragment, node.parent, node, 0 );
+					charactersCount = this.boundaries.end.offset;
+					item = new TextProxy( node, 0, charactersCount );
 					position = Position.createAfter( item );
+					prevPosition = Position.createBefore( item );
 				} else {
 					// If not just move forward.
 					position.offset++;
@@ -221,13 +220,11 @@ export default class TreeWalker {
 
 				this.position = position;
 
-				return formatReturnValue( 'TEXT', item, previousPosition, position, charactersCount );
+				return formatReturnValue( 'TEXT', item, prevPosition, position, charactersCount );
 			}
 		} else if ( typeof node == 'string' ) {
+			const textProxy = new TextProxy( parent, position.offset, 1 );
 			position.offset++;
-
-			const textProxy = new TextProxy( node, parent.parent, parent, position.offset );
-
 			this.position = position;
 
 			return formatReturnValue( 'TEXT', textProxy, previousPosition, position, 1 );
@@ -302,16 +299,18 @@ export default class TreeWalker {
 				return this._previous();
 			} else {
 				let charactersCount = node._data.length;
+				let prevPosition = previousPosition;
 				let item = node;
 
-				// If text stick out of walker range, we need to cut it.
+				// If text stick out of walker range, we need to cut it and wrap by TextProxy.
 				if ( node == this._boundaryStartParent ) {
 					const offset = this.boundaries.start.offset;
-					const textFragment = node._data.substring( offset, charactersCount );
 
-					charactersCount = textFragment.length;
-					item = new TextProxy( textFragment, node.parent, node, offset );
+					item = new TextProxy( node, offset );
+
 					position = Position.createBefore( item );
+					prevPosition = Position.createAfter( item );
+					charactersCount = item._data.length;
 				} else {
 					// If not just move backward.
 					position.offset--;
@@ -319,12 +318,12 @@ export default class TreeWalker {
 
 				this.position = position;
 
-				return formatReturnValue( 'TEXT', item, previousPosition, position, charactersCount );
+				return formatReturnValue( 'TEXT', item, prevPosition, position, charactersCount );
 			}
 		} else if ( typeof node == 'string' ) {
 			position.offset--;
 
-			const textProxy = new TextProxy( node, parent.parent, parent, position.offset );
+			const textProxy = new TextProxy( parent, position.offset, 1 );
 
 			this.position = position;
 

+ 85 - 9
packages/ckeditor5-engine/tests/view/textproxy.js

@@ -10,21 +10,97 @@
 import TextProxy from '/ckeditor5/engine/view/textproxy.js';
 import Text from '/ckeditor5/engine/view/text.js';
 import ContainerElement from '/ckeditor5/engine/view/containerelement.js';
+import DocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
+import RootEditableElement from '/ckeditor5/engine/view/rooteditableelement.js';
+
+import createDocumentMock from '/tests/engine/view/_utils/createdocumentmock.js';
 
 describe( 'TextProxy', () => {
+	let text, parent, wrapper, textProxy;
+
+	beforeEach( () => {
+		text = new Text( 'abcdefgh' );
+		parent = new ContainerElement( 'p', [], [ text ] );
+		wrapper = new ContainerElement( 'div', [], parent );
+
+		textProxy = new TextProxy( text, 2, 3 );
+	} );
+
 	describe( 'constructor', () => {
 		it( 'should create TextProxy instance with specified properties', () => {
-			const textElement = new Text( 'abcdefgh' );
-			const textParent = new ContainerElement( 'p', [], [ textElement ] );
-			const textFragment = 'cdef';
-			const index = 2;
+			expect( textProxy ).to.have.property( 'parent' ).to.equal( parent );
+			expect( textProxy ).to.have.property( '_data' ).to.equal( 'cde' );
+			expect( textProxy ).to.have.property( '_textNode' ).to.equal( text );
+			expect( textProxy ).to.have.property( '_index' ).to.equal( 2 );
+		} );
+	} );
+
+	describe( 'getDocument', () => {
+		it( 'should return null if any parent has not set Document', () => {
+			expect( textProxy.getDocument() ).to.be.null;
+		} );
+
+		it( 'should return Document attached to the parent element', () => {
+			const docMock = createDocumentMock();
+			const root = new RootEditableElement( docMock, 'div' );
+
+			wrapper.parent = root;
+
+			expect( textProxy.getDocument() ).to.equal( docMock );
+		} );
+
+		it( 'should return null if element is inside DocumentFragment', () => {
+			new DocumentFragment( [ wrapper ] );
+
+			expect( textProxy.getDocument() ).to.be.null;
+		} );
+	} );
+
+	describe( 'getRoot', () => {
+		it( 'should return root element', () => {
+			const root = new RootEditableElement( createDocumentMock(), 'div' );
+
+			wrapper.parent = root;
+
+			expect( textProxy.getRoot() ).to.equal( root );
+		} );
+	} );
+
+	describe( 'getAncestors', () => {
+		it( 'should return array of ancestors', () => {
+			const result = textProxy.getAncestors();
+
+			expect( result ).to.be.an( 'array' );
+			expect( result ).to.length( 2 );
+			expect( result[ 0 ] ).to.equal( wrapper );
+			expect( result[ 1 ] ).to.equal( parent );
+		} );
+
+		it( 'should return array of ancestors starting from parent `parentFirst`', () => {
+			const result = textProxy.getAncestors( { parentFirst: true } );
+
+			expect( result.length ).to.equal( 2 );
+			expect( result[ 0 ] ).to.equal( parent );
+			expect( result[ 1 ] ).to.equal( wrapper );
+		} );
+
+		it( 'should return array including node itself `includeNode`', () => {
+			const result = textProxy.getAncestors( { includeNode: true } );
+
+			expect( result ).to.be.an( 'array' );
+			expect( result ).to.length( 3 );
+			expect( result[ 0 ] ).to.equal( wrapper );
+			expect( result[ 1 ] ).to.equal( parent );
+			expect( result[ 2 ] ).to.equal( textProxy );
+		} );
 
-			const textProxy = new TextProxy( textFragment, textParent, textElement, index );
+		it( 'should return array of ancestors including node itself `includeNode` starting from parent `parentFirst`', () => {
+			const result = textProxy.getAncestors( { includeNode: true, parentFirst: true } );
 
-			expect( textProxy ).to.have.property( 'parent' ).to.equal( textParent );
-			expect( textProxy ).to.have.property( '_data' ).to.equal( textFragment );
-			expect( textProxy ).to.have.property( '_textNodeParent' ).to.equal( textElement );
-			expect( textProxy ).to.have.property( '_index' ).to.equal( index );
+			expect( result.length ).to.equal( 3 );
+			expect( result[ 0 ] ).to.equal( textProxy );
+			expect( result[ 1 ] ).to.equal( parent );
+			expect( result[ 2 ] ).to.equal( wrapper );
 		} );
 	} );
 } );

+ 16 - 7
packages/ckeditor5-engine/tests/view/treewalker.js

@@ -558,7 +558,7 @@ describe( 'TreeWalker', () => {
 	} );
 } );
 
-function expectValue( value, expected, options ) {
+function expectValue( value, expected, options = {} ) {
 	expect( value.type ).to.equal( expected.type );
 
 	if ( value.type == 'TEXT' ) {
@@ -570,16 +570,25 @@ function expectValue( value, expected, options ) {
 	}
 }
 
-function expectText( value, expected ) {
+function expectText( value, expected, options ) {
+	let previousPosition, nextPosition;
+
 	expect( value.item._data ).to.equal( expected.text );
 	expect( value.length ).to.equal( value.item._data.length );
 
-	/**
-	 * @TODO: Checking previous and next position.
-	 */
+	if ( options.direction == 'BACKWARD' ) {
+		previousPosition = Position.createAfter( value.item );
+		nextPosition = Position.createBefore( value.item );
+	} else {
+		previousPosition = Position.createBefore( value.item );
+		nextPosition = Position.createAfter( value.item );
+	}
+
+	expect( value.previousPosition ).to.deep.equal( previousPosition );
+	expect( value.nextPosition ).to.deep.equal( nextPosition );
 }
 
-function expectStart( value, expected, options = {} ) {
+function expectStart( value, expected, options ) {
 	let previousPosition, nextPosition;
 
 	expect( value.item ).to.equal( expected.item );
@@ -600,7 +609,7 @@ function expectStart( value, expected, options = {} ) {
 	}
 }
 
-function expectEnd( value, expected, options = {} ) {
+function expectEnd( value, expected, options ) {
 	let previousPosition, nextPosition;
 
 	expect( value.item ).to.equal( expected.item );