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

Commit f913aee

Browse files
authored
Merge pull request #1035 from ckeditor/t/1033
Feature: Introduced `model.Node#getCommonAncestor()` and `view.Node#getCommonAncestor()`. Closes #1033.
2 parents 7c014f7 + 537eac9 commit f913aee

File tree

4 files changed

+159
-17
lines changed

4 files changed

+159
-17
lines changed

src/model/node.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,26 @@ export default class Node {
269269
return ancestors;
270270
}
271271

272+
/**
273+
* Returns a {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
274+
* which is a common ancestor of both nodes.
275+
*
276+
* @param {module:engine/model/node~Node} node The second node.
277+
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
278+
*/
279+
getCommonAncestor( node ) {
280+
const ancestorsA = this.getAncestors();
281+
const ancestorsB = node.getAncestors();
282+
283+
let i = 0;
284+
285+
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
286+
i++;
287+
}
288+
289+
return i === 0 ? null : ancestorsA[ i - 1 ];
290+
}
291+
272292
/**
273293
* Removes this node from it's parent.
274294
*/

src/view/node.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,26 @@ export default class Node {
138138
return ancestors;
139139
}
140140

141+
/**
142+
* Returns a {@link module:engine/view/element~Element} or {@link module:engine/view/documentfragment~DocumentFragment}
143+
* which is a common ancestor of both nodes.
144+
*
145+
* @param {module:engine/view/node~Node} node The second node.
146+
* @returns {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|null}
147+
*/
148+
getCommonAncestor( node ) {
149+
const ancestorsA = this.getAncestors();
150+
const ancestorsB = node.getAncestors();
151+
152+
let i = 0;
153+
154+
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
155+
i++;
156+
}
157+
158+
return i === 0 ? null : ancestorsA[ i - 1 ];
159+
}
160+
141161
/**
142162
* Removes node from parent.
143163
*/

tests/model/node.js

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ describe( 'Node', () => {
108108
} );
109109
} );
110110

111-
describe( 'getIndex', () => {
111+
describe( 'getIndex()', () => {
112112
it( 'should return null if the parent is null', () => {
113113
expect( root.index ).to.be.null;
114114
} );
@@ -134,7 +134,7 @@ describe( 'Node', () => {
134134
} );
135135
} );
136136

137-
describe( 'clone', () => {
137+
describe( 'clone()', () => {
138138
it( 'should return a copy of cloned node', () => {
139139
const node = new Node( { foo: 'bar' } );
140140
const copy = node.clone();
@@ -144,7 +144,7 @@ describe( 'Node', () => {
144144
} );
145145
} );
146146

147-
describe( 'remove', () => {
147+
describe( 'remove()', () => {
148148
it( 'should remove node from it\'s parent', () => {
149149
const element = new Element( 'p' );
150150
element.appendChildren( node );
@@ -204,7 +204,7 @@ describe( 'Node', () => {
204204
} );
205205
} );
206206

207-
describe( 'getPath', () => {
207+
describe( 'getPath()', () => {
208208
it( 'should return proper path', () => {
209209
expect( root.getPath() ).to.deep.equal( [] );
210210

@@ -218,7 +218,7 @@ describe( 'Node', () => {
218218
} );
219219
} );
220220

221-
describe( 'getAncestors', () => {
221+
describe( 'getAncestors()', () => {
222222
it( 'should return proper array of ancestor nodes', () => {
223223
expect( root.getAncestors() ).to.deep.equal( [] );
224224
expect( two.getAncestors() ).to.deep.equal( [ root ] );
@@ -242,6 +242,57 @@ describe( 'Node', () => {
242242
} );
243243
} );
244244

245+
describe( 'getCommonAncestor()', () => {
246+
it( 'should return the parent element for the same node', () => {
247+
expect( img.getCommonAncestor( img ) ).to.equal( two );
248+
} );
249+
250+
it( 'should return null for detached subtrees', () => {
251+
const detached = new Element( 'foo' );
252+
253+
expect( img.getCommonAncestor( detached ) ).to.be.null;
254+
expect( detached.getCommonAncestor( img ) ).to.be.null;
255+
} );
256+
257+
it( 'should return null when one of the nodes is a tree root itself', () => {
258+
expect( root.getCommonAncestor( img ) ).to.be.null;
259+
expect( img.getCommonAncestor( root ) ).to.be.null;
260+
expect( root.getCommonAncestor( root ) ).to.be.null;
261+
} );
262+
263+
it( 'should return parent of the nodes at the same level', () => {
264+
expect( img.getCommonAncestor( textBA ) ).to.equal( two );
265+
expect( textR.getCommonAncestor( textBA ) ).to.equal( two );
266+
} );
267+
268+
it( 'should return proper element for nodes in different branches and on different levels', () => {
269+
const foo = new Text( 'foo' );
270+
const bar = new Text( 'bar' );
271+
const bom = new Text( 'bom' );
272+
const d = new Element( 'd', null, [ bar ] );
273+
const c = new Element( 'c', null, [ foo, d ] );
274+
const b = new Element( 'b', null, [ c ] );
275+
const e = new Element( 'e', null, [ bom ] );
276+
const a = new Element( 'a', null, [ b, e ] );
277+
278+
// <a><b><c>foo<d>bar</d></c></b><e>bom</e></a>
279+
280+
expect( bar.getCommonAncestor( foo ), 1 ).to.equal( c );
281+
expect( foo.getCommonAncestor( d ), 2 ).to.equal( c );
282+
expect( c.getCommonAncestor( b ), 3 ).to.equal( a );
283+
expect( bom.getCommonAncestor( d ), 4 ).to.equal( a );
284+
expect( b.getCommonAncestor( bom ), 5 ).to.equal( a );
285+
} );
286+
287+
it( 'should return document fragment', () => {
288+
const foo = new Text( 'foo' );
289+
const bar = new Text( 'bar' );
290+
const df = new DocumentFragment( [ foo, bar ] );
291+
292+
expect( foo.getCommonAncestor( bar ) ).to.equal( df );
293+
} );
294+
} );
295+
245296
describe( 'attributes interface', () => {
246297
const node = new Node( { foo: 'bar' } );
247298

tests/view/node.js

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe( 'Node', () => {
2929
root = new Element( null, null, [ one, two, three ] );
3030
} );
3131

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

@@ -57,7 +57,7 @@ describe( 'Node', () => {
5757
} );
5858
} );
5959

60-
describe( 'getAncestors', () => {
60+
describe( 'getAncestors()', () => {
6161
it( 'should return empty array for node without ancestors', () => {
6262
const result = root.getAncestors();
6363
expect( result ).to.be.an( 'array' );
@@ -109,7 +109,58 @@ describe( 'Node', () => {
109109
} );
110110
} );
111111

112-
describe( 'getIndex', () => {
112+
describe( 'getCommonAncestor()', () => {
113+
it( 'should return the parent element for the same node', () => {
114+
expect( img.getCommonAncestor( img ) ).to.equal( two );
115+
} );
116+
117+
it( 'should return null for detached subtrees', () => {
118+
const detached = new Element( 'foo' );
119+
120+
expect( img.getCommonAncestor( detached ) ).to.be.null;
121+
expect( detached.getCommonAncestor( img ) ).to.be.null;
122+
} );
123+
124+
it( 'should return null when one of the nodes is a tree root itself', () => {
125+
expect( root.getCommonAncestor( img ) ).to.be.null;
126+
expect( img.getCommonAncestor( root ) ).to.be.null;
127+
expect( root.getCommonAncestor( root ) ).to.be.null;
128+
} );
129+
130+
it( 'should return parent of the nodes at the same level', () => {
131+
expect( img.getCommonAncestor( charA ) ).to.equal( two );
132+
expect( charB.getCommonAncestor( charA ) ).to.equal( two );
133+
} );
134+
135+
it( 'should return proper element for nodes in different branches and on different levels', () => {
136+
const foo = new Text( 'foo' );
137+
const bar = new Text( 'bar' );
138+
const bom = new Text( 'bom' );
139+
const d = new Element( 'd', null, [ bar ] );
140+
const c = new Element( 'c', null, [ foo, d ] );
141+
const b = new Element( 'b', null, [ c ] );
142+
const e = new Element( 'e', null, [ bom ] );
143+
const a = new Element( 'a', null, [ b, e ] );
144+
145+
// <a><b><c>foo<d>bar</d></c></b><e>bom</e></a>
146+
147+
expect( bar.getCommonAncestor( foo ), 1 ).to.equal( c );
148+
expect( foo.getCommonAncestor( d ), 2 ).to.equal( c );
149+
expect( c.getCommonAncestor( b ), 3 ).to.equal( a );
150+
expect( bom.getCommonAncestor( d ), 4 ).to.equal( a );
151+
expect( b.getCommonAncestor( bom ), 5 ).to.equal( a );
152+
} );
153+
154+
it( 'should return document fragment', () => {
155+
const foo = new Text( 'foo' );
156+
const bar = new Text( 'bar' );
157+
const df = new DocumentFragment( [ foo, bar ] );
158+
159+
expect( foo.getCommonAncestor( bar ) ).to.equal( df );
160+
} );
161+
} );
162+
163+
describe( 'getIndex()', () => {
113164
it( 'should return null if the parent is null', () => {
114165
expect( root.index ).to.be.null;
115166
} );
@@ -139,7 +190,7 @@ describe( 'Node', () => {
139190
} );
140191
} );
141192

142-
describe( 'getDocument', () => {
193+
describe( 'getDocument()', () => {
143194
it( 'should return null if any parent has not set Document', () => {
144195
expect( charA.document ).to.be.null;
145196
} );
@@ -164,7 +215,7 @@ describe( 'Node', () => {
164215
} );
165216
} );
166217

167-
describe( 'getRoot', () => {
218+
describe( 'getRoot()', () => {
168219
it( 'should return this element if it has no parent', () => {
169220
const child = new Element( 'p' );
170221

@@ -183,7 +234,7 @@ describe( 'Node', () => {
183234
} );
184235
} );
185236

186-
describe( 'remove', () => {
237+
describe( 'remove()', () => {
187238
it( 'should remove node from its parent', () => {
188239
const char = new Text( 'a' );
189240
const parent = new Element( 'p', null, [ char ] );
@@ -246,7 +297,7 @@ describe( 'Node', () => {
246297
sinon.assert.calledWith( rootChangeSpy, 'attributes', img );
247298
} );
248299

249-
describe( 'setAttribute', () => {
300+
describe( 'setAttribute()', () => {
250301
it( 'should fire change event', () => {
251302
img.setAttribute( 'width', 100 );
252303

@@ -255,7 +306,7 @@ describe( 'Node', () => {
255306
} );
256307
} );
257308

258-
describe( 'removeAttribute', () => {
309+
describe( 'removeAttribute()', () => {
259310
it( 'should fire change event', () => {
260311
img.removeAttribute( 'src' );
261312

@@ -264,7 +315,7 @@ describe( 'Node', () => {
264315
} );
265316
} );
266317

267-
describe( 'insertChildren', () => {
318+
describe( 'insertChildren()', () => {
268319
it( 'should fire change event', () => {
269320
root.insertChildren( 1, new Element( 'img' ) );
270321

@@ -273,7 +324,7 @@ describe( 'Node', () => {
273324
} );
274325
} );
275326

276-
describe( 'appendChildren', () => {
327+
describe( 'appendChildren()', () => {
277328
it( 'should fire change event', () => {
278329
root.appendChildren( new Element( 'img' ) );
279330

@@ -282,7 +333,7 @@ describe( 'Node', () => {
282333
} );
283334
} );
284335

285-
describe( 'removeChildren', () => {
336+
describe( 'removeChildren()', () => {
286337
it( 'should fire change event', () => {
287338
root.removeChildren( 1, 1 );
288339

@@ -291,7 +342,7 @@ describe( 'Node', () => {
291342
} );
292343
} );
293344

294-
describe( 'removeChildren', () => {
345+
describe( 'removeChildren()', () => {
295346
it( 'should fire change event', () => {
296347
text.data = 'bar';
297348

0 commit comments

Comments
 (0)