diff --git a/src/controller/datacontroller.js b/src/controller/datacontroller.js
index b605f87e2..5b126d602 100644
--- a/src/controller/datacontroller.js
+++ b/src/controller/datacontroller.js
@@ -194,8 +194,8 @@ export default class DataController {
this.model.enqueueChange( 'transparent', writer => {
// Clearing selection is a workaround for ticket #569 (LiveRange loses position after removing data from document).
// After fixing it this code should be removed.
- this.model.document.selection.removeAllRanges();
- this.model.document.selection.clearAttributes();
+ writer.setSelection( null );
+ writer.removeSelectionAttribute( this.model.document.selection.getAttributeKeys() );
writer.remove( ModelRange.createIn( modelRoot ) );
writer.insert( this.parse( data ), modelRoot );
diff --git a/src/conversion/model-selection-to-view-converters.js b/src/conversion/model-selection-to-view-converters.js
index 46bbf7832..b842152ae 100644
--- a/src/conversion/model-selection-to-view-converters.js
+++ b/src/conversion/model-selection-to-view-converters.js
@@ -3,7 +3,6 @@
* For licensing, see LICENSE.md.
*/
-import ViewRange from '../view/range';
import viewWriter from '../view/writer';
/**
@@ -35,12 +34,14 @@ export function convertRangeSelection() {
return;
}
- conversionApi.viewSelection.removeAllRanges();
+ const viewRanges = [];
for ( const range of selection.getRanges() ) {
const viewRange = conversionApi.mapper.toViewRange( range );
- conversionApi.viewSelection.addRange( viewRange, selection.isBackward );
+ viewRanges.push( viewRange );
}
+
+ conversionApi.viewSelection.setTo( viewRanges, selection.isBackward );
};
}
@@ -82,8 +83,7 @@ export function convertCollapsedSelection() {
const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
const brokenPosition = viewWriter.breakAttributes( viewPosition );
- conversionApi.viewSelection.removeAllRanges();
- conversionApi.viewSelection.addRange( new ViewRange( brokenPosition, brokenPosition ) );
+ conversionApi.viewSelection.setTo( brokenPosition );
};
}
@@ -122,7 +122,7 @@ export function clearAttributes() {
}
}
}
- conversionApi.viewSelection.removeAllRanges();
+ conversionApi.viewSelection.setTo( null );
};
}
diff --git a/src/conversion/model-to-view-converters.js b/src/conversion/model-to-view-converters.js
index 23c76827c..3e0fd3386 100644
--- a/src/conversion/model-to-view-converters.js
+++ b/src/conversion/model-to-view-converters.js
@@ -12,6 +12,7 @@ import ViewAttributeElement from '../view/attributeelement';
import ViewText from '../view/text';
import ViewRange from '../view/range';
import viewWriter from '../view/writer';
+import DocumentSelection from '../model/documentselection';
/**
* Contains model to view converters for
@@ -329,7 +330,7 @@ export function wrap( elementCreator ) {
return;
}
- if ( data.item instanceof ModelSelection ) {
+ if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
// Selection attribute conversion.
viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), newViewElement, conversionApi.viewSelection );
} else {
@@ -369,7 +370,7 @@ export function highlightText( highlightDescriptor ) {
return;
}
- if ( !( data.item instanceof ModelSelection ) && !data.item.is( 'textProxy' ) ) {
+ if ( !( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) && !data.item.is( 'textProxy' ) ) {
return;
}
@@ -385,7 +386,7 @@ export function highlightText( highlightDescriptor ) {
const viewElement = createViewElementFromHighlightDescriptor( descriptor );
- if ( data.item instanceof ModelSelection ) {
+ if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), viewElement, conversionApi.viewSelection );
} else {
const viewRange = conversionApi.mapper.toViewRange( data.range );
diff --git a/src/conversion/view-selection-to-model-converters.js b/src/conversion/view-selection-to-model-converters.js
index 9dd89e083..983ba811b 100644
--- a/src/conversion/view-selection-to-model-converters.js
+++ b/src/conversion/view-selection-to-model-converters.js
@@ -37,11 +37,11 @@ export function convertSelectionChange( model, mapper ) {
ranges.push( mapper.toModelRange( viewRange ) );
}
- modelSelection.setRanges( ranges, viewSelection.isBackward );
+ modelSelection.setTo( ranges, viewSelection.isBackward );
if ( !modelSelection.isEqual( model.document.selection ) ) {
- model.change( () => {
- model.document.selection.setTo( modelSelection );
+ model.change( writer => {
+ writer.setSelection( modelSelection );
} );
}
};
diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js
index 6c3118906..12f141be2 100644
--- a/src/dev-utils/model.js
+++ b/src/dev-utils/model.js
@@ -19,6 +19,7 @@ import ModelPosition from '../model/position';
import ModelConversionDispatcher from '../conversion/modelconversiondispatcher';
import ModelSelection from '../model/selection';
import ModelDocumentFragment from '../model/documentfragment';
+import DocumentSelection from '../model/documentselection';
import ViewConversionDispatcher from '../conversion/viewconversiondispatcher';
import ViewSelection from '../view/selection';
@@ -46,7 +47,7 @@ import isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObjec
* @param {Object} [options]
* @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true` selection will
* be not included in returned string.
- * @param {Boolean} [options.rootName='main'] Name of the root from which data should be stringified. If not provided
+ * @param {String} [options.rootName='main'] Name of the root from which data should be stringified. If not provided
* default `main` name will be used.
* @returns {String} The stringified data.
*/
@@ -114,8 +115,8 @@ export function setData( model, data, options = {} ) {
writer.insert( modelDocumentFragment, modelRoot );
// Clean up previous document selection.
- model.document.selection.clearAttributes();
- model.document.selection.removeAllRanges();
+ writer.setSelection( null );
+ writer.removeSelectionAttribute( model.document.selection.getAttributeKeys() );
// Update document selection if specified.
if ( selection ) {
@@ -128,10 +129,12 @@ export function setData( model, data, options = {} ) {
ranges.push( new ModelRange( start, end ) );
}
- model.document.selection.setRanges( ranges, selection.isBackward );
+ writer.setSelection( ranges, selection.isBackward );
if ( options.selectionAttributes ) {
- model.document.selection.setAttributesTo( selection.getAttributes() );
+ for ( const [ key, value ] of selection.getAttributes() ) {
+ writer.setSelectionAttribute( key, value );
+ }
}
}
} );
@@ -180,12 +183,14 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
// Get selection from passed selection or position or range if at least one is specified.
if ( selectionOrPositionOrRange instanceof ModelSelection ) {
selection = selectionOrPositionOrRange;
+ } else if ( selectionOrPositionOrRange instanceof DocumentSelection ) {
+ selection = selectionOrPositionOrRange;
} else if ( selectionOrPositionOrRange instanceof ModelRange ) {
selection = new ModelSelection();
- selection.addRange( selectionOrPositionOrRange );
+ selection.setTo( selectionOrPositionOrRange );
} else if ( selectionOrPositionOrRange instanceof ModelPosition ) {
selection = new ModelSelection();
- selection.addRange( new ModelRange( selectionOrPositionOrRange, selectionOrPositionOrRange ) );
+ selection.setTo( selectionOrPositionOrRange );
}
// Setup model to view converter.
@@ -198,7 +203,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
modelToView.on( 'insert:$text', insertText() );
modelToView.on( 'attribute', wrap( ( value, data ) => {
- if ( data.item instanceof ModelSelection || data.item.is( 'textProxy' ) ) {
+ if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection || data.item.is( 'textProxy' ) ) {
return new ViewAttributeElement( 'model-text-with-attributes', { [ data.attributeKey ]: stringifyAttributeValue( value ) } );
}
} ) );
@@ -216,7 +221,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
// Convert model selection to view selection.
if ( selection ) {
- modelToView.convertSelection( selection, [] );
+ modelToView.convertSelection( selection );
}
// Parse view to data string.
@@ -295,11 +300,14 @@ export function parse( data, schema, options = {} ) {
// Create new selection.
selection = new ModelSelection();
- selection.setRanges( ranges, viewSelection.isBackward );
+ selection.setTo( ranges, viewSelection.isBackward );
// Set attributes to selection if specified.
if ( options.selectionAttributes ) {
- selection.setAttributesTo( options.selectionAttributes );
+ for ( const key in options.selectionAttributes ) {
+ const value = options.selectionAttributes[ key ];
+ selection.setAttribute( key, value );
+ }
}
}
diff --git a/src/dev-utils/view.js b/src/dev-utils/view.js
index 58444bf34..2056ea734 100644
--- a/src/dev-utils/view.js
+++ b/src/dev-utils/view.js
@@ -126,7 +126,7 @@ setData._parse = parse;
* const b = new Element( 'b', null, text );
* const p = new Element( 'p', null, b );
* const selection = new Selection();
- * selection.addRange( Range.createFromParentsAndOffsets( p, 0, p, 1 ) );
+ * selection.setTo( Range.createFromParentsAndOffsets( p, 0, p, 1 ) );
*
* stringify( p, selection ); // '
[foobar]
'
*
@@ -135,8 +135,7 @@ setData._parse = parse;
* const text = new Text( 'foobar' );
* const b = new Element( 'b', null, text );
* const p = new Element( 'p', null, b );
- * const selection = new Selection();
- * selection.addRange( Range.createFromParentsAndOffsets( text, 1, text, 5 ) );
+ * const selection = new Selection( [ Range.createFromParentsAndOffsets( text, 1, text, 5 ) ] );
*
* stringify( p, selection ); // 'f{ooba}r
'
*
@@ -148,8 +147,10 @@ setData._parse = parse;
*
* const text = new Text( 'foobar' );
* const selection = new Selection();
- * selection.addRange( Range.createFromParentsAndOffsets( text, 0, text, 1 ) );
- * selection.addRange( Range.createFromParentsAndOffsets( text, 3, text, 5 ) );
+ * selection.setTo( [
+ Range.createFromParentsAndOffsets( text, 0, text, 1 ) ),
+ * Range.createFromParentsAndOffsets( text, 3, text, 5 ) )
+ * ] );
*
* stringify( text, selection ); // '{f}oo{ba}r'
*
@@ -209,12 +210,12 @@ setData._parse = parse;
export function stringify( node, selectionOrPositionOrRange = null, options = {} ) {
let selection;
- if ( selectionOrPositionOrRange instanceof Position ) {
- selection = new Selection();
- selection.addRange( new Range( selectionOrPositionOrRange, selectionOrPositionOrRange ) );
- } else if ( selectionOrPositionOrRange instanceof Range ) {
+ if (
+ selectionOrPositionOrRange instanceof Position ||
+ selectionOrPositionOrRange instanceof Range
+ ) {
selection = new Selection();
- selection.addRange( selectionOrPositionOrRange );
+ selection.setTo( selectionOrPositionOrRange );
} else {
selection = selectionOrPositionOrRange;
}
@@ -332,8 +333,7 @@ export function parse( data, options = {} ) {
// When ranges are present - return object containing view, and selection.
if ( ranges.length ) {
- const selection = new Selection();
- selection.setRanges( ranges, !!options.lastRangeBackward );
+ const selection = new Selection( ranges, !!options.lastRangeBackward );
return {
view,
diff --git a/src/model/documentselection.js b/src/model/documentselection.js
index c1938bf59..5782a6fa7 100644
--- a/src/model/documentselection.js
+++ b/src/model/documentselection.js
@@ -7,28 +7,20 @@
* @module engine/model/documentselection
*/
-import Position from './position';
-import Range from './range';
-import LiveRange from './liverange';
-import Text from './text';
-import TextProxy from './textproxy';
-import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import log from '@ckeditor/ckeditor5-utils/src/log';
-
-import Selection from './selection';
-
-const storePrefix = 'selection:';
-
-const attrOpTypes = new Set(
- [ 'addAttribute', 'removeAttribute', 'changeAttribute', 'addRootAttribute', 'removeRootAttribute', 'changeRootAttribute' ]
-);
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
+import LiveSelection from './liveselection';
/**
* `DocumentSelection` is a special selection which is used as the
* {@link module:engine/model/document~Document#selection document's selection}.
* There can be only one instance of `DocumentSelection` per document.
*
+ * `DocumentSelection` is a proxy to {@link module:engine/model/liveselection~LiveSelection} that provides
+ * all getters to the original selection and delegate all events.
+ * All selection modifiers should be used from the {@link module:engine/model/writer~Writer} instance
+ * inside the {@link module:engine/model/model~Model#change} block, as it provides a secure way to modify model.
+ *
* `DocumentSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
* to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
*
@@ -44,731 +36,351 @@ const attrOpTypes = new Set(
* that are inside {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
* If you need to represent a selection in document fragment,
* use {@link module:engine/model/selection~Selection Selection class} instead.
- *
- * @extends module:engine/model/selection~Selection
*/
-export default class DocumentSelection extends Selection {
+export default class DocumentSelection {
/**
* Creates an empty live selection for given {@link module:engine/model/document~Document}.
*
* @param {module:engine/model/document~Document} doc Document which owns this selection.
*/
constructor( doc ) {
- super();
-
/**
- * Document which owns this selection.
+ * TODO
*
* @protected
- * @member {module:engine/model/model~Model}
- */
- this._model = doc.model;
-
- /**
- * Document which owns this selection.
- *
- * @protected
- * @member {module:engine/model/document~Document}
- */
- this._document = doc;
-
- /**
- * Keeps mapping of attribute name to priority with which the attribute got modified (added/changed/removed)
- * last time. Possible values of priority are: `'low'` and `'normal'`.
- *
- * Priorities are used by internal `DocumentSelection` mechanisms. All attributes set using `DocumentSelection`
- * attributes API are set with `'normal'` priority.
- *
- * @private
- * @member {Map} module:engine/model/documentselection~DocumentSelection#_attributePriority
*/
- this._attributePriority = new Map();
-
- // Add events that will ensure selection correctness.
- this.on( 'change:range', () => {
- for ( const range of this.getRanges() ) {
- if ( !this._document._validateSelectionRange( range ) ) {
- /**
- * Range from {@link module:engine/model/documentselection~DocumentSelection document selection}
- * starts or ends at incorrect position.
- *
- * @error document-selection-wrong-position
- * @param {module:engine/model/range~Range} range
- */
- throw new CKEditorError(
- 'document-selection-wrong-position: Range from document selection starts or ends at incorrect position.',
- { range }
- );
- }
- }
- } );
-
- this.listenTo( this._model, 'applyOperation', ( evt, args ) => {
- const operation = args[ 0 ];
-
- if ( !operation.isDocumentOperation ) {
- return;
- }
-
- // Whenever attribute operation is performed on document, update selection attributes.
- // This is not the most efficient way to update selection attributes, but should be okay for now.
- if ( attrOpTypes.has( operation.type ) ) {
- this._updateAttributes( false );
- }
+ this._selection = new LiveSelection( doc );
- const batch = operation.delta.batch;
-
- // Batch may not be passed to the document#change event in some tests.
- // See https://github.com/ckeditor/ckeditor5-engine/issues/1001#issuecomment-314202352
- if ( batch ) {
- // Whenever element which had selection's attributes stored in it stops being empty,
- // the attributes need to be removed.
- clearAttributesStoredInElement( operation, this._model, batch );
- }
- }, { priority: 'low' } );
+ this._selection.delegate( 'change:range' ).to( this );
+ this._selection.delegate( 'change:attribute' ).to( this );
}
/**
- * @inheritDoc
+ * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
+ * collapsed.
+ *
+ * @readonly
+ * @type {Boolean}
*/
get isCollapsed() {
- const length = this._ranges.length;
-
- return length === 0 ? this._document._getDefaultRange().isCollapsed : super.isCollapsed;
+ return this._selection.isCollapsed;
}
/**
- * @inheritDoc
+ * Selection anchor. Anchor may be described as a position where the most recent part of the selection starts.
+ * Together with {@link #focus} they define the direction of selection, which is important
+ * when expanding/shrinking selection. Anchor is always {@link module:engine/model/range~Range#start start} or
+ * {@link module:engine/model/range~Range#end end} position of the most recently added range.
+ *
+ * Is set to `null` if there are no ranges in selection.
+ *
+ * @see #focus
+ * @readonly
+ * @type {module:engine/model/position~Position|null}
*/
get anchor() {
- return super.anchor || this._document._getDefaultRange().start;
+ return this._selection.anchor;
}
/**
- * @inheritDoc
+ * Selection focus. Focus is a position where the selection ends.
+ *
+ * Is set to `null` if there are no ranges in selection.
+ *
+ * @see #anchor
+ * @readonly
+ * @type {module:engine/model/position~Position|null}
*/
get focus() {
- return super.focus || this._document._getDefaultRange().end;
+ return this._selection.focus;
}
/**
- * @inheritDoc
+ * Returns number of ranges in selection.
+ *
+ * @readonly
+ * @type {Number}
*/
get rangeCount() {
- return this._ranges.length ? this._ranges.length : 1;
+ return this._selection.rangeCount;
}
/**
- * Describes whether `DocumentSelection` has own range(s) set, or if it is defaulted to
+ * Describes whether `Documentselection` has own range(s) set, or if it is defaulted to
* {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
*
* @readonly
* @type {Boolean}
*/
get hasOwnRange() {
- return this._ranges.length > 0;
- }
-
- /**
- * Unbinds all events previously bound by document selection.
- */
- destroy() {
- for ( let i = 0; i < this._ranges.length; i++ ) {
- this._ranges[ i ].detach();
- }
-
- this.stopListening();
- }
-
- /**
- * @inheritDoc
- */
- * getRanges() {
- if ( this._ranges.length ) {
- yield* super.getRanges();
- } else {
- yield this._document._getDefaultRange();
- }
- }
-
- /**
- * @inheritDoc
- */
- getFirstRange() {
- return super.getFirstRange() || this._document._getDefaultRange();
- }
-
- /**
- * @inheritDoc
- */
- getLastRange() {
- return super.getLastRange() || this._document._getDefaultRange();
- }
-
- /**
- * @inheritDoc
- */
- addRange( range, isBackward = false ) {
- super.addRange( range, isBackward );
- this.refreshAttributes();
- }
-
- /**
- * @inheritDoc
- */
- removeAllRanges() {
- super.removeAllRanges();
- this.refreshAttributes();
- }
-
- /**
- * @inheritDoc
- */
- setRanges( newRanges, isLastBackward = false ) {
- super.setRanges( newRanges, isLastBackward );
- this.refreshAttributes();
+ return this._selection.hasOwnRange;
}
/**
- * @inheritDoc
+ * Specifies whether the {@link #focus}
+ * precedes {@link #anchor}.
+ *
+ * @readonly
+ * @type {Boolean}
*/
- setAttribute( key, value ) {
- // Store attribute in parent element if the selection is collapsed in an empty node.
- if ( this.isCollapsed && this.anchor.parent.isEmpty ) {
- this._storeAttribute( key, value );
- }
-
- if ( this._setAttribute( key, value ) ) {
- // Fire event with exact data.
- const attributeKeys = [ key ];
- this.fire( 'change:attribute', { attributeKeys, directChange: true } );
- }
+ get isBackward() {
+ return this._selection.isBackward;
}
/**
- * @inheritDoc
+ * Used for the compatibility with the {@link module:engine/model/selection~Selection#isEqual} method.
+ *
+ * @protected
*/
- removeAttribute( key ) {
- // Remove stored attribute from parent element if the selection is collapsed in an empty node.
- if ( this.isCollapsed && this.anchor.parent.isEmpty ) {
- this._removeStoredAttribute( key );
- }
-
- if ( this._removeAttribute( key ) ) {
- // Fire event with exact data.
- const attributeKeys = [ key ];
- this.fire( 'change:attribute', { attributeKeys, directChange: true } );
- }
+ get _ranges() {
+ return this._selection._ranges;
}
/**
- * @inheritDoc
- */
- setAttributesTo( attrs ) {
- attrs = toMap( attrs );
-
- if ( this.isCollapsed && this.anchor.parent.isEmpty ) {
- this._setStoredAttributesTo( attrs );
- }
-
- const changed = this._setAttributesTo( attrs );
-
- if ( changed.size > 0 ) {
- // Fire event with exact data (fire only if anything changed).
- const attributeKeys = Array.from( changed );
- this.fire( 'change:attribute', { attributeKeys, directChange: true } );
- }
- }
-
- /**
- * @inheritDoc
+ * Returns an iterable that iterates over copies of selection ranges.
+ *
+ * @returns {Iterable.}
*/
- clearAttributes() {
- this.setAttributesTo( [] );
+ getRanges() {
+ return this._selection.getRanges();
}
/**
- * Removes all attributes from the selection and sets attributes according to the surrounding nodes.
+ * Returns the first position in the selection.
+ * First position is the position that {@link module:engine/model/position~Position#isBefore is before}
+ * any other position in the selection.
+ *
+ * Returns `null` if there are no ranges in selection.
+ *
+ * @returns {module:engine/model/position~Position|null}
*/
- refreshAttributes() {
- this._updateAttributes( true );
+ getFirstPosition() {
+ return this._selection.getFirstPosition();
}
/**
- * This method is not available in `DocumentSelection`. There can be only one
- * `DocumentSelection` per document instance, so creating new `DocumentSelection`s this way
- * would be unsafe.
+ * Returns the last position in the selection.
+ * Last position is the position that {@link module:engine/model/position~Position#isAfter is after}
+ * any other position in the selection.
+ *
+ * Returns `null` if there are no ranges in selection.
+ *
+ * @returns {module:engine/model/position~Position|null}
*/
- static createFromSelection() {
- /**
- * Cannot create a new `DocumentSelection` instance.
- *
- * `DocumentSelection#createFromSelection()` is not available. There can be only one
- * `DocumentSelection` per document instance, so creating new `DocumentSelection`s this way
- * would be unsafe.
- *
- * @error documentselection-cannot-create
- */
- throw new CKEditorError( 'documentselection-cannot-create: Cannot create a new DocumentSelection instance.' );
+ getLastPosition() {
+ return this._selection.getLastPosition();
}
/**
- * @inheritDoc
+ * Returns a copy of the first range in the selection.
+ * First range is the one which {@link module:engine/model/range~Range#start start} position
+ * {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
+ * (not to confuse with the first range added to the selection).
+ *
+ * Returns `null` if there are no ranges in selection.
+ *
+ * @returns {module:engine/model/range~Range|null}
*/
- _popRange() {
- this._ranges.pop().detach();
+ getFirstRange() {
+ return this._selection.getFirstRange();
}
/**
- * @inheritDoc
+ * Returns a copy of the last range in the selection.
+ * Last range is the one which {@link module:engine/model/range~Range#end end} position
+ * {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
+ * recently added to the selection).
+ *
+ * Returns `null` if there are no ranges in selection.
+ *
+ * @returns {module:engine/model/range~Range|null}
*/
- _pushRange( range ) {
- const liveRange = this._prepareRange( range );
-
- // `undefined` is returned when given `range` is in graveyard root.
- if ( liveRange ) {
- this._ranges.push( liveRange );
- }
+ getLastRange() {
+ return this._selection.getLastRange();
}
/**
- * Prepares given range to be added to selection. Checks if it is correct,
- * converts it to {@link module:engine/model/liverange~LiveRange LiveRange}
- * and sets listeners listening to the range's change event.
+ * Gets elements of type "block" touched by the selection.
+ *
+ * This method's result can be used for example to apply block styling to all blocks covered by this selection.
+ *
+ * **Note:** `getSelectedBlocks()` always returns the deepest block.
+ *
+ * In this case the function will return exactly all 3 paragraphs:
+ *
+ * [a
+ *
+ * b
+ *
+ * c]d
+ *
+ * In this case the paragraph will also be returned, despite the collapsed selection:
+ *
+ * []a
*
- * @private
- * @param {module:engine/model/range~Range} range
+ * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
+ * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
+ *
+ * [a
+ * b
+ * ]c // this block will not be returned
+ *
+ * @returns {Iterator.}
*/
- _prepareRange( range ) {
- if ( !( range instanceof Range ) ) {
- /**
- * Trying to add an object that is not an instance of Range.
- *
- * @error model-selection-added-not-range
- */
- throw new CKEditorError( 'model-selection-added-not-range: Trying to add an object that is not an instance of Range.' );
- }
-
- if ( range.root == this._document.graveyard ) {
- /**
- * Trying to add a Range that is in the graveyard root. Range rejected.
- *
- * @warning model-selection-range-in-graveyard
- */
- log.warn( 'model-selection-range-in-graveyard: Trying to add a Range that is in the graveyard root. Range rejected.' );
-
- return;
- }
-
- this._checkRange( range );
-
- const liveRange = LiveRange.createFromRange( range );
-
- liveRange.on( 'change:range', ( evt, oldRange, data ) => {
- // If `LiveRange` is in whole moved to the graveyard, fix that range.
- if ( liveRange.root == this._document.graveyard ) {
- this._fixGraveyardSelection( liveRange, data.sourcePosition );
- }
-
- // Whenever a live range from selection changes, fire an event informing about that change.
- this.fire( 'change:range', { directChange: false } );
- } );
-
- return liveRange;
+ getSelectedBlocks() {
+ return this._selection.getSelectedBlocks();
}
/**
- * Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
+ * Checks whether the selection contains the entire content of the given element. This means that selection must start
+ * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
+ * touching the element's end.
*
- * @protected
- * @param {Boolean} clearAll
- * @fires change:attribute
+ * By default, this method will check whether the entire content of the selection's current root is selected.
+ * Useful to check if e.g. the user has just pressed Ctrl + A.
+ *
+ * @param {module:engine/model/element~Element} [element=this.anchor.root]
+ * @returns {Boolean}
*/
- _updateAttributes( clearAll ) {
- const newAttributes = toMap( this._getSurroundingAttributes() );
- const oldAttributes = toMap( this.getAttributes() );
-
- if ( clearAll ) {
- // If `clearAll` remove all attributes and reset priorities.
- this._attributePriority = new Map();
- this._attrs = new Map();
- } else {
- // If not, remove only attributes added with `low` priority.
- for ( const [ key, priority ] of this._attributePriority ) {
- if ( priority == 'low' ) {
- this._attrs.delete( key );
- this._attributePriority.delete( key );
- }
- }
- }
-
- this._setAttributesTo( newAttributes, false );
-
- // Let's evaluate which attributes really changed.
- const changed = [];
-
- // First, loop through all attributes that are set on selection right now.
- // Check which of them are different than old attributes.
- for ( const [ newKey, newValue ] of this.getAttributes() ) {
- if ( !oldAttributes.has( newKey ) || oldAttributes.get( newKey ) !== newValue ) {
- changed.push( newKey );
- }
- }
-
- // Then, check which of old attributes got removed.
- for ( const [ oldKey ] of oldAttributes ) {
- if ( !this.hasAttribute( oldKey ) ) {
- changed.push( oldKey );
- }
- }
-
- // Fire event with exact data (fire only if anything changed).
- if ( changed.length > 0 ) {
- this.fire( 'change:attribute', { attributeKeys: changed, directChange: false } );
- }
+ containsEntireContent( element ) {
+ return this._selection.containsEntireContent( element );
}
/**
- * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
+ * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
+ * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionFocus} method.
+ *
+ * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
*
+ * @see module:engine/model/writer~Writer#setSelectionFocus
* @protected
- * @param {String} key Attribute key to convert.
- * @returns {String} Converted attribute key, applicable for selection store.
+ * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+ * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
+ * first parameter is a {@link module:engine/model/item~Item model item}.
*/
- static _getStoreAttributeKey( key ) {
- return storePrefix + key;
+ _setFocus( itemOrPosition, offset ) {
+ this._selection.setFocus( itemOrPosition, offset );
}
/**
- * Checks whether the given attribute key is an attribute stored on an element.
+ * Sets this selection's ranges and direction to the specified location based on the given
+ * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
+ * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
+ * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
+ * Should be used only within the {@link module:engine/model/writer~Writer#setSelection} method.
*
+ * @see module:engine/model/writer~Writer#setSelection
* @protected
- * @param {String} key
- * @returns {Boolean}
+ * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+ * module:engine/model/position~Position|module:engine/model/element~Element|
+ * Iterable.|module:engine/model/range~Range|null} selectable
+ * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
*/
- static _isStoreAttributeKey( key ) {
- return key.startsWith( storePrefix );
+ _setTo( selectable, backwardSelectionOrOffset ) {
+ this._selection.setTo( selectable, backwardSelectionOrOffset );
}
/**
- * Internal method for setting `DocumentSelection` attribute. Supports attribute priorities (through `directChange`
- * parameter).
- *
- * @private
- * @param {String} key Attribute key.
- * @param {*} value Attribute value.
- * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
- * is caused by `Batch` API.
- * @returns {Boolean} Whether value has changed.
+ * Unbinds all events previously bound by document selection.
*/
- _setAttribute( key, value, directChange = true ) {
- const priority = directChange ? 'normal' : 'low';
-
- if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
- // Priority too low.
- return false;
- }
-
- const oldValue = super.getAttribute( key );
-
- // Don't do anything if value has not changed.
- if ( oldValue === value ) {
- return false;
- }
-
- this._attrs.set( key, value );
-
- // Update priorities map.
- this._attributePriority.set( key, priority );
-
- return true;
+ destroy() {
+ this._selection.destroy();
}
- /**
- * Internal method for removing `DocumentSelection` attribute. Supports attribute priorities (through `directChange`
- * parameter).
- *
- * @private
- * @param {String} key Attribute key.
- * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
- * is caused by `Batch` API.
- * @returns {Boolean} Whether attribute was removed. May not be true if such attributes didn't exist or the
- * existing attribute had higher priority.
- */
- _removeAttribute( key, directChange = true ) {
- const priority = directChange ? 'normal' : 'low';
-
- if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
- // Priority too low.
- return false;
- }
-
- // Don't do anything if value has not changed.
- if ( !super.hasAttribute( key ) ) {
- return false;
- }
-
- this._attrs.delete( key );
-
- // Update priorities map.
- this._attributePriority.set( key, priority );
-
- return true;
+ getAttributeKeys() {
+ return this._selection.getAttributeKeys();
}
/**
- * Internal method for setting multiple `DocumentSelection` attributes. Supports attribute priorities (through
- * `directChange` parameter).
- *
- * @private
- * @param {Map} attrs Iterable object containing attributes to be set.
- * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
- * is caused by `Batch` API.
- * @returns {Set.} Changed attribute keys.
+ * Returns iterable that iterates over this selection's attributes.
+ *
+ * Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
+ * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
+ *
+ * @returns {Iterable.<*>}
*/
- _setAttributesTo( attrs, directChange = true ) {
- const changed = new Set();
-
- for ( const [ oldKey, oldValue ] of this.getAttributes() ) {
- // Do not remove attribute if attribute with same key and value is about to be set.
- if ( attrs.get( oldKey ) === oldValue ) {
- continue;
- }
-
- // Attribute still might not get removed because of priorities.
- if ( this._removeAttribute( oldKey, directChange ) ) {
- changed.add( oldKey );
- }
- }
-
- for ( const [ key, value ] of attrs ) {
- // Attribute may not be set because of attributes or because same key/value is already added.
- const gotAdded = this._setAttribute( key, value, directChange );
-
- if ( gotAdded ) {
- changed.add( key );
- }
- }
-
- return changed;
+ getAttributes() {
+ return this._selection.getAttributes();
}
/**
- * Returns an iterator that iterates through all selection attributes stored in current selection's parent.
+ * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
*
- * @private
- * @returns {Iterable.<*>}
+ * @param {String} key Key of attribute to look for.
+ * @returns {*} Attribute value or `undefined`.
*/
- * _getStoredAttributes() {
- const selectionParent = this.getFirstPosition().parent;
-
- if ( this.isCollapsed && selectionParent.isEmpty ) {
- for ( const key of selectionParent.getAttributeKeys() ) {
- if ( key.startsWith( storePrefix ) ) {
- const realKey = key.substr( storePrefix.length );
-
- yield [ realKey, selectionParent.getAttribute( key ) ];
- }
- }
- }
+ getAttribute( key ) {
+ return this._selection.getAttribute( key );
}
/**
- * Removes attribute with given key from attributes stored in current selection's parent node.
+ * Checks if the selection has an attribute for given key.
*
- * @private
- * @param {String} key Key of attribute to remove.
+ * @param {String} key Key of attribute to check.
+ * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
*/
- _removeStoredAttribute( key ) {
- const storeKey = DocumentSelection._getStoreAttributeKey( key );
-
- this._model.change( writer => {
- writer.removeAttribute( storeKey, this.anchor.parent );
- } );
+ hasAttribute( key ) {
+ return this._selection.hasAttribute( key );
}
/**
- * Stores given attribute key and value in current selection's parent node.
+ * Sets attribute on the selection. If attribute with the same key already is set, it's value is overwritten.
+ * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionAttribute} method.
*
- * @private
- * @param {String} key Key of attribute to set.
+ * @see module:engine/model/writer~Writer#setSelectionAttribute
+ * @protected
+ * @param {String} key Key of the attribute to set.
* @param {*} value Attribute value.
*/
- _storeAttribute( key, value ) {
- const storeKey = DocumentSelection._getStoreAttributeKey( key );
-
- this._model.change( writer => {
- writer.setAttribute( storeKey, value, this.anchor.parent );
- } );
+ _setAttribute( key, value ) {
+ this._selection.setAttribute( key, value );
}
/**
- * Sets selection attributes stored in current selection's parent node to given set of attributes.
+ * Removes an attribute with given key from the selection.
+ * If the given attribute was set on the selection, fires the {@link module:engine/model/liveselection~LiveSelection#event:change}
+ * event with removed attribute key.
+ * Should be used only within the {@link module:engine/model/writer~Writer#removeSelectionAttribute} method.
*
- * @private
- * @param {Iterable} attrs Iterable object containing attributes to be set.
+ * @see module:engine/model/writer~Writer#removeSelectionAttribute
+ * @protected
+ * @param {String} key Key of the attribute to remove.
*/
- _setStoredAttributesTo( attrs ) {
- const selectionParent = this.anchor.parent;
-
- this._model.change( writer => {
- for ( const [ oldKey ] of this._getStoredAttributes() ) {
- const storeKey = DocumentSelection._getStoreAttributeKey( oldKey );
-
- writer.removeAttribute( storeKey, selectionParent );
- }
-
- for ( const [ key, value ] of attrs ) {
- const storeKey = DocumentSelection._getStoreAttributeKey( key );
-
- writer.setAttribute( storeKey, value, selectionParent );
- }
- } );
+ _removeAttribute( key ) {
+ this._selection.removeAttribute( key );
}
/**
- * Checks model text nodes that are closest to the selection's first position and returns attributes of first
- * found element. If there are no text nodes in selection's first position parent, it returns selection
- * attributes stored in that parent.
+ * Returns an iterable that iterates through all selection attributes stored in current selection's parent.
*
- * @private
- * @returns {Iterable.<*>} Collection of attributes.
+ * @protected
+ * @returns {Iterable.<*>}
*/
- _getSurroundingAttributes() {
- const position = this.getFirstPosition();
- const schema = this._model.schema;
-
- let attrs = null;
-
- if ( !this.isCollapsed ) {
- // 1. If selection is a range...
- const range = this.getFirstRange();
-
- // ...look for a first character node in that range and take attributes from it.
- for ( const value of range ) {
- // If the item is an object, we don't want to get attributes from its children.
- if ( value.item.is( 'element' ) && schema.isObject( value.item ) ) {
- break;
- }
-
- // This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
- // It can be done better by using `break;` instead of checking `attrs === null`.
- if ( value.type == 'text' && attrs === null ) {
- attrs = value.item.getAttributes();
- }
- }
- } else {
- // 2. If the selection is a caret or the range does not contain a character node...
-
- const nodeBefore = position.textNode ? position.textNode : position.nodeBefore;
- const nodeAfter = position.textNode ? position.textNode : position.nodeAfter;
-
- // ...look at the node before caret and take attributes from it if it is a character node.
- attrs = getAttrsIfCharacter( nodeBefore );
-
- // 3. If not, look at the node after caret...
- if ( !attrs ) {
- attrs = getAttrsIfCharacter( nodeAfter );
- }
-
- // 4. If not, try to find the first character on the left, that is in the same node.
- if ( !attrs ) {
- let node = nodeBefore;
-
- while ( node && !attrs ) {
- node = node.previousSibling;
- attrs = getAttrsIfCharacter( node );
- }
- }
-
- // 5. If not found, try to find the first character on the right, that is in the same node.
- if ( !attrs ) {
- let node = nodeAfter;
-
- while ( node && !attrs ) {
- node = node.nextSibling;
- attrs = getAttrsIfCharacter( node );
- }
- }
-
- // 6. If not found, selection should retrieve attributes from parent.
- if ( !attrs ) {
- attrs = this._getStoredAttributes();
- }
- }
-
- return attrs;
+ _getStoredAttributes() {
+ return this._selection._getStoredAttributes();
}
/**
- * Fixes a selection range after it ends up in graveyard root.
+ * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
*
- * @private
- * @param {module:engine/model/liverange~LiveRange} liveRange The range from selection, that ended up in the graveyard root.
- * @param {module:engine/model/position~Position} removedRangeStart Start position of a range which was removed.
+ * @protected
+ * @param {String} key Attribute key to convert.
+ * @returns {String} Converted attribute key, applicable for selection store.
*/
- _fixGraveyardSelection( liveRange, removedRangeStart ) {
- // The start of the removed range is the closest position to the `liveRange` - the original selection range.
- // This is a good candidate for a fixed selection range.
- const positionCandidate = Position.createFromPosition( removedRangeStart );
-
- // Find a range that is a correct selection range and is closest to the start of removed range.
- const selectionRange = this._document.getNearestSelectionRange( positionCandidate );
-
- // Remove the old selection range before preparing and adding new selection range. This order is important,
- // because new range, in some cases, may intersect with old range (it depends on `getNearestSelectionRange()` result).
- const index = this._ranges.indexOf( liveRange );
- this._ranges.splice( index, 1 );
- liveRange.detach();
-
- // If nearest valid selection range has been found - add it in the place of old range.
- if ( selectionRange ) {
- // Check the range, convert it to live range, bind events, etc.
- const newRange = this._prepareRange( selectionRange );
-
- // Add new range in the place of old range.
- this._ranges.splice( index, 0, newRange );
- }
- // If nearest valid selection range cannot be found - just removing the old range is fine.
-
- // Fire an event informing about selection change.
- this.fire( 'change:range', { directChange: false } );
+ static _getStoreAttributeKey( key ) {
+ return LiveSelection._getStoreAttributeKey( key );
}
-}
-// Helper function for {@link module:engine/model/documentselection~DocumentSelection#_updateAttributes}.
-//
-// It takes model item, checks whether it is a text node (or text proxy) and, if so, returns it's attributes. If not, returns `null`.
-//
-// @param {module:engine/model/item~Item|null} node
-// @returns {Boolean|Iterable}
-function getAttrsIfCharacter( node ) {
- if ( node instanceof TextProxy || node instanceof Text ) {
- return node.getAttributes();
+ /**
+ * Checks whether the given attribute key is an attribute stored on an element.
+ *
+ * @protected
+ * @param {String} storePrefix
+ * @returns {Boolean}
+ */
+ static _isStoreAttributeKey( storePrefix ) {
+ return LiveSelection._isStoreAttributeKey( storePrefix );
}
-
- return null;
}
-// Removes selection attributes from element which is not empty anymore.
-function clearAttributesStoredInElement( operation, model, batch ) {
- let changeParent = null;
-
- if ( operation.type == 'insert' ) {
- changeParent = operation.position.parent;
- } else if ( operation.type == 'move' || operation.type == 'reinsert' || operation.type == 'remove' ) {
- changeParent = operation.getMovedRangeStart().parent;
- }
-
- if ( !changeParent || changeParent.isEmpty ) {
- return;
- }
-
- model.enqueueChange( batch, writer => {
- const storedAttributes = Array.from( changeParent.getAttributeKeys() ).filter( key => key.startsWith( storePrefix ) );
-
- for ( const key of storedAttributes ) {
- writer.removeAttribute( key, changeParent );
- }
- } );
-}
+mix( DocumentSelection, EmitterMixin );
diff --git a/src/model/liveselection.js b/src/model/liveselection.js
new file mode 100644
index 000000000..1fae9e258
--- /dev/null
+++ b/src/model/liveselection.js
@@ -0,0 +1,661 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/liveselection
+ */
+
+import Position from './position';
+import LiveRange from './liverange';
+import Text from './text';
+import TextProxy from './textproxy';
+import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import log from '@ckeditor/ckeditor5-utils/src/log';
+
+import Selection from './selection';
+
+const storePrefix = 'selection:';
+
+const attrOpTypes = new Set(
+ [ 'addAttribute', 'removeAttribute', 'changeAttribute', 'addRootAttribute', 'removeRootAttribute', 'changeRootAttribute' ]
+);
+
+/**
+ * `LiveSelection` is used internally by {@link module:engine/model/documentselection~DocumentSelection} and shouldn't be used directly.
+ *
+ * LiveSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
+ * to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
+ *
+ * Differences between {@link module:engine/model/selection~Selection} and `LiveSelection` are:
+ * * there is always a range in `LiveSelection` - even if no ranges were added there is a "default range"
+ * present in the selection,
+ * * ranges added to this selection updates automatically when the document changes,
+ * * attributes of `LiveSelection` are updated automatically according to selection ranges.
+ *
+ * @extends module:engine/model/selection~Selection
+ */
+export default class LiveSelection extends Selection {
+ /**
+ * Creates an empty live selection for given {@link module:engine/model/document~Document}.
+ *
+ * @param {module:engine/model/document~Document} doc Document which owns this selection.
+ */
+ constructor( doc ) {
+ super();
+
+ /**
+ * Document which owns this selection.
+ *
+ * @protected
+ * @member {module:engine/model/model~Model}
+ */
+ this._model = doc.model;
+
+ /**
+ * Document which owns this selection.
+ *
+ * @protected
+ * @member {module:engine/model/document~Document}
+ */
+ this._document = doc;
+
+ /**
+ * Keeps mapping of attribute name to priority with which the attribute got modified (added/changed/removed)
+ * last time. Possible values of priority are: `'low'` and `'normal'`.
+ *
+ * Priorities are used by internal `LiveSelection` mechanisms. All attributes set using `LiveSelection`
+ * attributes API are set with `'normal'` priority.
+ *
+ * @private
+ * @member {Map} module:engine/model/liveselection~LiveSelection#_attributePriority
+ */
+ this._attributePriority = new Map();
+
+ // Add events that will ensure selection correctness.
+ this.on( 'change:range', () => {
+ for ( const range of this.getRanges() ) {
+ if ( !this._document._validateSelectionRange( range ) ) {
+ /**
+ * Range from {@link module:engine/model/documentselection~DocumentSelection document selection}
+ * starts or ends at incorrect position.
+ *
+ * @error document-selection-wrong-position
+ * @param {module:engine/model/range~Range} range
+ */
+ throw new CKEditorError(
+ 'document-selection-wrong-position: Range from document selection starts or ends at incorrect position.',
+ { range }
+ );
+ }
+ }
+ } );
+
+ this.listenTo( this._model, 'applyOperation', ( evt, args ) => {
+ const operation = args[ 0 ];
+
+ if ( !operation.isDocumentOperation ) {
+ return;
+ }
+
+ // Whenever attribute operation is performed on document, update selection attributes.
+ // This is not the most efficient way to update selection attributes, but should be okay for now.
+ if ( attrOpTypes.has( operation.type ) ) {
+ this._updateAttributes( false );
+ }
+
+ const batch = operation.delta.batch;
+
+ // Batch may not be passed to the document#change event in some tests.
+ // See https://github.com/ckeditor/ckeditor5-engine/issues/1001#issuecomment-314202352
+ if ( batch ) {
+ // Whenever element which had selection's attributes stored in it stops being empty,
+ // the attributes need to be removed.
+ clearAttributesStoredInElement( operation, this._model, batch );
+ }
+ }, { priority: 'low' } );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ get isCollapsed() {
+ const length = this._ranges.length;
+
+ return length === 0 ? this._document._getDefaultRange().isCollapsed : super.isCollapsed;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ get anchor() {
+ return super.anchor || this._document._getDefaultRange().start;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ get focus() {
+ return super.focus || this._document._getDefaultRange().end;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ get rangeCount() {
+ return this._ranges.length ? this._ranges.length : 1;
+ }
+
+ /**
+ * Describes whether `LiveSelection` has own range(s) set, or if it is defaulted to
+ * {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
+ *
+ * @readonly
+ * @type {Boolean}
+ */
+ get hasOwnRange() {
+ return this._ranges.length > 0;
+ }
+
+ /**
+ * Unbinds all events previously bound by live selection.
+ */
+ destroy() {
+ for ( let i = 0; i < this._ranges.length; i++ ) {
+ this._ranges[ i ].detach();
+ }
+
+ this.stopListening();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ * getRanges() {
+ if ( this._ranges.length ) {
+ yield* super.getRanges();
+ } else {
+ yield this._document._getDefaultRange();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ getFirstRange() {
+ return super.getFirstRange() || this._document._getDefaultRange();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ getLastRange() {
+ return super.getLastRange() || this._document._getDefaultRange();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ setTo( selectable, backwardSelectionOrOffset ) {
+ super.setTo( selectable, backwardSelectionOrOffset );
+ this._refreshAttributes();
+ }
+
+ setFocus( itemOrPosition, offset ) {
+ super.setFocus( itemOrPosition, offset );
+ this._refreshAttributes();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ setAttribute( key, value ) {
+ if ( this._setAttribute( key, value ) ) {
+ // Fire event with exact data.
+ const attributeKeys = [ key ];
+ this.fire( 'change:attribute', { attributeKeys, directChange: true } );
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ removeAttribute( key ) {
+ if ( this._removeAttribute( key ) ) {
+ // Fire event with exact data.
+ const attributeKeys = [ key ];
+ this.fire( 'change:attribute', { attributeKeys, directChange: true } );
+ }
+ }
+
+ /**
+ * Removes all attributes from the selection and sets attributes according to the surrounding nodes.
+ *
+ * @protected
+ */
+ _refreshAttributes() {
+ this._updateAttributes( true );
+ }
+
+ /**
+ * This method is not available in `LiveSelection`. There can be only one
+ * `LiveSelection` per document instance, so creating new `LiveSelection`s this way
+ * would be unsafe.
+ */
+ static createFromSelection() {
+ /**
+ * Cannot create a new `LiveSelection` instance.
+ *
+ * `LiveSelection#createFromSelection()` is not available. There can be only one
+ * `LiveSelection` per document instance, so creating new `LiveSelection`s this way
+ * would be unsafe.
+ *
+ * @error liveselection-cannot-create
+ */
+ throw new CKEditorError( 'liveselection-cannot-create: Cannot create a new LiveSelection instance.' );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ _popRange() {
+ this._ranges.pop().detach();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ _pushRange( range ) {
+ const liveRange = this._prepareRange( range );
+
+ // `undefined` is returned when given `range` is in graveyard root.
+ if ( liveRange ) {
+ this._ranges.push( liveRange );
+ }
+ }
+
+ /**
+ * Prepares given range to be added to selection. Checks if it is correct,
+ * converts it to {@link module:engine/model/liverange~LiveRange LiveRange}
+ * and sets listeners listening to the range's change event.
+ *
+ * @private
+ * @param {module:engine/model/range~Range} range
+ */
+ _prepareRange( range ) {
+ this._checkRange( range );
+
+ if ( range.root == this._document.graveyard ) {
+ /**
+ * Trying to add a Range that is in the graveyard root. Range rejected.
+ *
+ * @warning model-selection-range-in-graveyard
+ */
+ log.warn( 'model-selection-range-in-graveyard: Trying to add a Range that is in the graveyard root. Range rejected.' );
+
+ return;
+ }
+
+ const liveRange = LiveRange.createFromRange( range );
+
+ liveRange.on( 'change:range', ( evt, oldRange, data ) => {
+ // If `LiveRange` is in whole moved to the graveyard, fix that range.
+ if ( liveRange.root == this._document.graveyard ) {
+ this._fixGraveyardSelection( liveRange, data.sourcePosition );
+ }
+
+ // Whenever a live range from selection changes, fire an event informing about that change.
+ this.fire( 'change:range', { directChange: false } );
+ } );
+
+ return liveRange;
+ }
+
+ /**
+ * Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
+ *
+ * @protected
+ * @param {Boolean} clearAll
+ * @fires change:attribute
+ */
+ _updateAttributes( clearAll ) {
+ const newAttributes = toMap( this._getSurroundingAttributes() );
+ const oldAttributes = toMap( this.getAttributes() );
+
+ if ( clearAll ) {
+ // If `clearAll` remove all attributes and reset priorities.
+ this._attributePriority = new Map();
+ this._attrs = new Map();
+ } else {
+ // If not, remove only attributes added with `low` priority.
+ for ( const [ key, priority ] of this._attributePriority ) {
+ if ( priority == 'low' ) {
+ this._attrs.delete( key );
+ this._attributePriority.delete( key );
+ }
+ }
+ }
+
+ this._setAttributesTo( newAttributes, false );
+
+ // Let's evaluate which attributes really changed.
+ const changed = [];
+
+ // First, loop through all attributes that are set on selection right now.
+ // Check which of them are different than old attributes.
+ for ( const [ newKey, newValue ] of this.getAttributes() ) {
+ if ( !oldAttributes.has( newKey ) || oldAttributes.get( newKey ) !== newValue ) {
+ changed.push( newKey );
+ }
+ }
+
+ // Then, check which of old attributes got removed.
+ for ( const [ oldKey ] of oldAttributes ) {
+ if ( !this.hasAttribute( oldKey ) ) {
+ changed.push( oldKey );
+ }
+ }
+
+ // Fire event with exact data (fire only if anything changed).
+ if ( changed.length > 0 ) {
+ this.fire( 'change:attribute', { attributeKeys: changed, directChange: false } );
+ }
+ }
+
+ /**
+ * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
+ *
+ * @protected
+ * @param {String} key Attribute key to convert.
+ * @returns {String} Converted attribute key, applicable for selection store.
+ */
+ static _getStoreAttributeKey( key ) {
+ return storePrefix + key;
+ }
+
+ /**
+ * Checks whether the given attribute key is an attribute stored on an element.
+ *
+ * @protected
+ * @param {String} key
+ * @returns {Boolean}
+ */
+ static _isStoreAttributeKey( key ) {
+ return key.startsWith( storePrefix );
+ }
+
+ /**
+ * Internal method for setting `LiveSelection` attribute. Supports attribute priorities (through `directChange`
+ * parameter).
+ *
+ * @private
+ * @param {String} key Attribute key.
+ * @param {*} value Attribute value.
+ * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
+ * is caused by `Batch` API.
+ * @returns {Boolean} Whether value has changed.
+ */
+ _setAttribute( key, value, directChange = true ) {
+ const priority = directChange ? 'normal' : 'low';
+
+ if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
+ // Priority too low.
+ return false;
+ }
+
+ const oldValue = super.getAttribute( key );
+
+ // Don't do anything if value has not changed.
+ if ( oldValue === value ) {
+ return false;
+ }
+
+ this._attrs.set( key, value );
+
+ // Update priorities map.
+ this._attributePriority.set( key, priority );
+
+ return true;
+ }
+
+ /**
+ * Internal method for removing `LiveSelection` attribute. Supports attribute priorities (through `directChange`
+ * parameter).
+ *
+ * @private
+ * @param {String} key Attribute key.
+ * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
+ * is caused by `Batch` API.
+ * @returns {Boolean} Whether attribute was removed. May not be true if such attributes didn't exist or the
+ * existing attribute had higher priority.
+ */
+ _removeAttribute( key, directChange = true ) {
+ const priority = directChange ? 'normal' : 'low';
+
+ if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
+ // Priority too low.
+ return false;
+ }
+
+ // Don't do anything if value has not changed.
+ if ( !super.hasAttribute( key ) ) {
+ return false;
+ }
+
+ this._attrs.delete( key );
+
+ // Update priorities map.
+ this._attributePriority.set( key, priority );
+
+ return true;
+ }
+
+ /**
+ * Internal method for setting multiple `LiveSelection` attributes. Supports attribute priorities (through
+ * `directChange` parameter).
+ *
+ * @private
+ * @param {Map.} attrs Iterable object containing attributes to be set.
+ * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
+ * is caused by `Batch` API.
+ * @returns {Set.} Changed attribute keys.
+ */
+ _setAttributesTo( attrs, directChange = true ) {
+ const changed = new Set();
+
+ for ( const [ oldKey, oldValue ] of this.getAttributes() ) {
+ // Do not remove attribute if attribute with same key and value is about to be set.
+ if ( attrs.get( oldKey ) === oldValue ) {
+ continue;
+ }
+
+ // Attribute still might not get removed because of priorities.
+ if ( this._removeAttribute( oldKey, directChange ) ) {
+ changed.add( oldKey );
+ }
+ }
+
+ for ( const [ key, value ] of attrs ) {
+ // Attribute may not be set because of attributes or because same key/value is already added.
+ const gotAdded = this._setAttribute( key, value, directChange );
+
+ if ( gotAdded ) {
+ changed.add( key );
+ }
+ }
+
+ return changed;
+ }
+
+ /**
+ * Returns an iterable that iterates through all selection attributes stored in current selection's parent.
+ *
+ * @protected
+ * @returns {Iterable.<*>}
+ */
+ * _getStoredAttributes() {
+ const selectionParent = this.getFirstPosition().parent;
+
+ if ( this.isCollapsed && selectionParent.isEmpty ) {
+ for ( const key of selectionParent.getAttributeKeys() ) {
+ if ( key.startsWith( storePrefix ) ) {
+ const realKey = key.substr( storePrefix.length );
+
+ yield [ realKey, selectionParent.getAttribute( key ) ];
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks model text nodes that are closest to the selection's first position and returns attributes of first
+ * found element. If there are no text nodes in selection's first position parent, it returns selection
+ * attributes stored in that parent.
+ *
+ * @private
+ * @returns {Iterable.<*>} Collection of attributes.
+ */
+ _getSurroundingAttributes() {
+ const position = this.getFirstPosition();
+ const schema = this._model.schema;
+
+ let attrs = null;
+
+ if ( !this.isCollapsed ) {
+ // 1. If selection is a range...
+ const range = this.getFirstRange();
+
+ // ...look for a first character node in that range and take attributes from it.
+ for ( const value of range ) {
+ // If the item is an object, we don't want to get attributes from its children.
+ if ( value.item.is( 'element' ) && schema.isObject( value.item ) ) {
+ break;
+ }
+
+ // This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
+ // It can be done better by using `break;` instead of checking `attrs === null`.
+ if ( value.type == 'text' && attrs === null ) {
+ attrs = value.item.getAttributes();
+ }
+ }
+ } else {
+ // 2. If the selection is a caret or the range does not contain a character node...
+
+ const nodeBefore = position.textNode ? position.textNode : position.nodeBefore;
+ const nodeAfter = position.textNode ? position.textNode : position.nodeAfter;
+
+ // ...look at the node before caret and take attributes from it if it is a character node.
+ attrs = getAttrsIfCharacter( nodeBefore );
+
+ // 3. If not, look at the node after caret...
+ if ( !attrs ) {
+ attrs = getAttrsIfCharacter( nodeAfter );
+ }
+
+ // 4. If not, try to find the first character on the left, that is in the same node.
+ if ( !attrs ) {
+ let node = nodeBefore;
+
+ while ( node && !attrs ) {
+ node = node.previousSibling;
+ attrs = getAttrsIfCharacter( node );
+ }
+ }
+
+ // 5. If not found, try to find the first character on the right, that is in the same node.
+ if ( !attrs ) {
+ let node = nodeAfter;
+
+ while ( node && !attrs ) {
+ node = node.nextSibling;
+ attrs = getAttrsIfCharacter( node );
+ }
+ }
+
+ // 6. If not found, selection should retrieve attributes from parent.
+ if ( !attrs ) {
+ attrs = this._getStoredAttributes();
+ }
+ }
+
+ return attrs;
+ }
+
+ /**
+ * Fixes a selection range after it ends up in graveyard root.
+ *
+ * @private
+ * @param {module:engine/model/liverange~LiveRange} liveRange The range from selection, that ended up in the graveyard root.
+ * @param {module:engine/model/position~Position} removedRangeStart Start position of a range which was removed.
+ */
+ _fixGraveyardSelection( liveRange, removedRangeStart ) {
+ // The start of the removed range is the closest position to the `liveRange` - the original selection range.
+ // This is a good candidate for a fixed selection range.
+ const positionCandidate = Position.createFromPosition( removedRangeStart );
+
+ // Find a range that is a correct selection range and is closest to the start of removed range.
+ const selectionRange = this._document.getNearestSelectionRange( positionCandidate );
+
+ // Remove the old selection range before preparing and adding new selection range. This order is important,
+ // because new range, in some cases, may intersect with old range (it depends on `getNearestSelectionRange()` result).
+ const index = this._ranges.indexOf( liveRange );
+ this._ranges.splice( index, 1 );
+ liveRange.detach();
+
+ // If nearest valid selection range has been found - add it in the place of old range.
+ if ( selectionRange ) {
+ // Check the range, convert it to live range, bind events, etc.
+ const newRange = this._prepareRange( selectionRange );
+
+ // Add new range in the place of old range.
+ this._ranges.splice( index, 0, newRange );
+ }
+ // If nearest valid selection range cannot be found - just removing the old range is fine.
+
+ // Fire an event informing about selection change.
+ this.fire( 'change:range', { directChange: false } );
+ }
+}
+
+/**
+ * @event change:attribute
+ */
+
+// Helper function for {@link module:engine/model/liveselection~LiveSelection#_updateAttributes}.
+//
+// It takes model item, checks whether it is a text node (or text proxy) and, if so, returns it's attributes. If not, returns `null`.
+//
+// @param {module:engine/model/item~Item|null} node
+// @returns {Boolean}
+function getAttrsIfCharacter( node ) {
+ if ( node instanceof TextProxy || node instanceof Text ) {
+ return node.getAttributes();
+ }
+
+ return null;
+}
+
+// Removes selection attributes from element which is not empty anymore.
+function clearAttributesStoredInElement( operation, model, batch ) {
+ let changeParent = null;
+
+ if ( operation.type == 'insert' ) {
+ changeParent = operation.position.parent;
+ } else if ( operation.type == 'move' || operation.type == 'reinsert' || operation.type == 'remove' ) {
+ changeParent = operation.getMovedRangeStart().parent;
+ }
+
+ if ( !changeParent || changeParent.isEmpty ) {
+ return;
+ }
+
+ model.enqueueChange( batch, writer => {
+ const storedAttributes = Array.from( changeParent.getAttributeKeys() ).filter( key => key.startsWith( storePrefix ) );
+
+ for ( const key of storedAttributes ) {
+ writer.removeAttribute( key, changeParent );
+ }
+ } );
+}
diff --git a/src/model/selection.js b/src/model/selection.js
index 5f1977b6d..0faae58f9 100644
--- a/src/model/selection.js
+++ b/src/model/selection.js
@@ -13,14 +13,15 @@ import Range from './range';
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
-import mapsEqual from '@ckeditor/ckeditor5-utils/src/mapsequal';
import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
+import DocumentSelection from './documentselection';
/**
* `Selection` is a group of {@link module:engine/model/range~Range ranges} which has a direction specified by
* {@link module:engine/model/selection~Selection#anchor anchor} and {@link module:engine/model/selection~Selection#focus focus}.
* Additionally, `Selection` may have it's own attributes.
+ *
+ * @mixes {module:utils/emittermixin~EmitterMixin}
*/
export default class Selection {
/**
@@ -35,7 +36,7 @@ export default class Selection {
* Specifies whether the last added range was added as a backward or forward range.
*
* @private
- * @member {Boolean}
+ * @type {Boolean}
*/
this._lastRangeBackward = false;
@@ -43,7 +44,7 @@ export default class Selection {
* Stores selection ranges.
*
* @protected
- * @member {Array.}
+ * @type {Array.}
*/
this._ranges = [];
@@ -51,12 +52,12 @@ export default class Selection {
* List of attributes set on current selection.
*
* @protected
- * @member {Map} module:engine/model/selection~Selection#_attrs
+ * @type {Map.}
*/
this._attrs = new Map();
if ( ranges ) {
- this.setRanges( ranges, isLastBackward );
+ this._setRanges( ranges, isLastBackward );
}
}
@@ -121,6 +122,7 @@ export default class Selection {
/**
* Returns number of ranges in selection.
*
+ * @readonly
* @type {Number}
*/
get rangeCount() {
@@ -131,6 +133,7 @@ export default class Selection {
* Specifies whether the {@link #focus}
* precedes {@link #anchor}.
*
+ * @readonly
* @type {Boolean}
*/
get isBackward() {
@@ -174,7 +177,7 @@ export default class Selection {
}
/**
- * Returns an iterator that iterates over copies of selection ranges.
+ * Returns an iterable that iterates over copies of selection ranges.
*
* @returns {Iterable.}
*/
@@ -259,52 +262,81 @@ export default class Selection {
}
/**
- * Adds a range to this selection. Added range is copied. This means that passed range is not saved in `Selection`
- * instance and operating on it will not change `Selection` state.
- *
- * Accepts a flag describing in which way the selection is made - passed range might be selected from
- * {@link module:engine/model/range~Range#start start} to {@link module:engine/model/range~Range#end end}
- * or from {@link module:engine/model/range~Range#end end}
- * to {@link module:engine/model/range~Range#start start}.
- * The flag is used to set {@link #anchor} and
- * {@link #focus} properties.
- *
- * @fires change:range
- * @param {module:engine/model/range~Range} range Range to add.
- * @param {Boolean} [isBackward=false] Flag describing if added range was selected forward - from start to end (`false`)
- * or backward - from end to start (`true`).
- */
- addRange( range, isBackward = false ) {
- this._pushRange( range );
- this._lastRangeBackward = !!isBackward;
-
- this.fire( 'change:range', { directChange: true } );
- }
-
- /**
- * Removes all ranges that were added to the selection.
- *
- * @fires change:range
- */
- removeAllRanges() {
- if ( this._ranges.length > 0 ) {
- this._removeAllRanges();
- this.fire( 'change:range', { directChange: true } );
+ * Sets this selection's ranges and direction to the specified location based on the given
+ * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
+ * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
+ * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
+ *
+ * // Sets ranges from the given range.
+ * const range = new Range( start, end );
+ * selection.setTo( range, isBackwardSelection );
+ *
+ * // Sets ranges from the iterable of ranges.
+ * const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+ * selection.setTo( range, isBackwardSelection );
+ *
+ * // Sets ranges from the other selection.
+ * const otherSelection = new Selection();
+ * selection.setTo( otherSelection );
+ *
+ * // Sets ranges from the given document selection's ranges.
+ * const documentSelection = new DocumentSelection( doc );
+ * selection.setTo( documentSelection );
+ *
+ * // Sets range at the given position.
+ * const position = new Position( root, path );
+ * selection.setTo( position );
+ *
+ * // Sets range at the given element.
+ * const paragraph = writer.createElement( 'paragraph' );
+ * selection.setTo( paragraph, offset );
+ *
+ * // Removes all ranges.
+ * selection.setTo( null );
+ *
+ * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+ * module:engine/model/position~Position|module:engine/model/element~Element|
+ * Iterable.|module:engine/model/range~Range|null} selectable
+ * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
+ */
+ setTo( selectable, backwardSelectionOrOffset ) {
+ if ( !selectable ) {
+ this._setRanges( [] );
+ } else if ( selectable instanceof Selection ) {
+ this._setRanges( selectable.getRanges(), selectable.isBackward );
+ } else if ( selectable instanceof DocumentSelection ) {
+ this._setRanges( selectable.getRanges(), selectable.isBackward );
+ } else if ( selectable instanceof Range ) {
+ this._setRanges( [ selectable ], backwardSelectionOrOffset );
+ } else if ( selectable instanceof Position ) {
+ this._setRanges( [ new Range( selectable ) ] );
+ } else if ( selectable instanceof Element ) {
+ this._setRanges( [ Range.createCollapsedAt( selectable, backwardSelectionOrOffset ) ] );
+ } else if ( isIterable( selectable ) ) {
+ // We assume that the selectable is an iterable of ranges.
+ this._setRanges( selectable, backwardSelectionOrOffset );
+ } else {
+ /**
+ * Cannot set selection to given place.
+ *
+ * @error model-selection-setTo-not-selectable
+ */
+ throw new CKEditorError( 'model-selection-setTo-not-selectable: Cannot set selection to given place.' );
}
}
/**
* Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
* is treated like the last added range and is used to set {@link module:engine/model/selection~Selection#anchor} and
- * {@link module:engine/model/selection~Selection#focus}. Accepts a flag describing in which direction the selection is made
- * (see {@link module:engine/model/selection~Selection#addRange}).
+ * {@link module:engine/model/selection~Selection#focus}. Accepts a flag describing in which direction the selection is made.
*
+ * @protected
* @fires change:range
* @param {Iterable.} newRanges Ranges to set.
* @param {Boolean} [isLastBackward=false] Flag describing if last added range was selected forward - from start to end (`false`)
* or backward - from end to start (`true`).
*/
- setRanges( newRanges, isLastBackward = false ) {
+ _setRanges( newRanges, isLastBackward = false ) {
newRanges = Array.from( newRanges );
// Check whether there is any range in new ranges set that is different than all already added ranges.
@@ -335,89 +367,14 @@ export default class Selection {
}
/**
- * Sets this selection's ranges and direction to the specified location based on the given
- * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
- * {@link module:engine/model/range~Range range} or an iterable of {@link module:engine/model/range~Range ranges}.
- *
- * @param {module:engine/model/selection~Selection|module:engine/model/position~Position|
- * Iterable.|module:engine/model/range~Range} selectable
- */
- setTo( selectable ) {
- if ( selectable instanceof Selection ) {
- this.setRanges( selectable.getRanges(), selectable.isBackward );
- } else if ( selectable instanceof Range ) {
- this.setRanges( [ selectable ] );
- } else if ( isIterable( selectable ) ) {
- // We assume that the selectable is an iterable of ranges.
- this.setRanges( selectable );
- } else {
- // We assume that the selectable is a position.
- this.setRanges( [ new Range( selectable ) ] );
- }
- }
-
- /**
- * Sets this selection in the provided element.
- *
- * @param {module:engine/model/element~Element} element
- */
- setIn( element ) {
- this.setRanges( [ Range.createIn( element ) ] );
- }
-
- /**
- * Sets this selection on the provided item.
- *
- * @param {module:engine/model/item~Item} item
- */
- setOn( item ) {
- this.setRanges( [ Range.createOn( item ) ] );
- }
-
- /**
- * Sets collapsed selection at the specified location.
- *
- * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
- *
- * @fires change:range
- * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
- * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
- * first parameter is a {@link module:engine/model/item~Item model item}.
- */
- setCollapsedAt( itemOrPosition, offset ) {
- const pos = Position.createAt( itemOrPosition, offset );
- const range = new Range( pos, pos );
-
- this.setRanges( [ range ] );
- }
-
- /**
- * Collapses selection to the selection's {@link module:engine/model/selection~Selection#getFirstPosition first position}.
- * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
- * inside selection.
- *
- * @fires change
- */
- collapseToStart() {
- const startPosition = this.getFirstPosition();
-
- if ( startPosition !== null ) {
- this.setRanges( [ new Range( startPosition, startPosition ) ] );
- }
- }
-
- /**
- * Collapses selection to the selection's {@link module:engine/model/selection~Selection#getLastPosition last position}.
- * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
- * inside selection.
+ * Deletes ranges from internal range array. Uses {@link #_popRange _popRange} to
+ * ensure proper ranges removal.
*
- * @fires change
+ * @private
*/
- collapseToEnd() {
- const endPosition = this.getLastPosition();
-
- if ( endPosition !== null ) {
- this.setRanges( [ new Range( endPosition, endPosition ) ] );
+ _removeAllRanges() {
+ while ( this._ranges.length > 0 ) {
+ this._popRange();
}
}
@@ -431,15 +388,15 @@ export default class Selection {
* @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
* first parameter is a {@link module:engine/model/item~Item model item}.
*/
- moveFocusTo( itemOrPosition, offset ) {
+ setFocus( itemOrPosition, offset ) {
if ( this.anchor === null ) {
/**
* Cannot set selection focus if there are no ranges in selection.
*
- * @error model-selection-moveFocusTo-no-ranges
+ * @error model-selection-setFocus-no-ranges
*/
throw new CKEditorError(
- 'model-selection-moveFocusTo-no-ranges: Cannot set selection focus if there are no ranges in selection.'
+ 'model-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.'
);
}
@@ -456,10 +413,14 @@ export default class Selection {
}
if ( newFocus.compareWith( anchor ) == 'before' ) {
- this.addRange( new Range( newFocus, anchor ), true );
+ this._pushRange( new Range( newFocus, anchor ) );
+ this._lastRangeBackward = true;
} else {
- this.addRange( new Range( anchor, newFocus ) );
+ this._pushRange( new Range( anchor, newFocus ) );
+ this._lastRangeBackward = false;
}
+
+ this.fire( 'change:range', { directChange: true } );
}
/**
@@ -473,7 +434,7 @@ export default class Selection {
}
/**
- * Returns iterator that iterates over this selection's attributes.
+ * Returns iterable that iterates over this selection's attributes.
*
* Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
* This format is accepted by native `Map` object and also can be passed in `Node` constructor.
@@ -485,7 +446,7 @@ export default class Selection {
}
/**
- * Returns iterator that iterates over this selection's attribute keys.
+ * Returns iterable that iterates over this selection's attribute keys.
*
* @returns {Iterable.}
*/
@@ -503,23 +464,6 @@ export default class Selection {
return this._attrs.has( key );
}
- /**
- * Removes all attributes from the selection.
- *
- * If there were any attributes in selection, fires the {@link #event:change} event with
- * removed attributes' keys.
- *
- * @fires change:attribute
- */
- clearAttributes() {
- if ( this._attrs.size > 0 ) {
- const attributeKeys = Array.from( this._attrs.keys() );
- this._attrs.clear();
-
- this.fire( 'change:attribute', { attributeKeys, directChange: true } );
- }
- }
-
/**
* Removes an attribute with given key from the selection.
*
@@ -555,35 +499,6 @@ export default class Selection {
}
}
- /**
- * Removes all attributes from the selection and sets given attributes.
- *
- * If given set of attributes is different than set of attributes already added to selection, fires
- * {@link #event:change change event} with keys of attributes that changed.
- *
- * @fires event:change:attribute
- * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
- */
- setAttributesTo( attrs ) {
- attrs = toMap( attrs );
-
- if ( !mapsEqual( attrs, this._attrs ) ) {
- // Create a set from keys of old and new attributes.
- const changed = new Set( Array.from( attrs.keys() ).concat( Array.from( this._attrs.keys() ) ) );
-
- for ( const [ key, value ] of attrs ) {
- // If the attribute remains unchanged, remove it from changed set.
- if ( this._attrs.get( key ) === value ) {
- changed.delete( key );
- }
- }
-
- this._attrs = attrs;
-
- this.fire( 'change:attribute', { attributeKeys: Array.from( changed ), directChange: true } );
- }
- }
-
/**
* Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
* one range in the selection, and that range contains exactly one element.
@@ -697,10 +612,6 @@ export default class Selection {
* @param {module:engine/model/range~Range} range Range to add.
*/
_pushRange( range ) {
- if ( !( range instanceof Range ) ) {
- throw new CKEditorError( 'model-selection-added-not-range: Trying to add an object that is not an instance of Range.' );
- }
-
this._checkRange( range );
this._ranges.push( Range.createFromRange( range ) );
}
@@ -738,18 +649,6 @@ export default class Selection {
this._ranges.pop();
}
- /**
- * Deletes ranges from internal range array. Uses {@link #_popRange _popRange} to
- * ensure proper ranges removal.
- *
- * @private
- */
- _removeAllRanges() {
- while ( this._ranges.length > 0 ) {
- this._popRange();
- }
- }
-
/**
* @event change
*/
diff --git a/src/model/utils/deletecontent.js b/src/model/utils/deletecontent.js
index 4911ac753..8a369095a 100644
--- a/src/model/utils/deletecontent.js
+++ b/src/model/utils/deletecontent.js
@@ -10,6 +10,7 @@
import LivePosition from '../liveposition';
import Position from '../position';
import Range from '../range';
+import DocumentSelection from '../documentselection';
/**
* Deletes content of the selection and merge siblings. The resulting selection is always collapsed.
@@ -82,7 +83,11 @@ export default function deleteContent( model, selection, options = {} ) {
schema.removeDisallowedAttributes( startPos.parent.getChildren(), writer );
}
- selection.setCollapsedAt( startPos );
+ if ( selection instanceof DocumentSelection ) {
+ selection._setTo( startPos );
+ } else {
+ selection.setTo( startPos );
+ }
// 4. Autoparagraphing.
// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
@@ -186,7 +191,12 @@ function insertParagraph( writer, position, selection ) {
const paragraph = writer.createElement( 'paragraph' );
writer.insert( paragraph, position );
- selection.setCollapsedAt( paragraph );
+
+ if ( selection instanceof DocumentSelection ) {
+ selection._setTo( paragraph );
+ } else {
+ selection.setTo( paragraph );
+ }
}
function replaceEntireContentWithParagraph( writer, selection ) {
diff --git a/src/model/utils/insertcontent.js b/src/model/utils/insertcontent.js
index b3baed37b..6257d52a4 100644
--- a/src/model/utils/insertcontent.js
+++ b/src/model/utils/insertcontent.js
@@ -12,6 +12,7 @@ import LivePosition from '../liveposition';
import Element from '../element';
import Range from '../range';
import log from '@ckeditor/ckeditor5-utils/src/log';
+import DocumentSelection from '../documentselection';
/**
* Inserts content into the editor (specified selection) as one would expect the paste
@@ -54,7 +55,11 @@ export default function insertContent( model, content, selection ) {
/* istanbul ignore else */
if ( newRange ) {
- selection.setRanges( [ newRange ] );
+ if ( selection instanceof DocumentSelection ) {
+ selection._setTo( newRange );
+ } else {
+ selection.setTo( newRange );
+ }
} else {
// We are not testing else because it's a safe check for unpredictable edge cases:
// an insertion without proper range to select.
diff --git a/src/model/utils/modifyselection.js b/src/model/utils/modifyselection.js
index 679e51707..8d593ff20 100644
--- a/src/model/utils/modifyselection.js
+++ b/src/model/utils/modifyselection.js
@@ -11,6 +11,7 @@ import Position from '../position';
import TreeWalker from '../treewalker';
import Range from '../range';
import { isInsideSurrogatePair, isInsideCombinedSymbol } from '@ckeditor/ckeditor5-utils/src/unicode';
+import DocumentSelection from '../documentselection';
/**
* Modifies the selection. Currently, the supported modifications are:
@@ -64,7 +65,11 @@ export default function modifySelection( model, selection, options = {} ) {
const position = tryExtendingTo( data, next.value );
if ( position ) {
- selection.moveFocusTo( position );
+ if ( selection instanceof DocumentSelection ) {
+ selection._setFocus( position );
+ } else {
+ selection.setFocus( position );
+ }
return;
}
diff --git a/src/model/writer.js b/src/model/writer.js
index be88b435b..3184592e1 100644
--- a/src/model/writer.js
+++ b/src/model/writer.js
@@ -35,6 +35,7 @@ import Element from './element';
import RootElement from './rootelement';
import Position from './position';
import Range from './range.js';
+import DocumentSelection from './documentselection';
import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
@@ -790,7 +791,139 @@ export default class Writer {
}
/**
- * Throws `writer-incorrect-use` error when the writer is used outside the `change()` block.
+ * Sets this selection's ranges and direction to the specified location based on the given
+ * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
+ * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
+ * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
+ *
+ * Uses internally {@link module:engine/model/documentselection~DocumentSelection#_setTo}.
+ *
+ * // Sets ranges from the given range.
+ * const range = new Range( start, end );
+ * selection.setTo( range, isBackwardSelection );
+ *
+ * // Sets ranges from the iterable of ranges.
+ * const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+ * selection.setTo( range, isBackwardSelection );
+ *
+ * // Sets ranges from the other selection.
+ * const otherSelection = new Selection();
+ * selection.setTo( otherSelection );
+ *
+ * // Sets ranges from the given document selection's ranges.
+ * const documentSelection = new DocumentSelection( doc );
+ * selection.setTo( documentSelection );
+ *
+ * // Sets range at the given position.
+ * const position = new Position( root, path );
+ * selection.setTo( position );
+ *
+ * // Sets range at the given element.
+ * const paragraph = writer.createElement( 'paragraph' );
+ * selection.setTo( paragraph, offset );
+ *
+ * // Removes all ranges.
+ * selection.setTo( null );
+ *
+ * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+ * module:engine/model/position~Position|module:engine/model/element~Element|
+ * Iterable.|module:engine/model/range~Range|null} selectable
+ * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
+ */
+ setSelection( selectable, backwardSelectionOrOffset ) {
+ this._assertWriterUsedCorrectly();
+
+ this.model.document.selection._setTo( selectable, backwardSelectionOrOffset );
+ }
+
+ /**
+ * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
+ *
+ * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
+ *
+ * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+ * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+ * first parameter is a {@link module:engine/model/item~Item model item}.
+ */
+ setSelectionFocus( itemOrPosition, offset ) {
+ this._assertWriterUsedCorrectly();
+
+ this.model.document.selection._setFocus( itemOrPosition, offset );
+ }
+
+ /**
+ * Sets attribute(s) on the selection. If attribute with the same key already is set, it's value is overwritten.
+ *
+ * @param {String|Object|Iterable.<*>} keyOrObjectOrIterable Key of the attribute to set
+ * or object / iterable of key - value attribute pairs.
+ * @param {*} [value] Attribute value.
+ */
+ setSelectionAttribute( keyOrObjectOrIterable, value ) {
+ this._assertWriterUsedCorrectly();
+
+ if ( typeof keyOrObjectOrIterable === 'string' ) {
+ this._setSelectionAttribute( keyOrObjectOrIterable, value );
+ } else {
+ for ( const [ key, value ] of toMap( keyOrObjectOrIterable ) ) {
+ this._setSelectionAttribute( key, value );
+ }
+ }
+ }
+
+ /**
+ * @private
+ * @param {String} key
+ * @param {*} value
+ */
+ _setSelectionAttribute( key, value ) {
+ const selection = this.model.document.selection;
+
+ // Store attribute in parent element if the selection is collapsed in an empty node.
+ if ( selection.isCollapsed && selection.anchor.parent.isEmpty ) {
+ const storeKey = DocumentSelection._getStoreAttributeKey( key );
+
+ this.setAttribute( storeKey, value, selection.anchor.parent );
+ }
+
+ selection._setAttribute( key, value );
+ }
+
+ /**
+ * Removes an attribute with given key from the selection.
+ *
+ * @param {String|Iterable.} keyOrIterableOfKeys Key of the attribute to remove.
+ */
+ removeSelectionAttribute( keyOrIterableOfKeys ) {
+ this._assertWriterUsedCorrectly();
+
+ if ( typeof keyOrIterableOfKeys === 'string' ) {
+ this._removeSelectionAttribute( keyOrIterableOfKeys );
+ } else {
+ for ( const key of keyOrIterableOfKeys ) {
+ this._removeSelectionAttribute( key );
+ }
+ }
+ }
+
+ /**
+ * @private
+ * @param {String} key Key of the attribute to remove.
+ */
+ _removeSelectionAttribute( key ) {
+ const selection = this.model.document.selection;
+
+ // Remove stored attribute from parent element if the selection is collapsed in an empty node.
+ if ( selection.isCollapsed && selection.anchor.parent.isEmpty ) {
+ const storeKey = DocumentSelection._getStoreAttributeKey( key );
+
+ this.removeAttribute( storeKey, selection.anchor.parent );
+ }
+
+ selection._removeAttribute( key );
+ }
+
+ /**
+ * Throws `writer-detached-writer-tries-to-modify-model` error when the writer is used outside of the `change()` block.
*
* @private
*/
diff --git a/src/view/domconverter.js b/src/view/domconverter.js
index ca1d4097f..0011594a6 100644
--- a/src/view/domconverter.js
+++ b/src/view/domconverter.js
@@ -480,7 +480,7 @@ export default class DomConverter {
const viewRange = this.domRangeToView( domRange );
if ( viewRange ) {
- viewSelection.addRange( viewRange, isBackward );
+ viewSelection.setTo( viewRange, isBackward );
}
}
diff --git a/src/view/observer/fakeselectionobserver.js b/src/view/observer/fakeselectionobserver.js
index 5f17b6417..31d44cf73 100644
--- a/src/view/observer/fakeselectionobserver.js
+++ b/src/view/observer/fakeselectionobserver.js
@@ -87,12 +87,12 @@ export default class FakeSelectionObserver extends Observer {
// Left or up arrow pressed - move selection to start.
if ( keyCode == keyCodes.arrowleft || keyCode == keyCodes.arrowup ) {
- newSelection.collapseToStart();
+ newSelection.setTo( newSelection.getFirstPosition() );
}
// Right or down arrow pressed - move selection to end.
if ( keyCode == keyCodes.arrowright || keyCode == keyCodes.arrowdown ) {
- newSelection.collapseToEnd();
+ newSelection.setTo( newSelection.getLastPosition() );
}
const data = {
diff --git a/src/view/observer/mutationobserver.js b/src/view/observer/mutationobserver.js
index afc2bee7e..11d5cdaac 100644
--- a/src/view/observer/mutationobserver.js
+++ b/src/view/observer/mutationobserver.js
@@ -240,8 +240,8 @@ export default class MutationObserver extends Observer {
// Anchor and focus has to be properly mapped to view.
if ( viewSelectionAnchor && viewSelectionFocus ) {
viewSelection = new ViewSelection();
- viewSelection.setCollapsedAt( viewSelectionAnchor );
- viewSelection.moveFocusTo( viewSelectionFocus );
+ viewSelection.setTo( viewSelectionAnchor );
+ viewSelection.setFocus( viewSelectionFocus );
}
}
diff --git a/src/view/selection.js b/src/view/selection.js
index c2c6ada32..63e55beb3 100644
--- a/src/view/selection.js
+++ b/src/view/selection.js
@@ -13,16 +13,16 @@ import Position from './position';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
import Element from './element';
+import Text from './text';
import count from '@ckeditor/ckeditor5-utils/src/count';
import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
/**
* Class representing selection in tree view.
*
- * Selection can consist of {@link module:engine/view/range~Range ranges} that can be added using
- * {@link module:engine/view/selection~Selection#addRange addRange}
- * and {@link module:engine/view/selection~Selection#setRanges setRanges} methods.
- * Both methods create copies of provided ranges and store those copies internally. Further modifications to passed
+ * Selection can consist of {@link module:engine/view/range~Range ranges} that can be set using
+ * {@link module:engine/view/selection~Selection#setTo} method.
+ * That method create copies of provided ranges and store those copies internally. Further modifications to passed
* ranges will not change selection's state.
* Selection's ranges can be obtained via {@link module:engine/view/selection~Selection#getRanges getRanges},
* {@link module:engine/view/selection~Selection#getFirstRange getFirstRange}
@@ -74,7 +74,7 @@ export default class Selection {
this._fakeSelectionLabel = '';
if ( ranges ) {
- this.setRanges( ranges, isLastBackward );
+ this._setRanges( ranges, isLastBackward );
}
}
@@ -196,33 +196,7 @@ export default class Selection {
}
/**
- * Adds a range to the selection. Added range is copied. This means that passed range is not saved in the
- * selection instance and you can safely operate on it.
- *
- * Accepts a flag describing in which way the selection is made - passed range might be selected from
- * {@link module:engine/view/range~Range#start start} to {@link module:engine/view/range~Range#end end}
- * or from {@link module:engine/view/range~Range#end end} to {@link module:engine/view/range~Range#start start}.
- * The flag is used to set {@link #anchor anchor} and {@link #focus focus} properties.
- *
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-selection-range-intersects` if added range intersects
- * with ranges already stored in Selection instance.
- *
- * @fires change
- * @param {module:engine/view/range~Range} range
- * @param {Boolean} isBackward
- */
- addRange( range, isBackward ) {
- if ( !( range instanceof Range ) ) {
- throw new CKEditorError( 'view-selection-invalid-range: Invalid Range.' );
- }
-
- this._pushRange( range );
- this._lastRangeBackward = !!isBackward;
- this.fire( 'change' );
- }
-
- /**
- * Returns an iterator that contains copies of all ranges added to the selection.
+ * Returns an iterable that contains copies of all ranges added to the selection.
*
* @returns {Iterable.}
*/
@@ -397,125 +371,94 @@ export default class Selection {
*
* @fires change
*/
- removeAllRanges() {
+ _removeAllRanges() {
if ( this._ranges.length ) {
this._ranges = [];
this.fire( 'change' );
}
}
- /**
- * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
- * is treated like the last added range and is used to set {@link #anchor anchor} and {@link #focus focus}.
- * Accepts a flag describing in which way the selection is made (see {@link #addRange addRange}).
- *
- * @fires change
- * @param {Iterable.} newRanges Iterable object 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`.
- */
- setRanges( newRanges, isLastBackward ) {
- this._ranges = [];
-
- for ( const range of newRanges ) {
- if ( !( range instanceof Range ) ) {
- throw new CKEditorError( 'view-selection-invalid-range: Invalid Range.' );
- }
-
- this._pushRange( range );
- }
-
- this._lastRangeBackward = !!isLastBackward;
- this.fire( 'change' );
- }
-
/**
* Sets this selection's ranges and direction to the specified location based on the given
* {@link module:engine/view/selection~Selection selection}, {@link module:engine/view/position~Position position},
- * {@link module:engine/view/range~Range range} or an iterable of {@link module:engine/view/range~Range ranges}.
+ * {@link module:engine/view/item~Item item}, {@link module:engine/view/range~Range range},
+ * an iterable of {@link module:engine/view/range~Range ranges} or null.
+ *
+ * // Sets ranges from the given range.
+ * const range = new Range( start, end );
+ * selection.setTo( range, isBackwardSelection );
*
+ * // Sets ranges from the iterable of ranges.
+ * const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+ * selection.setTo( range, isBackwardSelection );
+ *
+ * // Sets ranges from the other selection.
+ * const otherSelection = new Selection();
+ * selection.setTo( otherSelection );
+ *
+ * // Sets collapsed range at the given position.
+ * const position = new Position( root, path );
+ * selection.setTo( position );
+ *
+ * // Sets collapsed range on the given item.
+ * const paragraph = writer.createElement( 'paragraph' );
+ * selection.setTo( paragraph, offset );
+ *
+ * // Removes all ranges.
+ * selection.setTo( null );
+
* @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
- * Iterable.|module:engine/view/range~Range} selectable
+ * Iterable.|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable
+ * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
*/
- setTo( selectable ) {
- if ( selectable instanceof Selection ) {
- this._isFake = selectable._isFake;
- this._fakeSelectionLabel = selectable._fakeSelectionLabel;
- this.setRanges( selectable.getRanges(), selectable.isBackward );
+ setTo( selectable, backwardSelectionOrOffset ) {
+ if ( selectable == null ) {
+ this._removeAllRanges();
+ } else if ( selectable instanceof Selection ) {
+ this._isFake = selectable.isFake;
+ this._fakeSelectionLabel = selectable.fakeSelectionLabel;
+ this._setRanges( selectable.getRanges(), selectable.isBackward );
} else if ( selectable instanceof Range ) {
- this.setRanges( [ selectable ] );
+ this._setRanges( [ selectable ], backwardSelectionOrOffset );
+ } else if ( selectable instanceof Position ) {
+ this._setRanges( [ new Range( selectable ) ] );
+ } else if ( selectable instanceof Text ) {
+ this._setRanges( [ Range.createCollapsedAt( selectable, backwardSelectionOrOffset ) ] );
+ } else if ( selectable instanceof Element ) {
+ this._setRanges( [ Range.createCollapsedAt( selectable, backwardSelectionOrOffset ) ] );
} else if ( isIterable( selectable ) ) {
// We assume that the selectable is an iterable of ranges.
- this.setRanges( selectable );
+ this._setRanges( selectable, backwardSelectionOrOffset );
} else {
- // We assume that the selectable is a position.
- this.setRanges( [ new Range( selectable ) ] );
+ /**
+ * Cannot set selection to given place.
+ *
+ * @error view-selection-setTo-not-selectable
+ */
+ throw new CKEditorError( 'view-selection-setTo-not-selectable: Cannot set selection to given place.' );
}
}
/**
- * Sets this selection in the provided element.
- *
- * @param {module:engine/view/element~Element} element
- */
- setIn( element ) {
- this.setRanges( [ Range.createIn( element ) ] );
- }
-
- /**
- * Sets this selection on the provided item.
- *
- * @param {module:engine/view/item~Item} item
- */
- setOn( item ) {
- this.setRanges( [ Range.createOn( item ) ] );
- }
-
- /**
- * Sets collapsed selection at the specified location.
- *
- * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
- *
- * @fires change
- * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
- * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
- * first parameter is a {@link module:engine/view/item~Item view item}.
- */
- setCollapsedAt( itemOrPosition, offset ) {
- const pos = Position.createAt( itemOrPosition, offset );
- const range = new Range( pos, pos );
-
- this.setRanges( [ range ] );
- }
-
- /**
- * Collapses selection to the selection's {@link #getFirstPosition first position}.
- * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
- * inside selection.
+ * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
+ * is treated like the last added range and is used to set {@link #anchor anchor} and {@link #focus focus}.
+ * Accepts a flag describing in which way the selection is made.
*
+ * @protected
* @fires change
+ * @param {Iterable.} newRanges Iterable object 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`.
*/
- collapseToStart() {
- const startPosition = this.getFirstPosition();
+ _setRanges( newRanges, isLastBackward ) {
+ this._ranges = [];
- if ( startPosition !== null ) {
- this.setRanges( [ new Range( startPosition, startPosition ) ] );
+ for ( const range of newRanges ) {
+ this._addRange( range );
}
- }
-
- /**
- * Collapses selection to the selection's {@link #getLastPosition last position}.
- * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
- * inside selection.
- *
- * @fires change
- */
- collapseToEnd() {
- const endPosition = this.getLastPosition();
- if ( endPosition !== null ) {
- this.setRanges( [ new Range( endPosition, endPosition ) ] );
- }
+ this._lastRangeBackward = !!isLastBackward;
+ this.fire( 'change' );
}
/**
@@ -528,15 +471,15 @@ export default class Selection {
* @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
* first parameter is a {@link module:engine/view/item~Item view item}.
*/
- moveFocusTo( itemOrPosition, offset ) {
+ setFocus( itemOrPosition, offset ) {
if ( this.anchor === null ) {
/**
* Cannot set selection focus if there are no ranges in selection.
*
- * @error view-selection-moveFocusTo-no-ranges
+ * @error view-selection-setFocus-no-ranges
*/
throw new CKEditorError(
- 'view-selection-moveFocusTo-no-ranges: Cannot set selection focus if there are no ranges in selection.'
+ 'view-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.'
);
}
@@ -551,10 +494,12 @@ export default class Selection {
this._ranges.pop();
if ( newFocus.compareWith( anchor ) == 'before' ) {
- this.addRange( new Range( newFocus, anchor ), true );
+ this._addRange( new Range( newFocus, anchor ), true );
} else {
- this.addRange( new Range( anchor, newFocus ) );
+ this._addRange( new Range( anchor, newFocus ) );
}
+
+ this.fire( 'change' );
}
/**
@@ -590,6 +535,32 @@ export default class Selection {
return selection;
}
+ /**
+ * Adds a range to the selection. Added range is copied. This means that passed range is not saved in the
+ * selection instance and you can safely operate on it.
+ *
+ * Accepts a flag describing in which way the selection is made - passed range might be selected from
+ * {@link module:engine/view/range~Range#start start} to {@link module:engine/view/range~Range#end end}
+ * or from {@link module:engine/view/range~Range#end end} to {@link module:engine/view/range~Range#start start}.
+ * The flag is used to set {@link #anchor anchor} and {@link #focus focus} properties.
+ *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-selection-range-intersects` if added range intersects
+ * with ranges already stored in Selection instance.
+ *
+ * @private
+ * @fires change
+ * @param {module:engine/view/range~Range} range
+ * @param {Boolean} [isBackward]
+ */
+ _addRange( range, isBackward = false ) {
+ if ( !( range instanceof Range ) ) {
+ throw new CKEditorError( 'view-selection-invalid-range: Invalid Range.' );
+ }
+
+ this._pushRange( range );
+ this._lastRangeBackward = !!isBackward;
+ }
+
/**
* Adds range to selection - creates copy of given range so it can be safely used and modified.
*
diff --git a/src/view/writer.js b/src/view/writer.js
index 0defd50c2..daef9fa72 100644
--- a/src/view/writer.js
+++ b/src/view/writer.js
@@ -496,7 +496,7 @@ export function wrap( range, attribute, viewSelection = null ) {
// If wrapping position is equal to view selection, move view selection inside wrapping attribute element.
if ( viewSelection && viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual( range.start ) ) {
- viewSelection.setRanges( [ new Range( position ) ] );
+ viewSelection.setTo( new Range( position ) );
}
return new Range( position );
diff --git a/tests/controller/editingcontroller.js b/tests/controller/editingcontroller.js
index 9832fda0d..baa866a3f 100644
--- a/tests/controller/editingcontroller.js
+++ b/tests/controller/editingcontroller.js
@@ -94,7 +94,10 @@ describe( 'EditingController', () => {
buildModelConverter().for( editing.modelToView ).fromMarker( 'marker' ).toHighlight( {} );
// Note: The below code is highly overcomplicated due to #455.
- model.document.selection.removeAllRanges();
+ model.change( writer => {
+ writer.setSelection( null );
+ } );
+
modelRoot.removeChildren( 0, modelRoot.childCount );
viewRoot.removeChildren( 0, viewRoot.childCount );
@@ -109,7 +112,7 @@ describe( 'EditingController', () => {
model.change( writer => {
writer.insert( modelData, model.document.getRoot() );
- model.document.selection.addRange( ModelRange.createFromParentsAndOffsets(
+ writer.setSelection( ModelRange.createFromParentsAndOffsets(
modelRoot.getChild( 0 ), 1, modelRoot.getChild( 0 ), 1 )
);
} );
@@ -131,9 +134,9 @@ describe( 'EditingController', () => {
model.change( writer => {
writer.split( model.document.selection.getFirstPosition() );
- model.document.selection.setRanges( [
+ writer.setSelection(
ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 1 ), 0, modelRoot.getChild( 1 ), 0 )
- ] );
+ );
} );
expect( getViewData( editing.view ) ).to.equal( 'f
{}oo
bar
' );
@@ -155,9 +158,9 @@ describe( 'EditingController', () => {
ModelRange.createFromPositionAndShift( model.document.selection.getFirstPosition(), 1 )
);
- model.document.selection.setRanges( [
+ writer.setSelection(
ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 1, modelRoot.getChild( 0 ), 1 )
- ] );
+ );
} );
expect( getViewData( editing.view ) ).to.equal( 'f{}o
bar
' );
@@ -189,38 +192,38 @@ describe( 'EditingController', () => {
} );
it( 'should convert collapsed selection', () => {
- model.change( () => {
- model.document.selection.setRanges( [
+ model.change( writer => {
+ writer.setSelection(
ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 1, modelRoot.getChild( 2 ), 1 )
- ] );
+ );
} );
expect( getViewData( editing.view ) ).to.equal( 'foo
b{}ar
' );
} );
it( 'should convert not collapsed selection', () => {
- model.change( () => {
- model.document.selection.setRanges( [
+ model.change( writer => {
+ writer.setSelection(
ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 1, modelRoot.getChild( 2 ), 2 )
- ] );
+ );
} );
expect( getViewData( editing.view ) ).to.equal( 'foo
b{a}r
' );
} );
it( 'should clear previous selection', () => {
- model.change( () => {
- model.document.selection.setRanges( [
+ model.change( writer => {
+ writer.setSelection(
ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 1, modelRoot.getChild( 2 ), 1 )
- ] );
+ );
} );
expect( getViewData( editing.view ) ).to.equal( 'foo
b{}ar
' );
- model.change( () => {
- model.document.selection.setRanges( [
+ model.change( writer => {
+ writer.setSelection(
ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 2, modelRoot.getChild( 2 ), 2 )
- ] );
+ );
} );
expect( getViewData( editing.view ) ).to.equal( 'foo
ba{}r
' );
@@ -369,7 +372,7 @@ describe( 'EditingController', () => {
writer.insert( modelData, modelRoot );
p1 = modelRoot.getChild( 0 );
- model.document.selection.addRange( ModelRange.createFromParentsAndOffsets( p1, 0, p1, 0 ) );
+ writer.setSelection( ModelRange.createFromParentsAndOffsets( p1, 0, p1, 0 ) );
} );
mcd = editing.modelToView;
diff --git a/tests/conversion/buildmodelconverter.js b/tests/conversion/buildmodelconverter.js
index b2a25299a..db7c18c68 100644
--- a/tests/conversion/buildmodelconverter.js
+++ b/tests/conversion/buildmodelconverter.js
@@ -184,8 +184,7 @@ describe( 'Model converter builder', () => {
// Set collapsed selection after "f".
const position = new ModelPosition( modelRoot, [ 1 ] );
- modelDoc.selection.setRanges( [ new ModelRange( position, position ) ] );
- modelDoc.selection._updateAttributes();
+ writer.setSelection( new ModelRange( position, position ) );
} );
// Check if view structure is ok.
@@ -199,8 +198,8 @@ describe( 'Model converter builder', () => {
expect( ranges[ 0 ].start.offset ).to.equal( 1 );
// Change selection attribute, convert it.
- model.change( () => {
- modelDoc.selection.setAttribute( 'italic', 'i' );
+ model.change( writer => {
+ writer.setSelectionAttribute( 'italic', 'i' );
} );
// Check if view structure has changed.
@@ -214,8 +213,8 @@ describe( 'Model converter builder', () => {
expect( ranges[ 0 ].start.offset ).to.equal( 0 );
// Some more tests checking how selection attributes changes are converted:
- model.change( () => {
- modelDoc.selection.removeAttribute( 'italic' );
+ model.change( writer => {
+ writer.removeSelectionAttribute( 'italic' );
} );
expect( viewToString( viewRoot ) ).to.equal( 'foo
' );
@@ -223,8 +222,8 @@ describe( 'Model converter builder', () => {
expect( ranges[ 0 ].start.parent.name ).to.equal( 'div' );
expect( ranges[ 0 ].start.offset ).to.equal( 1 );
- model.change( () => {
- modelDoc.selection.setAttribute( 'italic', 'em' );
+ model.change( writer => {
+ writer.setSelectionAttribute( 'italic', 'em' );
} );
expect( viewToString( viewRoot ) ).to.equal( 'foo
' );
diff --git a/tests/conversion/model-selection-to-view-converters.js b/tests/conversion/model-selection-to-view-converters.js
index 77c9d38f2..e09ac0ca5 100644
--- a/tests/conversion/model-selection-to-view-converters.js
+++ b/tests/conversion/model-selection-to-view-converters.js
@@ -37,13 +37,13 @@ import { stringify as stringifyView } from '../../src/dev-utils/view';
import { setData as setModelData } from '../../src/dev-utils/model';
describe( 'model-selection-to-view-converters', () => {
- let dispatcher, mapper, model, modelDoc, modelRoot, modelSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor;
+ let dispatcher, mapper, model, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor;
beforeEach( () => {
model = new Model();
modelDoc = model.document;
modelRoot = modelDoc.createRoot();
- modelSelection = modelDoc.selection;
+ docSelection = modelDoc.selection;
model.schema.extend( '$text', { allowIn: '$root' } );
@@ -196,10 +196,9 @@ describe( 'model-selection-to-view-converters', () => {
setModelData( model, 'fo<$text bold="true">ob$text>ar' );
const marker = model.markers.set( 'marker', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
- modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
-
- // Update selection attributes according to model.
- modelSelection.refreshAttributes();
+ model.change( writer => {
+ writer.setSelection( new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) );
+ } );
// Remove view children manually (without firing additional conversion).
viewRoot.removeChildren( 0, viewRoot.childCount );
@@ -208,8 +207,8 @@ describe( 'model-selection-to-view-converters', () => {
dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
dispatcher.convertMarkerAdd( marker.name, marker.getRange() );
- const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
- dispatcher.convertSelection( modelSelection, markers );
+ const markers = Array.from( model.markers.getMarkersAtPosition( docSelection.getFirstPosition() ) );
+ dispatcher.convertSelection( docSelection, markers );
// Stringify view and check if it is same as expected.
expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal(
@@ -221,12 +220,10 @@ describe( 'model-selection-to-view-converters', () => {
setModelData( model, 'fo<$text bold="true">ob$text>ar' );
const marker = model.markers.set( 'marker', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
- modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
-
- // Update selection attributes according to model.
- modelSelection.refreshAttributes();
-
- modelSelection.removeAttribute( 'bold' );
+ model.change( writer => {
+ writer.setSelection( new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) );
+ writer.removeSelectionAttribute( 'bold' );
+ } );
// Remove view children manually (without firing additional conversion).
viewRoot.removeChildren( 0, viewRoot.childCount );
@@ -235,8 +232,8 @@ describe( 'model-selection-to-view-converters', () => {
dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
dispatcher.convertMarkerAdd( marker.name, marker.getRange() );
- const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
- dispatcher.convertSelection( modelSelection, markers );
+ const markers = Array.from( model.markers.getMarkersAtPosition( docSelection.getFirstPosition() ) );
+ dispatcher.convertSelection( docSelection, markers );
// Stringify view and check if it is same as expected.
expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -251,7 +248,9 @@ describe( 'model-selection-to-view-converters', () => {
setModelData( model, 'foobar' );
const marker = model.markers.set( 'marker2', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
- modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
+ model.change( writer => {
+ writer.setSelection( new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) );
+ } );
// Remove view children manually (without firing additional conversion).
viewRoot.removeChildren( 0, viewRoot.childCount );
@@ -260,8 +259,8 @@ describe( 'model-selection-to-view-converters', () => {
dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
dispatcher.convertMarkerAdd( marker.name, marker.getRange() );
- const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
- dispatcher.convertSelection( modelSelection, markers );
+ const markers = Array.from( model.markers.getMarkersAtPosition( docSelection.getFirstPosition() ) );
+ dispatcher.convertSelection( docSelection, markers );
// Stringify view and check if it is same as expected.
expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -274,7 +273,9 @@ describe( 'model-selection-to-view-converters', () => {
setModelData( model, 'foobar' );
const marker = model.markers.set( 'marker3', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
- modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
+ model.change( writer => {
+ writer.setSelection( new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) );
+ } );
// Remove view children manually (without firing additional conversion).
viewRoot.removeChildren( 0, viewRoot.childCount );
@@ -283,8 +284,8 @@ describe( 'model-selection-to-view-converters', () => {
dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
dispatcher.convertMarkerAdd( marker.name, marker.getRange() );
- const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
- dispatcher.convertSelection( modelSelection, markers );
+ const markers = Array.from( model.markers.getMarkersAtPosition( docSelection.getFirstPosition() ) );
+ dispatcher.convertSelection( docSelection, markers );
// Stringify view and check if it is same as expected.
expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -301,11 +302,13 @@ describe( 'model-selection-to-view-converters', () => {
new ViewUIElement( 'span' )
] );
- modelSelection.setRanges( [ new ModelRange( new ModelPosition( modelRoot, [ 0 ] ) ) ] );
- modelSelection.setAttribute( 'bold', true );
+ model.change( writer => {
+ writer.setSelection( new ModelRange( new ModelPosition( modelRoot, [ 0 ] ) ) );
+ writer.setSelectionAttribute( 'bold', true );
+ } );
// Convert model to view.
- dispatcher.convertSelection( modelSelection, [] );
+ dispatcher.convertSelection( docSelection, [] );
// Stringify view and check if it is same as expected.
expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -316,8 +319,10 @@ describe( 'model-selection-to-view-converters', () => {
it( 'selection with attribute before ui element - has non-ui children #1', () => {
setModelData( model, 'x' );
- modelSelection.setRanges( [ new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) ] );
- modelSelection.setAttribute( 'bold', true );
+ model.change( writer => {
+ writer.setSelection( new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) );
+ writer.setSelectionAttribute( 'bold', true );
+ } );
// Convert model to view.
dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
@@ -326,7 +331,7 @@ describe( 'model-selection-to-view-converters', () => {
const uiElement = new ViewUIElement( 'span' );
viewRoot.insertChildren( 1, uiElement );
- dispatcher.convertSelection( modelSelection, [] );
+ dispatcher.convertSelection( docSelection, [] );
// Stringify view and check if it is same as expected.
expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -337,8 +342,10 @@ describe( 'model-selection-to-view-converters', () => {
it( 'selection with attribute before ui element - has non-ui children #2', () => {
setModelData( model, '<$text bold="true">x$text>y' );
- modelSelection.setRanges( [ new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) ] );
- modelSelection.setAttribute( 'bold', true );
+ model.change( writer => {
+ writer.setSelection( new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) );
+ writer.setSelectionAttribute( 'bold', true );
+ } );
// Convert model to view.
dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
@@ -347,7 +354,7 @@ describe( 'model-selection-to-view-converters', () => {
const uiElement = new ViewUIElement( 'span' );
viewRoot.insertChildren( 1, uiElement );
- dispatcher.convertSelection( modelSelection, [] );
+ dispatcher.convertSelection( docSelection, [] );
// Stringify view and check if it is same as expected.
expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -422,7 +429,9 @@ describe( 'model-selection-to-view-converters', () => {
);
const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
- modelDoc.selection.setRanges( [ modelRange ] );
+ model.change( writer => {
+ writer.setSelection( modelRange );
+ } );
dispatcher.convertSelection( modelDoc.selection, [] );
@@ -444,7 +453,9 @@ describe( 'model-selection-to-view-converters', () => {
mergeAttributes( viewSelection.getFirstPosition() );
const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
- modelDoc.selection.setRanges( [ modelRange ] );
+ model.change( writer => {
+ writer.setSelection( modelRange );
+ } );
dispatcher.convertSelection( modelDoc.selection, [] );
@@ -460,7 +471,7 @@ describe( 'model-selection-to-view-converters', () => {
dispatcher.on( 'selection', clearFakeSelection() );
viewSelection.setFake( true );
- dispatcher.convertSelection( modelSelection, [] );
+ dispatcher.convertSelection( docSelection, [] );
expect( viewSelection.isFake ).to.be.false;
} );
@@ -549,28 +560,27 @@ describe( 'model-selection-to-view-converters', () => {
const endPos = new ModelPosition( modelRoot, endPath );
const isBackward = selectionPaths[ 2 ] === 'backward';
- modelSelection.setRanges( [ new ModelRange( startPos, endPos ) ], isBackward );
+ model.change( writer => {
+ writer.setSelection( new ModelRange( startPos, endPos ), isBackward );
- // Update selection attributes according to model.
- modelSelection.refreshAttributes();
+ // And add or remove passed attributes.
+ for ( const key in selectionAttributes ) {
+ const value = selectionAttributes[ key ];
- // And add or remove passed attributes.
- for ( const key in selectionAttributes ) {
- const value = selectionAttributes[ key ];
-
- if ( value ) {
- modelSelection.setAttribute( key, value );
- } else {
- modelSelection.removeAttribute( key );
+ if ( value ) {
+ writer.setSelectionAttribute( key, value );
+ } else {
+ writer.removeSelectionAttribute( key );
+ }
}
- }
+ } );
// Remove view children manually (without firing additional conversion).
viewRoot.removeChildren( 0, viewRoot.childCount );
// Convert model to view.
dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
- dispatcher.convertSelection( modelSelection, [] );
+ dispatcher.convertSelection( docSelection, [] );
// Stringify view and check if it is same as expected.
expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '' + expectedView + '
' );
diff --git a/tests/conversion/modelconversiondispatcher.js b/tests/conversion/modelconversiondispatcher.js
index f7979b84c..287d850ec 100644
--- a/tests/conversion/modelconversiondispatcher.js
+++ b/tests/conversion/modelconversiondispatcher.js
@@ -239,10 +239,12 @@ describe( 'ModelConversionDispatcher', () => {
dispatcher.off( 'selection' );
root.appendChildren( new ModelText( 'foobar' ) );
- doc.selection.setRanges( [
- new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 3 ] ) ),
- new ModelRange( new ModelPosition( root, [ 4 ] ), new ModelPosition( root, [ 5 ] ) )
- ] );
+ model.change( writer => {
+ writer.setSelection( [
+ new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 3 ] ) ),
+ new ModelRange( new ModelPosition( root, [ 4 ] ), new ModelPosition( root, [ 5 ] ) )
+ ] );
+ } );
} );
it( 'should fire selection event', () => {
@@ -286,9 +288,11 @@ describe( 'ModelConversionDispatcher', () => {
} );
it( 'should fire attributes events for collapsed selection', () => {
- doc.selection.setRanges( [
- new ModelRange( new ModelPosition( root, [ 2 ] ), new ModelPosition( root, [ 2 ] ) )
- ] );
+ model.change( writer => {
+ writer.setSelection(
+ new ModelRange( new ModelPosition( root, [ 2 ] ), new ModelPosition( root, [ 2 ] ) )
+ );
+ } );
model.change( writer => {
writer.setAttribute( 'bold', true, ModelRange.createIn( root ) );
@@ -302,9 +306,11 @@ describe( 'ModelConversionDispatcher', () => {
} );
it( 'should not fire attributes events if attribute has been consumed', () => {
- doc.selection.setRanges( [
- new ModelRange( new ModelPosition( root, [ 2 ] ), new ModelPosition( root, [ 2 ] ) )
- ] );
+ model.change( writer => {
+ writer.setSelection(
+ new ModelRange( new ModelPosition( root, [ 2 ] ), new ModelPosition( root, [ 2 ] ) )
+ );
+ } );
model.change( writer => {
writer.setAttribute( 'bold', true, ModelRange.createIn( root ) );
@@ -323,9 +329,11 @@ describe( 'ModelConversionDispatcher', () => {
} );
it( 'should fire events for markers for collapsed selection', () => {
- doc.selection.setRanges( [
- new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 1 ] ) )
- ] );
+ model.change( writer => {
+ writer.setSelection(
+ new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 1 ] ) )
+ );
+ } );
model.markers.set( 'name', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
@@ -362,8 +370,8 @@ describe( 'ModelConversionDispatcher', () => {
const viewFigure = new ViewContainerElement( 'figure', null, viewCaption );
// Create custom highlight handler mock.
- viewFigure.setCustomProperty( 'addHighlight', () => {} );
- viewFigure.setCustomProperty( 'removeHighlight', () => {} );
+ viewFigure.setCustomProperty( 'addHighlight', () => { } );
+ viewFigure.setCustomProperty( 'removeHighlight', () => { } );
// Create mapper mock.
dispatcher.conversionApi.mapper = {
@@ -377,8 +385,9 @@ describe( 'ModelConversionDispatcher', () => {
};
model.markers.set( 'name', ModelRange.createFromParentsAndOffsets( root, 0, root, 1 ) );
- doc.selection.setRanges( [ ModelRange.createFromParentsAndOffsets( caption, 1, caption, 1 ) ] );
-
+ model.change( writer => {
+ writer.setSelection( ModelRange.createFromParentsAndOffsets( caption, 1, caption, 1 ) );
+ } );
sinon.spy( dispatcher, 'fire' );
const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
@@ -389,9 +398,11 @@ describe( 'ModelConversionDispatcher', () => {
} );
it( 'should not fire events if information about marker has been consumed', () => {
- doc.selection.setRanges( [
- new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 1 ] ) )
- ] );
+ model.change( writer => {
+ writer.setSelection(
+ new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 1 ] ) )
+ );
+ } );
model.markers.set( 'foo', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
model.markers.set( 'bar', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
diff --git a/tests/conversion/view-selection-to-model-converters.js b/tests/conversion/view-selection-to-model-converters.js
index 681963f75..62dbb9c2b 100644
--- a/tests/conversion/view-selection-to-model-converters.js
+++ b/tests/conversion/view-selection-to-model-converters.js
@@ -45,7 +45,7 @@ describe( 'convertSelectionChange', () => {
it( 'should convert collapsed selection', () => {
const viewSelection = new ViewSelection();
- viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
+ viewSelection.setTo( ViewRange.createFromParentsAndOffsets(
viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) );
convertSelection( null, { newSelection: viewSelection } );
@@ -61,10 +61,9 @@ describe( 'convertSelectionChange', () => {
// Re-bind elements that were just re-set.
mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) );
- const viewSelection = new ViewSelection();
- viewSelection.addRange(
+ const viewSelection = new ViewSelection( [
ViewRange.createFromParentsAndOffsets( viewRoot.getChild( 0 ).getChild( 0 ), 2, viewRoot.getChild( 0 ).getChild( 0 ), 6 )
- );
+ ] );
convertSelection( null, { newSelection: viewSelection } );
@@ -72,11 +71,12 @@ describe( 'convertSelectionChange', () => {
} );
it( 'should convert multi ranges selection', () => {
- const viewSelection = new ViewSelection();
- viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
- viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ) );
- viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
- viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ) );
+ const viewSelection = new ViewSelection( [
+ ViewRange.createFromParentsAndOffsets(
+ viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ),
+ ViewRange.createFromParentsAndOffsets(
+ viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 )
+ ] );
convertSelection( null, { newSelection: viewSelection } );
@@ -98,11 +98,12 @@ describe( 'convertSelectionChange', () => {
} );
it( 'should convert reverse selection', () => {
- const viewSelection = new ViewSelection();
- viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
- viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), true );
- viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
- viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ), true );
+ const viewSelection = new ViewSelection( [
+ ViewRange.createFromParentsAndOffsets(
+ viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ),
+ ViewRange.createFromParentsAndOffsets(
+ viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 )
+ ], true );
convertSelection( null, { newSelection: viewSelection } );
@@ -111,9 +112,10 @@ describe( 'convertSelectionChange', () => {
} );
it( 'should not enqueue changes if selection has not changed', () => {
- const viewSelection = new ViewSelection();
- viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
- viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) );
+ const viewSelection = new ViewSelection( [
+ ViewRange.createFromParentsAndOffsets(
+ viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 )
+ ] );
convertSelection( null, { newSelection: viewSelection } );
diff --git a/tests/dev-utils/model.js b/tests/dev-utils/model.js
index 41601e34e..689603243 100644
--- a/tests/dev-utils/model.js
+++ b/tests/dev-utils/model.js
@@ -21,7 +21,10 @@ describe( 'model test utils', () => {
root = document.createRoot();
selection = document.selection;
sandbox = sinon.sandbox.create();
- selection.removeAllRanges();
+
+ model.change( writer => {
+ writer.setSelection( null );
+ } );
model.schema.register( 'a', {
allowWhere: '$text',
@@ -64,8 +67,9 @@ describe( 'model test utils', () => {
it( 'should use stringify method with selection', () => {
const stringifySpy = sandbox.spy( getData, '_stringify' );
root.appendChildren( new Element( 'b', null, new Text( 'btext' ) ) );
- document.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
-
+ model.change( writer => {
+ writer.setSelection( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+ } );
expect( getData( model ) ).to.equal( '[btext]' );
sinon.assert.calledOnce( stringifySpy );
sinon.assert.calledWithExactly( stringifySpy, root, document.selection );
@@ -278,7 +282,9 @@ describe( 'model test utils', () => {
it( 'writes selection in an empty root', () => {
const root = document.createRoot( '$root', 'empty' );
- selection.setCollapsedAt( root );
+ model.change( writer => {
+ writer.setSelection( root );
+ } );
expect( stringify( root, selection ) ).to.equal(
'[]'
@@ -286,7 +292,9 @@ describe( 'model test utils', () => {
} );
it( 'writes selection collapsed in an element', () => {
- selection.setCollapsedAt( root );
+ model.change( writer => {
+ writer.setSelection( root );
+ } );
expect( stringify( root, selection ) ).to.equal(
'[]foo<$text bold="true">bar$text>'
@@ -294,7 +302,9 @@ describe( 'model test utils', () => {
} );
it( 'writes selection collapsed in a text', () => {
- selection.setCollapsedAt( root, 3 );
+ model.change( writer => {
+ writer.setSelection( root, 3 );
+ } );
expect( stringify( root, selection ) ).to.equal(
'fo[]o<$text bold="true">bar$text>'
@@ -302,7 +312,9 @@ describe( 'model test utils', () => {
} );
it( 'writes selection collapsed at the text left boundary', () => {
- selection.setCollapsedAt( elA, 'after' );
+ model.change( writer => {
+ writer.setSelection( elA, 'after' );
+ } );
expect( stringify( root, selection ) ).to.equal(
'[]foo<$text bold="true">bar$text>'
@@ -310,7 +322,9 @@ describe( 'model test utils', () => {
} );
it( 'writes selection collapsed at the text right boundary', () => {
- selection.setCollapsedAt( elB, 'before' );
+ model.change( writer => {
+ writer.setSelection( elB, 'before' );
+ } );
expect( stringify( root, selection ) ).to.equal(
'foo<$text bold="true">bar[]$text>'
@@ -318,10 +332,12 @@ describe( 'model test utils', () => {
} );
it( 'writes selection collapsed at the end of the root', () => {
- selection.setCollapsedAt( root, 'end' );
+ model.change( writer => {
+ writer.setSelection( root, 'end' );
- // Needed due to https://github.com/ckeditor/ckeditor5-engine/issues/320.
- selection.clearAttributes();
+ // Needed due to https://github.com/ckeditor/ckeditor5-engine/issues/320.
+ writer.removeSelectionAttribute( model.document.selection.getAttributeKeys() );
+ } );
expect( stringify( root, selection ) ).to.equal(
'foo<$text bold="true">bar$text>[]'
@@ -329,7 +345,9 @@ describe( 'model test utils', () => {
} );
it( 'writes selection collapsed selection in a text with attributes', () => {
- selection.setCollapsedAt( root, 5 );
+ model.change( writer => {
+ writer.setSelection( root, 5 );
+ } );
expect( stringify( root, selection ) ).to.equal(
'foo<$text bold="true">b[]ar$text>'
@@ -337,9 +355,9 @@ describe( 'model test utils', () => {
} );
it( 'writes flat selection containing couple of nodes', () => {
- selection.addRange(
- Range.createFromParentsAndOffsets( root, 0, root, 4 )
- );
+ model.change( writer => {
+ writer.setSelection( Range.createFromParentsAndOffsets( root, 0, root, 4 ) );
+ } );
expect( stringify( root, selection ) ).to.equal(
'[foo]<$text bold="true">bar$text>'
@@ -347,9 +365,9 @@ describe( 'model test utils', () => {
} );
it( 'writes flat selection within text', () => {
- selection.addRange(
- Range.createFromParentsAndOffsets( root, 2, root, 3 )
- );
+ model.change( writer => {
+ writer.setSelection( Range.createFromParentsAndOffsets( root, 2, root, 3 ) );
+ } );
expect( stringify( root, selection ) ).to.equal(
'f[o]o<$text bold="true">bar$text>'
@@ -357,9 +375,9 @@ describe( 'model test utils', () => {
} );
it( 'writes multi-level selection', () => {
- selection.addRange(
- Range.createFromParentsAndOffsets( elA, 0, elB, 0 )
- );
+ model.change( writer => {
+ writer.setSelection( Range.createFromParentsAndOffsets( elA, 0, elB, 0 ) );
+ } );
expect( stringify( root, selection ) ).to.equal(
'[foo<$text bold="true">bar$text>]'
@@ -367,10 +385,9 @@ describe( 'model test utils', () => {
} );
it( 'writes selection when is backward', () => {
- selection.addRange(
- Range.createFromParentsAndOffsets( elA, 0, elB, 0 ),
- true
- );
+ model.change( writer => {
+ writer.setSelection( Range.createFromParentsAndOffsets( elA, 0, elB, 0 ), true );
+ } );
expect( stringify( root, selection ) ).to.equal(
'[foo<$text bold="true">bar$text>]'
@@ -381,7 +398,9 @@ describe( 'model test utils', () => {
const root = document.createRoot( '$root', 'empty' );
root.appendChildren( new Text( 'நிலைக்கு' ) );
- selection.addRange( Range.createFromParentsAndOffsets( root, 2, root, 6 ) );
+ model.change( writer => {
+ writer.setSelection( Range.createFromParentsAndOffsets( root, 2, root, 6 ) );
+ } );
expect( stringify( root, selection ) ).to.equal( 'நி[லைக்]கு' );
} );
@@ -562,10 +581,12 @@ describe( 'model test utils', () => {
} );
it( 'sets selection attributes', () => {
- const result = parse( 'foo[]bar', model.schema, { selectionAttributes: {
- bold: true,
- italic: true
- } } );
+ const result = parse( 'foo[]bar', model.schema, {
+ selectionAttributes: {
+ bold: true,
+ italic: true
+ }
+ } );
expect( stringify( result.model, result.selection ) ).to.equal( 'foo<$text bold="true" italic="true">[]$text>bar' );
} );
@@ -583,9 +604,11 @@ describe( 'model test utils', () => {
} );
it( 'sets selection with attribute containing an element', () => {
- const result = parse( 'x[]', model.schema, { selectionAttributes: {
- bold: true
- } } );
+ const result = parse( 'x[]', model.schema, {
+ selectionAttributes: {
+ bold: true
+ }
+ } );
expect( stringify( result.model, result.selection ) ).to.equal( 'x[]' );
expect( result.selection.getAttribute( 'bold' ) ).to.be.true;
diff --git a/tests/dev-utils/view.js b/tests/dev-utils/view.js
index 816eacf89..0c278b649 100644
--- a/tests/dev-utils/view.js
+++ b/tests/dev-utils/view.js
@@ -61,7 +61,7 @@ describe( 'view test utils', () => {
const root = createAttachedRoot( viewDocument, element );
root.appendChildren( new Element( 'p' ) );
- viewDocument.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+ viewDocument.selection.setTo( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
expect( getData( viewDocument, options ) ).to.equal( '[]' );
sinon.assert.calledOnce( stringifySpy );
@@ -161,8 +161,7 @@ describe( 'view test utils', () => {
const b2 = new Element( 'b', null, text2 );
const p = new Element( 'p', null, [ b1, b2 ] );
const range = Range.createFromParentsAndOffsets( p, 1, p, 2 );
- const selection = new Selection();
- selection.addRange( range );
+ const selection = new Selection( [ range ] );
expect( stringify( p, selection ) ).to.equal( 'foobar[bazqux]
' );
} );
@@ -171,8 +170,7 @@ describe( 'view test utils', () => {
const b = new Element( 'b', null, text );
const p = new Element( 'p', null, b );
const range = Range.createFromParentsAndOffsets( p, 0, text, 4 );
- const selection = new Selection();
- selection.addRange( range );
+ const selection = new Selection( [ range ] );
expect( stringify( p, selection ) ).to.equal( '[நிலை}க்கு
' );
} );
@@ -181,8 +179,7 @@ describe( 'view test utils', () => {
const text = new Text( 'foobar' );
const p = new Element( 'p', null, text );
const range = Range.createFromParentsAndOffsets( p, 0, p, 0 );
- const selection = new Selection();
- selection.addRange( range );
+ const selection = new Selection( [ range ] );
expect( stringify( p, selection ) ).to.equal( '[]foobar
' );
} );
@@ -193,8 +190,7 @@ describe( 'view test utils', () => {
const b2 = new Element( 'b', null, text2 );
const p = new Element( 'p', null, [ b1, b2 ] );
const range = Range.createFromParentsAndOffsets( text1, 1, text1, 5 );
- const selection = new Selection();
- selection.addRange( range );
+ const selection = new Selection( [ range ] );
expect( stringify( p, selection ) ).to.equal( 'f{ooba}rbazqux
' );
} );
@@ -205,8 +201,7 @@ describe( 'view test utils', () => {
const b2 = new Element( 'b', null, text2 );
const p = new Element( 'p', null, [ b1, b2 ] );
const range = Range.createFromParentsAndOffsets( text1, 1, text1, 5 );
- const selection = new Selection();
- selection.addRange( range );
+ const selection = new Selection( [ range ] );
expect( stringify( p, selection, { sameSelectionCharacters: true } ) )
.to.equal( 'f[ooba]rbazqux
' );
} );
@@ -215,8 +210,7 @@ describe( 'view test utils', () => {
const text = new Text( 'foobar' );
const p = new Element( 'p', null, text );
const range = Range.createFromParentsAndOffsets( text, 0, text, 0 );
- const selection = new Selection();
- selection.addRange( range );
+ const selection = new Selection( [ range ] );
expect( stringify( p, selection ) ).to.equal( '{}foobar
' );
} );
@@ -227,8 +221,7 @@ describe( 'view test utils', () => {
const b2 = new Element( 'b', null, text2 );
const p = new Element( 'p', null, [ b1, b2 ] );
const range = Range.createFromParentsAndOffsets( p, 0, text2, 5 );
- const selection = new Selection();
- selection.addRange( range );
+ const selection = new Selection( [ range ] );
expect( stringify( p, selection ) ).to.equal( '[foobarbazqu}x
' );
} );
@@ -308,8 +301,7 @@ describe( 'view test utils', () => {
const p = new Element( 'p', null, [ b1, b2 ] );
const range1 = Range.createFromParentsAndOffsets( p, 0, p, 1 );
const range2 = Range.createFromParentsAndOffsets( p, 1, p, 1 );
- const selection = new Selection();
- selection.setRanges( [ range2, range1 ] );
+ const selection = new Selection( [ range2, range1 ] );
expect( stringify( p, selection ) ).to.equal( '[foobar][]bazqux
' );
} );
@@ -323,8 +315,7 @@ describe( 'view test utils', () => {
const range2 = Range.createFromParentsAndOffsets( text2, 0, text2, 3 );
const range3 = Range.createFromParentsAndOffsets( text2, 3, text2, 4 );
const range4 = Range.createFromParentsAndOffsets( p, 1, p, 1 );
- const selection = new Selection();
- selection.setRanges( [ range1, range2, range3, range4 ] );
+ const selection = new Selection( [ range1, range2, range3, range4 ] );
expect( stringify( p, selection ) ).to.equal( '[foobar][]{baz}{q}ux
' );
} );
diff --git a/tests/model/document.js b/tests/model/document.js
index ae6bda849..9a054be3b 100644
--- a/tests/model/document.js
+++ b/tests/model/document.js
@@ -391,7 +391,9 @@ describe( 'Document', () => {
if ( expected === null ) {
expect( range ).to.be.null;
} else {
- selection.setRanges( [ range ] );
+ model.change( writer => {
+ writer.setSelection( range );
+ } );
expect( getData( model ) ).to.equal( expected );
}
} );
@@ -578,8 +580,8 @@ describe( 'Document', () => {
doc.on( 'change', spy );
- model.change( () => {
- doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 2, root, 2 ) ] );
+ model.change( writer => {
+ writer.setSelection( Range.createFromParentsAndOffsets( root, 2, root, 2 ) );
} );
expect( spy.calledOnce ).to.be.true;
diff --git a/tests/model/documentselection.js b/tests/model/documentselection.js
index 9c74d8ead..2c9ae5f61 100644
--- a/tests/model/documentselection.js
+++ b/tests/model/documentselection.js
@@ -22,8 +22,6 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
import { wrapInDelta } from '../../tests/model/_utils/utils';
import { setData, getData } from '../../src/dev-utils/model';
-import log from '@ckeditor/ckeditor5-utils/src/log';
-
testUtils.createSinonSandbox();
describe( 'DocumentSelection', () => {
@@ -121,7 +119,7 @@ describe( 'DocumentSelection', () => {
} );
it( 'should back off to the default algorithm if selection has ranges', () => {
- selection.addRange( range );
+ selection._setTo( range );
expect( selection.isCollapsed ).to.be.false;
} );
@@ -143,7 +141,7 @@ describe( 'DocumentSelection', () => {
} );
it( 'should back off to the default algorithm if selection has ranges', () => {
- selection.addRange( range );
+ selection._setTo( range );
expect( selection.anchor.isEqual( range.start ) ).to.be.true;
} );
@@ -166,7 +164,7 @@ describe( 'DocumentSelection', () => {
} );
it( 'should back off to the default algorithm if selection has ranges', () => {
- selection.addRange( range );
+ selection._setTo( range );
expect( selection.focus.isEqual( range.end ) ).to.be.true;
} );
@@ -176,11 +174,14 @@ describe( 'DocumentSelection', () => {
it( 'should return proper range count', () => {
expect( selection.rangeCount ).to.equal( 1 );
- selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
+ selection._setTo( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
expect( selection.rangeCount ).to.equal( 1 );
- selection.addRange( new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) );
+ selection._setTo( [
+ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ),
+ new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) )
+ ] );
expect( selection.rangeCount ).to.equal( 2 );
} );
@@ -188,7 +189,7 @@ describe( 'DocumentSelection', () => {
describe( 'hasOwnRange', () => {
it( 'should return true if selection has any ranges set', () => {
- selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
+ selection._setTo( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
expect( selection.hasOwnRange ).to.be.true;
} );
@@ -198,44 +199,12 @@ describe( 'DocumentSelection', () => {
} );
} );
- describe( 'addRange()', () => {
- it( 'should convert added Range to LiveRange', () => {
- selection.addRange( range );
-
- expect( selection._ranges[ 0 ] ).to.be.instanceof( LiveRange );
- } );
-
- it( 'should throw an error when range is invalid', () => {
- expect( () => {
- selection.addRange( { invalid: 'range' } );
- } ).to.throw( CKEditorError, /model-selection-added-not-range/ );
- } );
-
- it( 'should not add a range that is in graveyard', () => {
- const spy = testUtils.sinon.stub( log, 'warn' );
-
- selection.addRange( Range.createIn( doc.graveyard ) );
-
- expect( selection._ranges.length ).to.equal( 0 );
- expect( spy.calledOnce ).to.be.true;
- } );
-
- it( 'should refresh attributes', () => {
- const spy = testUtils.sinon.spy( selection, '_updateAttributes' );
-
- selection.addRange( range );
-
- expect( spy.called ).to.be.true;
- } );
- } );
-
- describe( 'setCollapsedAt()', () => {
+ describe( 'setTo - set collapsed at', () => {
it( 'detaches all existing ranges', () => {
- selection.addRange( range );
- selection.addRange( liveRange );
+ selection._setTo( [ range, liveRange ] );
const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
- selection.setCollapsedAt( root );
+ selection._setTo( root );
expect( spy.calledTwice ).to.be.true;
} );
@@ -243,10 +212,9 @@ describe( 'DocumentSelection', () => {
describe( 'destroy()', () => {
it( 'should unbind all events', () => {
- selection.addRange( liveRange );
- selection.addRange( range );
+ selection._setTo( [ range, liveRange ] );
- const ranges = selection._ranges;
+ const ranges = Array.from( selection._selection._ranges );
sinon.spy( ranges[ 0 ], 'detach' );
sinon.spy( ranges[ 1 ], 'detach' );
@@ -261,12 +229,12 @@ describe( 'DocumentSelection', () => {
} );
} );
- describe( 'moveFocusTo()', () => {
+ describe( 'setFocus()', () => {
it( 'modifies default range', () => {
const startPos = selection.getFirstPosition();
const endPos = Position.createAt( root, 'end' );
- selection.moveFocusTo( endPos );
+ selection._setFocus( endPos );
expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' );
@@ -278,30 +246,37 @@ describe( 'DocumentSelection', () => {
const newEndPos = Position.createAt( root, 4 );
const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
- selection.addRange( new Range( startPos, endPos ) );
+ selection._setTo( new Range( startPos, endPos ) );
- selection.moveFocusTo( newEndPos );
+ selection._setFocus( newEndPos );
expect( spy.calledOnce ).to.be.true;
} );
+
+ it( 'refreshes attributes', () => {
+ const spy = sinon.spy( selection._selection, '_updateAttributes' );
+
+ selection._setFocus( Position.createAt( root, 1 ) );
+
+ expect( spy.called ).to.be.true;
+ } );
} );
- describe( 'removeAllRanges()', () => {
+ describe( 'setTo - removeAllRanges', () => {
let spy, ranges;
beforeEach( () => {
- selection.addRange( liveRange );
- selection.addRange( range );
+ selection._setTo( [ liveRange, range ] );
spy = sinon.spy();
selection.on( 'change:range', spy );
- ranges = Array.from( selection._ranges );
+ ranges = Array.from( selection._selection._ranges );
sinon.spy( ranges[ 0 ], 'detach' );
sinon.spy( ranges[ 1 ], 'detach' );
- selection.removeAllRanges();
+ selection._setTo( null );
} );
afterEach( () => {
@@ -321,40 +296,40 @@ describe( 'DocumentSelection', () => {
} );
it( 'should refresh attributes', () => {
- const spy = sinon.spy( selection, '_updateAttributes' );
+ const spy = sinon.spy( selection._selection, '_updateAttributes' );
- selection.removeAllRanges();
+ selection._setTo( null );
expect( spy.called ).to.be.true;
} );
} );
- describe( 'setRanges()', () => {
+ // TODO - merge with other setTo's
+ describe( 'setTo()', () => {
it( 'should throw an error when range is invalid', () => {
expect( () => {
- selection.setRanges( [ { invalid: 'range' } ] );
+ selection._setTo( [ { invalid: 'range' } ] );
} ).to.throw( CKEditorError, /model-selection-added-not-range/ );
} );
it( 'should detach removed ranges', () => {
- selection.addRange( liveRange );
- selection.addRange( range );
+ selection._setTo( [ liveRange, range ] );
- const oldRanges = Array.from( selection._ranges );
+ const oldRanges = Array.from( selection._selection._ranges );
sinon.spy( oldRanges[ 0 ], 'detach' );
sinon.spy( oldRanges[ 1 ], 'detach' );
- selection.setRanges( [] );
+ selection._setTo( [] );
expect( oldRanges[ 0 ].detach.called ).to.be.true;
expect( oldRanges[ 1 ].detach.called ).to.be.true;
} );
it( 'should refresh attributes', () => {
- const spy = sinon.spy( selection, '_updateAttributes' );
+ const spy = sinon.spy( selection._selection, '_updateAttributes' );
- selection.setRanges( [ range ] );
+ selection._setTo( [ range ] );
expect( spy.called ).to.be.true;
} );
@@ -365,7 +340,7 @@ describe( 'DocumentSelection', () => {
setData( model, 'f<$text italic="true">[o$text><$text bold="true">ob]a$text>r' );
- selection.setRanges( [ Range.createFromPositionAndShift( selection.getLastRange().end, 0 ) ] );
+ selection._setTo( [ Range.createFromPositionAndShift( selection.getLastRange().end, 0 ) ] );
expect( selection.getAttribute( 'bold' ) ).to.equal( true );
expect( selection.hasAttribute( 'italic' ) ).to.equal( false );
@@ -393,16 +368,6 @@ describe( 'DocumentSelection', () => {
} );
} );
- describe( 'createFromSelection()', () => {
- it( 'should throw', () => {
- selection.addRange( range, true );
-
- expect( () => {
- DocumentSelection.createFromSelection( selection );
- } ).to.throw( CKEditorError, /^documentselection-cannot-create:/ );
- } );
- } );
-
describe( '_isStoreAttributeKey', () => {
it( 'should return true if given key is a key of an attribute stored in element by DocumentSelection', () => {
expect( DocumentSelection._isStoreAttributeKey( fooStoreAttrKey ) ).to.be.true;
@@ -426,7 +391,7 @@ describe( 'DocumentSelection', () => {
new Text( 'xyz' )
] );
- selection.addRange( new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 4 ] ) ) );
+ selection._setTo( new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 4 ] ) ) );
spyRange = sinon.spy();
selection.on( 'change:range', spyRange );
@@ -621,7 +586,7 @@ describe( 'DocumentSelection', () => {
} );
it( 'should not overwrite previously set attributes', () => {
- selection.setAttribute( 'foo', 'xyz' );
+ selection._setAttribute( 'foo', 'xyz' );
const spyAttribute = sinon.spy();
selection.on( 'change:attribute', spyAttribute );
@@ -641,8 +606,8 @@ describe( 'DocumentSelection', () => {
} );
it( 'should not overwrite previously removed attributes', () => {
- selection.setAttribute( 'foo', 'xyz' );
- selection.removeAttribute( 'foo' );
+ selection._setAttribute( 'foo', 'xyz' );
+ selection._removeAttribute( 'foo' );
const spyAttribute = sinon.spy();
selection.on( 'change:attribute', spyAttribute );
@@ -664,7 +629,7 @@ describe( 'DocumentSelection', () => {
describe( 'RemoveOperation', () => {
it( 'fix selection range if it ends up in graveyard #1', () => {
- selection.setCollapsedAt( new Position( root, [ 1, 3 ] ) );
+ selection._setTo( new Position( root, [ 1, 3 ] ) );
model.applyOperation( wrapInDelta(
new RemoveOperation(
@@ -679,7 +644,7 @@ describe( 'DocumentSelection', () => {
} );
it( 'fix selection range if it ends up in graveyard #2', () => {
- selection.setRanges( [ new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 1, 4 ] ) ) ] );
+ selection._setTo( [ new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 1, 4 ] ) ) ] );
model.applyOperation( wrapInDelta(
new RemoveOperation(
@@ -694,7 +659,7 @@ describe( 'DocumentSelection', () => {
} );
it( 'fix selection range if it ends up in graveyard #3', () => {
- selection.setRanges( [ new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ) ] );
+ selection._setTo( [ new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ) ] );
model.applyOperation( wrapInDelta(
new RemoveOperation(
@@ -755,90 +720,22 @@ describe( 'DocumentSelection', () => {
expect( root.childCount ).to.equal( 2 );
} );
- describe( 'setAttribute()', () => {
- it( 'should store attribute if the selection is in empty node', () => {
- selection.setRanges( [ rangeInEmptyP ] );
- selection.setAttribute( 'foo', 'bar' );
-
- expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
-
- expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
- } );
- } );
-
- describe( 'setAttributesTo()', () => {
- it( 'should fire change:attribute event with correct parameters', done => {
- selection.setAttributesTo( { foo: 'bar', abc: 'def' } );
-
- selection.on( 'change:attribute', ( evt, data ) => {
- expect( data.directChange ).to.be.true;
- expect( data.attributeKeys ).to.deep.equal( [ 'abc', 'xxx' ] );
-
- done();
- } );
-
- selection.setAttributesTo( { foo: 'bar', xxx: 'yyy' } );
- } );
-
- it( 'should not fire change:attribute event if same attributes are set', () => {
- selection.setAttributesTo( { foo: 'bar', abc: 'def' } );
-
- const spy = sinon.spy();
- selection.on( 'change:attribute', spy );
-
- selection.setAttributesTo( { foo: 'bar', abc: 'def' } );
-
- expect( spy.called ).to.be.false;
- } );
-
- it( 'should remove all stored attributes and store the given ones if the selection is in empty node', () => {
- selection.setRanges( [ rangeInEmptyP ] );
- selection.setAttribute( 'abc', 'xyz' );
- selection.setAttributesTo( { foo: 'bar' } );
+ describe( '_setAttribute()', () => {
+ it( 'should set attribute', () => {
+ selection._setTo( [ rangeInEmptyP ] );
+ selection._setAttribute( 'foo', 'bar' );
expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
- expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
-
- expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
- expect( emptyP.hasAttribute( abcStoreAttrKey ) ).to.be.false;
} );
} );
describe( 'removeAttribute()', () => {
it( 'should remove attribute set on the text fragment', () => {
- selection.setRanges( [ rangeInFullP ] );
- selection.setAttribute( 'foo', 'bar' );
- selection.removeAttribute( 'foo' );
+ selection._setTo( [ rangeInFullP ] );
+ selection._setAttribute( 'foo', 'bar' );
+ selection._removeAttribute( 'foo' );
expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
-
- expect( fullP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
- } );
-
- it( 'should remove stored attribute if the selection is in empty node', () => {
- selection.setRanges( [ rangeInEmptyP ] );
- selection.setAttribute( 'foo', 'bar' );
- selection.removeAttribute( 'foo' );
-
- expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
-
- expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
- } );
- } );
-
- describe( 'clearAttributes()', () => {
- it( 'should remove all stored attributes if the selection is in empty node', () => {
- selection.setRanges( [ rangeInEmptyP ] );
- selection.setAttribute( 'foo', 'bar' );
- selection.setAttribute( 'abc', 'xyz' );
-
- selection.clearAttributes();
-
- expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
- expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
-
- expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
- expect( emptyP.hasAttribute( abcStoreAttrKey ) ).to.be.false;
} );
} );
@@ -867,50 +764,50 @@ describe( 'DocumentSelection', () => {
} );
it( 'if selection is a range, should find first character in it and copy it\'s attributes', () => {
- selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
+ selection._setTo( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] );
// Step into elements when looking for first character:
- selection.setRanges( [ new Range( new Position( root, [ 5 ] ), new Position( root, [ 7 ] ) ) ] );
+ selection._setTo( [ new Range( new Position( root, [ 5 ] ), new Position( root, [ 7 ] ) ) ] );
expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] );
} );
it( 'if selection is collapsed it should seek a character to copy that character\'s attributes', () => {
// Take styles from character before selection.
- selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) ] );
+ selection._setTo( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) ] );
expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
// If there are none,
// Take styles from character after selection.
- selection.setRanges( [ new Range( new Position( root, [ 3 ] ), new Position( root, [ 3 ] ) ) ] );
+ selection._setTo( [ new Range( new Position( root, [ 3 ] ), new Position( root, [ 3 ] ) ) ] );
expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] );
// If there are none,
// Look from the selection position to the beginning of node looking for character to take attributes from.
- selection.setRanges( [ new Range( new Position( root, [ 6 ] ), new Position( root, [ 6 ] ) ) ] );
+ selection._setTo( [ new Range( new Position( root, [ 6 ] ), new Position( root, [ 6 ] ) ) ] );
expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'c', true ] ] );
// If there are none,
// Look from the selection position to the end of node looking for character to take attributes from.
- selection.setRanges( [ new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) ] );
+ selection._setTo( [ new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) ] );
expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
// If there are no characters to copy attributes from, use stored attributes.
- selection.setRanges( [ new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) ) ] );
+ selection._setTo( [ new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) ) ] );
expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [] );
} );
it( 'should overwrite any previously set attributes', () => {
- selection.setCollapsedAt( new Position( root, [ 5, 0 ] ) );
+ selection._setTo( new Position( root, [ 5, 0 ] ) );
- selection.setAttribute( 'x', true );
- selection.setAttribute( 'y', true );
+ selection._setAttribute( 'x', true );
+ selection._setAttribute( 'y', true );
expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ], [ 'x', true ], [ 'y', true ] ] );
- selection.setCollapsedAt( new Position( root, [ 1 ] ) );
+ selection._setTo( new Position( root, [ 1 ] ) );
expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
} );
@@ -919,20 +816,20 @@ describe( 'DocumentSelection', () => {
const spy = sinon.spy();
selection.on( 'change:attribute', spy );
- selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
+ selection._setTo( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
expect( spy.calledOnce ).to.be.true;
} );
it( 'should not fire change:attribute event if attributes did not change', () => {
- selection.setCollapsedAt( new Position( root, [ 5, 0 ] ) );
+ selection._setTo( new Position( root, [ 5, 0 ] ) );
expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] );
const spy = sinon.spy();
selection.on( 'change:attribute', spy );
- selection.setCollapsedAt( new Position( root, [ 5, 1 ] ) );
+ selection._setTo( new Position( root, [ 5, 1 ] ) );
expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] );
expect( spy.called ).to.be.false;
@@ -1004,8 +901,11 @@ describe( 'DocumentSelection', () => {
batchTypes.push( batch.type );
} );
- selection.setRanges( [ rangeInEmptyP ] );
- selection.setAttribute( 'foo', 'bar' );
+ selection._setTo( [ rangeInEmptyP ] );
+
+ model.change( writer => {
+ writer.setSelectionAttribute( 'foo', 'bar' );
+ } );
expect( batchTypes ).to.deep.equal( [ 'default' ] );
expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
@@ -1015,9 +915,9 @@ describe( 'DocumentSelection', () => {
// Dedupe batches by using a map (multiple change events will be fired).
const batchTypes = new Map();
- selection.setRanges( [ rangeInEmptyP ] );
- selection.setAttribute( 'foo', 'bar' );
- selection.setAttribute( 'abc', 'bar' );
+ selection._setTo( rangeInEmptyP );
+ selection._setAttribute( 'foo', 'bar' );
+ selection._setAttribute( 'abc', 'bar' );
model.on( 'applyOperation', ( event, args ) => {
const operation = args[ 0 ];
@@ -1037,8 +937,8 @@ describe( 'DocumentSelection', () => {
} );
it( 'are removed when any content is moved into', () => {
- selection.setRanges( [ rangeInEmptyP ] );
- selection.setAttribute( 'foo', 'bar' );
+ selection._setTo( rangeInEmptyP );
+ selection._setAttribute( 'foo', 'bar' );
model.change( writer => {
writer.move( Range.createOn( fullP.getChild( 0 ) ), rangeInEmptyP.start );
@@ -1066,7 +966,7 @@ describe( 'DocumentSelection', () => {
it( 'are removed even when there is no selection in it', () => {
emptyP.setAttribute( fooStoreAttrKey, 'bar' );
- selection.setRanges( [ rangeInFullP ] );
+ selection._setTo( [ rangeInFullP ] );
model.change( writer => {
writer.insertText( 'x', rangeInEmptyP.start );
@@ -1095,10 +995,10 @@ describe( 'DocumentSelection', () => {
} );
it( 'uses model change to clear attributes', () => {
- selection.setRanges( [ rangeInEmptyP ] );
- selection.setAttribute( 'foo', 'bar' );
+ selection._setTo( [ rangeInEmptyP ] );
model.change( writer => {
+ writer.setSelectionAttribute( 'foo', 'bar' );
writer.insertText( 'x', rangeInEmptyP.start );
// `emptyP` still has the attribute, because attribute clearing is in enqueued block.
@@ -1131,8 +1031,10 @@ describe( 'DocumentSelection', () => {
// Rename and some other deltas don't specify range in doc#change event.
// So let's see if there's no crash or something.
it( 'are not removed on rename', () => {
- selection.setRanges( [ rangeInEmptyP ] );
- selection.setAttribute( 'foo', 'bar' );
+ model.change( writer => {
+ writer.setSelection( rangeInEmptyP );
+ writer.setSelectionAttribute( 'foo', 'bar' );
+ } );
sinon.spy( model, 'enqueueChange' );
@@ -1151,11 +1053,11 @@ describe( 'DocumentSelection', () => {
root.appendChildren( '\uD83D\uDCA9' );
expect( () => {
- doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 0, root, 1 ) ] );
+ doc.selection._setTo( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
expect( () => {
- doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 1, root, 2 ) ] );
+ doc.selection._setTo( Range.createFromParentsAndOffsets( root, 1, root, 2 ) );
} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
} );
@@ -1164,27 +1066,27 @@ describe( 'DocumentSelection', () => {
root.appendChildren( 'foo̻̐ͩbar' );
expect( () => {
- doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 3, root, 9 ) ] );
+ doc.selection._setTo( Range.createFromParentsAndOffsets( root, 3, root, 9 ) );
} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
expect( () => {
- doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 4, root, 9 ) ] );
+ doc.selection._setTo( Range.createFromParentsAndOffsets( root, 4, root, 9 ) );
} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
expect( () => {
- doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 5, root, 9 ) ] );
+ doc.selection._setTo( Range.createFromParentsAndOffsets( root, 5, root, 9 ) );
} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
expect( () => {
- doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 1, root, 3 ) ] );
+ doc.selection._setTo( Range.createFromParentsAndOffsets( root, 1, root, 3 ) );
} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
expect( () => {
- doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 1, root, 4 ) ] );
+ doc.selection._setTo( Range.createFromParentsAndOffsets( root, 1, root, 4 ) );
} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
expect( () => {
- doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 1, root, 5 ) ] );
+ doc.selection._setTo( Range.createFromParentsAndOffsets( root, 1, root, 5 ) );
} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
} );
} );
diff --git a/tests/model/schema.js b/tests/model/schema.js
index fac673b43..7fb0335f5 100644
--- a/tests/model/schema.js
+++ b/tests/model/schema.js
@@ -743,8 +743,7 @@ describe( 'Schema', () => {
setData( model, '[foo
xxxbar
]' );
const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
- const sel = new Selection();
- sel.setRanges( validRanges );
+ const sel = new Selection( validRanges );
expect( stringify( root, sel ) ).to.equal( '[foo
]xxx[bar
]' );
} );
@@ -766,8 +765,7 @@ describe( 'Schema', () => {
setData( model, '[foo
xxxbar
]' );
const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
- const sel = new Selection();
- sel.setRanges( validRanges );
+ const sel = new Selection( validRanges );
expect( stringify( root, sel ) ).to.equal( '[foo]
[xxx][bar
]' );
} );
@@ -776,8 +774,7 @@ describe( 'Schema', () => {
setData( model, '[foo
bar]x[bar
foo]
' );
const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
- const sel = new Selection();
- sel.setRanges( validRanges );
+ const sel = new Selection( validRanges );
expect( stringify( root, sel ) ).to.equal( '[foo]
[bar]x[bar]
[foo]
' );
} );
@@ -788,8 +785,7 @@ describe( 'Schema', () => {
setData( model, '[foo
bar]bom
' );
const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
- const sel = new Selection();
- sel.setRanges( validRanges );
+ const sel = new Selection( validRanges );
expect( stringify( root, sel ) ).to.equal( '[foo]
barbom
' );
} );
diff --git a/tests/model/selection.js b/tests/model/selection.js
index 16fb26fa6..4ea24a757 100644
--- a/tests/model/selection.js
+++ b/tests/model/selection.js
@@ -69,6 +69,18 @@ describe( 'Selection', () => {
expect( selection.isBackward ).to.be.true;
} );
+
+ it( 'should uses internal _setRanges() method to set ranges', () => {
+ const ranges = [ range1, range2, range3 ];
+ const spy = sinon.spy( Selection.prototype, '_setRanges' );
+
+ const selection = new Selection( ranges );
+
+ expect( spy.calledOnce ).to.be.true;
+ expect( Array.from( selection.getRanges() ) ).to.deep.equal( ranges );
+
+ spy.restore();
+ } );
} );
describe( 'isCollapsed', () => {
@@ -77,20 +89,22 @@ describe( 'Selection', () => {
} );
it( 'should return true when there is single collapsed ranges', () => {
- selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
+ selection.setTo( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
expect( selection.isCollapsed ).to.be.true;
} );
it( 'should return false when there are multiple ranges', () => {
- selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
- selection.addRange( new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) );
+ selection.setTo( [
+ new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ),
+ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) )
+ ] );
expect( selection.isCollapsed ).to.be.false;
} );
it( 'should return false when there is not collapsed range', () => {
- selection.addRange( range );
+ selection.setTo( range );
expect( selection.isCollapsed ).to.be.false;
} );
@@ -100,11 +114,14 @@ describe( 'Selection', () => {
it( 'should return proper range count', () => {
expect( selection.rangeCount ).to.equal( 0 );
- selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
+ selection.setTo( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
expect( selection.rangeCount ).to.equal( 1 );
- selection.addRange( new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) );
+ selection.setTo( [
+ new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ),
+ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) )
+ ] );
expect( selection.rangeCount ).to.equal( 2 );
} );
@@ -112,134 +129,55 @@ describe( 'Selection', () => {
describe( 'isBackward', () => {
it( 'is defined by the last added range', () => {
- selection.addRange( range, true );
+ selection.setTo( [ range ], true );
expect( selection ).to.have.property( 'isBackward', true );
- selection.addRange( liveRange );
+ selection.setTo( 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 );
+ selection.setTo( [ new Range( pos, pos ) ], true );
expect( selection.isBackward ).to.be.false;
} );
} );
describe( 'focus', () => {
- let r3;
+ let r1, r2, r3;
beforeEach( () => {
- const r1 = Range.createFromParentsAndOffsets( root, 2, root, 4 );
- const r2 = Range.createFromParentsAndOffsets( root, 4, root, 6 );
+ r1 = Range.createFromParentsAndOffsets( root, 2, root, 4 );
+ r2 = Range.createFromParentsAndOffsets( root, 4, root, 6 );
r3 = Range.createFromParentsAndOffsets( root, 1, root, 2 );
- selection.addRange( r1 );
- selection.addRange( r2 );
+ selection.setTo( [ r1, r2 ] );
} );
it( 'should return correct focus when last added range is not backward one', () => {
- selection.addRange( r3 );
+ selection.setTo( [ r1, r2, r3 ] );
expect( selection.focus.isEqual( r3.end ) ).to.be.true;
} );
it( 'should return correct focus when last added range is backward one', () => {
- selection.addRange( r3, true );
+ selection.setTo( [ r1, r2, r3 ], true );
expect( selection.focus.isEqual( r3.start ) ).to.be.true;
} );
it( 'should return null if no ranges in selection', () => {
- selection.removeAllRanges();
+ selection.setTo( null );
expect( selection.focus ).to.be.null;
} );
} );
- describe( 'addRange()', () => {
- it( 'should copy added ranges and store multiple ranges', () => {
- selection.addRange( liveRange );
- selection.addRange( range );
-
- const ranges = selection._ranges;
-
- 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 );
-
- expect( selection.anchor.path ).to.deep.equal( [ 0 ] );
- expect( selection.focus.path ).to.deep.equal( [ 1 ] );
-
- selection.addRange( range );
-
- 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 );
-
- expect( selection.anchor.path ).to.deep.equal( [ 1 ] );
- expect( selection.focus.path ).to.deep.equal( [ 0 ] );
-
- selection.addRange( range, true );
-
- 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 );
-
- const ranges = Array.from( selection.getRanges() );
-
- selection.addRange( range );
-
- expect( ranges.length ).to.equal( 1 );
- expect( ranges[ 0 ].isEqual( liveRange ) ).to.be.true;
- } );
-
- it( 'should fire change:range event when adding a range', () => {
- const spy = sinon.spy();
- selection.on( 'change:range', spy );
-
- selection.addRange( range );
-
- expect( spy.called ).to.be.true;
- } );
-
- it( 'should throw an error when range is invalid', () => {
- expect( () => {
- selection.addRange( { invalid: 'range' } );
- } ).to.throw( CKEditorError, /model-selection-added-not-range/ );
- } );
-
- 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, /model-selection-range-intersects/ );
- } );
- } );
-
- describe( 'setIn()', () => {
+ describe( 'setTo() - setting selection inside element', () => {
it( 'should set selection inside an element', () => {
const element = new Element( 'p', null, [ new Text( 'foo' ), new Text( 'bar' ) ] );
- selection.setIn( element );
+ selection.setTo( Range.createIn( element ) );
const ranges = Array.from( selection.getRanges() );
expect( ranges.length ).to.equal( 1 );
@@ -250,14 +188,14 @@ describe( 'Selection', () => {
} );
} );
- describe( 'setOn()', () => {
+ describe( 'setTo() - setting selection on item', () => {
it( 'should set selection on an item', () => {
const textNode1 = new Text( 'foo' );
const textNode2 = new Text( 'bar' );
const textNode3 = new Text( 'baz' );
const element = new Element( 'p', null, [ textNode1, textNode2, textNode3 ] );
- selection.setOn( textNode2 );
+ selection.setTo( Range.createOn( textNode2 ) );
const ranges = Array.from( selection.getRanges() );
expect( ranges.length ).to.equal( 1 );
@@ -268,19 +206,19 @@ describe( 'Selection', () => {
} );
} );
- describe( 'setCollapsedAt()', () => {
+ describe( 'setTo() - setting selection to position or item', () => {
it( 'fires change:range', () => {
const spy = sinon.spy();
selection.on( 'change:range', spy );
- selection.setCollapsedAt( root );
+ selection.setTo( root );
expect( spy.calledOnce ).to.be.true;
} );
it( 'sets selection at the 0 offset if second parameter not passed', () => {
- selection.setCollapsedAt( root );
+ selection.setTo( root );
expect( selection ).to.have.property( 'isCollapsed', true );
@@ -290,7 +228,7 @@ describe( 'Selection', () => {
} );
it( 'sets selection at given offset in given parent', () => {
- selection.setCollapsedAt( root, 3 );
+ selection.setTo( root, 3 );
expect( selection ).to.have.property( 'isCollapsed', true );
@@ -300,7 +238,7 @@ describe( 'Selection', () => {
} );
it( 'sets selection at the end of the given parent', () => {
- selection.setCollapsedAt( root, 'end' );
+ selection.setTo( root, 'end' );
expect( selection ).to.have.property( 'isCollapsed', true );
@@ -310,7 +248,7 @@ describe( 'Selection', () => {
} );
it( 'sets selection before the specified element', () => {
- selection.setCollapsedAt( root.getChild( 1 ), 'before' );
+ selection.setTo( root.getChild( 1 ), 'before' );
expect( selection ).to.have.property( 'isCollapsed', true );
@@ -320,7 +258,7 @@ describe( 'Selection', () => {
} );
it( 'sets selection after the specified element', () => {
- selection.setCollapsedAt( root.getChild( 1 ), 'after' );
+ selection.setTo( root.getChild( 1 ), 'after' );
expect( selection ).to.have.property( 'isCollapsed', true );
@@ -332,7 +270,7 @@ describe( 'Selection', () => {
it( 'sets selection at the specified position', () => {
const pos = Position.createFromParentAndOffset( root, 3 );
- selection.setCollapsedAt( pos );
+ selection.setTo( pos );
expect( selection ).to.have.property( 'isCollapsed', true );
@@ -342,27 +280,26 @@ describe( 'Selection', () => {
} );
} );
- describe( 'moveFocusTo()', () => {
+ describe( 'setFocus()', () => {
it( 'keeps all existing ranges and fires no change:range when no modifications needed', () => {
- selection.addRange( range );
- selection.addRange( liveRange );
+ selection.setTo( [ range, liveRange ] );
const spy = sinon.spy();
selection.on( 'change:range', spy );
- selection.moveFocusTo( selection.focus );
+ selection.setFocus( selection.focus );
expect( count( selection.getRanges() ) ).to.equal( 2 );
expect( spy.callCount ).to.equal( 0 );
} );
it( 'fires change:range', () => {
- selection.addRange( range );
+ selection.setTo( range );
const spy = sinon.spy();
selection.on( 'change:range', spy );
- selection.moveFocusTo( Position.createAt( root, 'end' ) );
+ selection.setFocus( Position.createAt( root, 'end' ) );
expect( spy.calledOnce ).to.be.true;
} );
@@ -371,17 +308,17 @@ describe( 'Selection', () => {
const endPos = Position.createAt( root, 'end' );
expect( () => {
- selection.moveFocusTo( endPos );
- } ).to.throw( CKEditorError, /model-selection-moveFocusTo-no-ranges/ );
+ selection.setFocus( endPos );
+ } ).to.throw( CKEditorError, /model-selection-setFocus-no-ranges/ );
} );
it( 'modifies existing collapsed selection', () => {
const startPos = Position.createAt( root, 1 );
const endPos = Position.createAt( root, 2 );
- selection.setCollapsedAt( startPos );
+ selection.setTo( startPos );
- selection.moveFocusTo( endPos );
+ selection.setFocus( endPos );
expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' );
@@ -391,9 +328,9 @@ describe( 'Selection', () => {
const startPos = Position.createAt( root, 1 );
const endPos = Position.createAt( root, 0 );
- selection.setCollapsedAt( startPos );
+ selection.setTo( startPos );
- selection.moveFocusTo( endPos );
+ selection.setFocus( endPos );
expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' );
@@ -405,9 +342,9 @@ describe( 'Selection', () => {
const endPos = Position.createAt( root, 2 );
const newEndPos = Position.createAt( root, 3 );
- selection.addRange( new Range( startPos, endPos ) );
+ selection.setTo( new Range( startPos, endPos ) );
- selection.moveFocusTo( newEndPos );
+ selection.setFocus( newEndPos );
expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -418,9 +355,9 @@ describe( 'Selection', () => {
const endPos = Position.createAt( root, 2 );
const newEndPos = Position.createAt( root, 0 );
- selection.addRange( new Range( startPos, endPos ) );
+ selection.setTo( new Range( startPos, endPos ) );
- selection.moveFocusTo( newEndPos );
+ selection.setFocus( newEndPos );
expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -432,9 +369,9 @@ describe( 'Selection', () => {
const endPos = Position.createAt( root, 2 );
const newEndPos = Position.createAt( root, 3 );
- selection.addRange( new Range( startPos, endPos ), true );
+ selection.setTo( new Range( startPos, endPos ), true );
- selection.moveFocusTo( newEndPos );
+ selection.setFocus( newEndPos );
expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -446,9 +383,9 @@ describe( 'Selection', () => {
const endPos = Position.createAt( root, 2 );
const newEndPos = Position.createAt( root, 0 );
- selection.addRange( new Range( startPos, endPos ), true );
+ selection.setTo( new Range( startPos, endPos ), true );
- selection.moveFocusTo( newEndPos );
+ selection.setFocus( newEndPos );
expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -464,14 +401,16 @@ describe( 'Selection', () => {
const newEndPos = Position.createAt( root, 0 );
- selection.addRange( new Range( startPos1, endPos1 ) );
- selection.addRange( new Range( startPos2, endPos2 ) );
+ selection.setTo( [
+ new Range( startPos1, endPos1 ),
+ new Range( startPos2, endPos2 )
+ ] );
const spy = sinon.spy();
selection.on( 'change:range', spy );
- selection.moveFocusTo( newEndPos );
+ selection.setFocus( newEndPos );
const ranges = Array.from( selection.getRanges() );
@@ -490,9 +429,9 @@ describe( 'Selection', () => {
const startPos = Position.createAt( root, 1 );
const endPos = Position.createAt( root, 2 );
- selection.addRange( new Range( startPos, endPos ) );
+ selection.setTo( new Range( startPos, endPos ) );
- selection.moveFocusTo( startPos );
+ selection.setFocus( startPos );
expect( selection.focus.compareWith( startPos ) ).to.equal( 'same' );
expect( selection.isCollapsed ).to.be.true;
@@ -504,35 +443,33 @@ describe( 'Selection', () => {
const newEndPos = Position.createAt( root, 4 );
const spy = testUtils.sinon.stub( Position, 'createAt' ).returns( newEndPos );
- selection.addRange( new Range( startPos, endPos ) );
+ selection.setTo( new Range( startPos, endPos ) );
- selection.moveFocusTo( root, 'end' );
+ selection.setFocus( root, 'end' );
expect( spy.calledOnce ).to.be.true;
expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
} );
} );
- describe( 'removeAllRanges()', () => {
+ describe( 'setTo - selection set to null', () => {
let spy;
it( 'should remove all stored ranges', () => {
- selection.addRange( liveRange );
- selection.addRange( range );
+ selection.setTo( [ liveRange, range ] );
- selection.removeAllRanges();
+ selection.setTo( null );
expect( Array.from( selection.getRanges() ).length ).to.equal( 0 );
} );
it( 'should fire exactly one change:range event', () => {
- selection.addRange( liveRange );
- selection.addRange( range );
+ selection.setTo( [ liveRange, range ] );
spy = sinon.spy();
selection.on( 'change:range', spy );
- selection.removeAllRanges();
+ selection.setTo( null );
expect( spy.calledOnce ).to.be.true;
} );
@@ -541,13 +478,13 @@ describe( 'Selection', () => {
spy = sinon.spy();
selection.on( 'change:range', spy );
- selection.removeAllRanges();
+ selection.setTo( null );
expect( spy.called ).to.be.false;
} );
} );
- describe( 'setRanges()', () => {
+ describe( '_setRanges()', () => {
let newRanges, spy;
beforeEach( () => {
@@ -556,8 +493,7 @@ describe( 'Selection', () => {
new Range( new Position( root, [ 5, 0 ] ), new Position( root, [ 6, 0 ] ) )
];
- selection.addRange( liveRange );
- selection.addRange( range );
+ selection.setTo( [ liveRange, range ] );
spy = sinon.spy();
selection.on( 'change:range', spy );
@@ -565,31 +501,31 @@ describe( 'Selection', () => {
it( 'should throw an error when range is invalid', () => {
expect( () => {
- selection.setRanges( [ { invalid: 'range' } ] );
+ selection._setRanges( [ { invalid: 'range' } ] );
} ).to.throw( CKEditorError, /model-selection-added-not-range/ );
} );
it( 'should remove all ranges and add given ranges', () => {
- selection.setRanges( newRanges );
+ selection._setRanges( newRanges );
const ranges = Array.from( selection.getRanges() );
expect( ranges ).to.deep.equal( newRanges );
} );
it( 'should use last range from given array to get anchor and focus position', () => {
- selection.setRanges( newRanges );
+ selection._setRanges( newRanges );
expect( selection.anchor.path ).to.deep.equal( [ 5, 0 ] );
expect( selection.focus.path ).to.deep.equal( [ 6, 0 ] );
} );
it( 'should acknowledge backward flag when setting anchor and focus', () => {
- selection.setRanges( newRanges, true );
+ selection._setRanges( newRanges, true );
expect( selection.anchor.path ).to.deep.equal( [ 6, 0 ] );
expect( selection.focus.path ).to.deep.equal( [ 5, 0 ] );
} );
it( 'should fire exactly one change:range event', () => {
- selection.setRanges( newRanges );
+ selection._setRanges( newRanges );
expect( spy.calledOnce ).to.be.true;
} );
@@ -598,55 +534,79 @@ describe( 'Selection', () => {
expect( data.directChange ).to.be.true;
} );
- selection.setRanges( newRanges );
+ selection._setRanges( newRanges );
} );
it( 'should not fire change:range event if given ranges are the same', () => {
- selection.setRanges( [ liveRange, range ] );
+ selection._setRanges( [ liveRange, range ] );
expect( spy.calledOnce ).to.be.false;
} );
+
+ it( 'should copy added ranges and store multiple ranges', () => {
+ selection._setRanges( [ liveRange, range ] );
+
+ const ranges = selection._ranges;
+
+ 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 last added range', () => {
+ selection._setRanges( [ liveRange, range ] );
+
+ 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._setRanges( [ liveRange, range ], true );
+
+ expect( selection.anchor.path ).to.deep.equal( [ 2 ] );
+ expect( selection.focus.path ).to.deep.equal( [ 2, 2 ] );
+ } );
} );
describe( 'setTo()', () => {
- it( 'should set selection to be same as given selection, using setRanges method', () => {
- const spy = sinon.spy( selection, 'setRanges' );
+ it( 'should set selection to be same as given selection, using _setRanges method', () => {
+ const spy = sinon.spy( selection, '_setRanges' );
- const otherSelection = new Selection();
- otherSelection.addRange( range1 );
- otherSelection.addRange( range2, true );
+ const otherSelection = new Selection( [ range1, range2 ], true );
selection.setTo( otherSelection );
expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1, range2 ] );
expect( selection.isBackward ).to.be.true;
- expect( selection.setRanges.calledOnce ).to.be.true;
+ expect( selection._setRanges.calledOnce ).to.be.true;
spy.restore();
} );
- it( 'should set selection on the given Range using setRanges method', () => {
- const spy = sinon.spy( selection, 'setRanges' );
+ it( 'should set selection on the given Range using _setRanges method', () => {
+ const spy = sinon.spy( selection, '_setRanges' );
selection.setTo( range1 );
expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1 ] );
expect( selection.isBackward ).to.be.false;
- expect( selection.setRanges.calledOnce ).to.be.true;
+ expect( selection._setRanges.calledOnce ).to.be.true;
spy.restore();
} );
- it( 'should set selection on the given iterable of Ranges using setRanges method', () => {
- const spy = sinon.spy( selection, 'setRanges' );
+ it( 'should set selection on the given iterable of Ranges using _setRanges method', () => {
+ const spy = sinon.spy( selection, '_setRanges' );
selection.setTo( new Set( [ range1, range2 ] ) );
expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1, range2 ] );
expect( selection.isBackward ).to.be.false;
- expect( selection.setRanges.calledOnce ).to.be.true;
+ expect( selection._setRanges.calledOnce ).to.be.true;
spy.restore();
} );
- it( 'should set collapsed selection on the given Position using setRanges method', () => {
- const spy = sinon.spy( selection, 'setRanges' );
+ it( 'should set collapsed selection on the given Position using _setRanges method', () => {
+ const spy = sinon.spy( selection, '_setRanges' );
const position = new Position( root, [ 4 ] );
selection.setTo( position );
@@ -655,9 +615,27 @@ describe( 'Selection', () => {
expect( Array.from( selection.getRanges() )[ 0 ].start ).to.deep.equal( position );
expect( selection.isBackward ).to.be.false;
expect( selection.isCollapsed ).to.be.true;
- expect( selection.setRanges.calledOnce ).to.be.true;
+ expect( selection._setRanges.calledOnce ).to.be.true;
spy.restore();
} );
+
+ it( 'should throw an error if added ranges intersects', () => {
+ expect( () => {
+ selection.setTo( [
+ liveRange,
+ new Range(
+ new Position( root, [ 0, 4 ] ),
+ new Position( root, [ 1, 2 ] )
+ )
+ ] );
+ } ).to.throw( CKEditorError, /model-selection-range-intersects/ );
+ } );
+
+ it( 'should throw an error when trying to set selection to not selectable', () => {
+ expect( () => {
+ selection.setTo( {} );
+ } ).to.throw( /model-selection-setTo-not-selectable/ );
+ } );
} );
describe( 'getFirstRange()', () => {
@@ -666,14 +644,14 @@ describe( 'Selection', () => {
} );
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( range2 );
-
- // This should be the first range.
- selection.addRange( range1 );
-
- // A random range that is not first.
- selection.addRange( range3 );
+ selection.setTo( [
+ // This will not be the first range despite being added as first
+ range2,
+ // This should be the first range.
+ range1,
+ // A random range that is not first.
+ range3
+ ] );
const range = selection.getFirstRange();
@@ -688,14 +666,14 @@ describe( 'Selection', () => {
} );
it( 'should return a position that is in selection and is before any other position from the selection', () => {
- // This will not be the first range despite being added as first
- selection.addRange( range2 );
-
- // This should be the first range.
- selection.addRange( range1 );
-
- // A random range that is not first.
- selection.addRange( range3 );
+ selection.setTo( [
+ // This will not be the first range despite being added as first
+ range2,
+ // This should be the first range.
+ range1,
+ // A random range that is not first.
+ range3
+ ] );
const position = selection.getFirstPosition();
@@ -709,9 +687,7 @@ describe( 'Selection', () => {
} );
it( 'should return a range which start position is before all other ranges\' start positions', () => {
- selection.addRange( range3 );
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range3, range1, range2 ] );
const range = selection.getLastRange();
@@ -726,9 +702,7 @@ describe( 'Selection', () => {
} );
it( 'should return a position that is in selection and is before any other position from the selection', () => {
- selection.addRange( range3 );
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range3, range1, range2 ] );
const position = selection.getLastPosition();
@@ -738,21 +712,17 @@ describe( 'Selection', () => {
describe( 'isEqual()', () => {
it( 'should return true if selections equal', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
- const otherSelection = new Selection();
- otherSelection.addRange( range1 );
- otherSelection.addRange( range2 );
+ const otherSelection = new Selection( [ range1, range2 ] );
expect( selection.isEqual( otherSelection ) ).to.be.true;
} );
it( 'should return true if backward selections equal', () => {
- selection.addRange( range1, true );
+ selection.setTo( [ range1 ], true );
- const otherSelection = new Selection();
- otherSelection.addRange( range1, true );
+ const otherSelection = new Selection( [ range1 ], true );
expect( selection.isEqual( otherSelection ) ).to.be.true;
} );
@@ -764,44 +734,38 @@ describe( 'Selection', () => {
} );
it( 'should return false if ranges count does not equal', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
- const otherSelection = new Selection();
- otherSelection.addRange( range2 );
+ const otherSelection = new Selection( [ range2 ] );
expect( selection.isEqual( otherSelection ) ).to.be.false;
} );
it( 'should return false if ranges (other than the last added range) do not equal', () => {
- selection.addRange( range1 );
- selection.addRange( range3 );
+ selection.setTo( [ range1, range3 ] );
- const otherSelection = new Selection();
- otherSelection.addRange( range2 );
- otherSelection.addRange( range3 );
+ const otherSelection = new Selection( [ range2, range3 ] );
expect( selection.isEqual( otherSelection ) ).to.be.false;
} );
it( 'should return false if directions do not equal', () => {
- selection.addRange( range1 );
+ selection.setTo( range1 );
- const otherSelection = new Selection();
- otherSelection.addRange( range1, true );
+ const otherSelection = new Selection( [ range1 ], true );
expect( selection.isEqual( otherSelection ) ).to.be.false;
} );
} );
- describe( 'collapseToStart()', () => {
+ describe( 'setTo - used to collapse at start', () => {
it( 'should collapse to start position and fire change event', () => {
- selection.setRanges( [ range2, range1, range3 ] );
+ selection.setTo( [ range2, range1, range3 ] );
const spy = sinon.spy();
selection.on( 'change:range', spy );
- selection.collapseToStart();
+ selection.setTo( selection.getFirstPosition() );
expect( selection.rangeCount ).to.equal( 1 );
expect( selection.isCollapsed ).to.be.true;
@@ -810,11 +774,11 @@ describe( 'Selection', () => {
} );
it( 'should do nothing if selection was already collapsed', () => {
- selection.setCollapsedAt( range1.start );
+ selection.setTo( range1.start );
const spy = sinon.spy( selection, 'fire' );
- selection.collapseToStart();
+ selection.setTo( selection.getFirstPosition() );
expect( spy.notCalled ).to.be.true;
spy.restore();
@@ -823,21 +787,21 @@ describe( 'Selection', () => {
it( 'should do nothing if no ranges present', () => {
const spy = sinon.spy( selection, 'fire' );
- selection.collapseToStart();
+ selection.setTo( selection.getFirstPosition() );
spy.restore();
expect( spy.notCalled ).to.be.true;
} );
} );
- describe( 'collapseToEnd()', () => {
+ describe( 'setTo - used to collapse at end', () => {
it( 'should collapse to start position and fire change:range event', () => {
- selection.setRanges( [ range2, range3, range1 ] );
+ selection.setTo( [ range2, range3, range1 ] );
const spy = sinon.spy();
selection.on( 'change:range', spy );
- selection.collapseToEnd();
+ selection.setTo( selection.getLastPosition() );
expect( selection.rangeCount ).to.equal( 1 );
expect( selection.isCollapsed ).to.be.true;
@@ -846,11 +810,11 @@ describe( 'Selection', () => {
} );
it( 'should do nothing if selection was already collapsed', () => {
- selection.setCollapsedAt( range1.start );
+ selection.setTo( range1.start );
const spy = sinon.spy( selection, 'fire' );
- selection.collapseToEnd();
+ selection.setTo( selection.getLastPosition() );
expect( spy.notCalled ).to.be.true;
spy.restore();
@@ -859,7 +823,7 @@ describe( 'Selection', () => {
it( 'should do nothing if selection has no ranges', () => {
const spy = sinon.spy( selection, 'fire' );
- selection.collapseToEnd();
+ selection.setTo( selection.getLastPosition() );
expect( spy.notCalled ).to.be.true;
spy.restore();
@@ -868,8 +832,7 @@ describe( 'Selection', () => {
describe( 'createFromSelection()', () => {
it( 'should return a Selection instance with same ranges and direction as given selection', () => {
- selection.addRange( liveRange );
- selection.addRange( range, true );
+ selection.setTo( [ liveRange, range ], true );
const snapshot = Selection.createFromSelection( selection );
@@ -1132,7 +1095,7 @@ describe( 'Selection', () => {
describe( 'setAttribute()', () => {
it( 'should set given attribute on the selection', () => {
- selection.setRanges( [ rangeInFullP ] );
+ selection.setTo( [ rangeInFullP ] );
selection.setAttribute( 'foo', 'bar' );
expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
@@ -1167,7 +1130,7 @@ describe( 'Selection', () => {
describe( 'getAttributes()', () => {
it( 'should return an iterator that iterates over all attributes set on selection', () => {
- selection.setRanges( [ rangeInFullP ] );
+ selection.setTo( [ rangeInFullP ] );
selection.setAttribute( 'foo', 'bar' );
selection.setAttribute( 'abc', 'xyz' );
@@ -1179,7 +1142,7 @@ describe( 'Selection', () => {
describe( 'getAttributeKeys()', () => {
it( 'should return iterator that iterates over all attribute keys set on selection', () => {
- selection.setRanges( [ rangeInFullP ] );
+ selection.setTo( [ rangeInFullP ] );
selection.setAttribute( 'foo', 'bar' );
selection.setAttribute( 'abc', 'xyz' );
@@ -1191,7 +1154,7 @@ describe( 'Selection', () => {
describe( 'hasAttribute()', () => {
it( 'should return true if element contains attribute with given key', () => {
- selection.setRanges( [ rangeInFullP ] );
+ selection.setTo( [ rangeInFullP ] );
selection.setAttribute( 'foo', 'bar' );
expect( selection.hasAttribute( 'foo' ) ).to.be.true;
@@ -1202,42 +1165,9 @@ describe( 'Selection', () => {
} );
} );
- describe( 'clearAttributes()', () => {
- it( 'should remove all attributes from the element', () => {
- selection.setRanges( [ rangeInFullP ] );
- selection.setAttribute( 'foo', 'bar' );
- selection.setAttribute( 'abc', 'xyz' );
-
- selection.clearAttributes();
-
- expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
- expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
- } );
-
- it( 'should fire change:attribute event with correct parameters', () => {
- selection.setAttribute( 'foo', 'bar' );
-
- selection.on( 'change:attribute', ( evt, data ) => {
- expect( data.directChange ).to.be.true;
- expect( data.attributeKeys ).to.deep.equal( [ 'foo' ] );
- } );
-
- selection.clearAttributes();
- } );
-
- it( 'should not fire change:attribute event if there were no attributes', () => {
- const spy = sinon.spy();
- selection.on( 'change:attribute', spy );
-
- selection.clearAttributes();
-
- expect( spy.called ).to.be.false;
- } );
- } );
-
describe( 'removeAttribute()', () => {
it( 'should remove attribute', () => {
- selection.setRanges( [ rangeInFullP ] );
+ selection.setTo( [ rangeInFullP ] );
selection.setAttribute( 'foo', 'bar' );
selection.removeAttribute( 'foo' );
@@ -1264,50 +1194,6 @@ describe( 'Selection', () => {
expect( spy.called ).to.be.false;
} );
} );
-
- describe( 'setAttributesTo()', () => {
- it( 'should remove all attributes set on element and set the given ones', () => {
- selection.setAttribute( 'abc', 'xyz' );
- selection.setAttributesTo( { foo: 'bar' } );
-
- expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
- expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
- } );
-
- it( 'should fire only one change:attribute event', () => {
- selection.setAttributesTo( { foo: 'bar', xxx: 'yyy' } );
-
- const spy = sinon.spy();
- selection.on( 'change:attribute', spy );
-
- selection.setAttributesTo( { foo: 'bar', abc: 'def' } );
-
- expect( spy.calledOnce ).to.be.true;
- } );
-
- it( 'should fire change:attribute event with correct parameters', () => {
- selection.setAttributesTo( { foo: 'bar', xxx: 'yyy' } );
-
- selection.on( 'change:attribute', ( evt, data ) => {
- expect( data.directChange ).to.be.true;
- expect( data.attributeKeys ).to.deep.equal( [ 'abc', 'xxx' ] );
- } );
-
- selection.setAttributesTo( { foo: 'bar', abc: 'def' } );
- } );
-
- it( 'should not fire change:attribute event if attributes had not changed', () => {
- selection.setRanges( [ rangeInFullP ] );
- selection.setAttributesTo( { foo: 'bar', xxx: 'yyy' } );
-
- const spy = sinon.spy();
- selection.on( 'change:attribute', spy );
-
- selection.setAttributesTo( { xxx: 'yyy', foo: 'bar' } );
-
- expect( spy.called ).to.be.false;
- } );
- } );
} );
describe( 'containsEntireContent()', () => {
diff --git a/tests/model/utils/deletecontent.js b/tests/model/utils/deletecontent.js
index e18a7a252..088f5ac41 100644
--- a/tests/model/utils/deletecontent.js
+++ b/tests/model/utils/deletecontent.js
@@ -335,7 +335,9 @@ describe( 'DataController utils', () => {
new Position( doc.getRoot(), [ 1, 0, 0, 1 ] ) // b]ar
);
- doc.selection.setRanges( [ range ] );
+ model.change( writer => {
+ writer.setSelection( range );
+ } );
deleteContent( model, doc.selection );
@@ -380,7 +382,9 @@ describe( 'DataController utils', () => {
new Position( doc.getRoot(), [ 1, 1 ] ) // b]om
);
- doc.selection.setRanges( [ range ] );
+ model.change( writer => {
+ writer.setSelection( range );
+ } );
deleteContent( model, doc.selection );
@@ -423,7 +427,9 @@ describe( 'DataController utils', () => {
new Position( doc.getRoot(), [ 1, 0, 0, 3 ] ) // bar]
);
- doc.selection.setRanges( [ range ] );
+ model.change( writer => {
+ writer.setSelection( range );
+ } );
deleteContent( model, doc.selection );
diff --git a/tests/model/writer.js b/tests/model/writer.js
index c12f5a812..c3c0a55c6 100644
--- a/tests/model/writer.js
+++ b/tests/model/writer.js
@@ -17,8 +17,8 @@ import Range from '../../src/model/range';
import count from '@ckeditor/ckeditor5-utils/src/count';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
import { getNodesAndText } from '../../tests/model/_utils/utils';
+import DocumentSelection from '../../src/model/documentselection';
describe( 'Writer', () => {
let model, doc, batch;
@@ -2026,6 +2026,137 @@ describe( 'Writer', () => {
} );
} );
+ describe( 'setSelection()', () => {
+ let root;
+
+ beforeEach( () => {
+ model.schema.register( 'p', { inheritAllFrom: '$block' } );
+ model.schema.extend( 'p', { allowIn: '$root' } );
+
+ root = doc.createRoot();
+ root.appendChildren( [
+ new Element( 'p' ),
+ new Element( 'p' ),
+ new Element( 'p', [], new Text( 'foo' ) )
+ ] );
+ } );
+
+ it( 'should use DocumentSelection#_setTo method', () => {
+ const firstParagraph = root.getNodeByPath( [ 1 ] );
+
+ const setToSpy = sinon.spy( DocumentSelection.prototype, '_setTo' );
+ setSelection( firstParagraph );
+
+ expect( setToSpy.calledOnce ).to.be.true;
+ setToSpy.restore();
+ } );
+
+ it( 'should change document selection ranges', () => {
+ const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2, 2 ] ) );
+
+ setSelection( range, true );
+
+ expect( model.document.selection._ranges.length ).to.equal( 1 );
+ expect( model.document.selection._ranges[ 0 ].start.path ).to.deep.equal( [ 1 ] );
+ expect( model.document.selection._ranges[ 0 ].end.path ).to.deep.equal( [ 2, 2 ] );
+ expect( model.document.selection.isBackward ).to.be.true;
+ } );
+ } );
+
+ describe( 'setSelectionFocus()', () => {
+ let root;
+
+ beforeEach( () => {
+ model.schema.register( 'p', { inheritAllFrom: '$block' } );
+ model.schema.extend( 'p', { allowIn: '$root' } );
+
+ root = doc.createRoot();
+ root.appendChildren( [
+ new Element( 'p' ),
+ new Element( 'p' ),
+ new Element( 'p', [], new Text( 'foo' ) )
+ ] );
+ } );
+
+ it( 'should use DocumentSelection#_setFocus method', () => {
+ const firstParagraph = root.getNodeByPath( [ 1 ] );
+
+ const setFocusSpy = sinon.spy( DocumentSelection.prototype, '_setFocus' );
+ setSelectionFocus( firstParagraph );
+
+ expect( setFocusSpy.calledOnce ).to.be.true;
+ setFocusSpy.restore();
+ } );
+
+ it( 'should change document selection ranges', () => {
+ setSelection( new Position( root, [ 1 ] ) );
+ setSelectionFocus( new Position( root, [ 2, 2 ] ) );
+
+ expect( model.document.selection._ranges.length ).to.equal( 1 );
+ expect( model.document.selection._ranges[ 0 ].start.path ).to.deep.equal( [ 1 ] );
+ expect( model.document.selection._ranges[ 0 ].end.path ).to.deep.equal( [ 2, 2 ] );
+ } );
+ } );
+
+ describe( 'setSelectionAttribute()', () => {
+ const fooStoreAttrKey = DocumentSelection._getStoreAttributeKey( 'foo' );
+ let root, rangeInEmptyP, emptyP;
+
+ beforeEach( () => {
+ model.schema.register( 'p', { inheritAllFrom: '$block' } );
+ model.schema.extend( 'p', { allowIn: '$root' } );
+
+ root = doc.createRoot();
+ root.appendChildren( [
+ new Element( 'p', [], [] ),
+ new Element( 'p' ),
+ new Element( 'p', [], new Text( 'foo' ) )
+ ] );
+
+ rangeInEmptyP = new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) );
+ emptyP = root.getChild( 0 );
+ } );
+
+ it( 'should store attribute if the selection is in empty node', () => {
+ setSelection( rangeInEmptyP );
+ setSelectionAttribute( 'foo', 'bar' );
+
+ expect( model.document.selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
+
+ expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
+ } );
+ } );
+
+ describe( 'removeSelectionAttribute()', () => {
+ const fooStoreAttrKey = DocumentSelection._getStoreAttributeKey( 'foo' );
+ let root, rangeInEmptyP, emptyP;
+
+ beforeEach( () => {
+ model.schema.register( 'p', { inheritAllFrom: '$block' } );
+ model.schema.extend( 'p', { allowIn: '$root' } );
+
+ root = doc.createRoot();
+ root.appendChildren( [
+ new Element( 'p', [], [] ),
+ new Element( 'p' ),
+ new Element( 'p', [], new Text( 'foo' ) )
+ ] );
+
+ rangeInEmptyP = new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) );
+ emptyP = root.getChild( 0 );
+ } );
+
+ it( 'should remove stored attribute if the selection is in empty node', () => {
+ setSelection( rangeInEmptyP );
+ setSelectionAttribute( 'foo', 'bar' );
+ removeSelectionAttribute( 'foo' );
+
+ expect( model.document.selection.getAttribute( 'foo' ) ).to.be.undefined;
+
+ expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
+ } );
+ } );
+
function createText( data, attributes ) {
return model.change( writer => {
return writer.createText( data, attributes );
@@ -2157,4 +2288,28 @@ describe( 'Writer', () => {
writer.removeMarker( markerOrName );
} );
}
+
+ function setSelection( selectable, backwardSelectionOrOffset ) {
+ model.enqueueChange( batch, writer => {
+ writer.setSelection( selectable, backwardSelectionOrOffset );
+ } );
+ }
+
+ function setSelectionFocus( itemOrPosition, offset ) {
+ model.enqueueChange( batch, writer => {
+ writer.setSelectionFocus( itemOrPosition, offset );
+ } );
+ }
+
+ function setSelectionAttribute( key, value ) {
+ model.enqueueChange( batch, writer => {
+ writer.setSelectionAttribute( key, value );
+ } );
+ }
+
+ function removeSelectionAttribute( key ) {
+ model.enqueueChange( batch, writer => {
+ writer.removeSelectionAttribute( key );
+ } );
+ }
} );
diff --git a/tests/view/document/document.js b/tests/view/document/document.js
index 4d992f2e2..43b50b24a 100644
--- a/tests/view/document/document.js
+++ b/tests/view/document/document.js
@@ -288,7 +288,7 @@ describe( 'Document', () => {
left: '-1000px'
} );
- viewDocument.selection.addRange( range );
+ viewDocument.selection.setTo( range );
viewDocument.scrollToTheSelection();
sinon.assert.calledWithMatch( stub, sinon.match.number, sinon.match.number );
@@ -346,7 +346,7 @@ describe( 'Document', () => {
document.body.appendChild( domEditable );
viewEditable = createViewRoot( viewDocument, 'div', 'main' );
viewDocument.attachDomRoot( domEditable );
- viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) );
} );
afterEach( () => {
@@ -383,7 +383,7 @@ describe( 'Document', () => {
it( 'should log warning when no selection', () => {
const logSpy = testUtils.sinon.stub( log, 'warn' );
- viewDocument.selection.removeAllRanges();
+ viewDocument.selection.setTo( null );
viewDocument.focus();
expect( logSpy.calledOnce ).to.be.true;
diff --git a/tests/view/document/jumpoverinlinefiller.js b/tests/view/document/jumpoverinlinefiller.js
index a64d9f2ad..f76ec6ce6 100644
--- a/tests/view/document/jumpoverinlinefiller.js
+++ b/tests/view/document/jumpoverinlinefiller.js
@@ -112,8 +112,7 @@ describe( 'Document', () => {
const viewB = viewDocument.selection.getFirstPosition().parent;
const viewTextX = parse( 'x' );
viewB.appendChildren( viewTextX );
- viewDocument.selection.removeAllRanges();
- viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewTextX, 1, viewTextX, 1 ) );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewTextX, 1, viewTextX, 1 ) );
const domB = viewDocument.getDomRoot( 'main' ).querySelector( 'b' );
const domSelection = document.getSelection();
diff --git a/tests/view/document/jumpoveruielement.js b/tests/view/document/jumpoveruielement.js
index fdea883e2..643cde15a 100644
--- a/tests/view/document/jumpoveruielement.js
+++ b/tests/view/document/jumpoveruielement.js
@@ -88,7 +88,7 @@ describe( 'Document', () => {
// fooxxx{}bar
const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
viewRoot.appendChildren( p );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( bar, 0, bar, 0 ) ] );
+ viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( bar, 0, bar, 0 ) ] );
renderAndFireKeydownEvent( { keyCode: keyCodes.arrowleft } );
@@ -103,7 +103,7 @@ describe( 'Document', () => {
// foo[]xxxbar
const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
viewRoot.appendChildren( p );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( p, 1, p, 1 ) ] );
+ viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( p, 1, p, 1 ) ] );
renderAndFireKeydownEvent();
@@ -120,7 +120,7 @@ describe( 'Document', () => {
// foo{}xxxbar
const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
viewRoot.appendChildren( p );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+ viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
renderAndFireKeydownEvent();
@@ -137,7 +137,7 @@ describe( 'Document', () => {
// foo{}xxxyyybar'
const p = new ViewContainerElement( 'p', null, [ foo, ui, ui2, bar ] );
viewRoot.appendChildren( p );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+ viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
renderAndFireKeydownEvent();
@@ -156,7 +156,7 @@ describe( 'Document', () => {
const div = new ViewContainerElement( 'div' );
viewRoot.appendChildren( p );
viewRoot.appendChildren( div );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+ viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
renderAndFireKeydownEvent();
@@ -174,7 +174,7 @@ describe( 'Document', () => {
const b = new ViewAttribtueElement( 'b', null, foo );
const p = new ViewContainerElement( 'p', null, [ b, ui, bar ] );
viewRoot.appendChildren( p );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
renderAndFireKeydownEvent();
@@ -192,7 +192,7 @@ describe( 'Document', () => {
const b = new ViewAttribtueElement( 'b', null, foo );
const p = new ViewContainerElement( 'p', null, [ b, ui, bar ] );
viewRoot.appendChildren( p );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( b, 1, b, 1 ) ] );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( b, 1, b, 1 ) );
renderAndFireKeydownEvent();
@@ -220,7 +220,7 @@ describe( 'Document', () => {
const p = new ViewContainerElement( 'p', null, [ i, ui, bar ] );
viewRoot.appendChildren( p );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
renderAndFireKeydownEvent();
@@ -247,7 +247,7 @@ describe( 'Document', () => {
const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] );
viewRoot.appendChildren( p );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
renderAndFireKeydownEvent();
@@ -275,7 +275,7 @@ describe( 'Document', () => {
const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] );
viewRoot.appendChildren( p );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
renderAndFireKeydownEvent( { shiftKey: true } );
@@ -351,7 +351,7 @@ describe( 'Document', () => {
const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
viewRoot.appendChildren( p );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ] );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) );
renderAndFireKeydownEvent( { shiftKey: true } );
@@ -376,7 +376,7 @@ describe( 'Document', () => {
const i = new ViewAttribtueElement( 'i', null, b );
const p = new ViewContainerElement( 'p', null, [ i, ui, bar ] );
viewRoot.appendChildren( p );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ] );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) );
renderAndFireKeydownEvent( { shiftKey: true } );
@@ -402,7 +402,7 @@ describe( 'Document', () => {
const b2 = new ViewAttribtueElement( 'b' );
const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] );
viewRoot.appendChildren( p );
- viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ] );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) );
renderAndFireKeydownEvent( { shiftKey: true } );
diff --git a/tests/view/domconverter/binding.js b/tests/view/domconverter/binding.js
index 3bf717257..adfddcb73 100644
--- a/tests/view/domconverter/binding.js
+++ b/tests/view/domconverter/binding.js
@@ -271,7 +271,7 @@ describe( 'DomConverter', () => {
viewElement = new ViewElement();
domEl = document.createElement( 'div' );
selection = new ViewSelection();
- selection.addRange( ViewRange.createIn( viewElement ) );
+ selection.setTo( ViewRange.createIn( viewElement ) );
converter.bindFakeSelection( domEl, selection );
} );
@@ -284,7 +284,7 @@ describe( 'DomConverter', () => {
it( 'should keep a copy of selection', () => {
const selectionCopy = ViewSelection.createFromSelection( selection );
- selection.addRange( ViewRange.createIn( new ViewElement() ), true );
+ selection.setTo( ViewRange.createIn( new ViewElement() ), true );
const bindSelection = converter.fakeSelectionToView( domEl );
expect( bindSelection ).to.not.equal( selection );
diff --git a/tests/view/domconverter/dom-to-view.js b/tests/view/domconverter/dom-to-view.js
index 8b09761e4..7655be6a8 100644
--- a/tests/view/domconverter/dom-to-view.js
+++ b/tests/view/domconverter/dom-to-view.js
@@ -860,7 +860,7 @@ describe( 'DomConverter', () => {
document.body.appendChild( domContainer );
const viewSelection = new ViewSelection();
- viewSelection.addRange( ViewRange.createIn( new ViewElement() ) );
+ viewSelection.setTo( ViewRange.createIn( new ViewElement() ) );
converter.bindFakeSelection( domContainer, viewSelection );
const domRange = document.createRange();
@@ -882,7 +882,7 @@ describe( 'DomConverter', () => {
document.body.appendChild( domContainer );
const viewSelection = new ViewSelection();
- viewSelection.addRange( ViewRange.createIn( new ViewElement() ) );
+ viewSelection.setTo( ViewRange.createIn( new ViewElement() ) );
converter.bindFakeSelection( domContainer, viewSelection );
const domRange = document.createRange();
diff --git a/tests/view/editableelement.js b/tests/view/editableelement.js
index 8af26d07d..7b77fe42b 100644
--- a/tests/view/editableelement.js
+++ b/tests/view/editableelement.js
@@ -78,13 +78,13 @@ describe( 'EditableElement', () => {
it( 'should change isFocused on document render event', () => {
const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 );
const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
- docMock.selection.addRange( rangeMain );
+ docMock.selection.setTo( rangeMain );
docMock.isFocused = true;
expect( viewMain.isFocused ).to.be.true;
expect( viewHeader.isFocused ).to.be.false;
- docMock.selection.setRanges( [ rangeHeader ] );
+ docMock.selection.setTo( [ rangeHeader ] );
docMock.fire( 'render' );
expect( viewMain.isFocused ).to.be.false;
@@ -96,13 +96,13 @@ describe( 'EditableElement', () => {
const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
docMock.render = sinon.spy();
- docMock.selection.addRange( rangeMain );
+ docMock.selection.setTo( rangeMain );
docMock.isFocused = true;
expect( viewMain.isFocused ).to.be.true;
expect( viewHeader.isFocused ).to.be.false;
- docMock.selection.setRanges( [ rangeHeader ] );
+ docMock.selection.setTo( [ rangeHeader ] );
viewHeader.on( 'change:isFocused', ( evt, propertyName, value ) => {
expect( value ).to.be.true;
@@ -116,7 +116,7 @@ describe( 'EditableElement', () => {
it( 'should change isFocused when document.isFocus changes', () => {
const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 );
const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
- docMock.selection.addRange( rangeMain );
+ docMock.selection.setTo( rangeMain );
docMock.isFocused = true;
expect( viewMain.isFocused ).to.be.true;
@@ -127,7 +127,7 @@ describe( 'EditableElement', () => {
expect( viewMain.isFocused ).to.be.false;
expect( viewHeader.isFocused ).to.be.false;
- docMock.selection.setRanges( [ rangeHeader ] );
+ docMock.selection.setTo( [ rangeHeader ] );
expect( viewMain.isFocused ).to.be.false;
expect( viewHeader.isFocused ).to.be.false;
diff --git a/tests/view/manual/fakeselection.js b/tests/view/manual/fakeselection.js
index 67c690f8d..59b15c47f 100644
--- a/tests/view/manual/fakeselection.js
+++ b/tests/view/manual/fakeselection.js
@@ -39,7 +39,7 @@ viewDocument.on( 'mouseup', ( evt, data ) => {
console.log( 'Making selection around the .' );
const range = ViewRange.createOn( viewStrong );
- viewDocument.selection.setRanges( [ range ] );
+ viewDocument.selection.setTo( [ range ] );
viewDocument.selection.setFake( true, { label: 'fake selection over bar' } );
viewDocument.render();
diff --git a/tests/view/observer/focusobserver.js b/tests/view/observer/focusobserver.js
index 1d2c2eda6..89d27bfae 100644
--- a/tests/view/observer/focusobserver.js
+++ b/tests/view/observer/focusobserver.js
@@ -94,7 +94,7 @@ describe( 'FocusObserver', () => {
} );
it( 'should set isFocused to false on blur when selection in same editable', () => {
- viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) );
observer.onDomEvent( { type: 'focus', target: domMain } );
@@ -106,7 +106,7 @@ describe( 'FocusObserver', () => {
} );
it( 'should not set isFocused to false on blur when it is fired on other editable', () => {
- viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) );
observer.onDomEvent( { type: 'focus', target: domMain } );
diff --git a/tests/view/observer/mutationobserver.js b/tests/view/observer/mutationobserver.js
index fffa58986..e7f933c27 100644
--- a/tests/view/observer/mutationobserver.js
+++ b/tests/view/observer/mutationobserver.js
@@ -25,7 +25,7 @@ describe( 'MutationObserver', () => {
createViewRoot( viewDocument );
viewDocument.attachDomRoot( domEditor );
- viewDocument.selection.removeAllRanges();
+ viewDocument.selection.setTo( null );
document.getSelection().removeAllRanges();
mutationObserver = viewDocument.getObserver( MutationObserver );
diff --git a/tests/view/observer/selectionobserver.js b/tests/view/observer/selectionobserver.js
index 40cde1a89..2f44a0948 100644
--- a/tests/view/observer/selectionobserver.js
+++ b/tests/view/observer/selectionobserver.js
@@ -41,7 +41,7 @@ describe( 'SelectionObserver', () => {
viewDocument.render();
- viewDocument.selection.removeAllRanges();
+ viewDocument.selection.setTo( null );
domDocument.getSelection().removeAllRanges();
viewDocument.isFocused = true;
@@ -103,7 +103,7 @@ describe( 'SelectionObserver', () => {
setTimeout( done, 70 );
const viewBar = viewDocument.getRoot().getChild( 1 ).getChild( 0 );
- viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) );
viewDocument.render();
} );
@@ -162,7 +162,7 @@ describe( 'SelectionObserver', () => {
let counter = 70;
const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
- viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
return new Promise( ( resolve, reject ) => {
testUtils.sinon.stub( log, 'warn' ).callsFake( msg => {
@@ -186,7 +186,7 @@ describe( 'SelectionObserver', () => {
it( 'should not be treated as an infinite loop if selection is changed only few times', done => {
const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
- viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
+ viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
const spy = testUtils.sinon.spy( log, 'warn' );
viewDocument.on( 'selectionChangeDone', () => {
@@ -319,8 +319,8 @@ describe( 'SelectionObserver', () => {
const viewAnchor = viewDocument.domConverter.domPositionToView( sel.anchorNode, sel.anchorOffset );
const viewFocus = viewDocument.domConverter.domPositionToView( sel.focusNode, sel.focusOffset );
- viewSel.setCollapsedAt( viewAnchor );
- viewSel.moveFocusTo( viewFocus );
+ viewSel.setTo( viewAnchor );
+ viewSel.setFocus( viewFocus );
viewDocument.render();
} );
diff --git a/tests/view/placeholder.js b/tests/view/placeholder.js
index 1adc212a4..3cb5ab131 100644
--- a/tests/view/placeholder.js
+++ b/tests/view/placeholder.js
@@ -100,7 +100,7 @@ describe( 'placeholder', () => {
expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
- viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] );
+ viewDocument.selection.setTo( [ ViewRange.createIn( element ) ] );
viewDocument.render();
expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
@@ -146,8 +146,8 @@ describe( 'placeholder', () => {
expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.true;
// Move selection to the elements with placeholders.
- viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] );
- secondDocument.selection.setRanges( [ ViewRange.createIn( secondElement ) ] );
+ viewDocument.selection.setTo( [ ViewRange.createIn( element ) ] );
+ secondDocument.selection.setTo( [ ViewRange.createIn( secondElement ) ] );
// Render changes.
viewDocument.render();
diff --git a/tests/view/renderer.js b/tests/view/renderer.js
index 004d9fc3a..64b4bfe9e 100644
--- a/tests/view/renderer.js
+++ b/tests/view/renderer.js
@@ -122,7 +122,7 @@ describe( 'Renderer', () => {
renderer.markedAttributes.clear();
renderer.markedChildren.clear();
- selection.removeAllRanges();
+ selection.setTo( null );
selection.setFake( false );
selectionEditable = viewRoot;
@@ -495,8 +495,7 @@ describe( 'Renderer', () => {
renderAndExpectNoChanges( renderer, domRoot );
// Step 3: foo{}
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewP.getChild( 0 ), 3, viewP.getChild( 0 ), 3 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewP.getChild( 0 ), 3, viewP.getChild( 0 ), 3 ) );
renderer.render();
@@ -549,8 +548,7 @@ describe( 'Renderer', () => {
renderAndExpectNoChanges( renderer, domRoot );
// Step 3: {}foo
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets(
+ selection.setTo( ViewRange.createFromParentsAndOffsets(
viewP.getChild( 0 ).getChild( 0 ), 0, viewP.getChild( 0 ).getChild( 0 ), 0 ) );
renderer.render();
@@ -601,8 +599,7 @@ describe( 'Renderer', () => {
renderAndExpectNoChanges( renderer, domRoot );
// Step 3: foo{}
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets(
+ selection.setTo( ViewRange.createFromParentsAndOffsets(
viewP.getChild( 0 ).getChild( 0 ), 3, viewP.getChild( 0 ).getChild( 0 ), 3 ) );
renderer.render();
@@ -674,9 +671,8 @@ describe( 'Renderer', () => {
expect( domP.childNodes[ 2 ].childNodes.length ).to.equal( 0 );
// Step 2: foo"FILLER{}"
- selection.removeAllRanges();
const viewI = viewP.getChild( 2 );
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) );
renderer.render();
@@ -709,14 +705,13 @@ describe( 'Renderer', () => {
// Step 2: Add text node.
const viewText = new ViewText( 'x' );
viewB.appendChildren( viewText );
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
renderer.markToSync( 'children', viewB );
renderer.render();
// Step 3: Remove selection from the view.
- selection.removeAllRanges();
+ selection.setTo( null );
renderer.render();
@@ -746,8 +741,7 @@ describe( 'Renderer', () => {
// Step 2: Remove the and update the selection (bar[]
).
viewP.removeChildren( 1 );
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewP, 1, viewP, 1 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 1, viewP, 1 ) );
renderer.markToSync( 'children', viewP );
renderer.render();
@@ -784,8 +778,7 @@ describe( 'Renderer', () => {
viewP2.appendChildren( removedChildren );
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
renderer.markToSync( 'children', viewP );
renderer.markToSync( 'children', viewP2 );
@@ -822,8 +815,7 @@ describe( 'Renderer', () => {
const viewI = parse( '' );
viewP.appendChildren( viewI );
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) );
renderer.markToSync( 'children', viewP );
renderer.render();
@@ -853,8 +845,7 @@ describe( 'Renderer', () => {
const viewAbc = parse( 'abc' );
viewP.appendChildren( viewAbc );
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewP, 3, viewP, 3 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 3, viewP, 3 ) );
renderer.markToSync( 'children', viewP );
renderer.render();
@@ -897,8 +888,7 @@ describe( 'Renderer', () => {
const viewText = new ViewText( 'x' );
viewP.appendChildren( viewText );
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
renderer.markToSync( 'children', viewP );
renderAndExpectNoChanges( renderer, domRoot );
@@ -928,8 +918,7 @@ describe( 'Renderer', () => {
// Add text node only in View x{}
const viewText = new ViewText( 'x' );
viewP.appendChildren( viewText );
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
renderer.markToSync( 'children', viewP );
renderer.render();
@@ -976,8 +965,7 @@ describe( 'Renderer', () => {
viewP.removeChildren( 0 );
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
renderer.markToSync( 'children', viewP );
renderAndExpectNoChanges( renderer, domRoot );
@@ -1028,8 +1016,7 @@ describe( 'Renderer', () => {
const viewText = new ViewText( 'x' );
viewB.appendChildren( viewText );
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
renderer.markToSync( 'children', viewP );
renderAndExpectNoChanges( renderer, domRoot );
@@ -1072,8 +1059,7 @@ describe( 'Renderer', () => {
const viewText = new ViewText( 'x' );
viewB.appendChildren( viewText );
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
renderer.markToSync( 'children', viewB );
renderer.render();
@@ -1136,8 +1122,7 @@ describe( 'Renderer', () => {
const viewText = new ViewText( 'x' );
viewB.appendChildren( viewText );
- selection.removeAllRanges();
- selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+ selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
renderer.markToSync( 'text', viewText );
renderer.render();
@@ -1297,7 +1282,7 @@ describe( 'Renderer', () => {
// Remove filler.
domB.childNodes[ 0 ].data = '';
- selection.removeAllRanges();
+ selection.setTo( null );
renderer.markToSync( 'children', viewB );
expect( () => {
@@ -1588,7 +1573,7 @@ describe( 'Renderer', () => {
selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
// foo{}bar
- selection.setRanges( [
+ selection.setTo( [
new ViewRange( new ViewPosition( viewB.getChild( 0 ), 0 ), new ViewPosition( viewB.getChild( 0 ), 0 ) )
] );
@@ -1627,7 +1612,7 @@ describe( 'Renderer', () => {
selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
// foo{}
- selection.setRanges( [
+ selection.setTo( [
new ViewRange( new ViewPosition( viewP.getChild( 0 ), 3 ), new ViewPosition( viewP.getChild( 0 ), 3 ) )
] );
@@ -1667,7 +1652,7 @@ describe( 'Renderer', () => {
selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
// fo{ob}ar
- selection.setRanges( [
+ selection.setTo( [
new ViewRange( new ViewPosition( viewP.getChild( 0 ), 2 ), new ViewPosition( viewB.getChild( 0 ), 1 ) )
] );
@@ -1748,7 +1733,7 @@ describe( 'Renderer', () => {
selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
// foo{ba}r
- selection.setRanges( [
+ selection.setTo( [
new ViewRange( new ViewPosition( viewP.getChild( 0 ), 3 ), new ViewPosition( viewB.getChild( 0 ), 2 ) )
] );
@@ -1786,7 +1771,7 @@ describe( 'Renderer', () => {
selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
// foob{ar}baz
- selection.setRanges( [
+ selection.setTo( [
new ViewRange( new ViewPosition( viewB.getChild( 0 ), 1 ), new ViewPosition( viewP.getChild( 2 ), 0 ) )
] );
@@ -1824,7 +1809,7 @@ describe( 'Renderer', () => {
selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
// foo{ba}r
- selection.setRanges( [
+ selection.setTo( [
new ViewRange( new ViewPosition( viewP.getChild( 0 ), 3 ), new ViewPosition( viewI.getChild( 0 ), 2 ) )
] );
@@ -1861,7 +1846,7 @@ describe( 'Renderer', () => {
selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
// f{oobar}baz
- selection.setRanges( [
+ selection.setTo( [
new ViewRange( new ViewPosition( viewP.getChild( 0 ), 1 ), new ViewPosition( viewP.getChild( 2 ), 0 ) )
] );
diff --git a/tests/view/selection.js b/tests/view/selection.js
index 0f9c47b80..622cc9d5b 100644
--- a/tests/view/selection.js
+++ b/tests/view/selection.js
@@ -56,7 +56,7 @@ describe( 'Selection', () => {
} );
it( 'should return start of single range in selection', () => {
- selection.addRange( range1 );
+ selection.setTo( range1 );
const anchor = selection.anchor;
expect( anchor.isEqual( range1.start ) ).to.be.true;
@@ -64,7 +64,7 @@ describe( 'Selection', () => {
} );
it( 'should return end of single range in selection when added as backward', () => {
- selection.addRange( range1, true );
+ selection.setTo( range1, true );
const anchor = selection.anchor;
expect( anchor.isEqual( range1.end ) ).to.be.true;
@@ -72,8 +72,7 @@ describe( 'Selection', () => {
} );
it( 'should get anchor from last inserted range', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
expect( selection.anchor.isEqual( range2.start ) ).to.be.true;
} );
@@ -85,14 +84,14 @@ describe( 'Selection', () => {
} );
it( 'should return end of single range in selection', () => {
- selection.addRange( range1 );
+ selection.setTo( range1 );
const focus = selection.focus;
expect( focus.isEqual( range1.end ) ).to.be.true;
} );
it( 'should return start of single range in selection when added as backward', () => {
- selection.addRange( range1, true );
+ selection.setTo( range1, true );
const focus = selection.focus;
expect( focus.isEqual( range1.start ) ).to.be.true;
@@ -100,17 +99,16 @@ describe( 'Selection', () => {
} );
it( 'should get focus from last inserted range', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
expect( selection.focus.isEqual( range2.end ) ).to.be.true;
} );
} );
- describe( 'moveFocusTo', () => {
+ describe( 'setFocus', () => {
it( 'keeps all existing ranges when no modifications needed', () => {
- selection.addRange( range1 );
- selection.moveFocusTo( selection.focus );
+ selection.setTo( range1 );
+ selection.setFocus( selection.focus );
expect( count( selection.getRanges() ) ).to.equal( 1 );
} );
@@ -119,17 +117,17 @@ describe( 'Selection', () => {
const endPos = Position.createAt( el, 'end' );
expect( () => {
- selection.moveFocusTo( endPos );
- } ).to.throw( CKEditorError, /view-selection-moveFocusTo-no-ranges/ );
+ selection.setFocus( endPos );
+ } ).to.throw( CKEditorError, /view-selection-setFocus-no-ranges/ );
} );
it( 'modifies existing collapsed selection', () => {
const startPos = Position.createAt( el, 1 );
const endPos = Position.createAt( el, 2 );
- selection.setCollapsedAt( startPos );
+ selection.setTo( startPos );
- selection.moveFocusTo( endPos );
+ selection.setFocus( endPos );
expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' );
@@ -139,9 +137,9 @@ describe( 'Selection', () => {
const startPos = Position.createAt( el, 1 );
const endPos = Position.createAt( el, 0 );
- selection.setCollapsedAt( startPos );
+ selection.setTo( startPos );
- selection.moveFocusTo( endPos );
+ selection.setFocus( endPos );
expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' );
@@ -153,9 +151,9 @@ describe( 'Selection', () => {
const endPos = Position.createAt( el, 2 );
const newEndPos = Position.createAt( el, 3 );
- selection.addRange( new Range( startPos, endPos ) );
+ selection.setTo( new Range( startPos, endPos ) );
- selection.moveFocusTo( newEndPos );
+ selection.setFocus( newEndPos );
expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -166,9 +164,9 @@ describe( 'Selection', () => {
const endPos = Position.createAt( el, 2 );
const newEndPos = Position.createAt( el, 0 );
- selection.addRange( new Range( startPos, endPos ) );
+ selection.setTo( new Range( startPos, endPos ) );
- selection.moveFocusTo( newEndPos );
+ selection.setFocus( newEndPos );
expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -180,9 +178,9 @@ describe( 'Selection', () => {
const endPos = Position.createAt( el, 2 );
const newEndPos = Position.createAt( el, 3 );
- selection.addRange( new Range( startPos, endPos ), true );
+ selection.setTo( new Range( startPos, endPos ), true );
- selection.moveFocusTo( newEndPos );
+ selection.setFocus( newEndPos );
expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -194,9 +192,9 @@ describe( 'Selection', () => {
const endPos = Position.createAt( el, 2 );
const newEndPos = Position.createAt( el, 0 );
- selection.addRange( new Range( startPos, endPos ), true );
+ selection.setTo( new Range( startPos, endPos ), true );
- selection.moveFocusTo( newEndPos );
+ selection.setFocus( newEndPos );
expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' );
expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -212,10 +210,12 @@ describe( 'Selection', () => {
const newEndPos = Position.createAt( el, 0 );
- selection.addRange( new Range( startPos1, endPos1 ) );
- selection.addRange( new Range( startPos2, endPos2 ) );
+ selection.setTo( [
+ new Range( startPos1, endPos1 ),
+ new Range( startPos2, endPos2 )
+ ] );
- selection.moveFocusTo( newEndPos );
+ selection.setFocus( newEndPos );
const ranges = Array.from( selection.getRanges() );
@@ -232,9 +232,9 @@ describe( 'Selection', () => {
const startPos = Position.createAt( el, 1 );
const endPos = Position.createAt( el, 2 );
- selection.addRange( new Range( startPos, endPos ) );
+ selection.setTo( new Range( startPos, endPos ) );
- selection.moveFocusTo( startPos );
+ selection.setFocus( startPos );
expect( selection.focus.compareWith( startPos ) ).to.equal( 'same' );
expect( selection.isCollapsed ).to.be.true;
@@ -247,8 +247,8 @@ describe( 'Selection', () => {
const spy = sinon.stub( Position, 'createAt' ).returns( newEndPos );
- selection.addRange( new Range( startPos, endPos ) );
- selection.moveFocusTo( el, 'end' );
+ selection.setTo( new Range( startPos, endPos ) );
+ selection.setFocus( el, 'end' );
expect( spy.calledOnce ).to.be.true;
expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -260,7 +260,7 @@ describe( 'Selection', () => {
describe( 'isCollapsed', () => {
it( 'should return true when there is single collapsed range', () => {
const range = Range.createFromParentsAndOffsets( el, 5, el, 5 );
- selection.addRange( range );
+ selection.setTo( range );
expect( selection.isCollapsed ).to.be.true;
} );
@@ -268,15 +268,14 @@ describe( 'Selection', () => {
it( 'should return false when there are multiple ranges', () => {
const range1 = Range.createFromParentsAndOffsets( el, 5, el, 5 );
const range2 = Range.createFromParentsAndOffsets( el, 15, el, 15 );
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
expect( selection.isCollapsed ).to.be.false;
} );
it( 'should return false when there is not collapsed range', () => {
const range = Range.createFromParentsAndOffsets( el, 15, el, 16 );
- selection.addRange( range );
+ selection.setTo( range );
expect( selection.isCollapsed ).to.be.false;
} );
@@ -286,11 +285,11 @@ describe( 'Selection', () => {
it( 'should return proper range count', () => {
expect( selection.rangeCount ).to.equal( 0 );
- selection.addRange( range1 );
+ selection.setTo( range1 );
expect( selection.rangeCount ).to.equal( 1 );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
expect( selection.rangeCount ).to.equal( 2 );
} );
@@ -301,61 +300,25 @@ describe( 'Selection', () => {
const range1 = Range.createFromParentsAndOffsets( el, 5, el, 10 );
const range2 = Range.createFromParentsAndOffsets( el, 15, el, 16 );
- selection.addRange( range1, true );
+ selection.setTo( range1, true );
expect( selection ).to.have.property( 'isBackward', true );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
expect( selection ).to.have.property( 'isBackward', false );
} );
it( 'is false when last range is collapsed', () => {
const range = Range.createFromParentsAndOffsets( el, 5, el, 5 );
- selection.addRange( range, true );
+ selection.setTo( range, true );
expect( selection.isBackward ).to.be.false;
} );
} );
- describe( 'addRange', () => {
- it( 'should throw an error when range is invalid', () => {
- expect( () => {
- selection.addRange( { invalid: 'range' } );
- } ).to.throw( CKEditorError, 'view-selection-invalid-range: Invalid Range.' );
- } );
-
- it( 'should add range to selection ranges', () => {
- selection.addRange( range1 );
- expect( selection._ranges[ 0 ].isEqual( range1 ) ).to.be.true;
- } );
-
- it( 'should fire change event', done => {
- selection.once( 'change', () => {
- expect( selection._ranges[ 0 ].isEqual( range1 ) ).to.be.true;
- done();
- } );
-
- selection.addRange( range1 );
- } );
-
- it( 'should throw when range is intersecting with already added range', () => {
- const text = el.getChild( 0 );
- const range2 = Range.createFromParentsAndOffsets( text, 7, text, 15 );
- selection.addRange( range1 );
- expect( () => {
- selection.addRange( range2 );
- } ).to.throw( CKEditorError, 'view-selection-range-intersects' );
-
- expect( () => {
- selection.addRange( range1 );
- } ).to.throw( CKEditorError, 'view-selection-range-intersects' );
- } );
- } );
-
describe( 'getRanges', () => {
it( 'should return iterator with copies of all ranges', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
const iterable = selection.getRanges();
const ranges = Array.from( iterable );
@@ -370,9 +333,7 @@ describe( 'Selection', () => {
describe( 'getFirstRange', () => {
it( 'should return copy of range with first position', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
- selection.addRange( range3 );
+ selection.setTo( [ range1, range2, range3 ] );
const range = selection.getFirstRange();
@@ -387,9 +348,7 @@ describe( 'Selection', () => {
describe( 'getLastRange', () => {
it( 'should return copy of range with last position', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
- selection.addRange( range3 );
+ selection.setTo( [ range1, range2, range3 ] );
const range = selection.getLastRange();
@@ -404,9 +363,7 @@ describe( 'Selection', () => {
describe( 'getFirstPosition', () => {
it( 'should return copy of first position', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
- selection.addRange( range3 );
+ selection.setTo( [ range1, range2, range3 ] );
const position = selection.getFirstPosition();
@@ -421,9 +378,7 @@ describe( 'Selection', () => {
describe( 'getLastPosition', () => {
it( 'should return copy of range with last position', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
- selection.addRange( range3 );
+ selection.setTo( [ range1, range2, range3 ] );
const position = selection.getLastPosition();
@@ -438,51 +393,42 @@ describe( 'Selection', () => {
describe( 'isEqual', () => {
it( 'should return true if selections equal', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
const otherSelection = new Selection();
- otherSelection.addRange( range1 );
- otherSelection.addRange( range2 );
+ otherSelection.setTo( [ range1, range2 ] );
expect( selection.isEqual( otherSelection ) ).to.be.true;
} );
it( 'should return true if backward selections equal', () => {
- selection.addRange( range1, true );
+ selection.setTo( range1, true );
- const otherSelection = new Selection();
- otherSelection.addRange( range1, true );
+ const otherSelection = new Selection( [ range1 ], true );
expect( selection.isEqual( otherSelection ) ).to.be.true;
} );
it( 'should return false if ranges count does not equal', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
- const otherSelection = new Selection();
- otherSelection.addRange( range1 );
+ const otherSelection = new Selection( [ range1 ] );
expect( selection.isEqual( otherSelection ) ).to.be.false;
} );
it( 'should return false if ranges (other than the last added one) do not equal', () => {
- selection.addRange( range1 );
- selection.addRange( range3 );
+ selection.setTo( [ range1, range3 ] );
- const otherSelection = new Selection();
- otherSelection.addRange( range2 );
- otherSelection.addRange( range3 );
+ const otherSelection = new Selection( [ range2, range3 ] );
expect( selection.isEqual( otherSelection ) ).to.be.false;
} );
it( 'should return false if directions do not equal', () => {
- selection.addRange( range1 );
+ selection.setTo( range1 );
- const otherSelection = new Selection();
- otherSelection.addRange( range1, true );
+ const otherSelection = new Selection( [ range1 ], true );
expect( selection.isEqual( otherSelection ) ).to.be.false;
} );
@@ -495,21 +441,19 @@ describe( 'Selection', () => {
} );
it( 'should return true if both selection are fake', () => {
- const otherSelection = new Selection();
- otherSelection.addRange( range1 );
+ const otherSelection = new Selection( [ range1 ] );
otherSelection.setFake( true );
selection.setFake( true );
- selection.addRange( range1 );
+ selection.setTo( range1 );
expect( selection.isEqual( otherSelection ) ).to.be.true;
} );
it( 'should return false if both selection are fake but have different label', () => {
- const otherSelection = new Selection();
- otherSelection.addRange( range1 );
+ const otherSelection = new Selection( [ range1 ] );
otherSelection.setFake( true, { label: 'foo bar baz' } );
selection.setFake( true );
- selection.addRange( range1 );
+ selection.setTo( range1 );
expect( selection.isEqual( otherSelection ) ).to.be.false;
} );
@@ -523,42 +467,33 @@ describe( 'Selection', () => {
describe( 'isSimilar', () => {
it( 'should return true if selections equal', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
- const otherSelection = new Selection();
- otherSelection.addRange( range1 );
- otherSelection.addRange( range2 );
+ const otherSelection = new Selection( [ range1, range2 ] );
expect( selection.isSimilar( otherSelection ) ).to.be.true;
} );
it( 'should return false if ranges count does not equal', () => {
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
- const otherSelection = new Selection();
- otherSelection.addRange( range1 );
+ const otherSelection = new Selection( [ range1 ] );
expect( selection.isSimilar( otherSelection ) ).to.be.false;
} );
it( 'should return false if trimmed ranges (other than the last added one) are not equal', () => {
- selection.addRange( range1 );
- selection.addRange( range3 );
+ selection.setTo( [ range1, range3 ] );
- const otherSelection = new Selection();
- otherSelection.addRange( range2 );
- otherSelection.addRange( range3 );
+ const otherSelection = new Selection( [ range2, range3 ] );
expect( selection.isSimilar( otherSelection ) ).to.be.false;
} );
it( 'should return false if directions are not equal', () => {
- selection.addRange( range1 );
+ selection.setTo( range1 );
- const otherSelection = new Selection();
- otherSelection.addRange( range1, true );
+ const otherSelection = new Selection( [ range1 ], true );
expect( selection.isSimilar( otherSelection ) ).to.be.false;
} );
@@ -586,12 +521,9 @@ describe( 'Selection', () => {
const rangeA2 = Range.createFromParentsAndOffsets( p2, 0, p2, 1 );
const rangeB2 = Range.createFromParentsAndOffsets( span2, 0, span2, 1 );
- selection.addRange( rangeA1 );
- selection.addRange( rangeA2 );
+ selection.setTo( [ rangeA1, rangeA2 ] );
- const otherSelection = new Selection();
- otherSelection.addRange( rangeB2 );
- otherSelection.addRange( rangeB1 );
+ const otherSelection = new Selection( [ rangeB2, rangeB1 ] );
expect( selection.isSimilar( otherSelection ) ).to.be.true;
expect( otherSelection.isSimilar( selection ) ).to.be.true;
@@ -601,37 +533,36 @@ describe( 'Selection', () => {
} );
} );
- describe( 'removeAllRanges()', () => {
+ describe( 'setTo - removeAllRanges', () => {
it( 'should remove all ranges and fire change event', done => {
- selection.addRange( range1 );
- selection.addRange( range2 );
+ selection.setTo( [ range1, range2 ] );
selection.once( 'change', () => {
expect( selection.rangeCount ).to.equal( 0 );
done();
} );
- selection.removeAllRanges();
+ selection.setTo( null );
} );
it( 'should do nothing when no ranges are present', () => {
const fireSpy = sinon.spy( selection, 'fire' );
- selection.removeAllRanges();
+ selection.setTo( null );
fireSpy.restore();
expect( fireSpy.notCalled ).to.be.true;
} );
} );
- describe( 'setRanges()', () => {
+ describe( '_setRanges()', () => {
it( 'should throw an error when range is invalid', () => {
expect( () => {
- selection.setRanges( [ { invalid: 'range' } ] );
+ selection._setRanges( [ { invalid: 'range' } ] );
} ).to.throw( CKEditorError, 'view-selection-invalid-range: Invalid Range.' );
} );
it( 'should add ranges and fire change event', done => {
- selection.addRange( range1 );
+ selection.setTo( range1 );
selection.once( 'change', () => {
expect( selection.rangeCount ).to.equal( 2 );
@@ -642,17 +573,24 @@ describe( 'Selection', () => {
done();
} );
- selection.setRanges( [ range2, range3 ] );
+ selection._setRanges( [ range2, range3 ] );
+ } );
+
+ it( 'should throw when range is intersecting with already added range', () => {
+ const text = el.getChild( 0 );
+ const range2 = Range.createFromParentsAndOffsets( text, 7, text, 15 );
+
+ expect( () => {
+ selection._setRanges( [ range1, range2 ] );
+ } ).to.throw( CKEditorError, 'view-selection-range-intersects' );
} );
} );
describe( 'setTo()', () => {
it( 'should set selection ranges from the given selection', () => {
- selection.addRange( range1 );
+ selection.setTo( range1 );
- const otherSelection = new Selection();
- otherSelection.addRange( range2 );
- otherSelection.addRange( range3, true );
+ const otherSelection = new Selection( [ range2, range3 ], true );
selection.setTo( otherSelection );
@@ -665,30 +603,30 @@ describe( 'Selection', () => {
expect( selection.anchor.isEqual( range3.end ) ).to.be.true;
} );
- it( 'should set selection on the given Range using setRanges method', () => {
- const spy = sinon.spy( selection, 'setRanges' );
+ it( 'should set selection on the given Range using _setRanges method', () => {
+ const spy = sinon.spy( selection, '_setRanges' );
selection.setTo( range1 );
expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1 ] );
expect( selection.isBackward ).to.be.false;
- expect( selection.setRanges.calledOnce ).to.be.true;
+ expect( selection._setRanges.calledOnce ).to.be.true;
spy.restore();
} );
- it( 'should set selection on the given iterable of Ranges using setRanges method', () => {
- const spy = sinon.spy( selection, 'setRanges' );
+ it( 'should set selection on the given iterable of Ranges using _setRanges method', () => {
+ const spy = sinon.spy( selection, '_setRanges' );
selection.setTo( new Set( [ range1, range2 ] ) );
expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1, range2 ] );
expect( selection.isBackward ).to.be.false;
- expect( selection.setRanges.calledOnce ).to.be.true;
+ expect( selection._setRanges.calledOnce ).to.be.true;
spy.restore();
} );
- it( 'should set collapsed selection on the given Position using setRanges method', () => {
- const spy = sinon.spy( selection, 'setRanges' );
+ it( 'should set collapsed selection on the given Position using _setRanges method', () => {
+ const spy = sinon.spy( selection, '_setRanges' );
selection.setTo( range1.start );
@@ -696,7 +634,7 @@ describe( 'Selection', () => {
expect( Array.from( selection.getRanges() )[ 0 ].start ).to.deep.equal( range1.start );
expect( selection.isBackward ).to.be.false;
expect( selection.isCollapsed ).to.be.true;
- expect( selection.setRanges.calledOnce ).to.be.true;
+ expect( selection._setRanges.calledOnce ).to.be.true;
spy.restore();
} );
@@ -707,8 +645,7 @@ describe( 'Selection', () => {
done();
} );
- const otherSelection = new Selection();
- otherSelection.addRange( range1 );
+ const otherSelection = new Selection( [ range1 ] );
selection.setTo( otherSelection );
} );
@@ -722,50 +659,25 @@ describe( 'Selection', () => {
expect( selection.isFake ).to.be.true;
expect( selection.fakeSelectionLabel ).to.equal( label );
} );
- } );
-
- describe( 'setIn()', () => {
- it( 'should set selection inside an element', () => {
- const element = new Element( 'p', null, [ new Text( 'foo' ), new Text( 'bar' ) ] );
- selection.setIn( element );
-
- const ranges = Array.from( selection.getRanges() );
- expect( ranges.length ).to.equal( 1 );
- expect( ranges[ 0 ].start.parent ).to.equal( element );
- expect( ranges[ 0 ].start.offset ).to.deep.equal( 0 );
- expect( ranges[ 0 ].end.parent ).to.equal( element );
- expect( ranges[ 0 ].end.offset ).to.deep.equal( 2 );
- } );
- } );
-
- describe( 'setOn()', () => {
- it( 'should set selection on an item', () => {
- const textNode1 = new Text( 'foo' );
- const textNode2 = new Text( 'bar' );
- const textNode3 = new Text( 'baz' );
- const element = new Element( 'p', null, [ textNode1, textNode2, textNode3 ] );
-
- selection.setOn( textNode2 );
+ it( 'should throw an error when trying to set to not selectable', () => {
+ const otherSelection = new Selection();
- const ranges = Array.from( selection.getRanges() );
- expect( ranges.length ).to.equal( 1 );
- expect( ranges[ 0 ].start.parent ).to.equal( element );
- expect( ranges[ 0 ].start.offset ).to.deep.equal( 1 );
- expect( ranges[ 0 ].end.parent ).to.equal( element );
- expect( ranges[ 0 ].end.offset ).to.deep.equal( 2 );
+ expect( () => {
+ otherSelection.setTo( {} );
+ } ).to.throw( /view-selection-setTo-not-selectable/ );
} );
} );
- describe( 'setCollapsedAt()', () => {
+ describe( 'setTo - set collapsed at', () => {
beforeEach( () => {
- selection.setRanges( [ range1, range2 ] );
+ selection.setTo( [ range1, range2 ] );
} );
it( 'should collapse selection at position', () => {
const position = new Position( el, 4 );
- selection.setCollapsedAt( position );
+ selection.setTo( position );
const range = selection.getFirstRange();
expect( range.start.parent ).to.equal( el );
@@ -777,14 +689,14 @@ describe( 'Selection', () => {
const foo = new Text( 'foo' );
const p = new Element( 'p', null, foo );
- selection.setCollapsedAt( foo );
+ selection.setTo( foo );
let range = selection.getFirstRange();
expect( range.start.parent ).to.equal( foo );
expect( range.start.offset ).to.equal( 0 );
expect( range.start.isEqual( range.end ) ).to.be.true;
- selection.setCollapsedAt( p, 1 );
+ selection.setTo( p, 1 );
range = selection.getFirstRange();
expect( range.start.parent ).to.equal( p );
@@ -796,21 +708,21 @@ describe( 'Selection', () => {
const foo = new Text( 'foo' );
const p = new Element( 'p', null, foo );
- selection.setCollapsedAt( foo, 'end' );
+ selection.setTo( foo, 'end' );
let range = selection.getFirstRange();
expect( range.start.parent ).to.equal( foo );
expect( range.start.offset ).to.equal( 3 );
expect( range.start.isEqual( range.end ) ).to.be.true;
- selection.setCollapsedAt( foo, 'before' );
+ selection.setTo( foo, 'before' );
range = selection.getFirstRange();
expect( range.start.parent ).to.equal( p );
expect( range.start.offset ).to.equal( 0 );
expect( range.start.isEqual( range.end ) ).to.be.true;
- selection.setCollapsedAt( foo, 'after' );
+ selection.setTo( foo, 'after' );
range = selection.getFirstRange();
expect( range.start.parent ).to.equal( p );
@@ -819,9 +731,9 @@ describe( 'Selection', () => {
} );
} );
- describe( 'collapseToStart()', () => {
+ describe( 'setTo - collapse at start', () => {
it( 'should collapse to start position and fire change event', done => {
- selection.setRanges( [ range1, range2, range3 ] );
+ selection.setTo( [ range1, range2, range3 ] );
selection.once( 'change', () => {
expect( selection.rangeCount ).to.equal( 1 );
expect( selection.isCollapsed ).to.be.true;
@@ -829,22 +741,22 @@ describe( 'Selection', () => {
done();
} );
- selection.collapseToStart();
+ selection.setTo( selection.getFirstPosition() );
} );
it( 'should do nothing if no ranges present', () => {
const fireSpy = sinon.spy( selection, 'fire' );
- selection.collapseToStart();
+ selection.setTo( selection.getFirstPosition() );
fireSpy.restore();
expect( fireSpy.notCalled ).to.be.true;
} );
} );
- describe( 'collapseToEnd()', () => {
+ describe( 'setTo - collapse to end', () => {
it( 'should collapse to end position and fire change event', done => {
- selection.setRanges( [ range1, range2, range3 ] );
+ selection.setTo( [ range1, range2, range3 ] );
selection.once( 'change', () => {
expect( selection.rangeCount ).to.equal( 1 );
expect( selection.isCollapsed ).to.be.true;
@@ -852,13 +764,13 @@ describe( 'Selection', () => {
done();
} );
- selection.collapseToEnd();
+ selection.setTo( selection.getLastPosition() );
} );
it( 'should do nothing if no ranges present', () => {
const fireSpy = sinon.spy( selection, 'fire' );
- selection.collapseToEnd();
+ selection.setTo( selection.getLastPosition() );
fireSpy.restore();
expect( fireSpy.notCalled ).to.be.true;
@@ -871,7 +783,7 @@ describe( 'Selection', () => {
} );
it( 'should return null if selection is placed in container that is not EditableElement', () => {
- selection.addRange( range1 );
+ selection.setTo( range1 );
expect( selection.editableElement ).to.be.null;
} );
@@ -883,7 +795,7 @@ describe( 'Selection', () => {
const element = new Element( 'p' );
root.appendChildren( element );
- selection.addRange( Range.createFromParentsAndOffsets( element, 0, element, 0 ) );
+ selection.setTo( Range.createFromParentsAndOffsets( element, 0, element, 0 ) );
expect( selection.editableElement ).to.equal( root );
@@ -893,7 +805,7 @@ describe( 'Selection', () => {
describe( 'createFromSelection', () => {
it( 'should return a Selection instance with same ranges and direction as given selection', () => {
- selection.setRanges( [ range1, range2 ], true );
+ selection.setTo( [ range1, range2 ], true );
const snapshot = Selection.createFromSelection( selection );