8
0
Pārlūkot izejas kodu

Implemented Selection.setFocus.

Piotrek Koszuliński 9 gadi atpakaļ
vecāks
revīzija
366f4d67d9

+ 1 - 1
packages/ckeditor5-engine/src/treemodel/position.js

@@ -425,7 +425,7 @@ export default class Position {
 	 * * 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).
 	 *
-	 * This method is a shortcut to other creators such as:
+	 * This method is a shortcut to other constructors such as:
 	 *
 	 * * {@link engine.treeModel.Position.createBefore},
 	 * * {@link engine.treeModel.Position.createAfter},

+ 43 - 16
packages/ckeditor5-engine/src/treemodel/selection.js

@@ -107,12 +107,12 @@ export default class Selection {
 	}
 
 	/**
-	 * Specifies whether the last added range was added as a backward or forward range.
+	 * Specifies whether the {@link engine.treeModel.Selection#focus} precedes {@link engine.treeModel.Selection#anchor}.
 	 *
 	 * @type {Boolean}
 	 */
 	get isBackward() {
-		return this._lastRangeBackward;
+		return !this.isCollapsed && this._lastRangeBackward;
 	}
 
 	/**
@@ -124,7 +124,7 @@ export default class Selection {
 	 * to {@link engine.treeModel.Range#start}. The flag is used to set {@link engine.treeModel.Selection#anchor} and
 	 * {@link engine.treeModel.Selection#focus} properties.
 	 *
-	 * @fires {@link engine.treeModel.Selection#change:range change:range}
+	 * @fires engine.treeModel.Selection#change:range
 	 * @param {engine.treeModel.Range} range Range to add.
 	 * @param {Boolean} [isBackward] Flag describing if added range was selected forward - from start to end (`false`)
 	 * or backward - from end to start (`true`). Defaults to `false`.
@@ -194,7 +194,7 @@ export default class Selection {
 	/**
 	 * Removes all ranges that were added to the selection. Fires update event.
 	 *
-	 * @fires {@link engine.treeModel.Selection#change:range change:range}
+	 * @fires engine.treeModel.Selection#change:range
 	 */
 	removeAllRanges() {
 		this.destroy();
@@ -208,7 +208,7 @@ export default class Selection {
 	 * is treated like the last added range and is used to set {@link #anchor} and {@link #focus}. Accepts a flag
 	 * describing in which way the selection is made (see {@link #addRange}).
 	 *
-	 * @fires {@link engine.treeModel.Selection#change:range change:range}
+	 * @fires engine.treeModel.Selection#change:range
 	 * @param {Array.<engine.treeModel.Range>} newRanges Array of ranges to set.
 	 * @param {Boolean} [isLastBackward] Flag describing if last added range was selected forward - from start to end (`false`)
 	 * or backward - from end to start (`true`). Defaults to `false`.
@@ -229,14 +229,9 @@ export default class Selection {
 	/**
 	 * Sets collapsed selection in the specified location.
 	 *
-	 * The location can be specified as:
+	 * The location can be specified in the same form as {@link engine.treeModel.Position.createAt} parameters.
 	 *
-	 * * a {@link engine.treeModel.Position position},
-	 * * 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}
+	 * @fires engine.treeModel.Selection#change:range
 	 * @param {engine.treeModel.Node|engine.treeModel.Position} nodeOrPosition
 	 * @param {Number|'END'|'BEFORE'|'AFTER'} [offset=0] Offset or one of the flags. Used only when
 	 * first parameter is a node.
@@ -248,6 +243,37 @@ export default class Selection {
 		this.setRanges( [ range ] );
 	}
 
+	/**
+	 * Sets {@link engine.treeModel.Selection#focus} in the specified location.
+	 *
+	 * The location can be specified in the same form as {@link engine.treeModel.Position.createAt} parameters.
+	 *
+	 * @fires engine.treeModel.Selection#change:range
+	 * @param {engine.treeModel.Node|engine.treeModel.Position} nodeOrPosition
+	 * @param {Number|'END'|'BEFORE'|'AFTER'} [offset=0] Offset or one of the flags. Used only when
+	 * first parameter is a node.
+	 */
+	setFocus( nodeOrPosition, offset ) {
+		const newFocus = Position.createAt( nodeOrPosition, offset );
+
+		if ( newFocus.compareWith( this.focus ) == 'SAME' ) {
+			return;
+		}
+
+		const anchor = this.anchor;
+
+		if ( this._ranges.length ) {
+			// TODO Replace with _popRange, so child classes can override this (needed for #329).
+			this._ranges.pop().detach();
+		}
+
+		if ( newFocus.compareWith( anchor ) == 'BEFORE' ) {
+			this.addRange( new Range( newFocus, anchor ), true );
+		} else {
+			this.addRange( new Range( anchor, newFocus ) );
+		}
+	}
+
 	/**
 	 * Removes all attributes from the selection.
 	 *
@@ -292,7 +318,7 @@ export default class Selection {
 	/**
 	 * Removes an attribute with given key from the selection.
 	 *
-	 * @fires {@link engine.treeModel.Selection#change:range change:range}
+	 * @fires engine.treeModel.Selection#change:attribute
 	 * @param {String} key Key of attribute to remove.
 	 */
 	removeAttribute( key ) {
@@ -305,7 +331,7 @@ export default class Selection {
 	/**
 	 * Sets attribute on the selection. If attribute with the same key already is set, it overwrites its values.
 	 *
-	 * @fires {@link engine.treeModel.Selection#change:range change:range}
+	 * @fires engine.treeModel.Selection#change:attribute
 	 * @param {String} key Key of attribute to set.
 	 * @param {*} value Attribute value.
 	 */
@@ -319,7 +345,7 @@ export default class Selection {
 	/**
 	 * Removes all attributes from the selection and sets given attributes.
 	 *
-	 * @fires {@link engine.treeModel.Selection#change:range change:range}
+	 * @fires engine.treeModel.Selection#change:attribute
 	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
 	 */
 	setAttributesTo( attrs ) {
@@ -532,8 +558,9 @@ export default class Selection {
 	 */
 	_getDefaultRange() {
 		const defaultRoot = this._document._getDefaultRoot();
+		const pos = new Position( defaultRoot, [ 0 ] );
 
-		return new Range( new Position( defaultRoot, [ 0 ] ), new Position( defaultRoot, [ 0 ] ) );
+		return new Range( pos, pos );
 	}
 
 	/**

+ 205 - 8
packages/ckeditor5-engine/tests/treemodel/selection.js

@@ -18,6 +18,7 @@ import InsertOperation from '/ckeditor5/engine/treemodel/operation/insertoperati
 import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import testUtils from '/tests/ckeditor5/_utils/utils.js';
+import utils from '/ckeditor5/utils/utils.js';
 
 testUtils.createSinonSandbox();
 
@@ -82,6 +83,24 @@ describe( 'Selection', () => {
 		} );
 	} );
 
+	describe( 'isBackward', () => {
+		it( 'is defined by the last added range', () => {
+			selection.addRange( range, true );
+			expect( selection ).to.have.property( 'isBackward', true );
+
+			selection.addRange( liveRange );
+			expect( selection ).to.have.property( 'isBackward', false );
+		} );
+
+		it( 'is false when last range is collapsed', () => {
+			const pos = Position.createAt( root, 0 );
+
+			selection.addRange( new Range( pos, pos ), true );
+
+			expect( selection.isBackward ).to.be.false;
+		} );
+	} );
+
 	describe( 'addRange', () => {
 		it( 'should copy added ranges and store multiple ranges', () => {
 			selection.addRange( liveRange );
@@ -120,14 +139,6 @@ describe( 'Selection', () => {
 			expect( selection.focus.path ).to.deep.equal( [ 2 ] );
 		} );
 
-		it( 'should set isBackward', () => {
-			selection.addRange( range, true );
-			expect( selection ).to.have.property( 'isBackward', true );
-
-			selection.addRange( liveRange );
-			expect( selection ).to.have.property( 'isBackward', false );
-		} );
-
 		it( 'should return a copy of (not a reference to) array of stored ranges', () => {
 			selection.addRange( liveRange );
 
@@ -272,6 +283,192 @@ describe( 'Selection', () => {
 		} );
 	} );
 
+	describe( 'setFocus', () => {
+		it( 'keeps all existing ranges and fires no change:range when no modifications needed', () => {
+			selection.addRange( range );
+			selection.addRange( liveRange );
+
+			const spy = sinon.spy();
+			selection.on( 'change:range', spy );
+
+			selection.setFocus( selection.focus );
+
+			expect( utils.count( selection.getRanges() ) ).to.equal( 2 );
+			expect( spy.callCount ).to.equal( 0 );
+		} );
+
+		it( 'fires change:range', () => {
+			selection.addRange( range );
+
+			const spy = sinon.spy();
+			selection.on( 'change:range', spy );
+
+			selection.setFocus( Position.createAt( root, 'END' ) );
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+
+		it( 'modifies default range', () => {
+			const startPos = selection.getFirstPosition();
+			const endPos = Position.createAt( root, 'END' );
+
+			selection.setFocus( endPos );
+
+			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'SAME' );
+			expect( selection.focus.compareWith( endPos ) ).to.equal( 'SAME' );
+		} );
+
+		it( 'modifies existing collapsed selection', () => {
+			const startPos = Position.createAt( root, 1 );
+			const endPos = Position.createAt( root, 2 );
+
+			selection.collapse( startPos );
+
+			selection.setFocus( endPos );
+
+			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'SAME' );
+			expect( selection.focus.compareWith( endPos ) ).to.equal( 'SAME' );
+		} );
+
+		it( 'makes existing collapsed selection a backward selection', () => {
+			const startPos = Position.createAt( root, 1 );
+			const endPos = Position.createAt( root, 0 );
+
+			selection.collapse( startPos );
+
+			selection.setFocus( endPos );
+
+			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'SAME' );
+			expect( selection.focus.compareWith( endPos ) ).to.equal( 'SAME' );
+			expect( selection.isBackward ).to.be.true;
+		} );
+
+		it( 'modifies existing non-collapsed selection', () => {
+			const startPos = Position.createAt( root, 1 );
+			const endPos = Position.createAt( root, 2 );
+			const newEndPos = Position.createAt( root, 3 );
+
+			selection.addRange( new Range( startPos, endPos ) );
+
+			selection.setFocus( newEndPos );
+
+			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'SAME' );
+			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
+		} );
+
+		it( 'makes existing non-collapsed selection a backward selection', () => {
+			const startPos = Position.createAt( root, 1 );
+			const endPos = Position.createAt( root, 2 );
+			const newEndPos = Position.createAt( root, 0 );
+
+			selection.addRange( new Range( startPos, endPos ) );
+
+			selection.setFocus( newEndPos );
+
+			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'SAME' );
+			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
+			expect( selection.isBackward ).to.be.true;
+		} );
+
+		it( 'makes existing backward selection a forward selection', () => {
+			const startPos = Position.createAt( root, 1 );
+			const endPos = Position.createAt( root, 2 );
+			const newEndPos = Position.createAt( root, 3 );
+
+			selection.addRange( new Range( startPos, endPos ), true );
+
+			selection.setFocus( newEndPos );
+
+			expect( selection.anchor.compareWith( endPos ) ).to.equal( 'SAME' );
+			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
+			expect( selection.isBackward ).to.be.false;
+		} );
+
+		it( 'modifies existing backward selection', () => {
+			const startPos = Position.createAt( root, 1 );
+			const endPos = Position.createAt( root, 2 );
+			const newEndPos = Position.createAt( root, 0 );
+
+			selection.addRange( new Range( startPos, endPos ), true );
+
+			selection.setFocus( newEndPos );
+
+			expect( selection.anchor.compareWith( endPos ) ).to.equal( 'SAME' );
+			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
+			expect( selection.isBackward ).to.be.true;
+		} );
+
+		it( 'modifies only the last range', () => {
+			// Offsets are chosen in this way that the order of adding ranges must count, not their document order.
+			const startPos1 = Position.createAt( root, 4 );
+			const endPos1 = Position.createAt( root, 5 );
+			const startPos2 = Position.createAt( root, 1 );
+			const endPos2 = Position.createAt( root, 2 );
+
+			const newEndPos = Position.createAt( root, 0 );
+
+			selection.addRange( new Range( startPos1, endPos1 ) );
+			selection.addRange( new Range( startPos2, endPos2 ) );
+
+			const spy = sinon.spy();
+
+			selection.on( 'change:range', spy );
+
+			selection.setFocus( newEndPos );
+
+			const ranges = Array.from( selection.getRanges() );
+
+			expect( ranges ).to.have.lengthOf( 2 );
+			expect( ranges[ 0 ].start.compareWith( startPos1 ) ).to.equal( 'SAME' );
+			expect( ranges[ 0 ].end.compareWith( endPos1 ) ).to.equal( 'SAME' );
+
+			expect( selection.anchor.compareWith( startPos2 ) ).to.equal( 'SAME' );
+			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
+			expect( selection.isBackward ).to.be.true;
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+
+		it( 'collapses the selection when extending to the anchor', () => {
+			const startPos = Position.createAt( root, 1 );
+			const endPos = Position.createAt( root, 2 );
+
+			selection.addRange( new Range( startPos, endPos ) );
+
+			selection.setFocus( startPos );
+
+			expect( selection.focus.compareWith( startPos ) ).to.equal( 'SAME' );
+			expect( selection.isCollapsed ).to.be.true;
+		} );
+
+		it( 'uses Position.createAt', () => {
+			const startPos = Position.createAt( root, 1 );
+			const endPos = Position.createAt( root, 2 );
+			const newEndPos = Position.createAt( root, 4 );
+			const spy = testUtils.sinon.stub( Position, 'createAt', () => newEndPos );
+
+			selection.addRange( new Range( startPos, endPos ) );
+
+			selection.setFocus( root, 'END' );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
+		} );
+
+		it( 'detaches the range it replaces', () => {
+			const startPos = Position.createAt( root, 1 );
+			const endPos = Position.createAt( root, 2 );
+			const newEndPos = Position.createAt( root, 4 );
+			const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
+
+			selection.addRange( new Range( startPos, endPos ) );
+
+			selection.setFocus( newEndPos );
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+	} );
+
 	describe( 'removeAllRanges', () => {
 		let spy, ranges;