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 #1035 from ckeditor/t/1033
Browse files Browse the repository at this point in the history
Feature: Introduced `model.Node#getCommonAncestor()` and `view.Node#getCommonAncestor()`. Closes #1033.
  • Loading branch information
pomek authored Jul 21, 2017
2 parents 7c014f7 + 537eac9 commit f913aee
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 17 deletions.
20 changes: 20 additions & 0 deletions src/model/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,26 @@ export default class Node {
return ancestors;
}

/**
* Returns a {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
* which is a common ancestor of both nodes.
*
* @param {module:engine/model/node~Node} node The second node.
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
getCommonAncestor( node ) {
const ancestorsA = this.getAncestors();
const ancestorsB = node.getAncestors();

let i = 0;

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

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

/**
* Removes this node from it's parent.
*/
Expand Down
20 changes: 20 additions & 0 deletions src/view/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,26 @@ export default class Node {
return ancestors;
}

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

let i = 0;

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

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

/**
* Removes node from parent.
*/
Expand Down
61 changes: 56 additions & 5 deletions tests/model/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe( 'Node', () => {
} );
} );

describe( 'getIndex', () => {
describe( 'getIndex()', () => {
it( 'should return null if the parent is null', () => {
expect( root.index ).to.be.null;
} );
Expand All @@ -134,7 +134,7 @@ describe( 'Node', () => {
} );
} );

describe( 'clone', () => {
describe( 'clone()', () => {
it( 'should return a copy of cloned node', () => {
const node = new Node( { foo: 'bar' } );
const copy = node.clone();
Expand All @@ -144,7 +144,7 @@ describe( 'Node', () => {
} );
} );

describe( 'remove', () => {
describe( 'remove()', () => {
it( 'should remove node from it\'s parent', () => {
const element = new Element( 'p' );
element.appendChildren( node );
Expand Down Expand Up @@ -204,7 +204,7 @@ describe( 'Node', () => {
} );
} );

describe( 'getPath', () => {
describe( 'getPath()', () => {
it( 'should return proper path', () => {
expect( root.getPath() ).to.deep.equal( [] );

Expand All @@ -218,7 +218,7 @@ describe( 'Node', () => {
} );
} );

describe( 'getAncestors', () => {
describe( 'getAncestors()', () => {
it( 'should return proper array of ancestor nodes', () => {
expect( root.getAncestors() ).to.deep.equal( [] );
expect( two.getAncestors() ).to.deep.equal( [ root ] );
Expand All @@ -242,6 +242,57 @@ describe( 'Node', () => {
} );
} );

describe( 'getCommonAncestor()', () => {
it( 'should return the parent element for the same node', () => {
expect( img.getCommonAncestor( img ) ).to.equal( two );
} );

it( 'should return null for detached subtrees', () => {
const detached = new Element( 'foo' );

expect( img.getCommonAncestor( detached ) ).to.be.null;
expect( detached.getCommonAncestor( img ) ).to.be.null;
} );

it( 'should return null when one of the nodes is a tree root itself', () => {
expect( root.getCommonAncestor( img ) ).to.be.null;
expect( img.getCommonAncestor( root ) ).to.be.null;
expect( root.getCommonAncestor( root ) ).to.be.null;
} );

it( 'should return parent of the nodes at the same level', () => {
expect( img.getCommonAncestor( textBA ) ).to.equal( two );
expect( textR.getCommonAncestor( textBA ) ).to.equal( two );
} );

it( 'should return proper element for nodes in different branches and on different levels', () => {
const foo = new Text( 'foo' );
const bar = new Text( 'bar' );
const bom = new Text( 'bom' );
const d = new Element( 'd', null, [ bar ] );
const c = new Element( 'c', null, [ foo, d ] );
const b = new Element( 'b', null, [ c ] );
const e = new Element( 'e', null, [ bom ] );
const a = new Element( 'a', null, [ b, e ] );

// <a><b><c>foo<d>bar</d></c></b><e>bom</e></a>

expect( bar.getCommonAncestor( foo ), 1 ).to.equal( c );
expect( foo.getCommonAncestor( d ), 2 ).to.equal( c );
expect( c.getCommonAncestor( b ), 3 ).to.equal( a );
expect( bom.getCommonAncestor( d ), 4 ).to.equal( a );
expect( b.getCommonAncestor( bom ), 5 ).to.equal( a );
} );

it( 'should return document fragment', () => {
const foo = new Text( 'foo' );
const bar = new Text( 'bar' );
const df = new DocumentFragment( [ foo, bar ] );

expect( foo.getCommonAncestor( bar ) ).to.equal( df );
} );
} );

describe( 'attributes interface', () => {
const node = new Node( { foo: 'bar' } );

Expand Down
75 changes: 63 additions & 12 deletions tests/view/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe( 'Node', () => {
root = new Element( null, null, [ one, two, three ] );
} );

describe( 'getNextSibling/getPreviousSibling', () => {
describe( 'getNextSibling/getPreviousSibling()', () => {
it( 'should return next sibling', () => {
expect( root.nextSibling ).to.be.null;

Expand Down Expand Up @@ -57,7 +57,7 @@ describe( 'Node', () => {
} );
} );

describe( 'getAncestors', () => {
describe( 'getAncestors()', () => {
it( 'should return empty array for node without ancestors', () => {
const result = root.getAncestors();
expect( result ).to.be.an( 'array' );
Expand Down Expand Up @@ -109,7 +109,58 @@ describe( 'Node', () => {
} );
} );

describe( 'getIndex', () => {
describe( 'getCommonAncestor()', () => {
it( 'should return the parent element for the same node', () => {
expect( img.getCommonAncestor( img ) ).to.equal( two );
} );

it( 'should return null for detached subtrees', () => {
const detached = new Element( 'foo' );

expect( img.getCommonAncestor( detached ) ).to.be.null;
expect( detached.getCommonAncestor( img ) ).to.be.null;
} );

it( 'should return null when one of the nodes is a tree root itself', () => {
expect( root.getCommonAncestor( img ) ).to.be.null;
expect( img.getCommonAncestor( root ) ).to.be.null;
expect( root.getCommonAncestor( root ) ).to.be.null;
} );

it( 'should return parent of the nodes at the same level', () => {
expect( img.getCommonAncestor( charA ) ).to.equal( two );
expect( charB.getCommonAncestor( charA ) ).to.equal( two );
} );

it( 'should return proper element for nodes in different branches and on different levels', () => {
const foo = new Text( 'foo' );
const bar = new Text( 'bar' );
const bom = new Text( 'bom' );
const d = new Element( 'd', null, [ bar ] );
const c = new Element( 'c', null, [ foo, d ] );
const b = new Element( 'b', null, [ c ] );
const e = new Element( 'e', null, [ bom ] );
const a = new Element( 'a', null, [ b, e ] );

// <a><b><c>foo<d>bar</d></c></b><e>bom</e></a>

expect( bar.getCommonAncestor( foo ), 1 ).to.equal( c );
expect( foo.getCommonAncestor( d ), 2 ).to.equal( c );
expect( c.getCommonAncestor( b ), 3 ).to.equal( a );
expect( bom.getCommonAncestor( d ), 4 ).to.equal( a );
expect( b.getCommonAncestor( bom ), 5 ).to.equal( a );
} );

it( 'should return document fragment', () => {
const foo = new Text( 'foo' );
const bar = new Text( 'bar' );
const df = new DocumentFragment( [ foo, bar ] );

expect( foo.getCommonAncestor( bar ) ).to.equal( df );
} );
} );

describe( 'getIndex()', () => {
it( 'should return null if the parent is null', () => {
expect( root.index ).to.be.null;
} );
Expand Down Expand Up @@ -139,7 +190,7 @@ describe( 'Node', () => {
} );
} );

describe( 'getDocument', () => {
describe( 'getDocument()', () => {
it( 'should return null if any parent has not set Document', () => {
expect( charA.document ).to.be.null;
} );
Expand All @@ -164,7 +215,7 @@ describe( 'Node', () => {
} );
} );

describe( 'getRoot', () => {
describe( 'getRoot()', () => {
it( 'should return this element if it has no parent', () => {
const child = new Element( 'p' );

Expand All @@ -183,7 +234,7 @@ describe( 'Node', () => {
} );
} );

describe( 'remove', () => {
describe( 'remove()', () => {
it( 'should remove node from its parent', () => {
const char = new Text( 'a' );
const parent = new Element( 'p', null, [ char ] );
Expand Down Expand Up @@ -246,7 +297,7 @@ describe( 'Node', () => {
sinon.assert.calledWith( rootChangeSpy, 'attributes', img );
} );

describe( 'setAttribute', () => {
describe( 'setAttribute()', () => {
it( 'should fire change event', () => {
img.setAttribute( 'width', 100 );

Expand All @@ -255,7 +306,7 @@ describe( 'Node', () => {
} );
} );

describe( 'removeAttribute', () => {
describe( 'removeAttribute()', () => {
it( 'should fire change event', () => {
img.removeAttribute( 'src' );

Expand All @@ -264,7 +315,7 @@ describe( 'Node', () => {
} );
} );

describe( 'insertChildren', () => {
describe( 'insertChildren()', () => {
it( 'should fire change event', () => {
root.insertChildren( 1, new Element( 'img' ) );

Expand All @@ -273,7 +324,7 @@ describe( 'Node', () => {
} );
} );

describe( 'appendChildren', () => {
describe( 'appendChildren()', () => {
it( 'should fire change event', () => {
root.appendChildren( new Element( 'img' ) );

Expand All @@ -282,7 +333,7 @@ describe( 'Node', () => {
} );
} );

describe( 'removeChildren', () => {
describe( 'removeChildren()', () => {
it( 'should fire change event', () => {
root.removeChildren( 1, 1 );

Expand All @@ -291,7 +342,7 @@ describe( 'Node', () => {
} );
} );

describe( 'removeChildren', () => {
describe( 'removeChildren()', () => {
it( 'should fire change event', () => {
text.data = 'bar';

Expand Down

0 comments on commit f913aee

Please sign in to comment.