Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #1005 from ckeditor/t/1002
Browse files Browse the repository at this point in the history
Feature: Introduced `Position#getCommonAncestor( position )` and `Range#getCommonAncestor()` methods for the view and model. Closes #1002.
  • Loading branch information
Reinmar authored Jul 11, 2017
2 parents 6217ea4 + 42f0b8e commit 0e29844
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 0 deletions.
21 changes: 21 additions & 0 deletions src/model/documentfragment.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,27 @@ export default class DocumentFragment {
return [];
}

/**
* Returns a descendant node by its path relative to this element.
*
* // <this>a<b>c</b></this>
* this.getNodeByPath( [ 0 ] ); // -> "a"
* this.getNodeByPath( [ 1 ] ); // -> <b>
* this.getNodeByPath( [ 1, 0 ] ); // -> "c"
*
* @param {Array.<Number>} relativePath Path of the node to find, relative to this element.
* @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment}
*/
getNodeByPath( relativePath ) {
let node = this; // eslint-disable-line consistent-this

for ( const index of relativePath ) {
node = node.getChild( index );
}

return node;
}

/**
* Converts offset "position" to index "position".
*
Expand Down
20 changes: 20 additions & 0 deletions src/model/position.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,26 @@ export default class Position {
return this.path.slice( 0, diffAt );
}

/**
* Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
* which is a common ancestor of both positions. The {@link #root roots} of these two positions must be identical.
*
* @param {module:engine/model/position~Position} position The second position.
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
getCommonAncestor( position ) {
const ancestorsA = this.getAncestors();
const ancestorsB = position.getAncestors();

let i = 0;

while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
i++;
}

return i === 0 ? null : ancestorsA[ i - 1 ];
}

/**
* Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset
* is shifted by `shift` value (can be a negative value).
Expand Down
10 changes: 10 additions & 0 deletions src/model/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,16 @@ export default class Range {
return ranges;
}

/**
* Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
* which is a common ancestor of the range's both ends (in which the entire range is contained).
*
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
getCommonAncestor() {
return this.start.getCommonAncestor( this.end );
}

/**
* Returns a range that is a result of transforming this range by a change in the model document.
*
Expand Down
20 changes: 20 additions & 0 deletions src/view/position.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,26 @@ export default class Position {
}
}

/**
* Returns a {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment}
* which is a common ancestor of both positions.
*
* @param {module:engine/view/position~Position} position
* @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null}
*/
getCommonAncestor( position ) {
const ancestorsA = this.getAncestors();
const ancestorsB = position.getAncestors();

let i = 0;

while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
i++;
}

return i === 0 ? null : ancestorsA[ i - 1 ];
}

/**
* Checks whether this position equals given position.
*
Expand Down
10 changes: 10 additions & 0 deletions src/view/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,16 @@ export default class Range {
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 iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
* them.
Expand Down
21 changes: 21 additions & 0 deletions tests/model/documentfragment.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,4 +300,25 @@ describe( 'DocumentFragment', () => {
expect( deserialized.getChild( 1 ).parent ).to.equal( deserialized );
} );
} );

describe( 'getNodeByPath', () => {
it( 'should return the whole document fragment if path is empty', () => {
const frag = new DocumentFragment();

expect( frag.getNodeByPath( [] ) ).to.equal( frag );
} );

it( 'should return a descendant of this node', () => {
const image = new Element( 'image' );
const element = new Element( 'elem', [], [
new Element( 'elem', [], [
new Text( 'foo' ),
image
] )
] );
const frag = new DocumentFragment( element );

expect( frag.getNodeByPath( [ 0, 0, 1 ] ) ).to.equal( image );
} );
} );
} );
70 changes: 70 additions & 0 deletions tests/model/position.js
Original file line number Diff line number Diff line change
Expand Up @@ -844,4 +844,74 @@ describe( 'Position', () => {
).to.throw( CKEditorError, /model-position-fromjson-no-root/ );
} );
} );

describe( 'getCommonAncestor()', () => {
it( 'returns null when roots of both positions are not the same', () => {
const pos1 = new Position( root, [ 0 ] );
const pos2 = new Position( otherRoot, [ 0 ] );

test( pos1, pos2, null );
} );

it( 'for two the same positions returns the parent element #1', () => {
const fPosition = new Position( root, [ 1, 0, 0 ] );
const otherPosition = new Position( root, [ 1, 0, 0 ] );

test( fPosition, otherPosition, li1 );
} );

it( 'for two the same positions returns the parent element #2', () => {
const doc = new Document();
const root = doc.createRoot();

const p = new Element( 'p', null, 'foobar' );

root.appendChildren( p );

const postion = new Position( root, [ 0, 3 ] ); // <p>foo^bar</p>

test( postion, postion, p );
} );

it( 'for two positions in the same element returns the element', () => {
const fPosition = new Position( root, [ 1, 0, 0 ] );
const zPosition = new Position( root, [ 1, 0, 2 ] );

test( fPosition, zPosition, li1 );
} );

it( 'works when one positions is nested deeper than the other', () => {
const zPosition = new Position( root, [ 1, 0, 2 ] );
const liPosition = new Position( root, [ 1, 1 ] );

test( liPosition, zPosition, ul );
} );

// Checks if by mistake someone didn't use getCommonPath() + getNodeByPath().
it( 'works if position is located before an element', () => {
const doc = new Document();
const root = doc.createRoot();

const p = new Element( 'p', null, new Element( 'a' ) );

root.appendChildren( p );

const postion = new Position( root, [ 0, 0 ] ); // <p>^<a></a></p>

test( postion, postion, p );
} );

it( 'works fine with positions located in DocumentFragment', () => {
const docFrag = new DocumentFragment( [ p, ul ] );
const zPosition = new Position( docFrag, [ 1, 0, 2 ] );
const afterLiPosition = new Position( docFrag, [ 1, 2 ] );

test( zPosition, afterLiPosition, ul );
} );

function test( positionA, positionB, lca ) {
expect( positionA.getCommonAncestor( positionB ) ).to.equal( lca );
expect( positionB.getCommonAncestor( positionA ) ).to.equal( lca );
}
} );
} );
6 changes: 6 additions & 0 deletions tests/model/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,12 @@ describe( 'Range', () => {
} );
} );

describe( 'getCommonAncestor()', () => {
it( 'should return common ancestor for positions from Range', () => {
expect( range.getCommonAncestor() ).to.equal( root );
} );
} );

function mapNodesToNames( nodes ) {
return nodes.map( node => {
return ( node instanceof Element ) ? 'E:' + node.name : 'T:' + node.data;
Expand Down
80 changes: 80 additions & 0 deletions tests/view/position.js
Original file line number Diff line number Diff line change
Expand Up @@ -519,4 +519,84 @@ describe( 'Position', () => {
document.destroy();
} );
} );

describe( 'getCommonAncestor()', () => {
let div, ul, liUl1, liUl2, texts, section, article, ol, liOl1, liOl2, p;

// |- div
// |- ul
// | |- li
// | | |- foz
// | |- li
// | |- bar
// |- section
// |- Sed id libero at libero tristique
// |- article
// | |- ol
// | | |- li
// | | | |- Lorem ipsum dolor sit amet.
// | | |- li
// | | |- Mauris tincidunt tincidunt leo ac rutrum.
// | |- p
// | | |- Maecenas accumsan tellus.

beforeEach( () => {
texts = {
foz: new Text( 'foz' ),
bar: new Text( 'bar' ),
lorem: new Text( 'Lorem ipsum dolor sit amet.' ),
mauris: new Text( 'Mauris tincidunt tincidunt leo ac rutrum.' ),
maecenas: new Text( 'Maecenas accumsan tellus.' ),
sed: new Text( 'Sed id libero at libero tristique.' )
};

liUl1 = new Element( 'li', null, texts.foz );
liUl2 = new Element( 'li', null, texts.bar );
ul = new Element( 'ul', null, [ liUl1, liUl2 ] );

liOl1 = new Element( 'li', null, texts.lorem );
liOl2 = new Element( 'li', null, texts.mauris );
ol = new Element( 'ol', null, [ liOl1, liOl2 ] );

p = new Element( 'p', null, texts.maecenas );

article = new Element( 'article', null, [ ol, p ] );
section = new Element( 'section', null, [ texts.sed, article ] );

div = new Element( 'div', null, [ ul, section ] );
} );

it( 'for two the same positions returns the parent element', () => {
const afterLoremPosition = new Position( liOl1, 5 );
const otherPosition = Position.createFromPosition( afterLoremPosition );

test( afterLoremPosition, otherPosition, liOl1 );
} );

it( 'for two positions in the same element returns the element', () => {
const startMaecenasPosition = Position.createAt( liOl2 );
const beforeTellusPosition = new Position( liOl2, 18 );

test( startMaecenasPosition, beforeTellusPosition, liOl2 );
} );

it( 'works when one of the positions is nested deeper than the other #1', () => {
const firstPosition = new Position( liUl1, 1 );
const secondPosition = new Position( p, 3 );

test( firstPosition, secondPosition, div );
} );

it( 'works when one of the positions is nested deeper than the other #2', () => {
const firstPosition = new Position( liOl2, 10 );
const secondPosition = new Position( section, 1 );

test( firstPosition, secondPosition, section );
} );

function test( positionA, positionB, lca ) {
expect( positionA.getCommonAncestor( positionB ) ).to.equal( lca );
expect( positionB.getCommonAncestor( positionA ) ).to.equal( lca );
}
} );
} );
16 changes: 16 additions & 0 deletions tests/view/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -692,4 +692,20 @@ describe( 'Range', () => {
} );
} );
} );

describe( 'getCommonAncestor()', () => {
it( 'should return common ancestor for positions from Range', () => {
const foz = new Text( 'foz' );
const bar = new Text( 'bar' );

const li1 = new Element( 'li', null, foz );
const li2 = new Element( 'li', null, bar );

const ul = new Element( 'ul', null, [ li1, li2 ] );

const range = new Range( new Position( li1, 0 ), new Position( li2, 2 ) );

expect( range.getCommonAncestor() ).to.equal( ul );
} );
} );
} );

0 comments on commit 0e29844

Please sign in to comment.