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

Commit

Permalink
Merge 7aee75b into f579c93
Browse files Browse the repository at this point in the history
  • Loading branch information
f1ames authored Feb 14, 2019
2 parents f579c93 + 7aee75b commit 0f5afe2
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 14 deletions.
14 changes: 11 additions & 3 deletions src/controller/datacontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,16 @@ export default class DataController {
* Returns the model's data converted by downcast dispatchers attached to {@link #downcastDispatcher} and
* formatted by the {@link #processor data processor}.
*
* @param {String} [rootName='main'] Root name.
* @param {Object} [options]
* @param {String} [options.rootName='main'] Root name.
* @param {String} [options.trim='empty'] Whether returned data should be trimmed. This option is set to `empty` by default,
* which means whenever editor content is considered empty, the empty string will be returned. To turn off trimming completely
* use `none`. In such cases exact content will be returned (for example `<p>&nbsp;</p>` for empty editor).
* @returns {String} Output data.
*/
get( rootName = 'main' ) {
get( options ) {
const { rootName = 'main', trim = 'empty' } = options || {};

if ( !this._checkIfRootsExists( [ rootName ] ) ) {
/**
* Cannot get data from a non-existing root. This error is thrown when {@link #get DataController#get() method}
Expand All @@ -134,8 +140,10 @@ export default class DataController {
throw new CKEditorError( 'datacontroller-get-non-existent-root: Attempting to get data from a non-existing root.' );
}

const root = this.model.document.getRoot( rootName );

// Get model range.
return this.stringify( this.model.document.getRoot( rootName ) );
return trim === 'empty' && !this.model.hasContent( root, { trimWhitespaces: true } ) ? '' : this.stringify( root );
}

/**
Expand Down
30 changes: 25 additions & 5 deletions src/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -445,15 +445,18 @@ export default class Model {

/**
* Checks whether the given {@link module:engine/model/range~Range range} or
* {@link module:engine/model/element~Element element}
* has any content.
* {@link module:engine/model/element~Element element} has any meaningful content.
*
* Content is any text node or element which is registered in the {@link module:engine/model/schema~Schema schema}.
* Meaningful content is any text node, element which is registered in the {@link module:engine/model/schema~Schema schema}
* or any {@link module:engine/model/markercollection~Marker marker} which
* {@link module:engine/model/markercollection~Marker#_affectsData affects data}.
*
* @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
* @param {Object} [options]
* @param {Boolean} [options.trimWhitespaces] Whether text node with whitespaces only should be considered to be empty element.
* @returns {Boolean}
*/
hasContent( rangeOrElement ) {
hasContent( rangeOrElement, options ) {
if ( rangeOrElement instanceof ModelElement ) {
rangeOrElement = ModelRange._createIn( rangeOrElement );
}
Expand All @@ -462,9 +465,26 @@ export default class Model {
return false;
}

// Check if there are any markers which affects data in this given range.
for ( const intersectingMarker of this.markers.getMarkersIntersectingRange( rangeOrElement ) ) {
if ( intersectingMarker.affectsData ) {
return true;
}
}

const { trimWhitespaces = false } = options || {};

for ( const item of rangeOrElement.getItems() ) {
// Remember, `TreeWalker` returns always `textProxy` nodes.
if ( item.is( 'textProxy' ) || this.schema.isObject( item ) ) {
if ( item.is( 'textProxy' ) ) {
if ( !trimWhitespaces ) {
return true;
} else if ( item.data.match( /\S+/gi ) !== null ) {
return true;
}
}

if ( this.schema.isObject( item ) ) {
return true;
}
}
Expand Down
25 changes: 20 additions & 5 deletions tests/controller/datacontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,15 +346,26 @@ describe( 'DataController', () => {
downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );

expect( data.get() ).to.equal( '<p>foo</p>' );
expect( data.get( { trim: 'empty' } ) ).to.equal( '<p>foo</p>' );
} );

it( 'should get empty paragraph', () => {
it( 'should trim empty paragraph by default', () => {
schema.register( 'paragraph', { inheritAllFrom: '$block' } );
setData( model, '<paragraph></paragraph>' );

downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );

expect( data.get() ).to.equal( '<p>&nbsp;</p>' );
expect( data.get() ).to.equal( '' );
expect( data.get( { trim: 'empty' } ) ).to.equal( '' );
} );

it( 'should get empty paragraph (with trim=none)', () => {
schema.register( 'paragraph', { inheritAllFrom: '$block' } );
setData( model, '<paragraph></paragraph>' );

downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );

expect( data.get( { trim: 'none' } ) ).to.equal( '<p>&nbsp;</p>' );
} );

it( 'should get two paragraphs', () => {
Expand All @@ -364,13 +375,15 @@ describe( 'DataController', () => {
downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );

expect( data.get() ).to.equal( '<p>foo</p><p>bar</p>' );
expect( data.get( { trim: 'empty' } ) ).to.equal( '<p>foo</p><p>bar</p>' );
} );

it( 'should get text directly in root', () => {
schema.extend( '$text', { allowIn: '$root' } );
setData( model, 'foo' );

expect( data.get() ).to.equal( 'foo' );
expect( data.get( { trim: 'empty' } ) ).to.equal( 'foo' );
} );

it( 'should get paragraphs without bold', () => {
Expand All @@ -380,6 +393,7 @@ describe( 'DataController', () => {
downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );

expect( data.get() ).to.equal( '<p>foobar</p>' );
expect( data.get( { trim: 'empty' } ) ).to.equal( '<p>foobar</p>' );
} );

it( 'should get paragraphs with bold', () => {
Expand All @@ -390,6 +404,7 @@ describe( 'DataController', () => {
downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );

expect( data.get() ).to.equal( '<p>foo<strong>bar</strong></p>' );
expect( data.get( { trim: 'empty' } ) ).to.equal( '<p>foo<strong>bar</strong></p>' );
} );

it( 'should get root name as a parameter', () => {
Expand All @@ -403,13 +418,13 @@ describe( 'DataController', () => {
downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );

expect( data.get() ).to.equal( '<p>foo</p>' );
expect( data.get( 'main' ) ).to.equal( '<p>foo</p>' );
expect( data.get( 'title' ) ).to.equal( 'Bar' );
expect( data.get( { rootName: 'main' } ) ).to.equal( '<p>foo</p>' );
expect( data.get( { rootName: 'title' } ) ).to.equal( 'Bar' );
} );

it( 'should throw an error when non-existent root is used', () => {
expect( () => {
data.get( 'nonexistent' );
data.get( { rootName: 'nonexistent' } );
} ).to.throw(
CKEditorError,
'datacontroller-get-non-existent-root: Attempting to get data from a non-existing root.'
Expand Down
143 changes: 142 additions & 1 deletion tests/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,9 @@ describe( 'Model', () => {
isObject: true
} );
schema.extend( 'image', { allowIn: 'div' } );
schema.register( 'listItem', {
inheritAllFrom: '$block'
} );

setData(
model,
Expand All @@ -510,7 +513,10 @@ describe( 'Model', () => {
'<paragraph>foo</paragraph>' +
'<div>' +
'<image></image>' +
'</div>'
'</div>' +
'<listItem></listItem>' +
'<listItem></listItem>' +
'<listItem></listItem>'
);

root = model.document.getRoot();
Expand All @@ -522,6 +528,34 @@ describe( 'Model', () => {
expect( model.hasContent( pFoo ) ).to.be.true;
} );

it( 'should return true if given element has text node (trimWhitespaces)', () => {
const pFoo = root.getChild( 1 );

expect( model.hasContent( pFoo, { trimWhitespaces: true } ) ).to.be.true;
} );

it( 'should return true if given element has text node containing spaces only', () => {
const pEmpty = root.getChild( 0 ).getChild( 0 );

model.enqueueChange( 'transparent', writer => {
// Model `setData()` method trims whitespaces so use writer here to insert whitespace only text.
writer.insertText( ' ', pEmpty, 'end' );
} );

expect( model.hasContent( pEmpty ) ).to.be.true;
} );

it( 'should false true if given element has text node containing spaces only (trimWhitespaces)', () => {
const pEmpty = root.getChild( 0 ).getChild( 0 );

model.enqueueChange( 'transparent', writer => {
// Model `setData()` method trims whitespaces so use writer here to insert whitespace only text.
writer.insertText( ' ', pEmpty, 'end' );
} );

expect( model.hasContent( pEmpty, { trimWhitespaces: true } ) ).to.be.false;
} );

it( 'should return true if given element has element that is an object', () => {
const divImg = root.getChild( 2 );

Expand Down Expand Up @@ -571,6 +605,113 @@ describe( 'Model', () => {

expect( model.hasContent( range ) ).to.be.false;
} );

it( 'should return false for empty list items', () => {
const range = new ModelRange( ModelPosition._createAt( root, 3 ), ModelPosition._createAt( root, 6 ) );

expect( model.hasContent( range ) ).to.be.false;
} );

it( 'should return false for empty element with marker (usingOperation=false, affectsData=false)', () => {
const pEmpty = root.getChild( 0 ).getChild( 0 );

model.enqueueChange( 'transparent', writer => {
// Insert marker.
const range = ModelRange._createIn( pEmpty );
writer.addMarker( 'comment1', { range, usingOperation: false, affectsData: false } );
} );

expect( model.hasContent( pEmpty ) ).to.be.false;
expect( model.hasContent( pEmpty, { trimWhitespaces: true } ) ).to.be.false;
} );

it( 'should return false for empty element with marker (usingOperation=true, affectsData=false)', () => {
const pEmpty = root.getChild( 0 ).getChild( 0 );

model.enqueueChange( 'transparent', writer => {
// Insert marker.
const range = ModelRange._createIn( pEmpty );
writer.addMarker( 'comment1', { range, usingOperation: true, affectsData: false } );
} );

expect( model.hasContent( pEmpty ) ).to.be.false;
expect( model.hasContent( pEmpty, { trimWhitespaces: true } ) ).to.be.false;
} );

it( 'should return false (trimWhitespaces) for empty text with marker (usingOperation=false, affectsData=false)', () => {
const pEmpty = root.getChild( 0 ).getChild( 0 );

model.enqueueChange( 'transparent', writer => {
// Insert empty text.
const text = writer.createText( ' ', { bold: true } );
writer.append( text, pEmpty );

// Insert marker.
const range = ModelRange._createIn( pEmpty );
writer.addMarker( 'comment1', { range, usingOperation: false, affectsData: false } );
} );

expect( model.hasContent( pEmpty, { trimWhitespaces: true } ) ).to.be.false;
} );

it( 'should return true for empty text with marker (usingOperation=false, affectsData=false)', () => {
const pEmpty = root.getChild( 0 ).getChild( 0 );

model.enqueueChange( 'transparent', writer => {
// Insert empty text.
const text = writer.createText( ' ', { bold: true } );
writer.append( text, pEmpty );

// Insert marker.
const range = ModelRange._createIn( pEmpty );
writer.addMarker( 'comment1', { range, usingOperation: false, affectsData: false } );
} );

expect( model.hasContent( pEmpty ) ).to.be.true;
} );

it( 'should return false for empty element with marker (usingOperation=false, affectsData=true)', () => {
const pEmpty = root.getChild( 0 ).getChild( 0 );

model.enqueueChange( 'transparent', writer => {
// Insert marker.
const range = ModelRange._createIn( pEmpty );
writer.addMarker( 'comment1', { range, usingOperation: false, affectsData: true } );
} );

expect( model.hasContent( pEmpty ) ).to.be.false;
expect( model.hasContent( pEmpty, { trimWhitespaces: true } ) ).to.be.false;
} );

it( 'should return false for empty element with marker (usingOperation=true, affectsData=true)', () => {
const pEmpty = root.getChild( 0 ).getChild( 0 );

model.enqueueChange( 'transparent', writer => {
// Insert marker.
const range = ModelRange._createIn( pEmpty );
writer.addMarker( 'comment1', { range, usingOperation: true, affectsData: true } );
} );

expect( model.hasContent( pEmpty ) ).to.be.false;
expect( model.hasContent( pEmpty, { trimWhitespaces: true } ) ).to.be.false;
} );

it( 'should return true (trimWhitespaces) for empty text with marker (usingOperation=false, affectsData=true)', () => {
const pEmpty = root.getChild( 0 ).getChild( 0 );

model.enqueueChange( 'transparent', writer => {
// Insert empty text.
const text = writer.createText( ' ', { bold: true } );
writer.append( text, pEmpty );

// Insert marker.
const range = ModelRange._createIn( pEmpty );
writer.addMarker( 'comment1', { range, usingOperation: false, affectsData: true } );
} );

expect( model.hasContent( pEmpty ) ).to.be.true;
expect( model.hasContent( pEmpty, { trimWhitespaces: true } ) ).to.be.true;
} );
} );

describe( 'createPositionFromPath()', () => {
Expand Down

0 comments on commit 0f5afe2

Please sign in to comment.