diff --git a/package.json b/package.json
index bf49578ff..8a5182796 100644
--- a/package.json
+++ b/package.json
@@ -44,8 +44,8 @@
"lint-staged": "^7.0.0"
},
"engines": {
- "node": ">=6.9.0",
- "npm": ">=3.0.0"
+ "node": ">=8.0.0",
+ "npm": ">=5.7.1"
},
"author": "CKSource (http://cksource.com/)",
"license": "GPL-2.0-or-later",
diff --git a/src/controller/datacontroller.js b/src/controller/datacontroller.js
index 14efa575a..67d502f0c 100644
--- a/src/controller/datacontroller.js
+++ b/src/controller/datacontroller.js
@@ -115,17 +115,23 @@ 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, an empty string will be returned. To turn off trimming completely
+ * use `'none'`. In such cases exact content will be returned (for example `
` for an 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}
* is called with non-existent root name. For example, if there is an editor instance with only `main` root,
* calling {@link #get} like:
*
- * data.get( 'root2' );
+ * data.get( 'root2' );
*
* will throw this error.
*
@@ -134,8 +140,13 @@ export default class DataController {
throw new CKEditorError( 'datacontroller-get-non-existent-root: Attempting to get data from a non-existing root.' );
}
- // Get model range.
- return this.stringify( this.model.document.getRoot( rootName ) );
+ const root = this.model.document.getRoot( rootName );
+
+ if ( trim === 'empty' && !this.model.hasContent( root, { ignoreWhitespaces: true } ) ) {
+ return '';
+ }
+
+ return this.stringify( root );
}
/**
diff --git a/src/model/model.js b/src/model/model.js
index ef2f1f9dc..500de84ae 100644
--- a/src/model/model.js
+++ b/src/model/model.js
@@ -446,26 +446,49 @@ 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 (`options.ignoreWhitespaces` allows controlling whether this text node must also contain
+ * any non-whitespace characters),
+ * * or any {@link module:engine/model/schema~Schema#isObject object element},
+ * * or any {@link module:engine/model/markercollection~Marker marker} which
+ * {@link module:engine/model/markercollection~Marker#_affectsData affects data}.
+ *
+ * This means that a range containing an empty `` is not considered to have a meaningful content.
+ * However, a range containing an `` (which would normally be marked in the schema as an object element)
+ * is considered non-empty.
*
* @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
+ * @param {Object} [options]
+ * @param {Boolean} [options.ignoreWhitespaces] Whether text node with whitespaces only should be considered empty.
* @returns {Boolean}
*/
- hasContent( rangeOrElement ) {
- if ( rangeOrElement instanceof ModelElement ) {
- rangeOrElement = ModelRange._createIn( rangeOrElement );
- }
+ hasContent( rangeOrElement, options ) {
+ const range = rangeOrElement instanceof ModelElement ? ModelRange._createIn( rangeOrElement ) : rangeOrElement;
- if ( rangeOrElement.isCollapsed ) {
+ if ( range.isCollapsed ) {
return false;
}
- for ( const item of rangeOrElement.getItems() ) {
- // Remember, `TreeWalker` returns always `textProxy` nodes.
- if ( item.is( 'textProxy' ) || this.schema.isObject( item ) ) {
+ // Check if there are any markers which affects data in this given range.
+ for ( const intersectingMarker of this.markers.getMarkersIntersectingRange( range ) ) {
+ if ( intersectingMarker.affectsData ) {
+ return true;
+ }
+ }
+
+ const { ignoreWhitespaces = false } = options || {};
+
+ for ( const item of range.getItems() ) {
+ if ( item.is( 'textProxy' ) ) {
+ if ( !ignoreWhitespaces ) {
+ return true;
+ } else if ( item.data.search( /\S/ ) !== -1 ) {
+ return true;
+ }
+ } else if ( this.schema.isObject( item ) ) {
return true;
}
}
diff --git a/src/model/operation/transform.js b/src/model/operation/transform.js
index a07f35723..b0b918fc6 100644
--- a/src/model/operation/transform.js
+++ b/src/model/operation/transform.js
@@ -306,7 +306,7 @@ export function transformSets( operationsA, operationsB, options ) {
originalOperationsBCount: operationsB.length
};
- const contextFactory = new ContextFactory( options.document, options.useRelations );
+ const contextFactory = new ContextFactory( options.document, options.useRelations, options.forceWeakRemove );
contextFactory.setOriginalOperations( operationsA );
contextFactory.setOriginalOperations( operationsB );
@@ -386,13 +386,17 @@ class ContextFactory {
// @param {module:engine/model/document~Document} document Document which the operations change.
// @param {Boolean} useRelations Whether during transformation relations should be used (used during undo for
// better conflict resolution).
- constructor( document, useRelations ) {
+ // @param {Boolean} [forceWeakRemove=false] If set to `false`, remove operation will be always stronger than move operation,
+ // so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
+ constructor( document, useRelations, forceWeakRemove = false ) {
// `model.History` instance which information about undone operations will be taken from.
this._history = document.history;
// Whether additional context should be used.
this._useRelations = useRelations;
+ this._forceWeakRemove = !!forceWeakRemove;
+
// For each operation that is created during transformation process, we keep a reference to the original operation
// which it comes from. The original operation works as a kind of "identifier". Every contextual information
// gathered during transformation that we want to save for given operation, is actually saved for the original operation.
@@ -583,7 +587,8 @@ class ContextFactory {
aWasUndone: this._wasUndone( opA ),
bWasUndone: this._wasUndone( opB ),
abRelation: this._useRelations ? this._getRelation( opA, opB ) : null,
- baRelation: this._useRelations ? this._getRelation( opB, opA ) : null
+ baRelation: this._useRelations ? this._getRelation( opB, opA ) : null,
+ forceWeakRemove: this._forceWeakRemove
};
}
@@ -1313,7 +1318,7 @@ setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
//
const removedRange = Range._createFromPositionAndShift( b.sourcePosition, b.howMany );
- if ( b.type == 'remove' && !context.bWasUndone ) {
+ if ( b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove ) {
if ( a.deletionPosition.hasSameParentAs( b.sourcePosition ) && removedRange.containsPosition( a.sourcePosition ) ) {
return [ new NoOperation( 0 ) ];
}
@@ -1596,9 +1601,9 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
//
// If only one of operations is a remove operation, we force remove operation to be the "stronger" one
// to provide more expected results.
- if ( a.type == 'remove' && b.type != 'remove' && !context.aWasUndone ) {
+ if ( a.type == 'remove' && b.type != 'remove' && !context.aWasUndone && !context.forceWeakRemove ) {
aIsStrong = true;
- } else if ( a.type != 'remove' && b.type == 'remove' && !context.bWasUndone ) {
+ } else if ( a.type != 'remove' && b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove ) {
aIsStrong = false;
}
@@ -1768,7 +1773,7 @@ setTransformation( MoveOperation, SplitOperation, ( a, b, context ) => {
if ( b.graveyardPosition ) {
const movesGraveyardElement = moveRange.start.isEqual( b.graveyardPosition ) || moveRange.containsPosition( b.graveyardPosition );
- if ( a.howMany > 1 && movesGraveyardElement ) {
+ if ( a.howMany > 1 && movesGraveyardElement && !context.aWasUndone ) {
ranges.push( Range._createFromPositionAndShift( b.insertionPosition, 1 ) );
}
}
@@ -1780,7 +1785,7 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
const movedRange = Range._createFromPositionAndShift( a.sourcePosition, a.howMany );
if ( b.deletionPosition.hasSameParentAs( a.sourcePosition ) && movedRange.containsPosition( b.sourcePosition ) ) {
- if ( a.type == 'remove' ) {
+ if ( a.type == 'remove' && !context.forceWeakRemove ) {
// Case 1:
//
// The element to remove got merged.
@@ -1794,21 +1799,22 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
const results = [];
let gyMoveSource = b.graveyardPosition.clone();
- let splitNodesMoveSource = b.targetPosition.clone();
+ let splitNodesMoveSource = b.targetPosition._getTransformedByMergeOperation( b );
if ( a.howMany > 1 ) {
results.push( new MoveOperation( a.sourcePosition, a.howMany - 1, a.targetPosition, 0 ) );
- gyMoveSource = gyMoveSource._getTransformedByInsertion( a.targetPosition, a.howMany - 1 );
+
+ gyMoveSource = gyMoveSource._getTransformedByMove( a.sourcePosition, a.targetPosition, a.howMany - 1 );
splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( a.sourcePosition, a.targetPosition, a.howMany - 1 );
}
const gyMoveTarget = b.deletionPosition._getCombined( a.sourcePosition, a.targetPosition );
const gyMove = new MoveOperation( gyMoveSource, 1, gyMoveTarget, 0 );
- const targetPositionPath = gyMove.getMovedRangeStart().path.slice();
- targetPositionPath.push( 0 );
+ const splitNodesMoveTargetPath = gyMove.getMovedRangeStart().path.slice();
+ splitNodesMoveTargetPath.push( 0 );
- const splitNodesMoveTarget = new Position( gyMove.targetPosition.root, targetPositionPath );
+ const splitNodesMoveTarget = new Position( gyMove.targetPosition.root, splitNodesMoveTargetPath );
splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( gyMoveSource, gyMoveTarget, 1 );
const splitNodesMove = new MoveOperation( splitNodesMoveSource, b.howMany, splitNodesMoveTarget, 0 );
@@ -2052,7 +2058,9 @@ setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
// is already moved to the correct position, we need to only move the nodes after the split position.
// This will be done by `MoveOperation` instead of `SplitOperation`.
//
- if ( rangeToMove.start.isEqual( a.graveyardPosition ) || rangeToMove.containsPosition( a.graveyardPosition ) ) {
+ const gyElementMoved = rangeToMove.start.isEqual( a.graveyardPosition ) || rangeToMove.containsPosition( a.graveyardPosition );
+
+ if ( !context.bWasUndone && gyElementMoved ) {
const sourcePosition = a.splitPosition._getTransformedByMoveOperation( b );
const newParentPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
diff --git a/tests/controller/datacontroller.js b/tests/controller/datacontroller.js
index fa8cc004c..e366e3ca0 100644
--- a/tests/controller/datacontroller.js
+++ b/tests/controller/datacontroller.js
@@ -346,15 +346,26 @@ describe( 'DataController', () => {
downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
expect( data.get() ).to.equal( 'foo
' );
+ expect( data.get( { trim: 'empty' } ) ).to.equal( 'foo
' );
} );
- it( 'should get empty paragraph', () => {
+ it( 'should trim empty paragraph by default', () => {
schema.register( 'paragraph', { inheritAllFrom: '$block' } );
setData( model, '' );
downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
- expect( data.get() ).to.equal( '
' );
+ 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, '' );
+
+ downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
+
+ expect( data.get( { trim: 'none' } ) ).to.equal( '
' );
} );
it( 'should get two paragraphs', () => {
@@ -364,6 +375,7 @@ describe( 'DataController', () => {
downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
expect( data.get() ).to.equal( 'foo
bar
' );
+ expect( data.get( { trim: 'empty' } ) ).to.equal( 'foo
bar
' );
} );
it( 'should get text directly in root', () => {
@@ -371,6 +383,7 @@ describe( 'DataController', () => {
setData( model, 'foo' );
expect( data.get() ).to.equal( 'foo' );
+ expect( data.get( { trim: 'empty' } ) ).to.equal( 'foo' );
} );
it( 'should get paragraphs without bold', () => {
@@ -380,6 +393,7 @@ describe( 'DataController', () => {
downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
expect( data.get() ).to.equal( 'foobar
' );
+ expect( data.get( { trim: 'empty' } ) ).to.equal( 'foobar
' );
} );
it( 'should get paragraphs with bold', () => {
@@ -390,6 +404,7 @@ describe( 'DataController', () => {
downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
expect( data.get() ).to.equal( 'foobar
' );
+ expect( data.get( { trim: 'empty' } ) ).to.equal( 'foobar
' );
} );
it( 'should get root name as a parameter', () => {
@@ -403,13 +418,13 @@ describe( 'DataController', () => {
downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
expect( data.get() ).to.equal( 'foo
' );
- expect( data.get( 'main' ) ).to.equal( 'foo
' );
- expect( data.get( 'title' ) ).to.equal( 'Bar' );
+ expect( data.get( { rootName: 'main' } ) ).to.equal( 'foo
' );
+ 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.'
diff --git a/tests/model/model.js b/tests/model/model.js
index e1b8bea74..a0ef069bc 100644
--- a/tests/model/model.js
+++ b/tests/model/model.js
@@ -500,6 +500,9 @@ describe( 'Model', () => {
isObject: true
} );
schema.extend( 'image', { allowIn: 'div' } );
+ schema.register( 'listItem', {
+ inheritAllFrom: '$block'
+ } );
setData(
model,
@@ -510,7 +513,10 @@ describe( 'Model', () => {
'foo' +
'' +
'' +
- '
'
+ '' +
+ '' +
+ '' +
+ ''
);
root = model.document.getRoot();
@@ -522,6 +528,34 @@ describe( 'Model', () => {
expect( model.hasContent( pFoo ) ).to.be.true;
} );
+ it( 'should return true if given element has text node (ignoreWhitespaces)', () => {
+ const pFoo = root.getChild( 1 );
+
+ expect( model.hasContent( pFoo, { ignoreWhitespaces: 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 (ignoreWhitespaces)', () => {
+ 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, { ignoreWhitespaces: true } ) ).to.be.false;
+ } );
+
it( 'should return true if given element has element that is an object', () => {
const divImg = root.getChild( 2 );
@@ -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, { ignoreWhitespaces: 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, { ignoreWhitespaces: true } ) ).to.be.false;
+ } );
+
+ it( 'should return false (ignoreWhitespaces) 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, { ignoreWhitespaces: 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, { ignoreWhitespaces: 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, { ignoreWhitespaces: true } ) ).to.be.false;
+ } );
+
+ it( 'should return true (ignoreWhitespaces) 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, { ignoreWhitespaces: true } ) ).to.be.true;
+ } );
} );
describe( 'createPositionFromPath()', () => {
diff --git a/tests/model/operation/transform/undo.js b/tests/model/operation/transform/undo.js
index 2e03fcea7..4870a309a 100644
--- a/tests/model/operation/transform/undo.js
+++ b/tests/model/operation/transform/undo.js
@@ -5,6 +5,9 @@
import { Client, expectClients, clearBuffer } from './utils.js';
+import Element from '../../../../src/model/element';
+import Text from '../../../../src/model/text';
+
describe( 'transform', () => {
let john;
@@ -574,4 +577,39 @@ describe( 'transform', () => {
expectClients( 'XY' );
} );
+
+ // https://github.com/ckeditor/ckeditor5/issues/1385
+ it( 'paste inside paste + undo, undo + redo, redo', () => {
+ const model = john.editor.model;
+
+ john.setData( '[]' );
+
+ model.insertContent( getPastedContent() );
+
+ john.setSelection( [ 0, 3 ] );
+
+ model.insertContent( getPastedContent() );
+
+ expectClients( 'FooFoobarbar' );
+
+ john.undo();
+
+ expectClients( 'Foobar' );
+
+ john.undo();
+
+ expectClients( '' );
+
+ john.redo();
+
+ expectClients( 'Foobar' );
+
+ john.redo();
+
+ expectClients( 'FooFoobarbar' );
+
+ function getPastedContent() {
+ return new Element( 'heading1', null, new Text( 'Foobar' ) );
+ }
+ } );
} );
diff --git a/tests/view/renderer.js b/tests/view/renderer.js
index 47e1d41c0..8113f0743 100644
--- a/tests/view/renderer.js
+++ b/tests/view/renderer.js
@@ -3198,6 +3198,111 @@ describe( 'Renderer', () => {
expect( domRoot.innerHTML ).to.equal( '1
2
' );
} );
} );
+
+ // ckeditor/ckeditor5-utils#269
+ // The expected times has a significant margin above the usual execution time (which is around 40-50%
+ // of the expected time) because it depends on the browser and environment in which tests are run.
+ // However, for larger data sets the difference between using `diff()` and `fastDiff()` (see above issue for context)
+ // is more than 10x in execution time so it is clearly visible in these tests when something goes wrong.
+ describe( 'rendering performance', () => {
+ before( function() {
+ // Ignore on Edge browser where performance is quite poor.
+ if ( env.isEdge ) {
+ this.skip();
+ }
+ } );
+
+ it( 'should not take more than 350ms to render around 300 element nodes (same html)', () => {
+ const renderingTime = measureRenderingTime( viewRoot, generateViewData1( 65 ), generateViewData1( 55 ) );
+ expect( renderingTime ).to.be.within( 0, 350 );
+ } );
+
+ it( 'should not take more than 350ms to render around 300 element nodes (different html)', () => {
+ const renderingTime = measureRenderingTime( viewRoot, generateViewData1( 55 ), generateViewData2( 65 ) );
+ expect( renderingTime ).to.be.within( 0, 350 );
+ } );
+
+ it( 'should not take more than 350ms to render around 500 element nodes (same html)', () => {
+ const renderingTime = measureRenderingTime( viewRoot, generateViewData1( 105 ), generateViewData1( 95 ) );
+ expect( renderingTime ).to.be.within( 0, 350 );
+ } );
+
+ it( 'should not take more than 350ms to render around 500 element nodes (different html)', () => {
+ const renderingTime = measureRenderingTime( viewRoot, generateViewData1( 95 ), generateViewData2( 105 ) );
+ expect( renderingTime ).to.be.within( 0, 350 );
+ } );
+
+ it( 'should not take more than 350ms to render around 1000 element nodes (same html)', () => {
+ const renderingTime = measureRenderingTime( viewRoot, generateViewData1( 195 ), generateViewData1( 205 ) );
+ expect( renderingTime ).to.be.within( 0, 350 );
+ } );
+
+ it( 'should not take more than 350ms to render around 1000 element nodes (different html)', () => {
+ const renderingTime = measureRenderingTime( viewRoot, generateViewData1( 205 ), generateViewData2( 195 ) );
+ expect( renderingTime ).to.be.within( 0, 350 );
+ } );
+
+ function measureRenderingTime( viewRoot, initialData, newData ) {
+ // Set initial data.
+ const initialView = parse( initialData );
+ viewRoot._appendChild( initialView );
+ renderer.markToSync( 'children', viewRoot );
+ renderer.render();
+
+ // Set new data.
+ const newView = parse( newData );
+ viewRoot._removeChildren( 0, viewRoot.childCount );
+ viewRoot._appendChild( newView );
+ renderer.markToSync( 'children', viewRoot );
+
+ // Measure render time.
+ const start = Date.now();
+
+ renderer.render();
+
+ return Date.now() - start;
+ }
+
+ function generateViewData1( repeat = 1 ) {
+ const viewData = '' +
+ '' +
+ 'CKEditor 5 h1 heading!' +
+ '' +
+ '' +
+ 'Foo Bar Baz and some text' +
+ '' +
+ '' +
+ 'Item 1' +
+ '' +
+ '' +
+ 'Item 2' +
+ '' +
+ '' +
+ 'Item 3' +
+ '';
+
+ return viewData.repeat( repeat );
+ }
+
+ function generateViewData2( repeat = 1 ) {
+ const viewData = '' +
+ '' +
+ '' +
+ 'Foo' +
+ '' +
+ '' +
+ '' +
+ 'Item 1' +
+ '' +
+ 'Heading 1' +
+ '' +
+ 'Heading 2' +
+ '' +
+ 'Heading 4';
+
+ return viewData.repeat( repeat );
+ }
+ } );
} );
describe( '#922', () => {