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 #1829 from ckeditor/i/6364
Browse files Browse the repository at this point in the history
Feature: Implemented the model and view `Range#getContainedElement()` methods. Closes ckeditor/ckeditor5#6364.
  • Loading branch information
Reinmar committed Mar 9, 2020
2 parents ffce577 + c0e1957 commit 8fb1efa
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 30 deletions.
22 changes: 22 additions & 0 deletions src/model/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,28 @@ export default class Range {
return this.start.getCommonAncestor( this.end );
}

/**
* Returns an {@link module:engine/model/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/model/element~Element|null}
*/
getContainedElement() {
if ( this.isCollapsed ) {
return null;
}

const nodeAfterStart = this.start.nodeAfter;
const nodeBeforeEnd = this.end.nodeBefore;

if ( nodeAfterStart && nodeAfterStart.is( 'element' ) && nodeAfterStart === nodeBeforeEnd ) {
return nodeAfterStart;
}

return null;
}

/**
* Converts `Range` to plain object and returns it.
*
Expand Down
7 changes: 1 addition & 6 deletions src/model/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

import Position from './position';
import Element from './element';
import Node from './node';
import Range from './range';
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
Expand Down Expand Up @@ -615,11 +614,7 @@ export default class Selection {
return null;
}

const range = this.getFirstRange();
const nodeAfterStart = range.start.nodeAfter;
const nodeBeforeEnd = range.end.nodeBefore;

return ( nodeAfterStart instanceof Element && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null;
return this.getFirstRange().getContainedElement();
}

/**
Expand Down
39 changes: 39 additions & 0 deletions src/view/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,45 @@ export default class Range {
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.
*
Expand Down
25 changes: 1 addition & 24 deletions src/view/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import Position from './position';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
import Node from './node';
import Element from './element';
import count from '@ckeditor/ckeditor5-utils/src/count';
import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
import DocumentSelection from './documentselection';
Expand Down Expand Up @@ -413,29 +412,7 @@ export default class Selection {
return null;
}

const range = this.getFirstRange();

let nodeAfterStart = range.start.nodeAfter;
let nodeBeforeEnd = range.end.nodeBefore;

// Handle the situation when selection position is at the beginning / at the end of a text node.
// In such situation `.nodeAfter` and `.nodeBefore` are `null` but the selection 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 selections, only the difference is if the selection position is at
// at the end/at the beginning of a text node or just before/just after the text node.
//
if ( range.start.parent.is( 'text' ) && range.start.isAtEnd && range.start.parent.nextSibling ) {
nodeAfterStart = range.start.parent.nextSibling;
}

if ( range.end.parent.is( 'text' ) && range.end.isAtStart && range.end.parent.previousSibling ) {
nodeBeforeEnd = range.end.parent.previousSibling;
}

return ( nodeAfterStart instanceof Element && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null;
return this.getFirstRange().getContainedElement();
}

/**
Expand Down
41 changes: 41 additions & 0 deletions tests/model/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,47 @@ describe( 'Range', () => {
} );
} );

describe( 'getContainedElement()', () => {
beforeEach( () => {
prepareRichRoot( root );
} );

it( 'should return an element when it is fully contained by the range', () => {
// <div><h>first</h><p>lorem ipsum</p></div>[<p>foo</p>]<p>bar</p><div><h>second</h><p>lorem</p></div>
const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );

expect( range.getContainedElement() ).to.equal( root.getNodeByPath( [ 1 ] ) );
} );

it( 'should return "null" if the range is collapsed', () => {
// <div><h>first</h><p>lorem ipsum</p></div>[]<p>foo</p><p>bar</p><div><h>second</h><p>lorem</p></div>
const range = new Range( new Position( root, [ 1 ] ) );

expect( range.getContainedElement() ).to.be.null;
} );

it( 'should return "null" if it contains 2+ elements', () => {
// <div><h>first</h><p>lorem ipsum</p></div>[<p>foo</p><p>bar</p>]<div><h>second</h><p>lorem</p></div>
const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 3 ] ) );

expect( range.getContainedElement() ).to.be.null;
} );

it( 'should return "null" if it contains an element and some other nodes', () => {
// <div><h>first</h><p>lorem ipsum</p></div>[<p>foo</p><p>ba]r</p><div><h>second</h><p>lorem</p></div>
const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2, 2 ] ) );

expect( range.getContainedElement() ).to.be.null;
} );

it( 'should return "null" if it fully contains a node but the node is not an element', () => {
// <div><h>first</h><p>lorem ipsum</p></div><p>foo</p><p>[bar]</p><div><h>second</h><p>lorem</p></div>
const range = new Range( new Position( root, [ 2, 0 ] ), new Position( root, [ 2, 3 ] ) );

expect( range.getContainedElement() ).to.be.null;
} );
} );

function mapNodesToNames( nodes ) {
return nodes.map( node => {
return ( node instanceof Element ) ? 'E:' + node.name : 'T:' + node.data;
Expand Down
46 changes: 46 additions & 0 deletions tests/view/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -772,4 +772,50 @@ describe( 'Range', () => {
expect( range.getCommonAncestor() ).to.equal( ul );
} );
} );

describe( 'getContainedElement()', () => {
it( 'should return an element when it is fully contained by the range', () => {
const { selection, view } = parse( 'foo [<b>bar</b>] baz' );
const range = selection.getFirstRange();
const element = view.getChild( 1 );

expect( range.getContainedElement() ).to.equal( element );
} );

it( 'should return selected element if the range is anchored at the end/at the beginning of a text node', () => {
const { selection, view } = parse( 'foo {<b>bar</b>} baz' );
const range = selection.getFirstRange();
const element = view.getChild( 1 );

expect( range.getContainedElement() ).to.equal( element );
} );

it( 'should return "null" if the selection is collapsed', () => {
const { selection } = parse( 'foo []<b>bar</b> baz' );
const range = selection.getFirstRange();

expect( range.getContainedElement() ).to.be.null;
} );

it( 'should return "null" if it contains 2+ elements', () => {
const { selection } = parse( 'foo [<b>bar</b><i>qux</i>] baz' );
const range = selection.getFirstRange();

expect( range.getContainedElement() ).to.be.null;
} );

it( 'should return "null" if the range spans over more than a single element', () => {
const { selection } = parse( 'foo [<b>bar</b> ba}z' );
const range = selection.getFirstRange();

expect( range.getContainedElement() ).to.be.null;
} );

it( 'should return "null" if the range spans over a single text node', () => {
const { selection } = parse( 'foo <b>{bar}</b> baz' );
const range = selection.getFirstRange();

expect( range.getContainedElement() ).to.be.null;
} );
} );
} );

0 comments on commit 8fb1efa

Please sign in to comment.