Selaa lähdekoodia

Improvements in treeModel.Selection: added hasAnyRange, getFirstRange, getFirstPosition.

Szymon Cofalik 9 vuotta sitten
vanhempi
sitoutus
60f4835c08

+ 33 - 2
packages/ckeditor5-engine/src/treemodel/selection.js

@@ -5,6 +5,8 @@
 
 'use strict';
 
+import Position from './position.js';
+import Range from './range.js';
 import LiveRange from './liverange.js';
 import EmitterMixin from '../emittermixin.js';
 import CKEditorError from '../ckeditorerror.js';
@@ -56,7 +58,7 @@ export default class Selection {
 	 * @type {core.treeModel.LivePosition|null}
 	 */
 	get anchor() {
-		if ( this._ranges.length > 0 ) {
+		if ( this.hasAnyRange ) {
 			let range = this._ranges[ this._ranges.length - 1 ];
 
 			return this._lastRangeBackward ? range.end : range.start;
@@ -73,7 +75,7 @@ export default class Selection {
 	 * @type {core.treeModel.LivePosition|null}
 	 */
 	get focus() {
-		if ( this._ranges.length > 0 ) {
+		if ( this.hasAnyRange ) {
 			let range = this._ranges[ this._ranges.length - 1 ];
 
 			return this._lastRangeBackward ? range.start : range.end;
@@ -82,12 +84,21 @@ export default class Selection {
 		return null;
 	}
 
+	get hasAnyRange() {
+		return this._ranges.length > 0;
+	}
+
 	/**
 	 * Returns whether the selection is collapsed. Selection is collapsed when all it's ranges are collapsed.
+	 * If selection has no ranges, returns null instead.
 	 *
 	 * @type {Boolean}
 	 */
 	get isCollapsed() {
+		if ( !this.hasAnyRange ) {
+			return null;
+		}
+
 		for ( let i = 0; i < this._ranges.length; i++ ) {
 			if ( !this._ranges[ i ].isCollapsed ) {
 				return false;
@@ -134,6 +145,26 @@ export default class Selection {
 		return this._ranges.slice();
 	}
 
+	getFirstRange() {
+		let first = null;
+
+		for ( let i = 0; i < this._ranges.length; i++ ) {
+			let range = this._ranges[ i ];
+
+			if ( !first || range.start.isBefore( first.start ) ) {
+				first = range;
+			}
+		}
+
+		return first && Range.createFromRange( first );
+	}
+
+	getFirstPosition() {
+		let firstRange = this.getFirstRange();
+
+		return firstRange && Position.createFromPosition( firstRange.start );
+	}
+
 	/**
 	 * Removes all ranges that were added to the selection. Fires update event.
 	 *

+ 64 - 11
packages/ckeditor5-engine/tests/treemodel/selection.js

@@ -50,22 +50,26 @@ describe( 'Selection', () => {
 		expect( selection._attrs.size ).to.equal( 0 );
 	} );
 
-	it( 'should be collapsed if it has no ranges or all ranges are collapsed', () => {
-		expect( selection.isCollapsed ).to.be.true;
+	describe( 'isCollapsed', () => {
+		it( 'should be null if there are no ranges in it', () => {
+			expect( selection.isCollapsed ).to.be.null;
+		} );
 
-		selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
+		it( 'should be true if all ranges are collapsed', () => {
+			selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
 
-		expect( selection.isCollapsed ).to.be.true;
-	} );
+			expect( selection.isCollapsed ).to.be.true;
+		} );
 
-	it( 'should not be collapsed when it has a range that is not collapsed', () => {
-		selection.addRange( liveRange );
+		it( 'should be false when it has a range that is not collapsed', () => {
+			selection.addRange( range );
 
-		expect( selection.isCollapsed ).to.be.false;
+			expect( selection.isCollapsed ).to.be.false;
 
-		selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
+			selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
 
-		expect( selection.isCollapsed ).to.be.false;
+			expect( selection.isCollapsed ).to.be.false;
+		} );
 	} );
 
 	it( 'should copy added ranges and store multiple ranges', () => {
@@ -191,7 +195,7 @@ describe( 'Selection', () => {
 			expect( selection.getRanges().length ).to.equal( 0 );
 			expect( selection.anchor ).to.be.null;
 			expect( selection.focus ).to.be.null;
-			expect( selection.isCollapsed ).to.be.true;
+			expect( selection.isCollapsed ).to.be.null;
 		} );
 
 		it( 'should fire exactly one update event', () => {
@@ -266,6 +270,55 @@ describe( 'Selection', () => {
 		} );
 	} );
 
+	describe( 'hasAnyRange', () => {
+		it( 'should return false if there are no ranges added to the selection', () => {
+			selection.removeAllRanges();
+			expect( selection.hasAnyRange ).to.be.false;
+		} );
+
+		it( 'should return true if there is at least on range in the selection', () => {
+			selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
+			expect( selection.hasAnyRange ).to.be.true;
+		} );
+	} );
+
+	describe( 'getFirstRange', () => {
+		it( 'should return null if there are no ranges in selection', () => {
+			selection.removeAllRanges();
+			expect( selection.getFirstRange() ).to.be.null;
+		} );
+
+		it( 'should return a range which start position is before all other ranges\' start positions', () => {
+			// This will not be the first range despite being added as first
+			selection.addRange( new Range( new Position( root, [ 4 ] ), new Position( root, [ 5 ] ) ) );
+
+			selection.addRange( new Range( new Position( root, [ 1 ] ), new Position( root, [ 4 ] ) ) );
+
+			let range = selection.getFirstRange();
+
+			expect( range.start.path ).to.deep.equal( [ 1 ] );
+			expect( range.end.path ).to.deep.equal( [ 4 ] );
+		} );
+	} );
+
+	describe( 'getFirstPosition', () => {
+		it( 'should return null if there are no ranges in selection', () => {
+			selection.removeAllRanges();
+			expect( selection.getFirstPosition() ).to.be.null;
+		} );
+
+		it( 'should return a position that is in selection and is before any other position from the selection', () => {
+			// This will not be a range containing the first position despite being added as first
+			selection.addRange( new Range( new Position( root, [ 4 ] ), new Position( root, [ 5 ] ) ) );
+
+			selection.addRange( new Range( new Position( root, [ 1 ] ), new Position( root, [ 4 ] ) ) );
+
+			let position = selection.getFirstPosition();
+
+			expect( position.path ).to.deep.equal( [ 1 ] );
+		} );
+	} );
+
 	// Selection uses LiveRanges so here are only simple test to see if integration is
 	// working well, without getting into complicated corner cases.
 	describe( 'after applying an operation should get updated and not fire update event', () => {