浏览代码

Added events, collapsing methods and tests to treeView.Selection.

Szymon Kupś 9 年之前
父节点
当前提交
2aa388f18e
共有 2 个文件被更改,包括 259 次插入29 次删除
  1. 133 29
      packages/ckeditor5-engine/src/treeview/selection.js
  2. 126 0
      packages/ckeditor5-engine/tests/treeview/selection.js

+ 133 - 29
packages/ckeditor5-engine/src/treeview/selection.js

@@ -8,6 +8,8 @@
 import CKEditorError from '../../utils/ckeditorerror.js';
 import Range from './range.js';
 import Position from './position.js';
+import utils from '../../utils/utils.js';
+import EmitterMixin from '../../utils/emittermixin.js';
 
 /**
  * @memberOf engine.treeModel
@@ -45,8 +47,9 @@ export default class Selection {
 			return null;
 		}
 		const range = this._ranges[ this._ranges.length - 1 ];
+		const anchor = this._lastRangeBackward ? range.end : range.start;
 
-		return this._lastRangeBackward ? range.end : range.start;
+		return Position.createFromPosition( anchor );
 	}
 
 	/**
@@ -60,8 +63,9 @@ export default class Selection {
 			return null;
 		}
 		const range = this._ranges[ this._ranges.length - 1 ];
+		const focus = this._lastRangeBackward ? range.start : range.end;
 
-		return this._lastRangeBackward ? range.start : range.end;
+		return Position.createFromPosition( focus );
 	}
 
 	/**
@@ -70,8 +74,8 @@ export default class Selection {
 	 * @type {Boolean}
 	 */
 	get isCollapsed() {
-		for ( let i = 0; i < this._ranges.length; i++ ) {
-			if ( !this._ranges[ i ].isCollapsed ) {
+		for ( let range of this._ranges ) {
+			if ( !range.isCollapsed ) {
 				return false;
 			}
 		}
@@ -80,9 +84,9 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns nuber of ranges in selection.
+	 * Returns number of ranges in selection.
 	 *
-	 * @returns {Number}
+	 * @type {Number}
      */
 	get rangeCount() {
 		return this._ranges.length;
@@ -103,24 +107,9 @@ export default class Selection {
 	 * @param {engine.treeView.Range} range
 	 */
 	addRange( range, isBackward ) {
-		for ( let storedRange of this._ranges ) {
-			if ( range.isIntersecting( storedRange ) ) {
-				/**
-				 * Trying to add a range that intersects with another range from selection.
-				 *
-				 * @error view-selection-range-intersects
-				 * @param {engine.treeView.Range} addedRange Range that was added to the selection.
-				 * @param {engine.treeView.Range} intersectingRange Range from selection that intersects with `addedRange`.
-				 */
-				throw new CKEditorError(
-					'view-selection-range-intersects: Trying to add a range that intersects with another range from selection.',
-					{ addedRange: range, intersectingRange: storedRange }
-				);
-			}
-		}
-
-		this._ranges.push( Range.createFromRange( range ) );
+		this._pushRange( range );
 		this._lastRangeBackward = !!isBackward;
+		this.fire( 'change:range' );
 	}
 
 	/**
@@ -134,9 +123,22 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns the first range in the selection. First range is the one which {@link engine.treeView.Range#start start}
-	 * position {@link engine.treeView.Position#isBefore is before} start position of all other ranges (not to confuse
-	 * with the first range added to the selection). Returns `null` if no ranges are added to selection.
+	 * Returns copy of a range at specified index, or `null` if there is no range under that index.
+	 *
+	 * @param {Number} index Index of range in selection.
+	 * @returns {engine.treeView.Range|null}
+	 */
+	getRangeAt( index ) {
+		const range = this._ranges[ index ];
+
+		return range ? Range.createFromRange( range ) : null;
+	}
+
+	/**
+	 * Returns copy of the first range in the selection. First range is the one which
+	 * {@link engine.treeView.Range#start start} position {@link engine.treeView.Position#isBefore is before} start
+	 * position of all other ranges (not to confuse with the first range added to the selection).
+	 * Returns `null` if no ranges are added to selection.
 	 *
 	 * @returns {engine.treeView.Range|null}
 	 */
@@ -153,7 +155,26 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns the first position in the selection. First position is the position that
+	 * Returns copy of the last range in the selection. Last range is the one which {@link engine.treeView.Range#end end}
+	 * position {@link engine.treeView.Position#isAfter is after} end position of all other ranges (not to confuse
+	 * with the last range added to the selection). Returns `null` if no ranges are added to selection.
+	 *
+	 * @returns {engine.treeView.Range|null}
+	 */
+	getLastRange() {
+		let last = null;
+
+		for ( let range of this._ranges ) {
+			if ( !last || range.end.isAfter( last.end ) ) {
+				last = range;
+			}
+		}
+
+		return last ? Range.createFromRange( last ) : null;
+	}
+
+	/**
+	 * Returns copy of the first position in the selection. First position is the position that
 	 * {@link engine.treeView.Position#isBefore is before} any other position in the selection ranges.
 	 * Returns `null` if no ranges are added to selection.
 	 *
@@ -162,7 +183,38 @@ export default class Selection {
 	getFirstPosition() {
 		const firstRange = this.getFirstRange();
 
-		return firstRange ? Position.createFromPosition( this.getFirstRange().start ) : null;
+		return firstRange ? Position.createFromPosition( firstRange.start ) : null;
+	}
+
+	/**
+	 * Returns copy of the last position in the selection. Last position is the position that
+	 * {@link engine.treeView.Position#isAfter is after} any other position in the selection ranges.
+	 * Returns `null` if no ranges are added to selection.
+	 *
+	 * @returns {engine.treeView.Position|null}
+	 */
+	getLastPosition() {
+		const lastRange = this.getLastRange();
+
+		return lastRange ? Position.createFromPosition( lastRange.end ) : null;
+	}
+
+	/**
+	 * Removes range at given index.
+	 *
+	 * @param {Number} index
+	 * @returns {engine.treeView.Range|null} Returns removed range or null if there is no range under given index.
+	 */
+	removeRangeAt( index ) {
+		const removed = this._ranges.splice( index, 1 );
+
+		if ( removed.length ) {
+			this.fire( 'change:range' );
+
+			return removed[ 0 ];
+		}
+
+		return null;
 	}
 
 	/**
@@ -170,6 +222,7 @@ export default class Selection {
 	 */
 	removeAllRanges() {
 		this._ranges = [];
+		this.fire( 'change:range' );
 	}
 
 	/**
@@ -186,9 +239,60 @@ export default class Selection {
 		this._ranges = [];
 
 		for ( let range of newRanges ) {
-			this.addRange( range );
+			this._pushRange( range );
 		}
 
 		this._lastRangeBackward = !!isLastBackward;
+		this.fire( 'change:range' );
+	}
+
+	/**
+	 * Collapses selection to the {@link engine.treeView.Selection#getLastPosition last position} in stored ranges.
+	 * All ranges will be removed beside one collapsed range. Nothing will be changed if there are no ranges stored
+	 * inside selection.
+	 */
+	collapseToEnd() {
+		const endPosition = this.getLastPosition();
+
+		if ( endPosition !== null ) {
+			this.setRanges( [ new Range( endPosition, endPosition ) ] );
+			this.fire( 'change:range' );
+		}
+	}
+
+	/**
+	 * Collapses selection to the {@link engine.treeView.Selection#getFirstPosition first position} in stored ranges.
+	 * All ranges will be removed beside one collapsed range. Nothing will be changed if there are no ranges stored
+	 * inside selection.
+	 */
+	collapseToStart() {
+		const startPosition = this.getFirstPosition();
+
+		if ( startPosition !== null ) {
+			this.setRanges( [ new Range( startPosition, startPosition ) ] );
+			this.fire( 'change:range' );
+		}
+	}
+
+	_pushRange( range ) {
+		for ( let storedRange of this._ranges ) {
+			if ( range.isIntersecting( storedRange ) ) {
+				/**
+				 * Trying to add a range that intersects with another range from selection.
+				 *
+				 * @error view-selection-range-intersects
+				 * @param {engine.treeView.Range} addedRange Range that was added to the selection.
+				 * @param {engine.treeView.Range} intersectingRange Range from selection that intersects with `addedRange`.
+				 */
+				throw new CKEditorError(
+					'view-selection-range-intersects: Trying to add a range that intersects with another range from selection.',
+					{ addedRange: range, intersectingRange: storedRange }
+				);
+			}
+		}
+
+		this._ranges.push( Range.createFromRange( range ) );
 	}
 }
+
+utils.mix( Selection, EmitterMixin );

+ 126 - 0
packages/ckeditor5-engine/tests/treeview/selection.js

@@ -19,7 +19,133 @@ describe( 'Selection', () => {
 		selection = new Selection();
 	} );
 
+	describe( 'anchor', () => {
+		it( 'should return null if no ranges in selection', () => {
+			expect( selection.anchor ).to.be.null;
+		} );
+
+		it( 'should return start of single range in selection', () => {
+			const el = new Element( 'p' );
+			const range = Range.createFromParentsAndOffsets( el, 5, el, 10 );
+			selection.addRange( range );
+			const anchor = selection.anchor;
+
+			expect( anchor.isEqual( range.start ) ).to.be.true;
+			expect( anchor ).to.not.equal( range.start );
+		} );
+
+		it( 'should return end of single range in selection when added as backward', () => {
+			const el = new Element( 'p' );
+			const range = Range.createFromParentsAndOffsets( el, 5, el, 10 );
+			selection.addRange( range, true );
+			const anchor = selection.anchor;
+
+			expect( anchor.isEqual( range.end ) ).to.be.true;
+			expect( anchor ).to.not.equal( range.end );
+		} );
+
+		it( 'should get anchor from last inserted range', () => {
+			const el = new Element( 'p' );
+			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 10 );
+			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 20 );
+			selection.addRange( range1 );
+			selection.addRange( range2 );
+
+			expect( selection.anchor.isEqual( range2.start ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'focus', () => {
+		it( 'should return null if no ranges in selection', () => {
+			expect( selection.focus ).to.be.null;
+		} );
+
+		it( 'should return end of single range in selection', () => {
+			const el = new Element( 'p' );
+			const range = Range.createFromParentsAndOffsets( el, 5, el, 10 );
+			selection.addRange( range );
+			const focus = selection.focus;
+
+			expect( focus.isEqual( range.end ) ).to.be.true;
+		} );
+
+		it( 'should return start of single range in selection when added as backward', () => {
+			const el = new Element( 'p' );
+			const range = Range.createFromParentsAndOffsets( el, 5, el, 10 );
+			selection.addRange( range, true );
+			const focus = selection.focus;
+
+			expect( focus.isEqual( range.start ) ).to.be.true;
+			expect( focus ).to.not.equal( range.start );
+		} );
+
+		it( 'should get focus from last inserted range', () => {
+			const el = new Element( 'p' );
+			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 10 );
+			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 20 );
+			selection.addRange( range1 );
+			selection.addRange( range2 );
+
+			expect( selection.focus.isEqual( range2.end ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'isCollapsed', () => {
+		it( 'should return true when all ranges are collapsed', () => {
+			const el = new Element( 'p' );
+			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 5 );
+			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 15 );
+			selection.addRange( range1 );
+			selection.addRange( range2 );
+
+			expect( selection.isCollapsed ).to.be.true;
+		} );
+
+		it( 'should return false when not all ranges are collapsed', () => {
+			const el = new Element( 'p' );
+			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 5 );
+			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 16 );
+			selection.addRange( range1 );
+			selection.addRange( range2 );
+
+			expect( selection.isCollapsed ).to.be.false;
+		} );
+	} );
+
+	describe( 'rangeCount', () => {
+		it( 'should return proper range count', () => {
+			const el = new Element( 'p' );
+			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 5 );
+			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 16 );
+			expect( selection.rangeCount ).to.equal( 0 );
+			selection.addRange( range1 );
+			expect( selection.rangeCount ).to.equal( 1 );
+			selection.addRange( range2 );
+			expect( selection.rangeCount ).to.equal( 2 );
+		} );
+	} );
+
 	describe( 'addRange', () => {
+		it( 'should add range to selection ranges', () => {
+			const el = new Element( 'p' );
+			const range = Range.createFromParentsAndOffsets( el, 0, el, 1 );
+
+			selection.addRange( range );
+			expect( selection.getRangeAt( 0 ).isEqual( range ) ).to.be.true;
+		} );
+
+		it( 'should fire change event', ( done ) => {
+			const el = new Element( 'p' );
+			const range = Range.createFromParentsAndOffsets( el, 0, el, 1 );
+
+			selection.once( 'change:range', () => {
+				expect( selection.getRangeAt( 0 ).isEqual( range ) ).to.be.true;
+				done();
+			} );
+
+			selection.addRange( range );
+		} );
+
 		it( 'should throw when range is intersecting with already added range', () => {
 			const el = new Element( 'p' );
 			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 10 );