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

Implemented the (View)Range#getContainedElement() method.

Aleksander Nowodzinski 5 лет назад
Родитель
Сommit
7c750250d2

+ 39 - 0
packages/ckeditor5-engine/src/view/range.js

@@ -335,6 +335,45 @@ export default class Range {
 		return this.start.getCommonAncestor( this.end );
 	}
 
+	/**
+	 * Returns an {@link module:engine/view/element~Element Element} contained by the range.
+	 * The element will be returned when it is the **only** node within the range and **fully–contained**
+	 * at the same time.
+	 *
+	 * @returns {module:engine/view/element~Element|null}
+	 */
+	getContainedElement() {
+		if ( this.isCollapsed ) {
+			return null;
+		}
+
+		let nodeAfterStart = this.start.nodeAfter;
+		let nodeBeforeEnd = this.end.nodeBefore;
+
+		// Handle the situation when the range position is at the beginning / at the end of a text node.
+		// In such situation `.nodeAfter` and `.nodeBefore` are `null` but the range still might be spanning
+		// over one element.
+		//
+		// <p>Foo{<span class="widget"></span>}bar</p> vs <p>Foo[<span class="widget"></span>]bar</p>
+		//
+		// These are basically the same range, only the difference is if the range position is at
+		// at the end/at the beginning of a text node or just before/just after the text node.
+		//
+		if ( this.start.parent.is( 'text' ) && this.start.isAtEnd && this.start.parent.nextSibling ) {
+			nodeAfterStart = this.start.parent.nextSibling;
+		}
+
+		if ( this.end.parent.is( 'text' ) && this.end.isAtStart && this.end.parent.previousSibling ) {
+			nodeBeforeEnd = this.end.parent.previousSibling;
+		}
+
+		if ( nodeAfterStart && nodeAfterStart.is( 'element' ) && nodeAfterStart === nodeBeforeEnd ) {
+			return nodeAfterStart;
+		}
+
+		return null;
+	}
+
 	/**
 	 * Clones this range.
 	 *

+ 46 - 0
packages/ckeditor5-engine/tests/view/range.js

@@ -772,4 +772,50 @@ describe( 'Range', () => {
 			expect( range.getCommonAncestor() ).to.equal( ul );
 		} );
 	} );
+
+	describe( 'getContainedElement()', () => {
+		it( 'should return an element when it is fully contained by the range', () => {
+			const { selection, view } = parse( 'foo [<b>bar</b>] baz' );
+			const range = selection.getFirstRange();
+			const element = view.getChild( 1 );
+
+			expect( range.getContainedElement() ).to.equal( element );
+		} );
+
+		it( 'should return selected element if the range is anchored at the end/at the beginning of a text node', () => {
+			const { selection, view } = parse( 'foo {<b>bar</b>} baz' );
+			const range = selection.getFirstRange();
+			const element = view.getChild( 1 );
+
+			expect( range.getContainedElement() ).to.equal( element );
+		} );
+
+		it( 'should return "null" if the selection is collapsed', () => {
+			const { selection } = parse( 'foo []<b>bar</b> baz' );
+			const range = selection.getFirstRange();
+
+			expect( range.getContainedElement() ).to.be.null;
+		} );
+
+		it( 'should return "null" if it contains 2+ elements', () => {
+			const { selection } = parse( 'foo [<b>bar</b><i>qux</i>] baz' );
+			const range = selection.getFirstRange();
+
+			expect( range.getContainedElement() ).to.be.null;
+		} );
+
+		it( 'should return "null" if the range spans over more than a single element', () => {
+			const { selection } = parse( 'foo [<b>bar</b> ba}z' );
+			const range = selection.getFirstRange();
+
+			expect( range.getContainedElement() ).to.be.null;
+		} );
+
+		it( 'should return "null" if the range spans over a single text node', () => {
+			const { selection } = parse( 'foo <b>{bar}</b> baz' );
+			const range = selection.getFirstRange();
+
+			expect( range.getContainedElement() ).to.be.null;
+		} );
+	} );
 } );