diff --git a/src/conversion/downcast-selection-converters.js b/src/conversion/downcast-selection-converters.js index af62a517f..2168c5cf7 100644 --- a/src/conversion/downcast-selection-converters.js +++ b/src/conversion/downcast-selection-converters.js @@ -5,7 +5,7 @@ /** * Contains {@link module:engine/model/selection~Selection model selection} to - * {@link module:engine/view/selection~Selection view selection} converters for + * {@link module:engine/view/documentselection~DocumentSelection view selection} converters for * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}. * * @module engine/conversion/downcast-selection-converters @@ -13,8 +13,8 @@ /** * Function factory, creates a converter that converts non-collapsed {@link module:engine/model/selection~Selection model selection} to - * {@link module:engine/view/selection~Selection view selection}. The converter consumes appropriate value from `consumable` object - * and maps model positions from selection to view positions. + * {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate + * value from `consumable` object and maps model positions from selection to view positions. * * modelDispatcher.on( 'selection', convertRangeSelection() ); * @@ -45,9 +45,9 @@ export function convertRangeSelection() { /** * Function factory, creates a converter that converts collapsed {@link module:engine/model/selection~Selection model selection} to - * {@link module:engine/view/selection~Selection view selection}. The converter consumes appropriate value from `consumable` object, - * maps model selection position to view position and breaks {@link module:engine/view/attributeelement~AttributeElement attribute elements} - * at the selection position. + * {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate + * value from `consumable` object, maps model selection position to view position and breaks + * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position. * * modelDispatcher.on( 'selection', convertCollapsedSelection() ); * diff --git a/src/conversion/upcast-selection-converters.js b/src/conversion/upcast-selection-converters.js index 3660e6fb5..5ff080497 100644 --- a/src/conversion/upcast-selection-converters.js +++ b/src/conversion/upcast-selection-converters.js @@ -4,7 +4,7 @@ */ /** - * Contains {@link module:engine/view/selection~Selection view selection} + * Contains {@link module:engine/view/documentselection~DocumentSelection view selection} * to {@link module:engine/model/selection~Selection model selection} conversion helpers. * * @module engine/conversion/upcast-selection-converters @@ -13,8 +13,8 @@ import ModelSelection from '../model/selection'; /** - * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection view selection} taken - * from the {@link module:engine/view/document~Document#event:selectionChange} event + * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection + * view selection} taken from the {@link module:engine/view/document~Document#event:selectionChange} event * and sets in on the {@link module:engine/model/document~Document#selection model}. * * **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model diff --git a/src/dev-utils/view.js b/src/dev-utils/view.js index 8b6077fab..6d50cfe7e 100644 --- a/src/dev-utils/view.js +++ b/src/dev-utils/view.js @@ -15,7 +15,7 @@ import View from '../view/view'; import ViewDocumentFragment from '../view/documentfragment'; import XmlDataProcessor from '../dataprocessor/xmldataprocessor'; import ViewElement from '../view/element'; -import Selection from '../view/selection'; +import DocumentSelection from '../view/documentselection'; import Range from '../view/range'; import Position from '../view/position'; import AttributeElement from '../view/attributeelement'; @@ -125,8 +125,8 @@ setData._parse = parse; * * stringify( fragment ); // '

foobar' * - * Additionally, a {@link module:engine/view/selection~Selection selection} instance can be provided. Ranges from the selection - * will then be included in output data. + * Additionally, a {@link module:engine/view/documentselection~DocumentSelection selection} instance can be provided. + * Ranges from the selection will then be included in output data. * If a range position is placed inside the element node, it will be represented with `[` and `]`: * * const text = new Text( 'foobar' ); @@ -163,9 +163,9 @@ setData._parse = parse; * stringify( text, selection ); // '{f}oo{ba}r' * * A {@link module:engine/view/range~Range range} or {@link module:engine/view/position~Position position} instance can be provided - * instead of the {@link module:engine/view/selection~Selection selection} instance. If a range instance is provided, it will be - * converted to a selection containing this range. If a position instance is provided, it will be converted to a selection - * containing one range collapsed at this position. + * instead of the {@link module:engine/view/documentselection~DocumentSelection selection} instance. If a range instance + * is provided, it will be converted to a selection containing this range. If a position instance is provided, it will + * be converted to a selection containing one range collapsed at this position. * * const text = new Text( 'foobar' ); * const range = Range.createFromParentsAndOffsets( text, 0, text, 1 ); @@ -206,7 +206,7 @@ setData._parse = parse; * * @param {module:engine/view/text~Text|module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} * node The node to stringify. - * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|module:engine/view/range~Range} + * @param {module:engine/view/documentselection~DocumentSelection|module:engine/view/position~Position|module:engine/view/range~Range} * [selectionOrPositionOrRange = null ] * A selection instance whose ranges will be included in the returned string data. If a range instance is provided, it will be * converted to a selection containing this range. If a position instance is provided, it will be converted to a selection @@ -231,7 +231,7 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {} selectionOrPositionOrRange instanceof Position || selectionOrPositionOrRange instanceof Range ) { - selection = new Selection( selectionOrPositionOrRange ); + selection = new DocumentSelection( selectionOrPositionOrRange ); } else { selection = selectionOrPositionOrRange; } @@ -257,7 +257,7 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {} * parse( 'foobar' ); // Returns a document fragment with two child elements. * * The method can parse multiple {@link module:engine/view/range~Range ranges} provided in string data and return a - * {@link module:engine/view/selection~Selection selection} instance containing these ranges. Ranges placed inside + * {@link module:engine/view/documentselection~DocumentSelection selection} instance containing these ranges. Ranges placed inside * {@link module:engine/view/text~Text text} nodes should be marked using `{` and `}` brackets: * * const { text, selection } = parse( 'f{ooba}r' ); @@ -278,8 +278,9 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {} * In the example above, the first range (`{fo}`) will be added to the selection as the second one, the second range (`{ar}`) will be * added as the third and the third range (`{ba}`) will be added as the first one. * - * If the selection's last range should be added as a backward one (so the {@link module:engine/view/selection~Selection#anchor selection - * anchor} is represented by the `end` position and {@link module:engine/view/selection~Selection#focus selection focus} is + * If the selection's last range should be added as a backward one + * (so the {@link module:engine/view/documentselection~DocumentSelection#anchor selection anchor} is represented + * by the `end` position and {@link module:engine/view/documentselection~DocumentSelection#focus selection focus} is * represented by the `start` position), use the `lastRangeBackward` flag: * * const { root, selection } = parse( `{foo}bar{baz}`, { lastRangeBackward: true } ); @@ -298,11 +299,11 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {} * @param {String} data An HTML-like string to be parsed. * @param {Object} options * @param {Array.} [options.order] An array with the order of parsed ranges added to the returned - * {@link module:engine/view/selection~Selection Selection} instance. Each element should represent the desired position of each range in - * the selection instance. For example: `[2, 3, 1]` means that the first range will be placed as the second, the second as the third and - * the third as the first. + * {@link module:engine/view/documentselection~DocumentSelection Selection} instance. Each element should represent the + * desired position of each range in the selection instance. For example: `[2, 3, 1]` means that the first range will be + * placed as the second, the second as the third and the third as the first. * @param {Boolean} [options.lastRangeBackward=false] If set to `true`, the last range will be added as backward to the returned - * {@link module:engine/view/selection~Selection selection} instance. + * {@link module:engine/view/documentselection~DocumentSelection selection} instance. * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} * [options.rootElement=null] The default root to use when parsing elements. * When set to `null`, the root element will be created automatically. If set to @@ -351,7 +352,7 @@ export function parse( data, options = {} ) { // When ranges are present - return object containing view, and selection. if ( ranges.length ) { - const selection = new Selection( ranges, { backward: !!options.lastRangeBackward } ); + const selection = new DocumentSelection( ranges, { backward: !!options.lastRangeBackward } ); return { view, @@ -594,7 +595,8 @@ class ViewStringify { * Creates a view stringify instance. * * @param root - * @param {module:engine/view/selection~Selection} selection A selection whose ranges should also be converted to a string. + * @param {module:engine/view/documentselection~DocumentSelection} selection A selection whose ranges + * should also be converted to a string. * @param {Object} options An options object. * @param {Boolean} [options.showType=false] When set to `true`, the type of elements will be printed (`` * instead of `

`, `` instead of `` and `` instead of ``). diff --git a/src/view/document.js b/src/view/document.js index 2b7b49af7..370d5551a 100644 --- a/src/view/document.js +++ b/src/view/document.js @@ -7,14 +7,14 @@ * @module engine/view/document */ -import Selection from './selection'; +import DocumentSelection from './documentselection'; import Collection from '@ckeditor/ckeditor5-utils/src/collection'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; /** * Document class creates an abstract layer over the content editable area, contains a tree of view elements and - * {@link module:engine/view/selection~Selection view selection} associated with this document. + * {@link module:engine/view/documentselection~DocumentSelection view selection} associated with this document. * * @mixes module:utils/observablemixin~ObservableMixin */ @@ -27,9 +27,9 @@ export default class Document { * Selection done on this document. * * @readonly - * @member {module:engine/view/selection~Selection} module:engine/view/document~Document#selection + * @member {module:engine/view/documentselection~DocumentSelection} module:engine/view/document~Document#selection */ - this.selection = new Selection(); + this.selection = new DocumentSelection(); /** * Roots of the view tree. Collection of the {module:engine/view/element~Element view elements}. diff --git a/src/view/documentselection.js b/src/view/documentselection.js new file mode 100644 index 000000000..9774cb977 --- /dev/null +++ b/src/view/documentselection.js @@ -0,0 +1,374 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module engine/view/documentselection + */ + +import Selection from './selection'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; +import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin'; + +/** + * Class representing document selection in tree view. It's instance is stored at + * {@link module:engine/view/document~Document#selection}. It is similar to {@link module:engine/view/selection~Selection} but + * it has read-only API and can be modified only by writer obtained from {@link module:engine/view/view~View#change} method. + * + * Selection can consist of {@link module:engine/view/range~Range ranges}. + * Selection's ranges can be obtained via {@link module:engine/view/documentselection~DocumentSelection#getRanges getRanges}, + * {@link module:engine/view/documentselection~DocumentSelection#getFirstRange getFirstRange} + * and {@link module:engine/view/documentselection~DocumentSelection#getLastRange getLastRange} + * methods, which return copies of ranges stored inside selection. Modifications made on these copies will not change + * selection's state. Similar situation occurs when getting {@link module:engine/view/documentselection~DocumentSelection#anchor anchor}, + * {@link module:engine/view/documentselection~DocumentSelection#focus focus}, + * {@link module:engine/view/documentselection~DocumentSelection#getFirstPosition first} and + * {@link module:engine/view/documentselection~DocumentSelection#getLastPosition last} positions - all will return + * copies of requested positions. + */ +export default class DocumentSelection { + /** + * Creates new DocumentSelection instance. + * + * // Creates empty selection without ranges. + * const selection = new DocumentSelection(); + * + * // Creates selection at the given range. + * const range = new Range( start, end ); + * const selection = new DocumentSelection( range ); + * + * // Creates selection at the given ranges + * const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ]; + * const selection = new DocumentSelection( ranges ); + * + * // Creates selection from the other selection. + * const otherSelection = new Selection(); + * const selection = new DocumentSelection( otherSelection ); + * + * // Creates selection at the given position. + * const position = new Position( root, path ); + * const selection = new DocumentSelection( position ); + * + * // Creates collapsed selection at the position of given item and offset. + * const paragraph = writer.createContainerElement( 'paragraph' ); + * const selection = new DocumentSelection( paragraph, offset ); + * + * // Creates a range inside an {@link module:engine/view/element~Element element} which starts before the + * // first child of that element and ends after the last child of that element. + * const selection = new DocumentSelection( paragraph, 'in' ); + * + * // Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends + * // just after the item. + * const selection = new DocumentSelection( paragraph, 'on' ); + * + * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument. + * + * // Creates backward selection. + * const selection = new DocumentSelection( range, { backward: true } ); + * + * Fake selection does not render as browser native selection over selected elements and is hidden to the user. + * This way, no native selection UI artifacts are displayed to the user and selection over elements can be + * represented in other way, for example by applying proper CSS class. + * + * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM + * (and be properly handled by screen readers). + * + * // Creates fake selection with label. + * const selection = new DocumentSelection( range, { fake: true, label: 'foo' } ); + * + * @param {module:engine/view/selection~Selection|module:engine/view/position~Position| + * Iterable.|module:engine/view/range~Range| + * module:engine/view/item~Item|null} [selectable=null] + * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`. + * @param {Object} [options] + * @param {Boolean} [options.backward] Sets this selection instance to be backward. + * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`. + * @param {String} [options.label] Label for the fake selection. + */ + constructor( selectable = null, placeOrOffset, options ) { + /** + * Selection is used internally (`DocumentSelection` is a proxy to that selection). + * + * @private + * @member {module:engine/view/selection~Selection} + */ + this._selection = new Selection(); + + // Delegate change event to be fired on DocumentSelection instance. + this._selection.delegate( 'change' ).to( this ); + + // Set selection data. + this._selection.setTo( selectable, placeOrOffset, options ); + } + + /** + * Returns true if selection instance is marked as `fake`. + * + * @see #_setTo + * @returns {Boolean} + */ + get isFake() { + return this._selection.isFake; + } + + /** + * Returns fake selection label. + * + * @see #_setTo + * @returns {String} + */ + get fakeSelectionLabel() { + return this._selection.fakeSelectionLabel; + } + + /** + * Selection anchor. Anchor may be described as a position where the selection starts. Together with + * {@link #focus focus} they define the direction of selection, which is important + * when expanding/shrinking selection. Anchor is always the start or end of the most recent added range. + * It may be a bit unintuitive when there are multiple ranges in selection. + * + * @see #focus + * @type {module:engine/view/position~Position} + */ + get anchor() { + return this._selection.anchor; + } + + /** + * Selection focus. Focus is a position where the selection ends. + * + * @see #anchor + * @type {module:engine/view/position~Position} + */ + get focus() { + return this._selection.focus; + } + + /** + * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is + * collapsed. + * + * @type {Boolean} + */ + get isCollapsed() { + return this._selection.isCollapsed; + } + + /** + * Returns number of ranges in selection. + * + * @type {Number} + */ + get rangeCount() { + return this._selection.rangeCount; + } + + /** + * Specifies whether the {@link #focus} precedes {@link #anchor}. + * + * @type {Boolean} + */ + get isBackward() { + return this._selection.isBackward; + } + + /** + * {@link module:engine/view/editableelement~EditableElement EditableElement} instance that contains this selection, or `null` + * if the selection is not inside an editable element. + * + * @type {module:engine/view/editableelement~EditableElement|null} + */ + get editableElement() { + return this._selection.editableElement; + } + + /** + * Used for the compatibility with the {@link module:engine/view/selection~Selection#isEqual} method. + * + * @protected + */ + get _ranges() { + return this._selection._ranges; + } + + /** + * Returns an iterable that contains copies of all ranges added to the selection. + * + * @returns {Iterable.} + */ + * getRanges() { + yield* this._selection.getRanges(); + } + + /** + * Returns copy of the first range in the selection. First range is the one which + * {@link module:engine/view/range~Range#start start} position {@link module:engine/view/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 no ranges are added to selection. + * + * @returns {module:engine/view/range~Range|null} + */ + getFirstRange() { + return this._selection.getFirstRange(); + } + + /** + * Returns copy of the last range in the selection. Last range is the one which {@link module:engine/view/range~Range#end end} + * position {@link module:engine/view/position~Position#isAfter is after} end position of all other ranges (not to confuse + * with the last range added to the selection). Returns `null` if no ranges are added to selection. + * + * @returns {module:engine/view/range~Range|null} + */ + getLastRange() { + return this._selection.getLastRange(); + } + + /** + * Returns copy of the first position in the selection. First position is the position that + * {@link module:engine/view/position~Position#isBefore is before} any other position in the selection ranges. + * Returns `null` if no ranges are added to selection. + * + * @returns {module:engine/view/position~Position|null} + */ + getFirstPosition() { + return this._selection.getFirstPosition(); + } + + /** + * Returns copy of the last position in the selection. Last position is the position that + * {@link module:engine/view/position~Position#isAfter is after} any other position in the selection ranges. + * Returns `null` if no ranges are added to selection. + * + * @returns {module:engine/view/position~Position|null} + */ + getLastPosition() { + return this._selection.getLastPosition(); + } + + /** + * Returns the selected element. {@link module:engine/view/element~Element Element} is considered as selected if there is only + * one range in the selection, and that range contains exactly one element. + * Returns `null` if there is no selected element. + * + * @returns {module:engine/view/element~Element|null} + */ + getSelectedElement() { + return this._selection.getSelectedElement(); + } + + /** + * Checks whether, this selection is equal to given selection. Selections are equal if they have same directions, + * same number of ranges and all ranges from one selection equal to a range from other selection. + * + * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection + * Selection to compare with. + * @returns {Boolean} `true` if selections are equal, `false` otherwise. + */ + isEqual( otherSelection ) { + return this._selection.isEqual( otherSelection ); + } + + /** + * Checks whether this selection is similar to given selection. Selections are similar if they have same directions, same + * number of ranges, and all {@link module:engine/view/range~Range#getTrimmed trimmed} ranges from one selection are + * equal to any trimmed range from other selection. + * + * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection + * Selection to compare with. + * @returns {Boolean} `true` if selections are similar, `false` otherwise. + */ + isSimilar( otherSelection ) { + return this._selection.isSimilar( otherSelection ); + } + + /** + * Sets this selection's ranges and direction to the specified location based on the given + * {@link module:engine/view/documentselection~DocumentSelection document selection}, + * {@link module:engine/view/selection~Selection selection}, {@link module:engine/view/position~Position position}, + * {@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 selection to the given range. + * const range = new Range( start, end ); + * documentSelection._setTo( range ); + * + * // Sets selection to given ranges. + * const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ]; + * documentSelection._setTo( range ); + * + * // Sets selection to the other selection. + * const otherSelection = new Selection(); + * documentSelection._setTo( otherSelection ); + * + * // Sets collapsed selection at the given position. + * const position = new Position( root, path ); + * documentSelection._setTo( position ); + * + * // Sets collapsed selection at the position of given item and offset. + * documentSelection._setTo( paragraph, offset ); + * + * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of + * that element and ends after the last child of that element. + * + * documentSelection._setTo( paragraph, 'in' ); + * + * Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends just after the item. + * + * documentSelection._setTo( paragraph, 'on' ); + * + * // Clears selection. Removes all ranges. + * documentSelection._setTo( null ); + * + * `Selection#_setTo()` method allow passing additional options (`backward`, `fake` and `label`) as the last argument. + * + * // Sets selection as backward. + * documentSelection._setTo( range, { backward: true } ); + * + * Fake selection does not render as browser native selection over selected elements and is hidden to the user. + * This way, no native selection UI artifacts are displayed to the user and selection over elements can be + * represented in other way, for example by applying proper CSS class. + * + * Additionally fake's selection label can be provided. It will be used to des cribe fake selection in DOM + * (and be properly handled by screen readers). + * + * // Creates fake selection with label. + * documentSelection._setTo( range, { fake: true, label: 'foo' } ); + * + * @protected + * @fires change + * @param {module:engine/view/selection~Selection|module:engine/view/position~Position| + * Iterable.|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable + * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection. + * @param {Object} [options] + * @param {Boolean} [options.backward] Sets this selection instance to be backward. + * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`. + * @param {String} [options.label] Label for the fake selection. + */ + _setTo( selectable, placeOrOffset, options ) { + this._selection.setTo( selectable, placeOrOffset, options ); + } + + /** + * Moves {@link #focus} to the specified location. + * + * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters. + * + * @protected + * @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}. + */ + _setFocus( itemOrPosition, offset ) { + this._selection.setFocus( itemOrPosition, offset ); + } + + /** + * Fired whenever selection ranges are changed through {@link ~DocumentSelection Selection API}. + * + * @event change + */ +} + +mix( DocumentSelection, EmitterMixin ); diff --git a/src/view/domconverter.js b/src/view/domconverter.js index 559f87cc2..3a58ebee2 100644 --- a/src/view/domconverter.js +++ b/src/view/domconverter.js @@ -103,20 +103,20 @@ export default class DomConverter { } /** - * Binds given DOM element that represents fake selection to {@link module:engine/view/selection~Selection view selection}. - * View selection copy is stored and can be retrieved by {@link module:engine/view/domconverter~DomConverter#fakeSelectionToView} - * method. + * Binds given DOM element that represents fake selection to {@link module:engine/view/documentselection~DocumentSelection + * document selection}. Document selection copy is stored and can be retrieved by + * {@link module:engine/view/domconverter~DomConverter#fakeSelectionToView} method. * * @param {HTMLElement} domElement - * @param {module:engine/view/selection~Selection} viewSelection + * @param {module:engine/view/documentselection~DocumentSelection} viewDocumentSelection */ - bindFakeSelection( domElement, viewSelection ) { - this._fakeSelectionMapping.set( domElement, new ViewSelection( viewSelection ) ); + bindFakeSelection( domElement, viewDocumentSelection ) { + this._fakeSelectionMapping.set( domElement, new ViewSelection( viewDocumentSelection ) ); } /** - * Returns {@link module:engine/view/selection~Selection view selection} instance corresponding to given DOM element that represents - * fake selection. Returns `undefined` if binding to given DOM element does not exists. + * Returns {@link module:engine/view/selection~Selection view selection} instance corresponding to + * given DOM element that represents fake selection. Returns `undefined` if binding to given DOM element does not exists. * * @param {HTMLElement} domElement * @returns {module:engine/view/selection~Selection|undefined} diff --git a/src/view/observer/fakeselectionobserver.js b/src/view/observer/fakeselectionobserver.js index 6af205e8c..00e04db0a 100644 --- a/src/view/observer/fakeselectionobserver.js +++ b/src/view/observer/fakeselectionobserver.js @@ -86,12 +86,12 @@ export default class FakeSelectionObserver extends Observer { // Left or up arrow pressed - move selection to start. if ( keyCode == keyCodes.arrowleft || keyCode == keyCodes.arrowup ) { - newSelection._setTo( newSelection.getFirstPosition() ); + newSelection.setTo( newSelection.getFirstPosition() ); } // Right or down arrow pressed - move selection to end. if ( keyCode == keyCodes.arrowright || keyCode == keyCodes.arrowdown ) { - newSelection._setTo( newSelection.getLastPosition() ); + newSelection.setTo( newSelection.getLastPosition() ); } const data = { diff --git a/src/view/observer/mutationobserver.js b/src/view/observer/mutationobserver.js index 620acc7a0..9e44280ae 100644 --- a/src/view/observer/mutationobserver.js +++ b/src/view/observer/mutationobserver.js @@ -240,7 +240,7 @@ export default class MutationObserver extends Observer { // Anchor and focus has to be properly mapped to view. if ( viewSelectionAnchor && viewSelectionFocus ) { viewSelection = new ViewSelection( viewSelectionAnchor ); - viewSelection._setFocus( viewSelectionFocus ); + viewSelection.setFocus( viewSelectionFocus ); } } diff --git a/src/view/observer/selectionobserver.js b/src/view/observer/selectionobserver.js index 7947508d4..dd4a5c454 100644 --- a/src/view/observer/selectionobserver.js +++ b/src/view/observer/selectionobserver.js @@ -42,10 +42,12 @@ export default class SelectionObserver extends Observer { this.mutationObserver = view.getObserver( MutationObserver ); /** - * Reference to the view {@link module:engine/view/selection~Selection} object used to compare new selection with it. + * Reference to the view {@link module:engine/view/documentselection~DocumentSelection} object used to compare + * new selection with it. * * @readonly - * @member {module:engine/view/selection~Selection} module:engine/view/observer/selectionobserver~SelectionObserver#selection + * @member {module:engine/view/documentselection~DocumentSelection} + * module:engine/view/observer/selectionobserver~SelectionObserver#selection */ this.selection = this.document.selection; @@ -205,7 +207,7 @@ export default class SelectionObserver extends Observer { * @see module:engine/view/observer/selectionobserver~SelectionObserver * @event module:engine/view/document~Document#event:selectionChange * @param {Object} data - * @param {module:engine/view/selection~Selection} data.oldSelection Old View selection which is + * @param {module:engine/view/documentselection~DocumentSelection} data.oldSelection Old View selection which is * {@link module:engine/view/document~Document#selection}. * @param {module:engine/view/selection~Selection} data.newSelection New View selection which is converted DOM selection. * @param {Selection} data.domSelection Native DOM selection. @@ -222,7 +224,7 @@ export default class SelectionObserver extends Observer { * @see module:engine/view/observer/selectionobserver~SelectionObserver * @event module:engine/view/document~Document#event:selectionChangeDone * @param {Object} data - * @param {module:engine/view/selection~Selection} data.oldSelection Old View selection which is + * @param {module:engine/view/documentselection~DocumentSelection} data.oldSelection Old View selection which is * {@link module:engine/view/document~Document#selection}. * @param {module:engine/view/selection~Selection} data.newSelection New View selection which is converted DOM selection. * @param {Selection} data.domSelection Native DOM selection. diff --git a/src/view/renderer.js b/src/view/renderer.js index 247c46ab3..48f637c92 100644 --- a/src/view/renderer.js +++ b/src/view/renderer.js @@ -37,7 +37,7 @@ export default class Renderer { * Creates a renderer instance. * * @param {module:engine/view/domconverter~DomConverter} domConverter Converter instance. - * @param {module:engine/view/selection~Selection} selection View selection. + * @param {module:engine/view/documentselection~DocumentSelection} selection View selection. */ constructor( domConverter, selection ) { /** @@ -84,7 +84,7 @@ export default class Renderer { * View selection. Renderer updates DOM selection based on the view selection. * * @readonly - * @member {module:engine/view/selection~Selection} + * @member {module:engine/view/documentselection~DocumentSelection} */ this.selection = selection; diff --git a/src/view/selection.js b/src/view/selection.js index 87eeb895f..bd132dac8 100644 --- a/src/view/selection.js +++ b/src/view/selection.js @@ -16,21 +16,23 @@ import Node from './node'; import Element from './element'; import count from '@ckeditor/ckeditor5-utils/src/count'; import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable'; +import DocumentSelection from './documentselection'; /** * Class representing selection in tree view. * * Selection can consist of {@link module:engine/view/range~Range ranges} that can be set using - * {@link module:engine/view/selection~Selection#_setTo} method. + * {@link module:engine/view/selection~Selection#setTo 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} - * and {@link module:engine/view/selection~Selection#getLastRange getLastRange} - * methods, which return copies of ranges stored inside selection. Modifications made on these copies will not change - * selection's state. Similar situation occurs when getting {@link module:engine/view/selection~Selection#anchor anchor}, - * {@link module:engine/view/selection~Selection#focus focus}, {@link module:engine/view/selection~Selection#getFirstPosition first} and - * {@link module:engine/view/selection~Selection#getLastPosition last} positions - all will return copies of requested positions. + * {@link module:engine/view/selection~Selection#getFirstRange getFirstRange} and + * {@link module:engine/view/selection~Selection#getLastRange getLastRange} methods, which return copies of ranges + * stored inside selection. Modifications made on these copies will not change selection's state. Similar situation + * occurs when getting {@link module:engine/view/selection~Selection#anchor anchor}, + * {@link module:engine/view/selection~Selection#focus focus}, {@link module:engine/view/selection~Selection#getFirstPosition first} + * and {@link module:engine/view/selection~Selection#getLastPosition last} positions - all will return + * copies of requested positions. */ export default class Selection { /** @@ -51,12 +53,15 @@ export default class Selection { * const otherSelection = new Selection(); * const selection = new Selection( otherSelection ); * + * // Creates selection from the document selection. + * const selection = new Selection( editor.editing.view.document.selection ); + * * // Creates selection at the given position. * const position = new Position( root, path ); * const selection = new Selection( position ); * * // Creates collapsed selection at the position of given item and offset. - * const paragraph = writer.createElement( 'paragraph' ); + * const paragraph = writer.createContainerElement( 'paragraph' ); * const selection = new Selection( paragraph, offset ); * * // Creates a range inside an {@link module:engine/view/element~Element element} which starts before the @@ -82,8 +87,9 @@ export default class Selection { * // Creates fake selection with label. * const selection = new Selection( range, { fake: true, label: 'foo' } ); * - * @param {module:engine/view/selection~Selection|module:engine/view/position~Position| - * Iterable.|module:engine/view/range~Range|module:engine/view/item~Item|null} [selectable=null] + * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection| + * module:engine/view/position~Position|Iterable.|module:engine/view/range~Range| + * module:engine/view/item~Item|null} [selectable=null] * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`. * @param {Object} [options] * @param {Boolean} [options.backward] Sets this selection instance to be backward. @@ -123,13 +129,13 @@ export default class Selection { */ this._fakeSelectionLabel = ''; - this._setTo( selectable, placeOrOffset, options ); + this.setTo( selectable, placeOrOffset, options ); } /** * Returns true if selection instance is marked as `fake`. * - * @see #_setTo + * @see #setTo * @returns {Boolean} */ get isFake() { @@ -139,7 +145,7 @@ export default class Selection { /** * Returns fake selection label. * - * @see #_setTo + * @see #setTo * @returns {String} */ get fakeSelectionLabel() { @@ -303,7 +309,8 @@ export default class Selection { * Checks whether, this selection is equal to given selection. Selections are equal if they have same directions, * same number of ranges and all ranges from one selection equal to a range from other selection. * - * @param {module:engine/view/selection~Selection} otherSelection Selection to compare with. + * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection + * Selection to compare with. * @returns {Boolean} `true` if selections are equal, `false` otherwise. */ isEqual( otherSelection ) { @@ -348,7 +355,8 @@ export default class Selection { * number of ranges, and all {@link module:engine/view/range~Range#getTrimmed trimmed} ranges from one selection are * equal to any trimmed range from other selection. * - * @param {module:engine/view/selection~Selection} otherSelection Selection to compare with. + * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection + * Selection to compare with. * @returns {Boolean} `true` if selections are similar, `false` otherwise. */ isSimilar( otherSelection ) { @@ -415,45 +423,49 @@ export default class Selection { /** * Sets this selection's ranges and direction to the specified location based on the given + * {@link module:engine/view/documentselection~DocumentSelection document selection}, * {@link module:engine/view/selection~Selection selection}, {@link module:engine/view/position~Position position}, * {@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 selection to the given range. * const range = new Range( start, end ); - * selection._setTo( range ); + * selection.setTo( range ); * * // Sets selection to given ranges. * const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ]; - * selection._setTo( range ); + * selection.setTo( range ); * * // Sets selection to the other selection. * const otherSelection = new Selection(); - * selection._setTo( otherSelection ); + * selection.setTo( otherSelection ); + * + * // Sets selection to contents of DocumentSelection. + * selection.setTo( editor.editing.view.document.selection ); * * // Sets collapsed selection at the given position. * const position = new Position( root, path ); - * selection._setTo( position ); + * selection.setTo( position ); * * // Sets collapsed selection at the position of given item and offset. - * selection._setTo( paragraph, offset ); + * selection.setTo( paragraph, offset ); * * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of * that element and ends after the last child of that element. * - * selection._setTo( paragraph, 'in' ); + * selection.setTo( paragraph, 'in' ); * * Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends just after the item. * - * selection._setTo( paragraph, 'on' ); + * selection.setTo( paragraph, 'on' ); * * // Clears selection. Removes all ranges. - * selection._setTo( null ); + * selection.setTo( null ); * - * `Selection#_setTo()` method allow passing additional options (`backward`, `fake` and `label`) as the last argument. + * `Selection#setTo()` method allow passing additional options (`backward`, `fake` and `label`) as the last argument. * * // Sets selection as backward. - * selection._setTo( range, { backward: true } ); + * selection.setTo( range, { backward: true } ); * * Fake selection does not render as browser native selection over selected elements and is hidden to the user. * This way, no native selection UI artifacts are displayed to the user and selection over elements can be @@ -463,23 +475,23 @@ export default class Selection { * (and be properly handled by screen readers). * * // Creates fake selection with label. - * selection._setTo( range, { fake: true, label: 'foo' } ); + * selection.setTo( range, { fake: true, label: 'foo' } ); * - * @protected * @fires change - * @param {module:engine/view/selection~Selection|module:engine/view/position~Position| - * Iterable.|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable + * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection| + * module:engine/view/position~Position|Iterable.|module:engine/view/range~Range| + * module:engine/view/item~Item|null} selectable * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection. * @param {Object} [options] * @param {Boolean} [options.backward] Sets this selection instance to be backward. * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`. * @param {String} [options.label] Label for the fake selection. */ - _setTo( selectable, placeOrOffset, options ) { + setTo( selectable, placeOrOffset, options ) { if ( selectable === null ) { this._setRanges( [] ); this._setFakeOptions( placeOrOffset ); - } else if ( selectable instanceof Selection ) { + } else if ( selectable instanceof Selection || selectable instanceof DocumentSelection ) { this._setRanges( selectable.getRanges(), selectable.isBackward ); this._setFakeOptions( { fake: selectable.isFake, label: selectable.fakeSelectionLabel } ); } else if ( selectable instanceof Range ) { @@ -529,42 +541,17 @@ export default class Selection { 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. - * - * @private - * @param {Iterable.} newRanges Iterable object of 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`). Defaults to `false`. - */ - _setRanges( newRanges, isLastBackward = false ) { - // New ranges should be copied to prevent removing them by setting them to `[]` first. - // Only applies to situations when selection is set to the same selection or same selection's ranges. - newRanges = Array.from( newRanges ); - - this._ranges = []; - - for ( const range of newRanges ) { - this._addRange( range ); - } - - this._lastRangeBackward = !!isLastBackward; - } - /** * Moves {@link #focus} to the specified location. * * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters. * - * @protected * @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}. */ - _setFocus( itemOrPosition, offset ) { + setFocus( itemOrPosition, offset ) { if ( this.anchor === null ) { /** * Cannot set selection focus if there are no ranges in selection. @@ -595,6 +582,30 @@ export default class Selection { 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. + * + * @private + * @param {Iterable.} newRanges Iterable object of 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`). Defaults to `false`. + */ + _setRanges( newRanges, isLastBackward = false ) { + // New ranges should be copied to prevent removing them by setting them to `[]` first. + // Only applies to situations when selection is set to the same selection or same selection's ranges. + newRanges = Array.from( newRanges ); + + this._ranges = []; + + for ( const range of newRanges ) { + this._addRange( range ); + } + + this._lastRangeBackward = !!isLastBackward; + } + /** * Sets this selection instance to be marked as `fake`. A fake selection does not render as browser native selection * over selected elements and is hidden to the user. This way, no native selection UI artifacts are displayed to @@ -667,12 +678,12 @@ export default class Selection { this._ranges.push( Range.createFromRange( range ) ); } + + /** + * Fired whenever selection ranges are changed through {@link ~Selection Selection API}. + * + * @event change + */ } mix( Selection, EmitterMixin ); - -/** - * Fired whenever selection ranges are changed through {@link ~Selection Selection API}. - * - * @event change - */ diff --git a/src/view/writer.js b/src/view/writer.js index 7ddd6c775..ce0a9df11 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -44,7 +44,8 @@ export default class Writer { } /** - * Sets {@link module:engine/view/selection~Selection selection's} ranges and direction to the specified location based on the given + * Sets {@link module:engine/view/documentselection~DocumentSelection selection's} ranges and direction to the + * specified location based on the given {@link module:engine/view/documentselection~DocumentSelection document selection}, * {@link module:engine/view/selection~Selection selection}, {@link module:engine/view/position~Position position}, * {@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. @@ -72,6 +73,7 @@ export default class Writer { * writer.setSelection( position ); * * // Sets collapsed selection at the position of given item and offset. + * const paragraph = writer.createContainerElement( 'paragraph' ); * writer.setSelection( paragraph, offset ); * * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of @@ -114,7 +116,7 @@ export default class Writer { } /** - * Moves {@link module:engine/view/selection~Selection#focus selection's focus} to the specified location. + * Moves {@link module:engine/view/documentselection~DocumentSelection#focus selection's focus} to the specified location. * * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters. * diff --git a/tests/conversion/upcast-selection-converters.js b/tests/conversion/upcast-selection-converters.js index 8beb0bf52..c62b1fc3b 100644 --- a/tests/conversion/upcast-selection-converters.js +++ b/tests/conversion/upcast-selection-converters.js @@ -46,7 +46,7 @@ describe( 'convertSelectionChange', () => { it( 'should convert collapsed selection', () => { const viewSelection = new ViewSelection(); - viewSelection._setTo( ViewRange.createFromParentsAndOffsets( + viewSelection.setTo( ViewRange.createFromParentsAndOffsets( viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) ); convertSelection( null, { newSelection: viewSelection } ); diff --git a/tests/dev-utils/view.js b/tests/dev-utils/view.js index 341f32998..ddd5394dd 100644 --- a/tests/dev-utils/view.js +++ b/tests/dev-utils/view.js @@ -14,7 +14,7 @@ import ContainerElement from '../../src/view/containerelement'; import EmptyElement from '../../src/view/emptyelement'; import UIElement from '../../src/view/uielement'; import Text from '../../src/view/text'; -import Selection from '../../src/view/selection'; +import DocumentSelection from '../../src/view/documentselection'; import Range from '../../src/view/range'; import View from '../../src/view/view'; import XmlDataProcessor from '../../src/dataprocessor/xmldataprocessor'; @@ -167,7 +167,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( [ range ] ); + const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection ) ).to.equal( '

foobar[bazqux]

' ); } ); @@ -176,7 +176,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( [ range ] ); + const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection ) ).to.equal( '

[நிலை}க்கு

' ); } ); @@ -185,7 +185,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( [ range ] ); + const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection ) ).to.equal( '

[]foobar

' ); } ); @@ -196,7 +196,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( [ range ] ); + const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection ) ).to.equal( '

f{ooba}rbazqux

' ); } ); @@ -207,7 +207,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( [ range ] ); + const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection, { sameSelectionCharacters: true } ) ) .to.equal( '

f[ooba]rbazqux

' ); } ); @@ -216,7 +216,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( [ range ] ); + const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection ) ).to.equal( '

{}foobar

' ); } ); @@ -227,7 +227,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( [ range ] ); + const selection = new DocumentSelection( [ range ] ); expect( stringify( p, selection ) ).to.equal( '

[foobarbazqu}x

' ); } ); @@ -317,7 +317,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( [ range2, range1 ] ); + const selection = new DocumentSelection( [ range2, range1 ] ); expect( stringify( p, selection ) ).to.equal( '

[foobar][]bazqux

' ); } ); @@ -331,7 +331,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( [ range1, range2, range3, range4 ] ); + const selection = new DocumentSelection( [ range1, range2, range3, range4 ] ); expect( stringify( p, selection ) ).to.equal( '

[foobar][]{baz}{q}ux

' ); } ); diff --git a/tests/view/_utils/createdocumentmock.js b/tests/view/_utils/createdocumentmock.js index 61b6490ba..e545e38b7 100644 --- a/tests/view/_utils/createdocumentmock.js +++ b/tests/view/_utils/createdocumentmock.js @@ -4,7 +4,7 @@ */ import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; -import Selection from '../../../src/view/selection'; +import DocumentSelection from '../../../src/view/documentselection'; /** * Creates {@link module:engine/view/document~Document view Document} mock. @@ -15,7 +15,7 @@ export default function createDocumentMock() { const doc = Object.create( ObservableMixin ); doc.set( 'isFocused', false ); doc.set( 'isReadOnly', false ); - doc.selection = new Selection(); + doc.selection = new DocumentSelection(); return doc; } diff --git a/tests/view/documentselection.js b/tests/view/documentselection.js new file mode 100644 index 000000000..4e93df8e9 --- /dev/null +++ b/tests/view/documentselection.js @@ -0,0 +1,1124 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import DocumentSelection from '../../src/view/documentselection'; +import Selection from '../../src/view/selection'; +import Range from '../../src/view/range'; +import Document from '../../src/view/document'; +import Element from '../../src/view/element'; +import Text from '../../src/view/text'; +import Position from '../../src/view/position'; +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; +import count from '@ckeditor/ckeditor5-utils/src/count'; +import createViewRoot from './_utils/createroot'; +import { parse } from '../../src/dev-utils/view'; + +describe( 'DocumentSelection', () => { + let documentSelection, el, range1, range2, range3; + + beforeEach( () => { + const text = new Text( 'xxxxxxxxxxxxxxxxxxxx' ); + el = new Element( 'p', null, text ); + + documentSelection = new DocumentSelection(); + + range1 = Range.createFromParentsAndOffsets( text, 5, text, 10 ); + range2 = Range.createFromParentsAndOffsets( text, 1, text, 2 ); + range3 = Range.createFromParentsAndOffsets( text, 12, text, 14 ); + } ); + + describe( 'constructor()', () => { + it( 'should be able to create an empty selection', () => { + const selection = new DocumentSelection(); + + expect( Array.from( selection.getRanges() ) ).to.deep.equal( [] ); + } ); + + it( 'should be able to create a selection from the given ranges', () => { + const ranges = [ range1, range2, range3 ]; + const selection = new DocumentSelection( ranges ); + + expect( Array.from( selection.getRanges() ) ).to.deep.equal( ranges ); + } ); + + it( 'should be able to create a selection from the given ranges and isLastBackward flag', () => { + const ranges = [ range1, range2, range3 ]; + const selection = new DocumentSelection( ranges, { backward: true } ); + + expect( selection.isBackward ).to.be.true; + } ); + + it( 'should be able to create a selection from the given range and isLastBackward flag', () => { + const selection = new DocumentSelection( range1, { backward: true } ); + + expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1 ] ); + expect( selection.isBackward ).to.be.true; + } ); + + it( 'should be able to create a selection from the given iterable of ranges and isLastBackward flag', () => { + const ranges = new Set( [ range1, range2, range3 ] ); + const selection = new DocumentSelection( ranges, { backward: false } ); + + expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1, range2, range3 ] ); + expect( selection.isBackward ).to.be.false; + } ); + + it( 'should be able to create a collapsed selection at the given position', () => { + const position = range1.start; + const selection = new DocumentSelection( position ); + + expect( Array.from( selection.getRanges() ).length ).to.equal( 1 ); + expect( selection.getFirstRange().start ).to.deep.equal( position ); + expect( selection.getFirstRange().end ).to.deep.equal( position ); + expect( selection.isBackward ).to.be.false; + } ); + + it( 'should be able to create a collapsed selection at the given position', () => { + const position = range1.start; + const selection = new DocumentSelection( position ); + + expect( Array.from( selection.getRanges() ).length ).to.equal( 1 ); + expect( selection.getFirstRange().start ).to.deep.equal( position ); + expect( selection.getFirstRange().end ).to.deep.equal( position ); + expect( selection.isBackward ).to.be.false; + } ); + + it( 'should be able to create a selection from the other document selection', () => { + const otherSelection = new DocumentSelection( [ range2, range3 ], { backward: true } ); + const selection = new DocumentSelection( otherSelection ); + + expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range2, range3 ] ); + expect( selection.isBackward ).to.be.true; + } ); + + it( 'should be able to create a selection from the other selection', () => { + const otherSelection = new Selection( [ range2, range3 ], { backward: true } ); + const selection = new DocumentSelection( otherSelection ); + + expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range2, range3 ] ); + expect( selection.isBackward ).to.be.true; + } ); + + it( 'should be able to create a fake selection from the other fake selection', () => { + const otherSelection = new DocumentSelection( [ range2, range3 ], { fake: true, label: 'foo bar baz' } ); + const selection = new DocumentSelection( otherSelection ); + + expect( selection.isFake ).to.be.true; + expect( selection.fakeSelectionLabel ).to.equal( 'foo bar baz' ); + } ); + + it( 'should throw an error when range is invalid', () => { + expect( () => { + // eslint-disable-next-line no-new + new DocumentSelection( [ { invalid: 'range' } ] ); + } ).to.throw( CKEditorError, 'view-selection-invalid-range: Invalid Range.' ); + } ); + + it( 'should throw an error when ranges intersects', () => { + const text = el.getChild( 0 ); + const range2 = Range.createFromParentsAndOffsets( text, 7, text, 15 ); + + expect( () => { + // eslint-disable-next-line no-new + new DocumentSelection( [ range1, range2 ] ); + } ).to.throw( CKEditorError, 'view-selection-range-intersects' ); + } ); + + it( 'should throw an error when trying to set to not selectable', () => { + expect( () => { + // eslint-disable-next-line no-new + new DocumentSelection( {} ); + } ).to.throw( /view-selection-setTo-not-selectable/ ); + } ); + } ); + + describe( 'anchor', () => { + it( 'should return null if no ranges in selection', () => { + expect( documentSelection.anchor ).to.be.null; + } ); + + it( 'should return start of single range in selection', () => { + documentSelection._setTo( range1 ); + const anchor = documentSelection.anchor; + + expect( anchor.isEqual( range1.start ) ).to.be.true; + expect( anchor ).to.not.equal( range1.start ); + } ); + + it( 'should return end of single range in selection when added as backward', () => { + documentSelection._setTo( range1, { backward: true } ); + const anchor = documentSelection.anchor; + + expect( anchor.isEqual( range1.end ) ).to.be.true; + expect( anchor ).to.not.equal( range1.end ); + } ); + + it( 'should get anchor from last inserted range', () => { + documentSelection._setTo( [ range1, range2 ] ); + + expect( documentSelection.anchor.isEqual( range2.start ) ).to.be.true; + } ); + } ); + + describe( 'focus', () => { + it( 'should return null if no ranges in selection', () => { + expect( documentSelection.focus ).to.be.null; + } ); + + it( 'should return end of single range in selection', () => { + documentSelection._setTo( range1 ); + const focus = documentSelection.focus; + + expect( focus.isEqual( range1.end ) ).to.be.true; + } ); + + it( 'should return start of single range in selection when added as backward', () => { + documentSelection._setTo( range1, { backward: true } ); + const focus = documentSelection.focus; + + expect( focus.isEqual( range1.start ) ).to.be.true; + expect( focus ).to.not.equal( range1.start ); + } ); + + it( 'should get focus from last inserted range', () => { + documentSelection._setTo( [ range1, range2 ] ); + + expect( documentSelection.focus.isEqual( range2.end ) ).to.be.true; + } ); + } ); + + describe( '_setFocus()', () => { + it( 'keeps all existing ranges when no modifications needed', () => { + documentSelection._setTo( range1 ); + documentSelection._setFocus( documentSelection.focus ); + + expect( count( documentSelection.getRanges() ) ).to.equal( 1 ); + } ); + + it( 'throws if there are no ranges in selection', () => { + const endPos = Position.createAt( el, 'end' ); + + expect( () => { + documentSelection._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 ); + + documentSelection._setTo( startPos ); + + documentSelection._setFocus( endPos ); + + expect( documentSelection.anchor.compareWith( startPos ) ).to.equal( 'same' ); + expect( documentSelection.focus.compareWith( endPos ) ).to.equal( 'same' ); + } ); + + it( 'makes existing collapsed selection a backward selection', () => { + const startPos = Position.createAt( el, 1 ); + const endPos = Position.createAt( el, 0 ); + + documentSelection._setTo( startPos ); + + documentSelection._setFocus( endPos ); + + expect( documentSelection.anchor.compareWith( startPos ) ).to.equal( 'same' ); + expect( documentSelection.focus.compareWith( endPos ) ).to.equal( 'same' ); + expect( documentSelection.isBackward ).to.be.true; + } ); + + it( 'modifies existing non-collapsed selection', () => { + const startPos = Position.createAt( el, 1 ); + const endPos = Position.createAt( el, 2 ); + const newEndPos = Position.createAt( el, 3 ); + + documentSelection._setTo( new Range( startPos, endPos ) ); + + documentSelection._setFocus( newEndPos ); + + expect( documentSelection.anchor.compareWith( startPos ) ).to.equal( 'same' ); + expect( documentSelection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); + } ); + + it( 'makes existing non-collapsed selection a backward selection', () => { + const startPos = Position.createAt( el, 1 ); + const endPos = Position.createAt( el, 2 ); + const newEndPos = Position.createAt( el, 0 ); + + documentSelection._setTo( new Range( startPos, endPos ) ); + + documentSelection._setFocus( newEndPos ); + + expect( documentSelection.anchor.compareWith( startPos ) ).to.equal( 'same' ); + expect( documentSelection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); + expect( documentSelection.isBackward ).to.be.true; + } ); + + it( 'makes existing backward selection a forward selection', () => { + const startPos = Position.createAt( el, 1 ); + const endPos = Position.createAt( el, 2 ); + const newEndPos = Position.createAt( el, 3 ); + + documentSelection._setTo( new Range( startPos, endPos ), { backward: true } ); + + documentSelection._setFocus( newEndPos ); + + expect( documentSelection.anchor.compareWith( endPos ) ).to.equal( 'same' ); + expect( documentSelection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); + expect( documentSelection.isBackward ).to.be.false; + } ); + + it( 'modifies existing backward selection', () => { + const startPos = Position.createAt( el, 1 ); + const endPos = Position.createAt( el, 2 ); + const newEndPos = Position.createAt( el, 0 ); + + documentSelection._setTo( new Range( startPos, endPos ), { backward: true } ); + + documentSelection._setFocus( newEndPos ); + + expect( documentSelection.anchor.compareWith( endPos ) ).to.equal( 'same' ); + expect( documentSelection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); + expect( documentSelection.isBackward ).to.be.true; + } ); + + it( 'modifies only the last range', () => { + // Offsets are chosen in this way that the order of adding ranges must count, not their document order. + const startPos1 = Position.createAt( el, 4 ); + const endPos1 = Position.createAt( el, 5 ); + const startPos2 = Position.createAt( el, 1 ); + const endPos2 = Position.createAt( el, 2 ); + + const newEndPos = Position.createAt( el, 0 ); + + documentSelection._setTo( [ + new Range( startPos1, endPos1 ), + new Range( startPos2, endPos2 ) + ] ); + + documentSelection._setFocus( newEndPos ); + + const ranges = Array.from( documentSelection.getRanges() ); + + expect( ranges ).to.have.lengthOf( 2 ); + expect( ranges[ 0 ].start.compareWith( startPos1 ) ).to.equal( 'same' ); + expect( ranges[ 0 ].end.compareWith( endPos1 ) ).to.equal( 'same' ); + + expect( documentSelection.anchor.compareWith( startPos2 ) ).to.equal( 'same' ); + expect( documentSelection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); + expect( documentSelection.isBackward ).to.be.true; + } ); + + it( 'collapses the selection when extending to the anchor', () => { + const startPos = Position.createAt( el, 1 ); + const endPos = Position.createAt( el, 2 ); + + documentSelection._setTo( new Range( startPos, endPos ) ); + + documentSelection._setFocus( startPos ); + + expect( documentSelection.focus.compareWith( startPos ) ).to.equal( 'same' ); + expect( documentSelection.isCollapsed ).to.be.true; + } ); + + it( 'uses Position.createAt', () => { + const startPos = Position.createAt( el, 1 ); + const endPos = Position.createAt( el, 2 ); + const newEndPos = Position.createAt( el, 4 ); + + const spy = sinon.stub( Position, 'createAt' ).returns( newEndPos ); + + documentSelection._setTo( new Range( startPos, endPos ) ); + documentSelection._setFocus( el, 'end' ); + + expect( spy.calledOnce ).to.be.true; + expect( documentSelection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); + + Position.createAt.restore(); + } ); + } ); + + describe( 'isCollapsed', () => { + it( 'should return true when there is single collapsed range', () => { + const range = Range.createFromParentsAndOffsets( el, 5, el, 5 ); + documentSelection._setTo( range ); + + expect( documentSelection.isCollapsed ).to.be.true; + } ); + + 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 ); + documentSelection._setTo( [ range1, range2 ] ); + + expect( documentSelection.isCollapsed ).to.be.false; + } ); + + it( 'should return false when there is not collapsed range', () => { + const range = Range.createFromParentsAndOffsets( el, 15, el, 16 ); + documentSelection._setTo( range ); + + expect( documentSelection.isCollapsed ).to.be.false; + } ); + } ); + + describe( 'rangeCount', () => { + it( 'should return proper range count', () => { + expect( documentSelection.rangeCount ).to.equal( 0 ); + + documentSelection._setTo( range1 ); + + expect( documentSelection.rangeCount ).to.equal( 1 ); + + documentSelection._setTo( [ range1, range2 ] ); + + expect( documentSelection.rangeCount ).to.equal( 2 ); + } ); + } ); + + describe( 'isBackward', () => { + it( 'is defined by the last added range', () => { + const range1 = Range.createFromParentsAndOffsets( el, 5, el, 10 ); + const range2 = Range.createFromParentsAndOffsets( el, 15, el, 16 ); + + documentSelection._setTo( range1, { backward: true } ); + expect( documentSelection ).to.have.property( 'isBackward', true ); + + documentSelection._setTo( [ range1, range2 ] ); + expect( documentSelection ).to.have.property( 'isBackward', false ); + } ); + + it( 'is false when last range is collapsed', () => { + const range = Range.createFromParentsAndOffsets( el, 5, el, 5 ); + + documentSelection._setTo( range, { backward: true } ); + + expect( documentSelection.isBackward ).to.be.false; + } ); + } ); + + describe( 'getRanges', () => { + it( 'should return iterator with copies of all ranges', () => { + documentSelection._setTo( [ range1, range2 ] ); + + const iterable = documentSelection.getRanges(); + const ranges = Array.from( iterable ); + + expect( ranges.length ).to.equal( 2 ); + expect( ranges[ 0 ].isEqual( range1 ) ).to.be.true; + expect( ranges[ 0 ] ).to.not.equal( range1 ); + expect( ranges[ 1 ].isEqual( range2 ) ).to.be.true; + expect( ranges[ 1 ] ).to.not.equal( range2 ); + } ); + } ); + + describe( 'getFirstRange', () => { + it( 'should return copy of range with first position', () => { + documentSelection._setTo( [ range1, range2, range3 ] ); + + const range = documentSelection.getFirstRange(); + + expect( range.isEqual( range2 ) ).to.be.true; + expect( range ).to.not.equal( range2 ); + } ); + + it( 'should return null if no ranges are present', () => { + expect( documentSelection.getFirstRange() ).to.be.null; + } ); + } ); + + describe( 'getLastRange', () => { + it( 'should return copy of range with last position', () => { + documentSelection._setTo( [ range1, range2, range3 ] ); + + const range = documentSelection.getLastRange(); + + expect( range.isEqual( range3 ) ).to.be.true; + expect( range ).to.not.equal( range3 ); + } ); + + it( 'should return null if no ranges are present', () => { + expect( documentSelection.getLastRange() ).to.be.null; + } ); + } ); + + describe( 'getFirstPosition', () => { + it( 'should return copy of first position', () => { + documentSelection._setTo( [ range1, range2, range3 ] ); + + const position = documentSelection.getFirstPosition(); + + expect( position.isEqual( range2.start ) ).to.be.true; + expect( position ).to.not.equal( range2.start ); + } ); + + it( 'should return null if no ranges are present', () => { + expect( documentSelection.getFirstPosition() ).to.be.null; + } ); + } ); + + describe( 'getLastPosition', () => { + it( 'should return copy of range with last position', () => { + documentSelection._setTo( [ range1, range2, range3 ] ); + + const position = documentSelection.getLastPosition(); + + expect( position.isEqual( range3.end ) ).to.be.true; + expect( position ).to.not.equal( range3.end ); + } ); + + it( 'should return null if no ranges are present', () => { + expect( documentSelection.getLastPosition() ).to.be.null; + } ); + } ); + + describe( 'isEqual', () => { + it( 'should return true if selections equal', () => { + documentSelection._setTo( [ range1, range2 ] ); + + const otherSelection = new DocumentSelection(); + otherSelection._setTo( [ range1, range2 ] ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.true; + } ); + + it( 'should return true if selections equal - DocumentSelection and Selection', () => { + documentSelection._setTo( [ range1, range2 ] ); + + const otherSelection = new Selection(); + otherSelection.setTo( [ range1, range2 ] ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.true; + } ); + + it( 'should return true if backward selections equal', () => { + documentSelection._setTo( range1, { backward: true } ); + + const otherSelection = new DocumentSelection( [ range1 ], { backward: true } ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.true; + } ); + + it( 'should return true if backward selections equal - DocumentSelection and Selection', () => { + documentSelection._setTo( range1, { backward: true } ); + + const otherSelection = new Selection( [ range1 ], { backward: true } ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.true; + } ); + + it( 'should return false if ranges count does not equal', () => { + documentSelection._setTo( [ range1, range2 ] ); + + const otherSelection = new DocumentSelection( [ range1 ] ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if ranges count does not equal - DocumentSelection and Selection', () => { + documentSelection._setTo( [ range1, range2 ] ); + + const otherSelection = new Selection( [ range1 ] ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if ranges (other than the last added one) do not equal', () => { + documentSelection._setTo( [ range1, range3 ] ); + + const otherSelection = new DocumentSelection( [ range2, range3 ] ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if ranges (other than the last added one) do not equal - DocumentSelection and Selection', () => { + documentSelection._setTo( [ range1, range3 ] ); + + const otherSelection = new Selection( [ range2, range3 ] ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if directions do not equal', () => { + documentSelection._setTo( range1 ); + + const otherSelection = new DocumentSelection( [ range1 ], { backward: true } ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if directions do not equal - DocumentSelection and Selection', () => { + documentSelection._setTo( range1 ); + + const otherSelection = new Selection( [ range1 ], { backward: true } ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if one selection is fake', () => { + const otherSelection = new DocumentSelection( null, { fake: true } ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if one selection is fake', () => { + const otherSelection = new DocumentSelection( null, { fake: true } ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.false; + } ); + + it( 'should return true if both selection are fake - DocumentSelection and Selection', () => { + const otherSelection = new Selection( range1, { fake: true } ); + documentSelection._setTo( range1, { fake: true } ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.true; + } ); + + it( 'should return false if both selection are fake but have different label', () => { + const otherSelection = new DocumentSelection( [ range1 ], { fake: true, label: 'foo bar baz' } ); + documentSelection._setTo( range1, { fake: true, label: 'foo' } ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if both selection are fake but have different label - DocumentSelection and Selection', () => { + const otherSelection = new Selection( [ range1 ], { fake: true, label: 'foo bar baz' } ); + documentSelection._setTo( range1, { fake: true, label: 'foo' } ); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.false; + } ); + + it( 'should return true if both selections are empty', () => { + const otherSelection = new DocumentSelection(); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.true; + } ); + + it( 'should return true if both selections are empty - DocumentSelection and Selection', () => { + const otherSelection = new Selection(); + + expect( documentSelection.isEqual( otherSelection ) ).to.be.true; + } ); + } ); + + describe( 'isSimilar', () => { + it( 'should return true if selections equal', () => { + documentSelection._setTo( [ range1, range2 ] ); + + const otherSelection = new DocumentSelection( [ range1, range2 ] ); + + expect( documentSelection.isSimilar( otherSelection ) ).to.be.true; + } ); + + it( 'should return true if selections equal - DocumentSelection and Selection', () => { + documentSelection._setTo( [ range1, range2 ] ); + + const otherSelection = new Selection( [ range1, range2 ] ); + + expect( documentSelection.isSimilar( otherSelection ) ).to.be.true; + } ); + + it( 'should return false if ranges count does not equal', () => { + documentSelection._setTo( [ range1, range2 ] ); + + const otherSelection = new DocumentSelection( [ range1 ] ); + + expect( documentSelection.isSimilar( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if ranges count does not equal - DocumentSelection and Selection', () => { + documentSelection._setTo( [ range1, range2 ] ); + + const otherSelection = new Selection( [ range1 ] ); + + expect( documentSelection.isSimilar( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if trimmed ranges (other than the last added one) are not equal', () => { + documentSelection._setTo( [ range1, range3 ] ); + + const otherSelection = new DocumentSelection( [ range2, range3 ] ); + + expect( documentSelection.isSimilar( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if trimmed ranges (other than the last added one) are not equal - with Selection', () => { + documentSelection._setTo( [ range1, range3 ] ); + + const otherSelection = new Selection( [ range2, range3 ] ); + + expect( documentSelection.isSimilar( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if directions are not equal', () => { + documentSelection._setTo( range1 ); + + const otherSelection = new DocumentSelection( [ range1 ], { backward: true } ); + + expect( documentSelection.isSimilar( otherSelection ) ).to.be.false; + } ); + + it( 'should return false if directions are not equal - DocumentSelection and Selection', () => { + documentSelection._setTo( range1 ); + + const otherSelection = new Selection( [ range1 ], { backward: true } ); + + expect( documentSelection.isSimilar( otherSelection ) ).to.be.false; + } ); + + it( 'should return true if both selections are empty', () => { + const otherSelection = new DocumentSelection(); + + expect( documentSelection.isSimilar( otherSelection ) ).to.be.true; + } ); + + it( 'should return true if both selections are empty - DocumentSelection and Selection', () => { + const otherSelection = new Selection(); + + expect( documentSelection.isSimilar( otherSelection ) ).to.be.true; + } ); + + it( 'should return true if all ranges trimmed from both selections are equal', () => { + const view = parse( + '' + + 'xx' + ); + + const p1 = view.getChild( 0 ); + const p2 = view.getChild( 1 ); + const span1 = p1.getChild( 0 ); + const span2 = p2.getChild( 0 ); + + //

[{]}

[{xx}]

+ const rangeA1 = Range.createFromParentsAndOffsets( p1, 0, span1, 0 ); + const rangeB1 = Range.createFromParentsAndOffsets( span1, 0, p1, 1 ); + const rangeA2 = Range.createFromParentsAndOffsets( p2, 0, p2, 1 ); + const rangeB2 = Range.createFromParentsAndOffsets( span2, 0, span2, 1 ); + + documentSelection._setTo( [ rangeA1, rangeA2 ] ); + + const otherSelection = new DocumentSelection( [ rangeB2, rangeB1 ] ); + + expect( documentSelection.isSimilar( otherSelection ) ).to.be.true; + expect( otherSelection.isSimilar( documentSelection ) ).to.be.true; + + expect( documentSelection.isEqual( otherSelection ) ).to.be.false; + expect( otherSelection.isEqual( documentSelection ) ).to.be.false; + } ); + + it( 'should return true if all ranges trimmed from both selections are equal - DocumentSelection and Selection', () => { + const view = parse( + '' + + 'xx' + ); + + const p1 = view.getChild( 0 ); + const p2 = view.getChild( 1 ); + const span1 = p1.getChild( 0 ); + const span2 = p2.getChild( 0 ); + + //

[{]}

[{xx}]

+ const rangeA1 = Range.createFromParentsAndOffsets( p1, 0, span1, 0 ); + const rangeB1 = Range.createFromParentsAndOffsets( span1, 0, p1, 1 ); + const rangeA2 = Range.createFromParentsAndOffsets( p2, 0, p2, 1 ); + const rangeB2 = Range.createFromParentsAndOffsets( span2, 0, span2, 1 ); + + documentSelection._setTo( [ rangeA1, rangeA2 ] ); + + const otherSelection = new Selection( [ rangeB2, rangeB1 ] ); + + expect( documentSelection.isSimilar( otherSelection ) ).to.be.true; + expect( otherSelection.isSimilar( documentSelection ) ).to.be.true; + + expect( documentSelection.isEqual( otherSelection ) ).to.be.false; + expect( otherSelection.isEqual( documentSelection ) ).to.be.false; + } ); + } ); + + describe( '_setTo()', () => { + describe( 'simple scenarios', () => { + it( 'should set selection ranges from the given selection', () => { + documentSelection._setTo( range1 ); + + const otherSelection = new DocumentSelection( [ range2, range3 ], { backward: true } ); + + documentSelection._setTo( otherSelection ); + + expect( documentSelection.rangeCount ).to.equal( 2 ); + expect( documentSelection._ranges[ 0 ].isEqual( range2 ) ).to.be.true; + expect( documentSelection._ranges[ 0 ] ).is.not.equal( range2 ); + expect( documentSelection._ranges[ 1 ].isEqual( range3 ) ).to.be.true; + expect( documentSelection._ranges[ 1 ] ).is.not.equal( range3 ); + + expect( documentSelection.anchor.isEqual( range3.end ) ).to.be.true; + } ); + + it( 'should set selection on the given Range', () => { + documentSelection._setTo( range1 ); + + expect( Array.from( documentSelection.getRanges() ) ).to.deep.equal( [ range1 ] ); + expect( documentSelection.isBackward ).to.be.false; + } ); + + it( 'should set selection on the given iterable of Ranges', () => { + documentSelection._setTo( new Set( [ range1, range2 ] ) ); + + expect( Array.from( documentSelection.getRanges() ) ).to.deep.equal( [ range1, range2 ] ); + expect( documentSelection.isBackward ).to.be.false; + } ); + + it( 'should set collapsed selection on the given Position', () => { + documentSelection._setTo( range1.start ); + + expect( Array.from( documentSelection.getRanges() ).length ).to.equal( 1 ); + expect( Array.from( documentSelection.getRanges() )[ 0 ].start ).to.deep.equal( range1.start ); + expect( documentSelection.isBackward ).to.be.false; + expect( documentSelection.isCollapsed ).to.be.true; + } ); + + it( 'should fire change event', done => { + documentSelection.on( 'change', () => { + expect( documentSelection.rangeCount ).to.equal( 1 ); + expect( documentSelection.getFirstRange().isEqual( range1 ) ).to.be.true; + done(); + } ); + + const otherSelection = new DocumentSelection( [ range1 ] ); + + documentSelection._setTo( otherSelection ); + } ); + + it( 'should set fake state and label', () => { + const label = 'foo bar baz'; + const otherSelection = new DocumentSelection( null, { fake: true, label } ); + documentSelection._setTo( otherSelection ); + + expect( documentSelection.isFake ).to.be.true; + expect( documentSelection.fakeSelectionLabel ).to.equal( label ); + } ); + + it( 'should throw an error when trying to set to not selectable', () => { + const otherSelection = new DocumentSelection(); + + expect( () => { + otherSelection._setTo( {} ); + } ).to.throw( /view-selection-setTo-not-selectable/ ); + } ); + + it( 'should throw an error when trying to set to not selectable #2', () => { + const otherSelection = new DocumentSelection(); + + expect( () => { + otherSelection._setTo(); + } ).to.throw( /view-selection-setTo-not-selectable/ ); + } ); + } ); + + describe( 'setting collapsed selection', () => { + beforeEach( () => { + documentSelection._setTo( [ range1, range2 ] ); + } ); + + it( 'should collapse selection at position', () => { + const position = new Position( el, 4 ); + + documentSelection._setTo( position ); + const range = documentSelection.getFirstRange(); + + expect( range.start.parent ).to.equal( el ); + expect( range.start.offset ).to.equal( 4 ); + expect( range.start.isEqual( range.end ) ).to.be.true; + } ); + + it( 'should collapse selection at node and offset', () => { + const foo = new Text( 'foo' ); + const p = new Element( 'p', null, foo ); + + documentSelection._setTo( foo, 0 ); + let range = documentSelection.getFirstRange(); + + expect( range.start.parent ).to.equal( foo ); + expect( range.start.offset ).to.equal( 0 ); + expect( range.start.isEqual( range.end ) ).to.be.true; + + documentSelection._setTo( p, 1 ); + range = documentSelection.getFirstRange(); + + expect( range.start.parent ).to.equal( p ); + expect( range.start.offset ).to.equal( 1 ); + expect( range.start.isEqual( range.end ) ).to.be.true; + } ); + + it( 'should throw an error when the second parameter is not passed and first is an item', () => { + const foo = new Text( 'foo' ); + + expect( () => { + documentSelection._setTo( foo ); + } ).to.throw( CKEditorError, /view-selection-setTo-required-second-parameter/ ); + } ); + + it( 'should collapse selection at node and flag', () => { + const foo = new Text( 'foo' ); + const p = new Element( 'p', null, foo ); + + documentSelection._setTo( foo, 'end' ); + let range = documentSelection.getFirstRange(); + + expect( range.start.parent ).to.equal( foo ); + expect( range.start.offset ).to.equal( 3 ); + expect( range.start.isEqual( range.end ) ).to.be.true; + + documentSelection._setTo( foo, 'before' ); + range = documentSelection.getFirstRange(); + + expect( range.start.parent ).to.equal( p ); + expect( range.start.offset ).to.equal( 0 ); + expect( range.start.isEqual( range.end ) ).to.be.true; + + documentSelection._setTo( foo, 'after' ); + range = documentSelection.getFirstRange(); + + expect( range.start.parent ).to.equal( p ); + expect( range.start.offset ).to.equal( 1 ); + expect( range.start.isEqual( range.end ) ).to.be.true; + } ); + } ); + + describe( 'setting collapsed selection at start', () => { + it( 'should collapse to start position and fire change event', done => { + documentSelection._setTo( [ range1, range2, range3 ] ); + documentSelection.once( 'change', () => { + expect( documentSelection.rangeCount ).to.equal( 1 ); + expect( documentSelection.isCollapsed ).to.be.true; + expect( documentSelection._ranges[ 0 ].start.isEqual( range2.start ) ).to.be.true; + done(); + } ); + + documentSelection._setTo( documentSelection.getFirstPosition() ); + } ); + } ); + + describe( 'setting collapsed selection to end', () => { + it( 'should collapse to end position and fire change event', done => { + documentSelection._setTo( [ range1, range2, range3 ] ); + documentSelection.once( 'change', () => { + expect( documentSelection.rangeCount ).to.equal( 1 ); + expect( documentSelection.isCollapsed ).to.be.true; + expect( documentSelection._ranges[ 0 ].end.isEqual( range3.end ) ).to.be.true; + done(); + } ); + + documentSelection._setTo( documentSelection.getLastPosition() ); + } ); + } ); + + describe( 'removing all ranges', () => { + it( 'should remove all ranges and fire change event', done => { + documentSelection._setTo( [ range1, range2 ] ); + + documentSelection.once( 'change', () => { + expect( documentSelection.rangeCount ).to.equal( 0 ); + done(); + } ); + + documentSelection._setTo( null ); + } ); + } ); + + describe( 'setting fake selection', () => { + it( 'should allow to set selection to fake', () => { + documentSelection._setTo( range1, { fake: true } ); + + expect( documentSelection.isFake ).to.be.true; + } ); + + it( 'should allow to set fake selection label', () => { + const label = 'foo bar baz'; + documentSelection._setTo( range1, { fake: true, label } ); + + expect( documentSelection.fakeSelectionLabel ).to.equal( label ); + } ); + + it( 'should not set label when set to false', () => { + const label = 'foo bar baz'; + documentSelection._setTo( range1, { fake: false, label } ); + + expect( documentSelection.fakeSelectionLabel ).to.equal( '' ); + } ); + + it( 'should reset label when set to false', () => { + const label = 'foo bar baz'; + documentSelection._setTo( range1, { fake: true, label } ); + documentSelection._setTo( range1 ); + + expect( documentSelection.fakeSelectionLabel ).to.equal( '' ); + } ); + + it( 'should fire change event', done => { + documentSelection.once( 'change', () => { + expect( documentSelection.isFake ).to.be.true; + expect( documentSelection.fakeSelectionLabel ).to.equal( 'foo bar baz' ); + + done(); + } ); + + documentSelection._setTo( range1, { fake: true, label: 'foo bar baz' } ); + } ); + + it( 'should be possible to create an empty fake selection', () => { + documentSelection._setTo( null, { fake: true, label: 'foo bar baz' } ); + + expect( documentSelection.fakeSelectionLabel ).to.equal( 'foo bar baz' ); + expect( documentSelection.isFake ).to.be.true; + } ); + } ); + + describe( 'setting selection to itself', () => { + it( 'should correctly set ranges when setting to the same selection', () => { + documentSelection._setTo( [ range1, range2 ] ); + documentSelection._setTo( documentSelection ); + + const ranges = Array.from( documentSelection.getRanges() ); + expect( ranges.length ).to.equal( 2 ); + + expect( ranges[ 0 ].isEqual( range1 ) ).to.be.true; + expect( ranges[ 1 ].isEqual( range2 ) ).to.be.true; + } ); + + it( 'should correctly set ranges when setting to the same selection\'s ranges', () => { + documentSelection._setTo( [ range1, range2 ] ); + documentSelection._setTo( documentSelection.getRanges() ); + + const ranges = Array.from( documentSelection.getRanges() ); + expect( ranges.length ).to.equal( 2 ); + + expect( ranges[ 0 ].isEqual( range1 ) ).to.be.true; + expect( ranges[ 1 ].isEqual( range2 ) ).to.be.true; + } ); + } ); + + describe( 'throwing errors', () => { + it( 'should throw an error when range is invalid', () => { + expect( () => { + documentSelection._setTo( [ { invalid: 'range' } ] ); + } ).to.throw( CKEditorError, 'view-selection-invalid-range: Invalid Range.' ); + } ); + + 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( () => { + documentSelection._setTo( [ range1, range2 ] ); + } ).to.throw( CKEditorError, 'view-selection-range-intersects' ); + } ); + } ); + + it( 'should allow setting 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 ] ); + + documentSelection._setTo( textNode2, 'on' ); + + const ranges = Array.from( documentSelection.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 ); + } ); + + it( 'should allow setting selection inside an element', () => { + const element = new Element( 'p', null, [ new Text( 'foo' ), new Text( 'bar' ) ] ); + + documentSelection._setTo( element, 'in' ); + + const ranges = Array.from( documentSelection.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 ); + } ); + + it( 'should allow setting backward selection inside an element', () => { + const element = new Element( 'p', null, [ new Text( 'foo' ), new Text( 'bar' ) ] ); + + documentSelection._setTo( element, 'in', { backward: true } ); + + const ranges = Array.from( documentSelection.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 ); + expect( documentSelection.isBackward ).to.be.true; + } ); + } ); + + describe( 'getEditableElement()', () => { + it( 'should return null if no ranges in selection', () => { + expect( documentSelection.editableElement ).to.be.null; + } ); + + it( 'should return null if selection is placed in container that is not EditableElement', () => { + documentSelection._setTo( range1 ); + + expect( documentSelection.editableElement ).to.be.null; + } ); + + it( 'should return EditableElement when selection is placed inside', () => { + const viewDocument = new Document(); + documentSelection._setTo( viewDocument.selection ); + const root = createViewRoot( viewDocument, 'div', 'main' ); + const element = new Element( 'p' ); + root._appendChildren( element ); + + documentSelection._setTo( Range.createFromParentsAndOffsets( element, 0, element, 0 ) ); + + expect( documentSelection.editableElement ).to.equal( root ); + } ); + } ); + + describe( 'isFake', () => { + it( 'should be false for newly created instance', () => { + expect( documentSelection.isFake ).to.be.false; + } ); + } ); + + describe( 'getSelectedElement()', () => { + it( 'should return selected element', () => { + const { selection: documentSelection, view } = parse( 'foo [bar] baz' ); + const b = view.getChild( 1 ); + + expect( documentSelection.getSelectedElement() ).to.equal( b ); + } ); + + it( 'should return null if there is more than one range', () => { + const { selection: documentSelection } = parse( 'foo [bar] [baz]' ); + + expect( documentSelection.getSelectedElement() ).to.be.null; + } ); + + it( 'should return null if there is no selection', () => { + expect( documentSelection.getSelectedElement() ).to.be.null; + } ); + + it( 'should return null if selection is not over single element #1', () => { + const { selection: documentSelection } = parse( 'foo [bar ba}z' ); + + expect( documentSelection.getSelectedElement() ).to.be.null; + } ); + + it( 'should return null if selection is not over single element #2', () => { + const { selection: documentSelection } = parse( 'foo {bar} baz' ); + + expect( documentSelection.getSelectedElement() ).to.be.null; + } ); + } ); +} ); diff --git a/tests/view/domconverter/binding.js b/tests/view/domconverter/binding.js index 568ac3cb8..289d37e2e 100644 --- a/tests/view/domconverter/binding.js +++ b/tests/view/domconverter/binding.js @@ -6,7 +6,7 @@ /* globals document */ import ViewElement from '../../../src/view/element'; -import ViewSelection from '../../../src/view/selection'; +import ViewDocumentSelection from '../../../src/view/documentselection'; import DomConverter from '../../../src/view/domconverter'; import ViewDocumentFragment from '../../../src/view/documentfragment'; import { INLINE_FILLER } from '../../../src/view/filler'; @@ -269,7 +269,7 @@ describe( 'DomConverter', () => { beforeEach( () => { viewElement = new ViewElement(); domEl = document.createElement( 'div' ); - selection = new ViewSelection( viewElement, 'in' ); + selection = new ViewDocumentSelection( viewElement, 'in' ); converter.bindFakeSelection( domEl, selection ); } ); @@ -280,7 +280,7 @@ describe( 'DomConverter', () => { } ); it( 'should keep a copy of selection', () => { - const selectionCopy = new ViewSelection( selection ); + const selectionCopy = new ViewDocumentSelection( selection ); selection._setTo( new ViewElement(), 'in', { backward: true } ); const bindSelection = converter.fakeSelectionToView( domEl ); diff --git a/tests/view/domconverter/dom-to-view.js b/tests/view/domconverter/dom-to-view.js index 558cde1ca..d01ec1df9 100644 --- a/tests/view/domconverter/dom-to-view.js +++ b/tests/view/domconverter/dom-to-view.js @@ -6,7 +6,7 @@ /* globals document */ import ViewElement from '../../../src/view/element'; -import ViewSelection from '../../../src/view/selection'; +import ViewDocumentSelection from '../../../src/view/documentselection'; import DomConverter from '../../../src/view/domconverter'; import ViewDocumentFragment from '../../../src/view/documentfragment'; import { INLINE_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER } from '../../../src/view/filler'; @@ -882,7 +882,7 @@ describe( 'DomConverter', () => { domContainer.innerHTML = 'fake selection container'; document.body.appendChild( domContainer ); - const viewSelection = new ViewSelection( new ViewElement(), 'in' ); + const viewSelection = new ViewDocumentSelection( new ViewElement(), 'in' ); converter.bindFakeSelection( domContainer, viewSelection ); const domRange = document.createRange(); @@ -903,7 +903,7 @@ describe( 'DomConverter', () => { domContainer.innerHTML = 'fake selection container'; document.body.appendChild( domContainer ); - const viewSelection = new ViewSelection( new ViewElement(), 'in' ); + const viewSelection = new ViewDocumentSelection( new ViewElement(), 'in' ); converter.bindFakeSelection( domContainer, viewSelection ); const domRange = document.createRange(); diff --git a/tests/view/observer/selectionobserver.js b/tests/view/observer/selectionobserver.js index 27bdf25d2..3e2ba711f 100644 --- a/tests/view/observer/selectionobserver.js +++ b/tests/view/observer/selectionobserver.js @@ -7,6 +7,7 @@ import ViewRange from '../../../src/view/range'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import DocumentSelection from '../../../src/view/documentselection'; import ViewSelection from '../../../src/view/selection'; import View from '../../../src/view/view'; import SelectionObserver from '../../../src/view/observer/selectionobserver'; @@ -64,7 +65,7 @@ describe( 'SelectionObserver', () => { viewDocument.on( 'selectionChange', ( evt, data ) => { expect( data ).to.have.property( 'domSelection' ).that.equals( domDocument.getSelection() ); - expect( data ).to.have.property( 'oldSelection' ).that.is.instanceof( ViewSelection ); + expect( data ).to.have.property( 'oldSelection' ).that.is.instanceof( DocumentSelection ); expect( data.oldSelection.rangeCount ).to.equal( 0 ); expect( data ).to.have.property( 'newSelection' ).that.is.instanceof( ViewSelection ); @@ -265,7 +266,7 @@ describe( 'SelectionObserver', () => { expect( spy.calledOnce ).to.true; expect( data ).to.have.property( 'domSelection' ).to.equal( domDocument.getSelection() ); - expect( data ).to.have.property( 'oldSelection' ).to.instanceof( ViewSelection ); + expect( data ).to.have.property( 'oldSelection' ).to.instanceof( DocumentSelection ); expect( data.oldSelection.rangeCount ).to.equal( 0 ); expect( data ).to.have.property( 'newSelection' ).to.instanceof( ViewSelection ); diff --git a/tests/view/renderer.js b/tests/view/renderer.js index 419c4decb..bd34cb614 100644 --- a/tests/view/renderer.js +++ b/tests/view/renderer.js @@ -12,7 +12,7 @@ import ViewAttributeElement from '../../src/view/attributeelement'; import ViewText from '../../src/view/text'; import ViewRange from '../../src/view/range'; import ViewPosition from '../../src/view/position'; -import Selection from '../../src/view/selection'; +import DocumentSelection from '../../src/view/documentselection'; import DomConverter from '../../src/view/domconverter'; import Renderer from '../../src/view/renderer'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; @@ -29,7 +29,7 @@ describe( 'Renderer', () => { let selection, domConverter, renderer; beforeEach( () => { - selection = new Selection(); + selection = new DocumentSelection(); domConverter = new DomConverter(); renderer = new Renderer( domConverter, selection ); renderer.domDocuments.add( document ); diff --git a/tests/view/selection.js b/tests/view/selection.js index f92b5f1a7..5b000d399 100644 --- a/tests/view/selection.js +++ b/tests/view/selection.js @@ -4,6 +4,7 @@ */ import Selection from '../../src/view/selection'; +import DocumentSelection from '../../src/view/documentselection'; import Range from '../../src/view/range'; import Document from '../../src/view/document'; import Element from '../../src/view/element'; @@ -92,6 +93,14 @@ describe( 'Selection', () => { expect( selection.isBackward ).to.be.true; } ); + it( 'should be able to create a selection from the other document selection', () => { + const otherSelection = new DocumentSelection( [ range2, range3 ], { backward: true } ); + const selection = new Selection( otherSelection ); + + expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range2, range3 ] ); + expect( selection.isBackward ).to.be.true; + } ); + it( 'should be able to create a fake selection from the other fake selection', () => { const otherSelection = new Selection( [ range2, range3 ], { fake: true, label: 'foo bar baz' } ); const selection = new Selection( otherSelection ); @@ -131,7 +140,7 @@ describe( 'Selection', () => { } ); it( 'should return start of single range in selection', () => { - selection._setTo( range1 ); + selection.setTo( range1 ); const anchor = selection.anchor; expect( anchor.isEqual( range1.start ) ).to.be.true; @@ -139,7 +148,7 @@ describe( 'Selection', () => { } ); it( 'should return end of single range in selection when added as backward', () => { - selection._setTo( range1, { backward: true } ); + selection.setTo( range1, { backward: true } ); const anchor = selection.anchor; expect( anchor.isEqual( range1.end ) ).to.be.true; @@ -147,7 +156,7 @@ describe( 'Selection', () => { } ); it( 'should get anchor from last inserted range', () => { - selection._setTo( [ range1, range2 ] ); + selection.setTo( [ range1, range2 ] ); expect( selection.anchor.isEqual( range2.start ) ).to.be.true; } ); @@ -159,14 +168,14 @@ describe( 'Selection', () => { } ); it( 'should return end of single range in selection', () => { - selection._setTo( 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._setTo( range1, { backward: true } ); + selection.setTo( range1, { backward: true } ); const focus = selection.focus; expect( focus.isEqual( range1.start ) ).to.be.true; @@ -174,16 +183,16 @@ describe( 'Selection', () => { } ); it( 'should get focus from last inserted range', () => { - selection._setTo( [ range1, range2 ] ); + selection.setTo( [ range1, range2 ] ); expect( selection.focus.isEqual( range2.end ) ).to.be.true; } ); } ); - describe( '_setFocus()', () => { + describe( 'setFocus()', () => { it( 'keeps all existing ranges when no modifications needed', () => { - selection._setTo( range1 ); - selection._setFocus( selection.focus ); + selection.setTo( range1 ); + selection.setFocus( selection.focus ); expect( count( selection.getRanges() ) ).to.equal( 1 ); } ); @@ -192,7 +201,7 @@ describe( 'Selection', () => { const endPos = Position.createAt( el, 'end' ); expect( () => { - selection._setFocus( endPos ); + selection.setFocus( endPos ); } ).to.throw( CKEditorError, /view-selection-setFocus-no-ranges/ ); } ); @@ -200,9 +209,9 @@ describe( 'Selection', () => { const startPos = Position.createAt( el, 1 ); const endPos = Position.createAt( el, 2 ); - selection._setTo( startPos ); + selection.setTo( startPos ); - selection._setFocus( endPos ); + selection.setFocus( endPos ); expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' ); @@ -212,9 +221,9 @@ describe( 'Selection', () => { const startPos = Position.createAt( el, 1 ); const endPos = Position.createAt( el, 0 ); - selection._setTo( startPos ); + selection.setTo( startPos ); - selection._setFocus( endPos ); + selection.setFocus( endPos ); expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' ); @@ -226,9 +235,9 @@ describe( 'Selection', () => { const endPos = Position.createAt( el, 2 ); const newEndPos = Position.createAt( el, 3 ); - selection._setTo( new Range( startPos, endPos ) ); + selection.setTo( new Range( startPos, endPos ) ); - selection._setFocus( newEndPos ); + selection.setFocus( newEndPos ); expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); @@ -239,9 +248,9 @@ describe( 'Selection', () => { const endPos = Position.createAt( el, 2 ); const newEndPos = Position.createAt( el, 0 ); - selection._setTo( new Range( startPos, endPos ) ); + selection.setTo( new Range( startPos, endPos ) ); - selection._setFocus( newEndPos ); + selection.setFocus( newEndPos ); expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); @@ -253,9 +262,9 @@ describe( 'Selection', () => { const endPos = Position.createAt( el, 2 ); const newEndPos = Position.createAt( el, 3 ); - selection._setTo( new Range( startPos, endPos ), { backward: true } ); + selection.setTo( new Range( startPos, endPos ), { backward: true } ); - selection._setFocus( newEndPos ); + selection.setFocus( newEndPos ); expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); @@ -267,9 +276,9 @@ describe( 'Selection', () => { const endPos = Position.createAt( el, 2 ); const newEndPos = Position.createAt( el, 0 ); - selection._setTo( new Range( startPos, endPos ), { backward: true } ); + selection.setTo( new Range( startPos, endPos ), { backward: true } ); - selection._setFocus( newEndPos ); + selection.setFocus( newEndPos ); expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); @@ -285,12 +294,12 @@ describe( 'Selection', () => { const newEndPos = Position.createAt( el, 0 ); - selection._setTo( [ + selection.setTo( [ new Range( startPos1, endPos1 ), new Range( startPos2, endPos2 ) ] ); - selection._setFocus( newEndPos ); + selection.setFocus( newEndPos ); const ranges = Array.from( selection.getRanges() ); @@ -307,9 +316,9 @@ describe( 'Selection', () => { const startPos = Position.createAt( el, 1 ); const endPos = Position.createAt( el, 2 ); - selection._setTo( new Range( startPos, endPos ) ); + selection.setTo( new Range( startPos, endPos ) ); - selection._setFocus( startPos ); + selection.setFocus( startPos ); expect( selection.focus.compareWith( startPos ) ).to.equal( 'same' ); expect( selection.isCollapsed ).to.be.true; @@ -322,8 +331,8 @@ describe( 'Selection', () => { const spy = sinon.stub( Position, 'createAt' ).returns( newEndPos ); - selection._setTo( new Range( startPos, endPos ) ); - selection._setFocus( 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' ); @@ -335,7 +344,7 @@ describe( 'Selection', () => { describe( 'isCollapsed', () => { it( 'should return true when there is single collapsed range', () => { const range = Range.createFromParentsAndOffsets( el, 5, el, 5 ); - selection._setTo( range ); + selection.setTo( range ); expect( selection.isCollapsed ).to.be.true; } ); @@ -343,14 +352,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._setTo( [ range1, 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._setTo( range ); + selection.setTo( range ); expect( selection.isCollapsed ).to.be.false; } ); @@ -360,11 +369,11 @@ describe( 'Selection', () => { it( 'should return proper range count', () => { expect( selection.rangeCount ).to.equal( 0 ); - selection._setTo( range1 ); + selection.setTo( range1 ); expect( selection.rangeCount ).to.equal( 1 ); - selection._setTo( [ range1, range2 ] ); + selection.setTo( [ range1, range2 ] ); expect( selection.rangeCount ).to.equal( 2 ); } ); @@ -375,17 +384,17 @@ describe( 'Selection', () => { const range1 = Range.createFromParentsAndOffsets( el, 5, el, 10 ); const range2 = Range.createFromParentsAndOffsets( el, 15, el, 16 ); - selection._setTo( range1, { backward: true } ); + selection.setTo( range1, { backward: true } ); expect( selection ).to.have.property( 'isBackward', true ); - selection._setTo( [ range1, 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._setTo( range, { backward: true } ); + selection.setTo( range, { backward: true } ); expect( selection.isBackward ).to.be.false; } ); @@ -393,7 +402,7 @@ describe( 'Selection', () => { describe( 'getRanges', () => { it( 'should return iterator with copies of all ranges', () => { - selection._setTo( [ range1, range2 ] ); + selection.setTo( [ range1, range2 ] ); const iterable = selection.getRanges(); const ranges = Array.from( iterable ); @@ -408,7 +417,7 @@ describe( 'Selection', () => { describe( 'getFirstRange', () => { it( 'should return copy of range with first position', () => { - selection._setTo( [ range1, range2, range3 ] ); + selection.setTo( [ range1, range2, range3 ] ); const range = selection.getFirstRange(); @@ -423,7 +432,7 @@ describe( 'Selection', () => { describe( 'getLastRange', () => { it( 'should return copy of range with last position', () => { - selection._setTo( [ range1, range2, range3 ] ); + selection.setTo( [ range1, range2, range3 ] ); const range = selection.getLastRange(); @@ -438,7 +447,7 @@ describe( 'Selection', () => { describe( 'getFirstPosition', () => { it( 'should return copy of first position', () => { - selection._setTo( [ range1, range2, range3 ] ); + selection.setTo( [ range1, range2, range3 ] ); const position = selection.getFirstPosition(); @@ -453,7 +462,7 @@ describe( 'Selection', () => { describe( 'getLastPosition', () => { it( 'should return copy of range with last position', () => { - selection._setTo( [ range1, range2, range3 ] ); + selection.setTo( [ range1, range2, range3 ] ); const position = selection.getLastPosition(); @@ -468,16 +477,16 @@ describe( 'Selection', () => { describe( 'isEqual', () => { it( 'should return true if selections equal', () => { - selection._setTo( [ range1, range2 ] ); + selection.setTo( [ range1, range2 ] ); const otherSelection = new Selection(); - otherSelection._setTo( [ range1, range2 ] ); + otherSelection.setTo( [ range1, range2 ] ); expect( selection.isEqual( otherSelection ) ).to.be.true; } ); it( 'should return true if backward selections equal', () => { - selection._setTo( range1, { backward: true } ); + selection.setTo( range1, { backward: true } ); const otherSelection = new Selection( [ range1 ], { backward: true } ); @@ -485,7 +494,7 @@ describe( 'Selection', () => { } ); it( 'should return false if ranges count does not equal', () => { - selection._setTo( [ range1, range2 ] ); + selection.setTo( [ range1, range2 ] ); const otherSelection = new Selection( [ range1 ] ); @@ -493,7 +502,7 @@ describe( 'Selection', () => { } ); it( 'should return false if ranges (other than the last added one) do not equal', () => { - selection._setTo( [ range1, range3 ] ); + selection.setTo( [ range1, range3 ] ); const otherSelection = new Selection( [ range2, range3 ] ); @@ -501,7 +510,7 @@ describe( 'Selection', () => { } ); it( 'should return false if directions do not equal', () => { - selection._setTo( range1 ); + selection.setTo( range1 ); const otherSelection = new Selection( [ range1 ], { backward: true } ); @@ -516,14 +525,14 @@ describe( 'Selection', () => { it( 'should return true if both selection are fake', () => { const otherSelection = new Selection( range1, { fake: true } ); - selection._setTo( range1, { fake: true } ); + selection.setTo( range1, { fake: true } ); expect( selection.isEqual( otherSelection ) ).to.be.true; } ); it( 'should return false if both selection are fake but have different label', () => { const otherSelection = new Selection( [ range1 ], { fake: true, label: 'foo bar baz' } ); - selection._setTo( range1, { fake: true, label: 'foo' } ); + selection.setTo( range1, { fake: true, label: 'foo' } ); expect( selection.isEqual( otherSelection ) ).to.be.false; } ); @@ -537,7 +546,7 @@ describe( 'Selection', () => { describe( 'isSimilar', () => { it( 'should return true if selections equal', () => { - selection._setTo( [ range1, range2 ] ); + selection.setTo( [ range1, range2 ] ); const otherSelection = new Selection( [ range1, range2 ] ); @@ -545,7 +554,7 @@ describe( 'Selection', () => { } ); it( 'should return false if ranges count does not equal', () => { - selection._setTo( [ range1, range2 ] ); + selection.setTo( [ range1, range2 ] ); const otherSelection = new Selection( [ range1 ] ); @@ -553,7 +562,7 @@ describe( 'Selection', () => { } ); it( 'should return false if trimmed ranges (other than the last added one) are not equal', () => { - selection._setTo( [ range1, range3 ] ); + selection.setTo( [ range1, range3 ] ); const otherSelection = new Selection( [ range2, range3 ] ); @@ -561,7 +570,7 @@ describe( 'Selection', () => { } ); it( 'should return false if directions are not equal', () => { - selection._setTo( range1 ); + selection.setTo( range1 ); const otherSelection = new Selection( [ range1 ], { backward: true } ); @@ -591,7 +600,7 @@ describe( 'Selection', () => { const rangeA2 = Range.createFromParentsAndOffsets( p2, 0, p2, 1 ); const rangeB2 = Range.createFromParentsAndOffsets( span2, 0, span2, 1 ); - selection._setTo( [ rangeA1, rangeA2 ] ); + selection.setTo( [ rangeA1, rangeA2 ] ); const otherSelection = new Selection( [ rangeB2, rangeB1 ] ); @@ -603,14 +612,14 @@ describe( 'Selection', () => { } ); } ); - describe( '_setTo()', () => { + describe( 'setTo()', () => { describe( 'simple scenarios', () => { it( 'should set selection ranges from the given selection', () => { - selection._setTo( range1 ); + selection.setTo( range1 ); const otherSelection = new Selection( [ range2, range3 ], { backward: true } ); - selection._setTo( otherSelection ); + selection.setTo( otherSelection ); expect( selection.rangeCount ).to.equal( 2 ); expect( selection._ranges[ 0 ].isEqual( range2 ) ).to.be.true; @@ -622,21 +631,21 @@ describe( 'Selection', () => { } ); it( 'should set selection on the given Range', () => { - selection._setTo( range1 ); + selection.setTo( range1 ); expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1 ] ); expect( selection.isBackward ).to.be.false; } ); it( 'should set selection on the given iterable of Ranges', () => { - selection._setTo( new Set( [ range1, range2 ] ) ); + selection.setTo( new Set( [ range1, range2 ] ) ); expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1, range2 ] ); expect( selection.isBackward ).to.be.false; } ); it( 'should set collapsed selection on the given Position', () => { - selection._setTo( range1.start ); + selection.setTo( range1.start ); expect( Array.from( selection.getRanges() ).length ).to.equal( 1 ); expect( Array.from( selection.getRanges() )[ 0 ].start ).to.deep.equal( range1.start ); @@ -653,13 +662,13 @@ describe( 'Selection', () => { const otherSelection = new Selection( [ range1 ] ); - selection._setTo( otherSelection ); + selection.setTo( otherSelection ); } ); it( 'should set fake state and label', () => { const label = 'foo bar baz'; const otherSelection = new Selection( null, { fake: true, label } ); - selection._setTo( otherSelection ); + selection.setTo( otherSelection ); expect( selection.isFake ).to.be.true; expect( selection.fakeSelectionLabel ).to.equal( label ); @@ -669,7 +678,7 @@ describe( 'Selection', () => { const otherSelection = new Selection(); expect( () => { - otherSelection._setTo( {} ); + otherSelection.setTo( {} ); } ).to.throw( /view-selection-setTo-not-selectable/ ); } ); @@ -677,20 +686,20 @@ describe( 'Selection', () => { const otherSelection = new Selection(); expect( () => { - otherSelection._setTo(); + otherSelection.setTo(); } ).to.throw( /view-selection-setTo-not-selectable/ ); } ); } ); describe( 'setting collapsed selection', () => { beforeEach( () => { - selection._setTo( [ range1, range2 ] ); + selection.setTo( [ range1, range2 ] ); } ); it( 'should collapse selection at position', () => { const position = new Position( el, 4 ); - selection._setTo( position ); + selection.setTo( position ); const range = selection.getFirstRange(); expect( range.start.parent ).to.equal( el ); @@ -702,14 +711,14 @@ describe( 'Selection', () => { const foo = new Text( 'foo' ); const p = new Element( 'p', null, foo ); - selection._setTo( foo, 0 ); + selection.setTo( foo, 0 ); 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._setTo( p, 1 ); + selection.setTo( p, 1 ); range = selection.getFirstRange(); expect( range.start.parent ).to.equal( p ); @@ -721,7 +730,7 @@ describe( 'Selection', () => { const foo = new Text( 'foo' ); expect( () => { - selection._setTo( foo ); + selection.setTo( foo ); } ).to.throw( CKEditorError, /view-selection-setTo-required-second-parameter/ ); } ); @@ -729,21 +738,21 @@ describe( 'Selection', () => { const foo = new Text( 'foo' ); const p = new Element( 'p', null, foo ); - selection._setTo( 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._setTo( 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._setTo( foo, 'after' ); + selection.setTo( foo, 'after' ); range = selection.getFirstRange(); expect( range.start.parent ).to.equal( p ); @@ -754,7 +763,7 @@ describe( 'Selection', () => { describe( 'setting collapsed selection at start', () => { it( 'should collapse to start position and fire change event', done => { - selection._setTo( [ range1, range2, range3 ] ); + selection.setTo( [ range1, range2, range3 ] ); selection.once( 'change', () => { expect( selection.rangeCount ).to.equal( 1 ); expect( selection.isCollapsed ).to.be.true; @@ -762,13 +771,13 @@ describe( 'Selection', () => { done(); } ); - selection._setTo( selection.getFirstPosition() ); + selection.setTo( selection.getFirstPosition() ); } ); } ); describe( 'setting collapsed selection to end', () => { it( 'should collapse to end position and fire change event', done => { - selection._setTo( [ range1, range2, range3 ] ); + selection.setTo( [ range1, range2, range3 ] ); selection.once( 'change', () => { expect( selection.rangeCount ).to.equal( 1 ); expect( selection.isCollapsed ).to.be.true; @@ -776,48 +785,48 @@ describe( 'Selection', () => { done(); } ); - selection._setTo( selection.getLastPosition() ); + selection.setTo( selection.getLastPosition() ); } ); } ); describe( 'removing all ranges', () => { it( 'should remove all ranges and fire change event', done => { - selection._setTo( [ range1, range2 ] ); + selection.setTo( [ range1, range2 ] ); selection.once( 'change', () => { expect( selection.rangeCount ).to.equal( 0 ); done(); } ); - selection._setTo( null ); + selection.setTo( null ); } ); } ); describe( 'setting fake selection', () => { it( 'should allow to set selection to fake', () => { - selection._setTo( range1, { fake: true } ); + selection.setTo( range1, { fake: true } ); expect( selection.isFake ).to.be.true; } ); it( 'should allow to set fake selection label', () => { const label = 'foo bar baz'; - selection._setTo( range1, { fake: true, label } ); + selection.setTo( range1, { fake: true, label } ); expect( selection.fakeSelectionLabel ).to.equal( label ); } ); it( 'should not set label when set to false', () => { const label = 'foo bar baz'; - selection._setTo( range1, { fake: false, label } ); + selection.setTo( range1, { fake: false, label } ); expect( selection.fakeSelectionLabel ).to.equal( '' ); } ); it( 'should reset label when set to false', () => { const label = 'foo bar baz'; - selection._setTo( range1, { fake: true, label } ); - selection._setTo( range1 ); + selection.setTo( range1, { fake: true, label } ); + selection.setTo( range1 ); expect( selection.fakeSelectionLabel ).to.equal( '' ); } ); @@ -830,11 +839,11 @@ describe( 'Selection', () => { done(); } ); - selection._setTo( range1, { fake: true, label: 'foo bar baz' } ); + selection.setTo( range1, { fake: true, label: 'foo bar baz' } ); } ); it( 'should be possible to create an empty fake selection', () => { - selection._setTo( null, { fake: true, label: 'foo bar baz' } ); + selection.setTo( null, { fake: true, label: 'foo bar baz' } ); expect( selection.fakeSelectionLabel ).to.equal( 'foo bar baz' ); expect( selection.isFake ).to.be.true; @@ -843,8 +852,8 @@ describe( 'Selection', () => { describe( 'setting selection to itself', () => { it( 'should correctly set ranges when setting to the same selection', () => { - selection._setTo( [ range1, range2 ] ); - selection._setTo( selection ); + selection.setTo( [ range1, range2 ] ); + selection.setTo( selection ); const ranges = Array.from( selection.getRanges() ); expect( ranges.length ).to.equal( 2 ); @@ -854,8 +863,8 @@ describe( 'Selection', () => { } ); it( 'should correctly set ranges when setting to the same selection\'s ranges', () => { - selection._setTo( [ range1, range2 ] ); - selection._setTo( selection.getRanges() ); + selection.setTo( [ range1, range2 ] ); + selection.setTo( selection.getRanges() ); const ranges = Array.from( selection.getRanges() ); expect( ranges.length ).to.equal( 2 ); @@ -868,7 +877,7 @@ describe( 'Selection', () => { describe( 'throwing errors', () => { it( 'should throw an error when range is invalid', () => { expect( () => { - selection._setTo( [ { invalid: 'range' } ] ); + selection.setTo( [ { invalid: 'range' } ] ); } ).to.throw( CKEditorError, 'view-selection-invalid-range: Invalid Range.' ); } ); @@ -877,7 +886,7 @@ describe( 'Selection', () => { const range2 = Range.createFromParentsAndOffsets( text, 7, text, 15 ); expect( () => { - selection._setTo( [ range1, range2 ] ); + selection.setTo( [ range1, range2 ] ); } ).to.throw( CKEditorError, 'view-selection-range-intersects' ); } ); } ); @@ -888,7 +897,7 @@ describe( 'Selection', () => { const textNode3 = new Text( 'baz' ); const element = new Element( 'p', null, [ textNode1, textNode2, textNode3 ] ); - selection._setTo( textNode2, 'on' ); + selection.setTo( textNode2, 'on' ); const ranges = Array.from( selection.getRanges() ); expect( ranges.length ).to.equal( 1 ); @@ -901,7 +910,7 @@ describe( 'Selection', () => { it( 'should allow setting selection inside an element', () => { const element = new Element( 'p', null, [ new Text( 'foo' ), new Text( 'bar' ) ] ); - selection._setTo( element, 'in' ); + selection.setTo( element, 'in' ); const ranges = Array.from( selection.getRanges() ); expect( ranges.length ).to.equal( 1 ); @@ -914,7 +923,7 @@ describe( 'Selection', () => { it( 'should allow setting backward selection inside an element', () => { const element = new Element( 'p', null, [ new Text( 'foo' ), new Text( 'bar' ) ] ); - selection._setTo( element, 'in', { backward: true } ); + selection.setTo( element, 'in', { backward: true } ); const ranges = Array.from( selection.getRanges() ); expect( ranges.length ).to.equal( 1 ); @@ -932,19 +941,19 @@ describe( 'Selection', () => { } ); it( 'should return null if selection is placed in container that is not EditableElement', () => { - selection._setTo( range1 ); + selection.setTo( range1 ); expect( selection.editableElement ).to.be.null; } ); it( 'should return EditableElement when selection is placed inside', () => { const viewDocument = new Document(); - const selection = viewDocument.selection; + selection.setTo( viewDocument.selection ); const root = createViewRoot( viewDocument, 'div', 'main' ); const element = new Element( 'p' ); root._appendChildren( element ); - selection._setTo( Range.createFromParentsAndOffsets( element, 0, element, 0 ) ); + selection.setTo( Range.createFromParentsAndOffsets( element, 0, element, 0 ) ); expect( selection.editableElement ).to.equal( root ); } ); @@ -958,14 +967,16 @@ describe( 'Selection', () => { describe( 'getSelectedElement()', () => { it( 'should return selected element', () => { - const { selection, view } = parse( 'foo [bar] baz' ); + const { selection: docSelection, view } = parse( 'foo [bar] baz' ); const b = view.getChild( 1 ); + const selection = new Selection( docSelection ); expect( selection.getSelectedElement() ).to.equal( b ); } ); it( 'should return null if there is more than one range', () => { - const { selection } = parse( 'foo [bar] [baz]' ); + const { selection: docSelection } = parse( 'foo [bar] [baz]' ); + const selection = new Selection( docSelection ); expect( selection.getSelectedElement() ).to.be.null; } ); @@ -975,13 +986,15 @@ describe( 'Selection', () => { } ); it( 'should return null if selection is not over single element #1', () => { - const { selection } = parse( 'foo [bar ba}z' ); + const { selection: docSelection } = parse( 'foo [bar ba}z' ); + const selection = new Selection( docSelection ); expect( selection.getSelectedElement() ).to.be.null; } ); it( 'should return null if selection is not over single element #2', () => { - const { selection } = parse( 'foo {bar} baz' ); + const { selection: docSelection } = parse( 'foo {bar} baz' ); + const selection = new Selection( docSelection ); expect( selection.getSelectedElement() ).to.be.null; } );