Przeglądaj źródła

Merge pull request #318 from ckeditor/t/317

Implemented Selection.collapse()
Szymon Cofalik 9 lat temu
rodzic
commit
713bc5ce91

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

@@ -213,9 +213,41 @@ export default class Selection {
 	}
 
 	/**
-	 * Removes all attributes from the selection.
+	 * Sets collapsed selection in the specified location.
+	 *
+	 * The `location` can be specified as:
+	 *
+	 * * parent element and offset (offset defaults to `0`),
+	 * * parent element and `'END'` (sets selection at the end of that element),
+	 * * node and `'BEFORE'` or `'AFTER'` (sets selection before or after the given node).
 	 *
 	 * @fires {@link engine.treeModel.Selection#change:range change:range}
+	 * @param {engine.treeModel.Node} node
+	 * @param {Number|'END'|'BEFORE'|'AFTER'} location Offset or one of the flags.
+	 */
+	collapse( node, location ) {
+		if ( location == 'END' ) {
+			location = node.getChildCount();
+		} else if ( location == 'BEFORE' ) {
+			location = node.getIndex();
+			node = node.parent;
+		} else if ( location == 'AFTER' ) {
+			location = node.getIndex() + 1;
+			node = node.parent;
+		} else if ( !location ) {
+			location = 0;
+		}
+
+		const pos = Position.createFromParentAndOffset( node, location );
+		const range = new Range( pos, pos );
+
+		this.setRanges( [ range ] );
+	}
+
+	/**
+	 * Removes all attributes from the selection.
+	 *
+	 * @fires engine.treeModel.Selection#change:attribute
 	 */
 	clearAttributes() {
 		this._attrs.clear();

+ 143 - 66
packages/ckeditor5-engine/tests/treemodel/selection.js

@@ -17,6 +17,9 @@ import Selection from '/ckeditor5/engine/treemodel/selection.js';
 import InsertOperation from '/ckeditor5/engine/treemodel/operation/insertoperation.js';
 import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import testUtils from '/tests/ckeditor5/_utils/utils.js';
+
+testUtils.createSinonSandbox();
 
 describe( 'Selection', () => {
 	let attrFooBar;
@@ -78,100 +81,174 @@ describe( 'Selection', () => {
 		} );
 	} );
 
-	it( 'should copy added ranges and store multiple ranges', () => {
-		selection.addRange( liveRange );
-		selection.addRange( range );
+	describe( 'addRange', () => {
+		it( 'should copy added ranges and store multiple ranges', () => {
+			selection.addRange( liveRange );
+			selection.addRange( range );
 
-		let ranges = selection.getRanges();
+			let ranges = selection.getRanges();
 
-		expect( ranges.length ).to.equal( 2 );
-		expect( ranges[ 0 ].isEqual( liveRange ) ).to.be.true;
-		expect( ranges[ 1 ].isEqual( range ) ).to.be.true;
-		expect( ranges[ 0 ] ).not.to.be.equal( liveRange );
-		expect( ranges[ 1 ] ).not.to.be.equal( range );
-	} );
+			expect( ranges.length ).to.equal( 2 );
+			expect( ranges[ 0 ].isEqual( liveRange ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( range ) ).to.be.true;
+			expect( ranges[ 0 ] ).not.to.be.equal( liveRange );
+			expect( ranges[ 1 ] ).not.to.be.equal( range );
+		} );
 
-	it( 'should set anchor and focus to the start and end of the most recently added range', () => {
-		selection.addRange( liveRange );
+		it( 'should set anchor and focus to the start and end of the most recently added range', () => {
+			selection.addRange( liveRange );
 
-		expect( selection.anchor.path ).to.deep.equal( [ 0 ] );
-		expect( selection.focus.path ).to.deep.equal( [ 1 ] );
+			expect( selection.anchor.path ).to.deep.equal( [ 0 ] );
+			expect( selection.focus.path ).to.deep.equal( [ 1 ] );
 
-		selection.addRange( range );
+			selection.addRange( range );
 
-		expect( selection.anchor.path ).to.deep.equal( [ 2 ] );
-		expect( selection.focus.path ).to.deep.equal( [ 2, 2 ] );
-	} );
+			expect( selection.anchor.path ).to.deep.equal( [ 2 ] );
+			expect( selection.focus.path ).to.deep.equal( [ 2, 2 ] );
+		} );
 
-	it( 'should set anchor and focus to the end and start of the most recently added range if backward flag was used', () => {
-		selection.addRange( liveRange, true );
+		it( 'should set anchor and focus to the end and start of the most recently added range if backward flag was used', () => {
+			selection.addRange( liveRange, true );
 
-		expect( selection.anchor.path ).to.deep.equal( [ 1 ] );
-		expect( selection.focus.path ).to.deep.equal( [ 0 ] );
+			expect( selection.anchor.path ).to.deep.equal( [ 1 ] );
+			expect( selection.focus.path ).to.deep.equal( [ 0 ] );
 
-		selection.addRange( range, true );
+			selection.addRange( range, true );
 
-		expect( selection.anchor.path ).to.deep.equal( [ 2, 2 ] );
-		expect( selection.focus.path ).to.deep.equal( [ 2 ] );
-	} );
+			expect( selection.anchor.path ).to.deep.equal( [ 2, 2 ] );
+			expect( selection.focus.path ).to.deep.equal( [ 2 ] );
+		} );
 
-	it( 'should return a copy of (not a reference to) array of stored ranges', () => {
-		selection.addRange( liveRange );
+		it( 'should return a copy of (not a reference to) array of stored ranges', () => {
+			selection.addRange( liveRange );
 
-		let ranges = selection.getRanges();
+			let ranges = selection.getRanges();
 
-		selection.addRange( range );
+			selection.addRange( range );
 
-		expect( ranges.length ).to.equal( 1 );
-		expect( ranges[ 0 ].isEqual( liveRange ) ).to.be.true;
-	} );
+			expect( ranges.length ).to.equal( 1 );
+			expect( ranges[ 0 ].isEqual( liveRange ) ).to.be.true;
+		} );
 
-	it( 'should convert added Range to LiveRange', () => {
-		selection.addRange( range );
+		it( 'should convert added Range to LiveRange', () => {
+			selection.addRange( range );
 
-		let ranges = selection.getRanges();
+			let ranges = selection.getRanges();
 
-		expect( ranges[ 0 ] ).to.be.instanceof( LiveRange );
-	} );
+			expect( ranges[ 0 ] ).to.be.instanceof( LiveRange );
+		} );
 
-	it( 'should fire change:range event when adding a range', () => {
-		let spy = sinon.spy();
-		selection.on( 'change:range', spy );
+		it( 'should fire change:range event when adding a range', () => {
+			let spy = sinon.spy();
+			selection.on( 'change:range', spy );
 
-		selection.addRange( range );
+			selection.addRange( range );
 
-		expect( spy.called ).to.be.true;
-	} );
+			expect( spy.called ).to.be.true;
+		} );
 
-	it( 'should unbind all events when destroyed', () => {
-		selection.addRange( liveRange );
-		selection.addRange( range );
+		it( 'should unbind all events when destroyed', () => {
+			selection.addRange( liveRange );
+			selection.addRange( range );
 
-		let ranges = selection.getRanges();
+			let ranges = selection.getRanges();
 
-		sinon.spy( ranges[ 0 ], 'detach' );
-		sinon.spy( ranges[ 1 ], 'detach' );
+			sinon.spy( ranges[ 0 ], 'detach' );
+			sinon.spy( ranges[ 1 ], 'detach' );
 
-		selection.destroy();
+			selection.destroy();
 
-		expect( ranges[ 0 ].detach.called ).to.be.true;
-		expect( ranges[ 1 ].detach.called ).to.be.true;
+			expect( ranges[ 0 ].detach.called ).to.be.true;
+			expect( ranges[ 1 ].detach.called ).to.be.true;
 
-		ranges[ 0 ].detach.restore();
-		ranges[ 1 ].detach.restore();
+			ranges[ 0 ].detach.restore();
+			ranges[ 1 ].detach.restore();
+		} );
+
+		it( 'should throw an error if added range intersects with already stored range', () => {
+			selection.addRange( liveRange );
+
+			expect( () => {
+				selection.addRange(
+					new Range(
+						new Position( root, [ 0, 4 ] ),
+						new Position( root, [ 1, 2 ] )
+					)
+				);
+			} ).to.throw( CKEditorError, /selection-range-intersects/ );
+		} );
 	} );
 
-	it( 'should throw an error if added range intersects with already stored range', () => {
-		selection.addRange( liveRange );
-
-		expect( () => {
-			selection.addRange(
-				new Range(
-					new Position( root, [ 0, 4 ] ),
-					new Position( root, [ 1, 2 ] )
-				)
-			);
-		} ).to.throw( CKEditorError, /selection-range-intersects/ );
+	describe( 'collapse', () => {
+		it( 'detaches all existing ranges', () => {
+			selection.addRange( range );
+			selection.addRange( liveRange );
+
+			const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
+			selection.collapse( root );
+
+			expect( spy.calledTwice ).to.be.true;
+		} );
+
+		it( 'fires change:range', () => {
+			const spy = sinon.spy();
+
+			selection.on( 'change:range', spy );
+
+			selection.collapse( root );
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+
+		it( 'sets selection at the 0 offset if second parameter not passed', () => {
+			selection.collapse( root );
+
+			expect( selection ).to.have.property( 'isCollapsed', true );
+
+			const focus = selection.focus;
+			expect( focus ).to.have.property( 'parent', root );
+			expect( focus ).to.have.property( 'offset', 0 );
+		} );
+
+		it( 'sets selection at given offset in given parent', () => {
+			selection.collapse( root, 3 );
+
+			expect( selection ).to.have.property( 'isCollapsed', true );
+
+			const focus = selection.focus;
+			expect( focus ).to.have.property( 'parent', root );
+			expect( focus ).to.have.property( 'offset', 3 );
+		} );
+
+		it( 'sets selection at the end of the given parent', () => {
+			selection.collapse( root, 'END' );
+
+			expect( selection ).to.have.property( 'isCollapsed', true );
+
+			const focus = selection.focus;
+			expect( focus ).to.have.property( 'parent', root );
+			expect( focus ).to.have.property( 'offset', root.getChildCount() );
+		} );
+
+		it( 'sets selection before the specified element', () => {
+			selection.collapse( root.getChild( 1 ), 'BEFORE' );
+
+			expect( selection ).to.have.property( 'isCollapsed', true );
+
+			const focus = selection.focus;
+			expect( focus ).to.have.property( 'parent', root );
+			expect( focus ).to.have.property( 'offset', 1 );
+		} );
+
+		it( 'sets selection after the specified element', () => {
+			selection.collapse( root.getChild( 1 ), 'AFTER' );
+
+			expect( selection ).to.have.property( 'isCollapsed', true );
+
+			const focus = selection.focus;
+			expect( focus ).to.have.property( 'parent', root );
+			expect( focus ).to.have.property( 'offset', 2 );
+		} );
 	} );
 
 	describe( 'removeAllRanges', () => {