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

Commit 0e29844

Browse files
authored
Merge pull request #1005 from ckeditor/t/1002
Feature: Introduced `Position#getCommonAncestor( position )` and `Range#getCommonAncestor()` methods for the view and model. Closes #1002.
2 parents 6217ea4 + 42f0b8e commit 0e29844

File tree

10 files changed

+274
-0
lines changed

10 files changed

+274
-0
lines changed

src/model/documentfragment.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,27 @@ export default class DocumentFragment {
171171
return [];
172172
}
173173

174+
/**
175+
* Returns a descendant node by its path relative to this element.
176+
*
177+
* // <this>a<b>c</b></this>
178+
* this.getNodeByPath( [ 0 ] ); // -> "a"
179+
* this.getNodeByPath( [ 1 ] ); // -> <b>
180+
* this.getNodeByPath( [ 1, 0 ] ); // -> "c"
181+
*
182+
* @param {Array.<Number>} relativePath Path of the node to find, relative to this element.
183+
* @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment}
184+
*/
185+
getNodeByPath( relativePath ) {
186+
let node = this; // eslint-disable-line consistent-this
187+
188+
for ( const index of relativePath ) {
189+
node = node.getChild( index );
190+
}
191+
192+
return node;
193+
}
194+
174195
/**
175196
* Converts offset "position" to index "position".
176197
*

src/model/position.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,26 @@ export default class Position {
318318
return this.path.slice( 0, diffAt );
319319
}
320320

321+
/**
322+
* Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
323+
* which is a common ancestor of both positions. The {@link #root roots} of these two positions must be identical.
324+
*
325+
* @param {module:engine/model/position~Position} position The second position.
326+
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
327+
*/
328+
getCommonAncestor( position ) {
329+
const ancestorsA = this.getAncestors();
330+
const ancestorsB = position.getAncestors();
331+
332+
let i = 0;
333+
334+
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
335+
i++;
336+
}
337+
338+
return i === 0 ? null : ancestorsA[ i - 1 ];
339+
}
340+
321341
/**
322342
* Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset
323343
* is shifted by `shift` value (can be a negative value).

src/model/range.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,16 @@ export default class Range {
445445
return ranges;
446446
}
447447

448+
/**
449+
* Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
450+
* which is a common ancestor of the range's both ends (in which the entire range is contained).
451+
*
452+
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
453+
*/
454+
getCommonAncestor() {
455+
return this.start.getCommonAncestor( this.end );
456+
}
457+
448458
/**
449459
* Returns a range that is a result of transforming this range by a change in the model document.
450460
*

src/view/position.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,26 @@ export default class Position {
175175
}
176176
}
177177

178+
/**
179+
* Returns a {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment}
180+
* which is a common ancestor of both positions.
181+
*
182+
* @param {module:engine/view/position~Position} position
183+
* @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null}
184+
*/
185+
getCommonAncestor( position ) {
186+
const ancestorsA = this.getAncestors();
187+
const ancestorsB = position.getAncestors();
188+
189+
let i = 0;
190+
191+
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
192+
i++;
193+
}
194+
195+
return i === 0 ? null : ancestorsA[ i - 1 ];
196+
}
197+
178198
/**
179199
* Checks whether this position equals given position.
180200
*

src/view/range.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,16 @@ export default class Range {
304304
return new TreeWalker( options );
305305
}
306306

307+
/**
308+
* Returns a {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment}
309+
* which is a common ancestor of range's both ends (in which the entire range is contained).
310+
*
311+
* @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null}
312+
*/
313+
getCommonAncestor() {
314+
return this.start.getCommonAncestor( this.end );
315+
}
316+
307317
/**
308318
* Returns an iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
309319
* them.

tests/model/documentfragment.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,25 @@ describe( 'DocumentFragment', () => {
300300
expect( deserialized.getChild( 1 ).parent ).to.equal( deserialized );
301301
} );
302302
} );
303+
304+
describe( 'getNodeByPath', () => {
305+
it( 'should return the whole document fragment if path is empty', () => {
306+
const frag = new DocumentFragment();
307+
308+
expect( frag.getNodeByPath( [] ) ).to.equal( frag );
309+
} );
310+
311+
it( 'should return a descendant of this node', () => {
312+
const image = new Element( 'image' );
313+
const element = new Element( 'elem', [], [
314+
new Element( 'elem', [], [
315+
new Text( 'foo' ),
316+
image
317+
] )
318+
] );
319+
const frag = new DocumentFragment( element );
320+
321+
expect( frag.getNodeByPath( [ 0, 0, 1 ] ) ).to.equal( image );
322+
} );
323+
} );
303324
} );

tests/model/position.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,4 +844,74 @@ describe( 'Position', () => {
844844
).to.throw( CKEditorError, /model-position-fromjson-no-root/ );
845845
} );
846846
} );
847+
848+
describe( 'getCommonAncestor()', () => {
849+
it( 'returns null when roots of both positions are not the same', () => {
850+
const pos1 = new Position( root, [ 0 ] );
851+
const pos2 = new Position( otherRoot, [ 0 ] );
852+
853+
test( pos1, pos2, null );
854+
} );
855+
856+
it( 'for two the same positions returns the parent element #1', () => {
857+
const fPosition = new Position( root, [ 1, 0, 0 ] );
858+
const otherPosition = new Position( root, [ 1, 0, 0 ] );
859+
860+
test( fPosition, otherPosition, li1 );
861+
} );
862+
863+
it( 'for two the same positions returns the parent element #2', () => {
864+
const doc = new Document();
865+
const root = doc.createRoot();
866+
867+
const p = new Element( 'p', null, 'foobar' );
868+
869+
root.appendChildren( p );
870+
871+
const postion = new Position( root, [ 0, 3 ] ); // <p>foo^bar</p>
872+
873+
test( postion, postion, p );
874+
} );
875+
876+
it( 'for two positions in the same element returns the element', () => {
877+
const fPosition = new Position( root, [ 1, 0, 0 ] );
878+
const zPosition = new Position( root, [ 1, 0, 2 ] );
879+
880+
test( fPosition, zPosition, li1 );
881+
} );
882+
883+
it( 'works when one positions is nested deeper than the other', () => {
884+
const zPosition = new Position( root, [ 1, 0, 2 ] );
885+
const liPosition = new Position( root, [ 1, 1 ] );
886+
887+
test( liPosition, zPosition, ul );
888+
} );
889+
890+
// Checks if by mistake someone didn't use getCommonPath() + getNodeByPath().
891+
it( 'works if position is located before an element', () => {
892+
const doc = new Document();
893+
const root = doc.createRoot();
894+
895+
const p = new Element( 'p', null, new Element( 'a' ) );
896+
897+
root.appendChildren( p );
898+
899+
const postion = new Position( root, [ 0, 0 ] ); // <p>^<a></a></p>
900+
901+
test( postion, postion, p );
902+
} );
903+
904+
it( 'works fine with positions located in DocumentFragment', () => {
905+
const docFrag = new DocumentFragment( [ p, ul ] );
906+
const zPosition = new Position( docFrag, [ 1, 0, 2 ] );
907+
const afterLiPosition = new Position( docFrag, [ 1, 2 ] );
908+
909+
test( zPosition, afterLiPosition, ul );
910+
} );
911+
912+
function test( positionA, positionB, lca ) {
913+
expect( positionA.getCommonAncestor( positionB ) ).to.equal( lca );
914+
expect( positionB.getCommonAncestor( positionA ) ).to.equal( lca );
915+
}
916+
} );
847917
} );

tests/model/range.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,12 @@ describe( 'Range', () => {
12231223
} );
12241224
} );
12251225

1226+
describe( 'getCommonAncestor()', () => {
1227+
it( 'should return common ancestor for positions from Range', () => {
1228+
expect( range.getCommonAncestor() ).to.equal( root );
1229+
} );
1230+
} );
1231+
12261232
function mapNodesToNames( nodes ) {
12271233
return nodes.map( node => {
12281234
return ( node instanceof Element ) ? 'E:' + node.name : 'T:' + node.data;

tests/view/position.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,4 +519,84 @@ describe( 'Position', () => {
519519
document.destroy();
520520
} );
521521
} );
522+
523+
describe( 'getCommonAncestor()', () => {
524+
let div, ul, liUl1, liUl2, texts, section, article, ol, liOl1, liOl2, p;
525+
526+
// |- div
527+
// |- ul
528+
// | |- li
529+
// | | |- foz
530+
// | |- li
531+
// | |- bar
532+
// |- section
533+
// |- Sed id libero at libero tristique
534+
// |- article
535+
// | |- ol
536+
// | | |- li
537+
// | | | |- Lorem ipsum dolor sit amet.
538+
// | | |- li
539+
// | | |- Mauris tincidunt tincidunt leo ac rutrum.
540+
// | |- p
541+
// | | |- Maecenas accumsan tellus.
542+
543+
beforeEach( () => {
544+
texts = {
545+
foz: new Text( 'foz' ),
546+
bar: new Text( 'bar' ),
547+
lorem: new Text( 'Lorem ipsum dolor sit amet.' ),
548+
mauris: new Text( 'Mauris tincidunt tincidunt leo ac rutrum.' ),
549+
maecenas: new Text( 'Maecenas accumsan tellus.' ),
550+
sed: new Text( 'Sed id libero at libero tristique.' )
551+
};
552+
553+
liUl1 = new Element( 'li', null, texts.foz );
554+
liUl2 = new Element( 'li', null, texts.bar );
555+
ul = new Element( 'ul', null, [ liUl1, liUl2 ] );
556+
557+
liOl1 = new Element( 'li', null, texts.lorem );
558+
liOl2 = new Element( 'li', null, texts.mauris );
559+
ol = new Element( 'ol', null, [ liOl1, liOl2 ] );
560+
561+
p = new Element( 'p', null, texts.maecenas );
562+
563+
article = new Element( 'article', null, [ ol, p ] );
564+
section = new Element( 'section', null, [ texts.sed, article ] );
565+
566+
div = new Element( 'div', null, [ ul, section ] );
567+
} );
568+
569+
it( 'for two the same positions returns the parent element', () => {
570+
const afterLoremPosition = new Position( liOl1, 5 );
571+
const otherPosition = Position.createFromPosition( afterLoremPosition );
572+
573+
test( afterLoremPosition, otherPosition, liOl1 );
574+
} );
575+
576+
it( 'for two positions in the same element returns the element', () => {
577+
const startMaecenasPosition = Position.createAt( liOl2 );
578+
const beforeTellusPosition = new Position( liOl2, 18 );
579+
580+
test( startMaecenasPosition, beforeTellusPosition, liOl2 );
581+
} );
582+
583+
it( 'works when one of the positions is nested deeper than the other #1', () => {
584+
const firstPosition = new Position( liUl1, 1 );
585+
const secondPosition = new Position( p, 3 );
586+
587+
test( firstPosition, secondPosition, div );
588+
} );
589+
590+
it( 'works when one of the positions is nested deeper than the other #2', () => {
591+
const firstPosition = new Position( liOl2, 10 );
592+
const secondPosition = new Position( section, 1 );
593+
594+
test( firstPosition, secondPosition, section );
595+
} );
596+
597+
function test( positionA, positionB, lca ) {
598+
expect( positionA.getCommonAncestor( positionB ) ).to.equal( lca );
599+
expect( positionB.getCommonAncestor( positionA ) ).to.equal( lca );
600+
}
601+
} );
522602
} );

tests/view/range.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,4 +692,20 @@ describe( 'Range', () => {
692692
} );
693693
} );
694694
} );
695+
696+
describe( 'getCommonAncestor()', () => {
697+
it( 'should return common ancestor for positions from Range', () => {
698+
const foz = new Text( 'foz' );
699+
const bar = new Text( 'bar' );
700+
701+
const li1 = new Element( 'li', null, foz );
702+
const li2 = new Element( 'li', null, bar );
703+
704+
const ul = new Element( 'ul', null, [ li1, li2 ] );
705+
706+
const range = new Range( new Position( li1, 0 ), new Position( li2, 2 ) );
707+
708+
expect( range.getCommonAncestor() ).to.equal( ul );
709+
} );
710+
} );
695711
} );

0 commit comments

Comments
 (0)