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">obar' ); 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">obar' ); 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">xy' ); - 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' @@ -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' @@ -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' @@ -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[]' @@ -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[]' @@ -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' @@ -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' @@ -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' @@ -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]' @@ -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]' @@ -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">[]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 bold="true">ob]ar' ); - 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, '[

fooxxxbar

]' ); 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, '[

fooxxxbar

]' ); 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, '

[foobar]x[barfoo]

' ); 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, '

[foobar]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 );