| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533 |
- /**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /**
- * @module engine/view/range
- */
- import Position from './position';
- import TreeWalker from './treewalker';
- /**
- * Range in the view tree. A range is represented by its start and end {@link module:engine/view/position~Position positions}.
- *
- * In order to create a new position instance use the `createPosition*()` factory methods available in:
- *
- * * {@link module:engine/view/view~View}
- * * {@link module:engine/view/downcastwriter~DowncastWriter}
- * * {@link module:engine/view/upcastwriter~UpcastWriter}
- */
- export default class Range {
- /**
- * Creates a range spanning from `start` position to `end` position.
- *
- * **Note:** Constructor creates it's own {@link module:engine/view/position~Position} instances basing on passed values.
- *
- * @param {module:engine/view/position~Position} start Start position.
- * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at the `start` position.
- */
- constructor( start, end = null ) {
- /**
- * Start position.
- *
- * @readonly
- * @member {module:engine/view/position~Position}
- */
- this.start = start.clone();
- /**
- * End position.
- *
- * @readonly
- * @member {module:engine/view/position~Position}
- */
- this.end = end ? end.clone() : start.clone();
- }
- /**
- * Iterable interface.
- *
- * Iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
- * them together with additional information like length or {@link module:engine/view/position~Position positions},
- * grouped as {@link module:engine/view/treewalker~TreeWalkerValue}.
- *
- * This iterator uses {@link module:engine/view/treewalker~TreeWalker TreeWalker} with `boundaries` set to this range and
- * `ignoreElementEnd` option
- * set to `true`.
- *
- * @returns {Iterable.<module:engine/view/treewalker~TreeWalkerValue>}
- */
- * [ Symbol.iterator ]() {
- yield* new TreeWalker( { boundaries: this, ignoreElementEnd: true } );
- }
- /**
- * Returns whether the range is collapsed, that is it start and end positions are equal.
- *
- * @type {Boolean}
- */
- get isCollapsed() {
- return this.start.isEqual( this.end );
- }
- /**
- * Returns whether this range is flat, that is if {@link module:engine/view/range~Range#start start} position and
- * {@link module:engine/view/range~Range#end end} position are in the same {@link module:engine/view/position~Position#parent parent}.
- *
- * @type {Boolean}
- */
- get isFlat() {
- return this.start.parent === this.end.parent;
- }
- /**
- * Range root element.
- *
- * @type {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
- */
- get root() {
- return this.start.root;
- }
- /**
- * Creates a maximal range that has the same content as this range but is expanded in both ways (at the beginning
- * and at the end).
- *
- * For example:
- *
- * <p>Foo</p><p><b>{Bar}</b></p> -> <p>Foo</p>[<p><b>Bar</b>]</p>
- * <p><b>foo</b>{bar}<span></span></p> -> <p><b>foo[</b>bar<span></span>]</p>
- *
- * Note that in the sample above:
- *
- * - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
- * - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
- * - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
- *
- * @returns {module:engine/view/range~Range} Enlarged range.
- */
- getEnlarged() {
- let start = this.start.getLastMatchingPosition( enlargeTrimSkip, { direction: 'backward' } );
- let end = this.end.getLastMatchingPosition( enlargeTrimSkip );
- // Fix positions, in case if they are in Text node.
- if ( start.parent.is( '$text' ) && start.isAtStart ) {
- start = Position._createBefore( start.parent );
- }
- if ( end.parent.is( '$text' ) && end.isAtEnd ) {
- end = Position._createAfter( end.parent );
- }
- return new Range( start, end );
- }
- /**
- * Creates a minimum range that has the same content as this range but is trimmed in both ways (at the beginning
- * and at the end).
- *
- * For example:
- *
- * <p>Foo</p>[<p><b>Bar</b>]</p> -> <p>Foo</p><p><b>{Bar}</b></p>
- * <p><b>foo[</b>bar<span></span>]</p> -> <p><b>foo</b>{bar}<span></span></p>
- *
- * Note that in the sample above:
- *
- * - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
- * - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
- * - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
- *
- * @returns {module:engine/view/range~Range} Shrink range.
- */
- getTrimmed() {
- let start = this.start.getLastMatchingPosition( enlargeTrimSkip );
- if ( start.isAfter( this.end ) || start.isEqual( this.end ) ) {
- return new Range( start, start );
- }
- let end = this.end.getLastMatchingPosition( enlargeTrimSkip, { direction: 'backward' } );
- const nodeAfterStart = start.nodeAfter;
- const nodeBeforeEnd = end.nodeBefore;
- // Because TreeWalker prefers positions next to text node, we need to move them manually into these text nodes.
- if ( nodeAfterStart && nodeAfterStart.is( '$text' ) ) {
- start = new Position( nodeAfterStart, 0 );
- }
- if ( nodeBeforeEnd && nodeBeforeEnd.is( '$text' ) ) {
- end = new Position( nodeBeforeEnd, nodeBeforeEnd.data.length );
- }
- return new Range( start, end );
- }
- /**
- * Two ranges are equal if their start and end positions are equal.
- *
- * @param {module:engine/view/range~Range} otherRange Range to compare with.
- * @returns {Boolean} `true` if ranges are equal, `false` otherwise
- */
- isEqual( otherRange ) {
- return this == otherRange || ( this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end ) );
- }
- /**
- * Checks whether this range contains given {@link module:engine/view/position~Position position}.
- *
- * @param {module:engine/view/position~Position} position Position to check.
- * @returns {Boolean} `true` if given {@link module:engine/view/position~Position position} is contained in this range,
- * `false` otherwise.
- */
- containsPosition( position ) {
- return position.isAfter( this.start ) && position.isBefore( this.end );
- }
- /**
- * Checks whether this range contains given {@link module:engine/view/range~Range range}.
- *
- * @param {module:engine/view/range~Range} otherRange Range to check.
- * @param {Boolean} [loose=false] Whether the check is loose or strict. If the check is strict (`false`), compared range cannot
- * start or end at the same position as this range boundaries. If the check is loose (`true`), compared range can start, end or
- * even be equal to this range. Note that collapsed ranges are always compared in strict mode.
- * @returns {Boolean} `true` if given {@link module:engine/view/range~Range range} boundaries are contained by this range, `false`
- * otherwise.
- */
- containsRange( otherRange, loose = false ) {
- if ( otherRange.isCollapsed ) {
- loose = false;
- }
- const containsStart = this.containsPosition( otherRange.start ) || ( loose && this.start.isEqual( otherRange.start ) );
- const containsEnd = this.containsPosition( otherRange.end ) || ( loose && this.end.isEqual( otherRange.end ) );
- return containsStart && containsEnd;
- }
- /**
- * Computes which part(s) of this {@link module:engine/view/range~Range range} is not a part of given
- * {@link module:engine/view/range~Range range}.
- * Returned array contains zero, one or two {@link module:engine/view/range~Range ranges}.
- *
- * Examples:
- *
- * let foo = downcastWriter.createText( 'foo' );
- * let img = downcastWriter.createContainerElement( 'img' );
- * let bar = downcastWriter.createText( 'bar' );
- * let p = downcastWriter.createContainerElement( 'p', null, [ foo, img, bar ] );
- *
- * let range = view.createRange( view.createPositionAt( foo, 2 ), view.createPositionAt( bar, 1 ); // "o", img, "b" are in range.
- * let otherRange = view.createRange( // "oo", img, "ba" are in range.
- * view.createPositionAt( foo, 1 ),
- * view.createPositionAt( bar, 2 )
- * );
- * let transformed = range.getDifference( otherRange );
- * // transformed array has no ranges because `otherRange` contains `range`
- *
- * otherRange = view.createRange( view.createPositionAt( foo, 1 ), view.createPositionAt( p, 2 ); // "oo", img are in range.
- * transformed = range.getDifference( otherRange );
- * // transformed array has one range: from ( p, 2 ) to ( bar, 1 )
- *
- * otherRange = view.createRange( view.createPositionAt( p, 1 ), view.createPositionAt( p, 2 ) ); // img is in range.
- * transformed = range.getDifference( otherRange );
- * // transformed array has two ranges: from ( foo, 1 ) to ( p, 1 ) and from ( p, 2 ) to ( bar, 1 )
- *
- * @param {module:engine/view/range~Range} otherRange Range to differentiate against.
- * @returns {Array.<module:engine/view/range~Range>} The difference between ranges.
- */
- getDifference( otherRange ) {
- const ranges = [];
- if ( this.isIntersecting( otherRange ) ) {
- // Ranges intersect.
- if ( this.containsPosition( otherRange.start ) ) {
- // Given range start is inside this range. This means that we have to
- // add shrunken range - from the start to the middle of this range.
- ranges.push( new Range( this.start, otherRange.start ) );
- }
- if ( this.containsPosition( otherRange.end ) ) {
- // Given range end is inside this range. This means that we have to
- // add shrunken range - from the middle of this range to the end.
- ranges.push( new Range( otherRange.end, this.end ) );
- }
- } else {
- // Ranges do not intersect, return the original range.
- ranges.push( this.clone() );
- }
- return ranges;
- }
- /**
- * Returns an intersection of this {@link module:engine/view/range~Range range} and given {@link module:engine/view/range~Range range}.
- * Intersection is a common part of both of those ranges. If ranges has no common part, returns `null`.
- *
- * Examples:
- *
- * let foo = downcastWriter.createText( 'foo' );
- * let img = downcastWriter.createContainerElement( 'img' );
- * let bar = downcastWriter.createText( 'bar' );
- * let p = downcastWriter.createContainerElement( 'p', null, [ foo, img, bar ] );
- *
- * let range = view.createRange( view.createPositionAt( foo, 2 ), view.createPositionAt( bar, 1 ); // "o", img, "b" are in range.
- * let otherRange = view.createRange( view.createPositionAt( foo, 1 ), view.createPositionAt( p, 2 ); // "oo", img are in range.
- * let transformed = range.getIntersection( otherRange ); // range from ( foo, 1 ) to ( p, 2 ).
- *
- * otherRange = view.createRange( view.createPositionAt( bar, 1 ), view.createPositionAt( bar, 3 ); "ar" is in range.
- * transformed = range.getIntersection( otherRange ); // null - no common part.
- *
- * @param {module:engine/view/range~Range} otherRange Range to check for intersection.
- * @returns {module:engine/view/range~Range|null} A common part of given ranges or `null` if ranges have no common part.
- */
- getIntersection( otherRange ) {
- if ( this.isIntersecting( otherRange ) ) {
- // Ranges intersect, so a common range will be returned.
- // At most, it will be same as this range.
- let commonRangeStart = this.start;
- let commonRangeEnd = this.end;
- if ( this.containsPosition( otherRange.start ) ) {
- // Given range start is inside this range. This means thaNt we have to
- // shrink common range to the given range start.
- commonRangeStart = otherRange.start;
- }
- if ( this.containsPosition( otherRange.end ) ) {
- // Given range end is inside this range. This means that we have to
- // shrink common range to the given range end.
- commonRangeEnd = otherRange.end;
- }
- return new Range( commonRangeStart, commonRangeEnd );
- }
- // Ranges do not intersect, so they do not have common part.
- return null;
- }
- /**
- * Creates a {@link module:engine/view/treewalker~TreeWalker TreeWalker} instance with this range as a boundary.
- *
- * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
- * @param {module:engine/view/position~Position} [options.startPosition]
- * @param {Boolean} [options.singleCharacters=false]
- * @param {Boolean} [options.shallow=false]
- * @param {Boolean} [options.ignoreElementEnd=false]
- * @returns {module:engine/view/treewalker~TreeWalker}
- */
- getWalker( options = {} ) {
- options.boundaries = this;
- return new TreeWalker( options );
- }
- /**
- * Returns a {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment}
- * which is a common ancestor of range's both ends (in which the entire range is contained).
- *
- * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null}
- */
- getCommonAncestor() {
- return this.start.getCommonAncestor( this.end );
- }
- /**
- * Returns an {@link module:engine/view/element~Element Element} contained by the range.
- * The element will be returned when it is the **only** node within the range and **fully–contained**
- * at the same time.
- *
- * @returns {module:engine/view/element~Element|null}
- */
- getContainedElement() {
- if ( this.isCollapsed ) {
- return null;
- }
- let nodeAfterStart = this.start.nodeAfter;
- let nodeBeforeEnd = this.end.nodeBefore;
- // Handle the situation when the range position is at the beginning / at the end of a text node.
- // In such situation `.nodeAfter` and `.nodeBefore` are `null` but the range still might be spanning
- // over one element.
- //
- // <p>Foo{<span class="widget"></span>}bar</p> vs <p>Foo[<span class="widget"></span>]bar</p>
- //
- // These are basically the same range, only the difference is if the range position is at
- // at the end/at the beginning of a text node or just before/just after the text node.
- //
- if ( this.start.parent.is( '$text' ) && this.start.isAtEnd && this.start.parent.nextSibling ) {
- nodeAfterStart = this.start.parent.nextSibling;
- }
- if ( this.end.parent.is( '$text' ) && this.end.isAtStart && this.end.parent.previousSibling ) {
- nodeBeforeEnd = this.end.parent.previousSibling;
- }
- if ( nodeAfterStart && nodeAfterStart.is( 'element' ) && nodeAfterStart === nodeBeforeEnd ) {
- return nodeAfterStart;
- }
- return null;
- }
- /**
- * Clones this range.
- *
- * @returns {module:engine/view/range~Range}
- */
- clone() {
- return new Range( this.start, this.end );
- }
- /**
- * Returns an iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
- * them.
- *
- * This method uses {@link module:engine/view/treewalker~TreeWalker} with `boundaries` set to this range and `ignoreElementEnd` option
- * set to `true`. However it returns only {@link module:engine/view/item~Item items},
- * not {@link module:engine/view/treewalker~TreeWalkerValue}.
- *
- * You may specify additional options for the tree walker. See {@link module:engine/view/treewalker~TreeWalker} for
- * a full list of available options.
- *
- * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
- * @returns {Iterable.<module:engine/view/item~Item>}
- */
- * getItems( options = {} ) {
- options.boundaries = this;
- options.ignoreElementEnd = true;
- const treeWalker = new TreeWalker( options );
- for ( const value of treeWalker ) {
- yield value.item;
- }
- }
- /**
- * Returns an iterator that iterates over all {@link module:engine/view/position~Position positions} that are boundaries or
- * contained in this range.
- *
- * This method uses {@link module:engine/view/treewalker~TreeWalker} with `boundaries` set to this range. However it returns only
- * {@link module:engine/view/position~Position positions}, not {@link module:engine/view/treewalker~TreeWalkerValue}.
- *
- * You may specify additional options for the tree walker. See {@link module:engine/view/treewalker~TreeWalker} for
- * a full list of available options.
- *
- * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}.
- * @returns {Iterable.<module:engine/view/position~Position>}
- */
- * getPositions( options = {} ) {
- options.boundaries = this;
- const treeWalker = new TreeWalker( options );
- yield treeWalker.position;
- for ( const value of treeWalker ) {
- yield value.nextPosition;
- }
- }
- /**
- * Checks whether this object is of the given type.
- *
- * range.is( 'range' ); // -> true
- * range.is( 'view:range' ); // -> true
- *
- * range.is( 'model:range' ); // -> false
- * range.is( 'element' ); // -> false
- * range.is( 'selection' ); // -> false
- *
- * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
- *
- * @param {String} type
- * @returns {Boolean}
- */
- is( type ) {
- return type === 'range' || type === 'view:range';
- }
- /**
- * Checks and returns whether this range intersects with the given range.
- *
- * @param {module:engine/view/range~Range} otherRange Range to compare with.
- * @returns {Boolean} True if ranges intersect.
- */
- isIntersecting( otherRange ) {
- return this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start );
- }
- /**
- * Creates a range from the given parents and offsets.
- *
- * @protected
- * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} startElement Start position
- * parent element.
- * @param {Number} startOffset Start position offset.
- * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} endElement End position
- * parent element.
- * @param {Number} endOffset End position offset.
- * @returns {module:engine/view/range~Range} Created range.
- */
- static _createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
- return new this(
- new Position( startElement, startOffset ),
- new Position( endElement, endOffset )
- );
- }
- /**
- * Creates a new range, spreading from specified {@link module:engine/view/position~Position position} to a position moved by
- * given `shift`. If `shift` is a negative value, shifted position is treated as the beginning of the range.
- *
- * @protected
- * @param {module:engine/view/position~Position} position Beginning of the range.
- * @param {Number} shift How long the range should be.
- * @returns {module:engine/view/range~Range}
- */
- static _createFromPositionAndShift( position, shift ) {
- const start = position;
- const end = position.getShiftedBy( shift );
- return shift > 0 ? new this( start, end ) : new this( end, start );
- }
- /**
- * 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.
- *
- * @protected
- * @param {module:engine/view/element~Element} element Element which is a parent for the range.
- * @returns {module:engine/view/range~Range}
- */
- static _createIn( element ) {
- return this._createFromParentsAndOffsets( element, 0, element, element.childCount );
- }
- /**
- * Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
- *
- * @protected
- * @param {module:engine/view/item~Item} item
- * @returns {module:engine/view/range~Range}
- */
- static _createOn( item ) {
- const size = item.is( '$textProxy' ) ? item.offsetSize : 1;
- return this._createFromPositionAndShift( Position._createBefore( item ), size );
- }
- }
- // Function used by getEnlarged and getTrimmed methods.
- function enlargeTrimSkip( value ) {
- if ( value.item.is( 'attributeElement' ) || value.item.is( 'uiElement' ) ) {
- return true;
- }
- return false;
- }
|