Browse Source

Merge pull request #1829 from ckeditor/i/6364

Feature: Implemented the model and view `Range#getContainedElement()` methods. Closes ckeditor/ckeditor5#6364.
Piotrek Koszuliński 5 years ago
parent
commit
f0d475e6a4

+ 22 - 0
packages/ckeditor5-engine/src/model/range.js

@@ -502,6 +502,28 @@ export default class Range {
 	}
 
 	/**
+	 * Returns an {@link module:engine/model/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/model/element~Element|null}
+	 */
+	getContainedElement() {
+		if ( this.isCollapsed ) {
+			return null;
+		}
+
+		const nodeAfterStart = this.start.nodeAfter;
+		const nodeBeforeEnd = this.end.nodeBefore;
+
+		if ( nodeAfterStart && nodeAfterStart.is( 'element' ) && nodeAfterStart === nodeBeforeEnd ) {
+			return nodeAfterStart;
+		}
+
+		return null;
+	}
+
+	/**
 	 * Converts `Range` to plain object and returns it.
 	 *
 	 * @returns {Object} `Node` converted to plain object.

+ 1 - 6
packages/ckeditor5-engine/src/model/selection.js

@@ -8,7 +8,6 @@
  */
 
 import Position from './position';
-import Element from './element';
 import Node from './node';
 import Range from './range';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
@@ -615,11 +614,7 @@ export default class Selection {
 			return null;
 		}
 
-		const range = this.getFirstRange();
-		const nodeAfterStart = range.start.nodeAfter;
-		const nodeBeforeEnd = range.end.nodeBefore;
-
-		return ( nodeAfterStart instanceof Element && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null;
+		return this.getFirstRange().getContainedElement();
 	}
 
 	/**

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

@@ -336,6 +336,45 @@ export default class Range {
 	}
 
 	/**
+	 * 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.
 	 *
 	 * @returns {module:engine/view/range~Range}

+ 1 - 24
packages/ckeditor5-engine/src/view/selection.js

@@ -13,7 +13,6 @@ import Position from './position';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import Node from './node';
-import Element from './element';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 import DocumentSelection from './documentselection';
@@ -413,29 +412,7 @@ export default class Selection {
 			return null;
 		}
 
-		const range = this.getFirstRange();
-
-		let nodeAfterStart = range.start.nodeAfter;
-		let nodeBeforeEnd = range.end.nodeBefore;
-
-		// Handle the situation when selection position is at the beginning / at the end of a text node.
-		// In such situation `.nodeAfter` and `.nodeBefore` are `null` but the selection 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 selections, only the difference is if the selection position is at
-		// at the end/at the beginning of a text node or just before/just after the text node.
-		//
-		if ( range.start.parent.is( 'text' ) && range.start.isAtEnd && range.start.parent.nextSibling ) {
-			nodeAfterStart = range.start.parent.nextSibling;
-		}
-
-		if ( range.end.parent.is( 'text' ) && range.end.isAtStart && range.end.parent.previousSibling ) {
-			nodeBeforeEnd = range.end.parent.previousSibling;
-		}
-
-		return ( nodeAfterStart instanceof Element && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null;
+		return this.getFirstRange().getContainedElement();
 	}
 
 	/**

+ 41 - 0
packages/ckeditor5-engine/tests/model/range.js

@@ -1351,6 +1351,47 @@ describe( 'Range', () => {
 		} );
 	} );
 
+	describe( 'getContainedElement()', () => {
+		beforeEach( () => {
+			prepareRichRoot( root );
+		} );
+
+		it( 'should return an element when it is fully contained by the range', () => {
+			// <div><h>first</h><p>lorem ipsum</p></div>[<p>foo</p>]<p>bar</p><div><h>second</h><p>lorem</p></div>
+			const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
+
+			expect( range.getContainedElement() ).to.equal( root.getNodeByPath( [ 1 ] ) );
+		} );
+
+		it( 'should return "null" if the range is collapsed', () => {
+			// <div><h>first</h><p>lorem ipsum</p></div>[]<p>foo</p><p>bar</p><div><h>second</h><p>lorem</p></div>
+			const range = new Range( new Position( root, [ 1 ] ) );
+
+			expect( range.getContainedElement() ).to.be.null;
+		} );
+
+		it( 'should return "null" if it contains 2+ elements', () => {
+			// <div><h>first</h><p>lorem ipsum</p></div>[<p>foo</p><p>bar</p>]<div><h>second</h><p>lorem</p></div>
+			const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 3 ] ) );
+
+			expect( range.getContainedElement() ).to.be.null;
+		} );
+
+		it( 'should return "null" if it contains an element and some other nodes', () => {
+			// <div><h>first</h><p>lorem ipsum</p></div>[<p>foo</p><p>ba]r</p><div><h>second</h><p>lorem</p></div>
+			const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2, 2 ] ) );
+
+			expect( range.getContainedElement() ).to.be.null;
+		} );
+
+		it( 'should return "null" if it fully contains a node but the node is not an element', () => {
+			// <div><h>first</h><p>lorem ipsum</p></div><p>foo</p><p>[bar]</p><div><h>second</h><p>lorem</p></div>
+			const range = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 3 ] ) );
+
+			expect( range.getContainedElement() ).to.be.null;
+		} );
+	} );
+
 	function mapNodesToNames( nodes ) {
 		return nodes.map( node => {
 			return ( node instanceof Element ) ? 'E:' + node.name : 'T:' + node.data;

+ 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;
+		} );
+	} );
 } );