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 #21 from ckeditor/t/19
Browse files Browse the repository at this point in the history
Feature: Paragraph will be automatically created if loaded empty data or if programmatically emptied a root element. Closes #19.
  • Loading branch information
Reinmar authored Apr 28, 2017
2 parents a804348 + 31039cc commit c42d33e
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 9 deletions.
58 changes: 49 additions & 9 deletions src/paragraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import modelWriter from '@ckeditor/ckeditor5-engine/src/model/writer';
import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';

import isArray from '@ckeditor/ckeditor5-utils/src/lib/lodash/isArray';

/**
* The paragraph feature for the editor.
* Introduces the `<paragraph>` element in the model which renders as a `<p>` element in the DOM and data.
Expand Down Expand Up @@ -75,6 +73,16 @@ export default class Paragraph extends Plugin {
}, { priority: 'low' } );

editor.commands.set( 'paragraph', new ParagraphCommand( editor ) );

// Post-fixer that takes care of adding empty paragraph elements to empty roots.
// Besides fixing content on #changesDone we also need to handle #dataReady because
// if initial data is empty or setData() wasn't even called there will be no #change fired.
doc.on( 'change', ( evt, type, changes, batch ) => findEmptyRoots( doc, batch ) );
doc.on( 'changesDone', autoparagraphEmptyRoots, { priority: 'lowest' } );
editor.on( 'dataReady', () => {
findEmptyRoots( doc, doc.batch( 'transparent' ) );
autoparagraphEmptyRoots();
}, { priority: 'lowest' } );
}
}

Expand Down Expand Up @@ -202,13 +210,7 @@ function mergeSubsequentParagraphs( evt, data ) {
return;
}

let node;

if ( isArray( data.output ) ) {
node = data.output[ 0 ];
} else {
node = data.output.getChild( 0 );
}
let node = data.output.getChild( 0 );

while ( node && node.nextSibling ) {
const nextSibling = node.nextSibling;
Expand All @@ -234,3 +236,41 @@ function hasParagraphLikeContent( element ) {

return false;
}

// Looks through all roots created in document and marks every empty root, saving which batch made it empty.
const rootsToFix = new Map();

function findEmptyRoots( doc, batch ) {
for ( let rootName of doc.getRootNames() ) {
const root = doc.getRoot( rootName );

if ( root.isEmpty ) {
if ( !rootsToFix.has( root ) ) {
rootsToFix.set( root, batch );
}
} else {
rootsToFix.delete( root );
}
}
}

// Fixes all empty roots.
function autoparagraphEmptyRoots() {
for ( let [ root, batch ] of rootsToFix ) {
// Only empty roots are in `rootsToFix`. Even if root got content during `changesDone` event (because of, for example
// other feature), this will fire `findEmptyRoots` and remove that root from `rootsToFix`. So we are guaranteed
// to have only empty roots here.
const query = { name: 'paragraph', inside: [ root ] };
const doc = batch.document;
const schema = doc.schema;

// If paragraph element is allowed in the root, create paragraph element.
if ( schema.check( query ) ) {
doc.enqueueChanges( () => {
batch.insert( ModelPosition.createAt( root ), new ModelElement( 'paragraph' ) );
} );
}
}

rootsToFix.clear();
}
71 changes: 71 additions & 0 deletions tests/paragraph-intergration.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import Paragraph from '../src/paragraph';
import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
import UndoEngine from '@ckeditor/ckeditor5-undo/src/undoengine';
import HeadingEngine from '@ckeditor/ckeditor5-heading/src/headingengine';
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import {
Expand Down Expand Up @@ -145,4 +146,74 @@ describe( 'Paragraph feature – integration', () => {
} );
} );
} );

describe( 'with undo', () => {
it( 'fixing empty roots should be transparent to undo', () => {
return VirtualTestEditor.create( {
plugins: [ Paragraph, UndoEngine ]
} )
.then( newEditor => {
const editor = newEditor;
const doc = editor.document;
const root = doc.getRoot();

expect( editor.getData() ).to.equal( '<p>&nbsp;</p>' );
expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false;

editor.setData( '<p>Foobar.</p>' );

doc.enqueueChanges( () => {
doc.batch().remove( root.getChild( 0 ) );
} );

expect( editor.getData() ).to.equal( '<p>&nbsp;</p>' );

editor.execute( 'undo' );

expect( editor.getData() ).to.equal( '<p>Foobar.</p>' );

editor.execute( 'redo' );

expect( editor.getData() ).to.equal( '<p>&nbsp;</p>' );

editor.execute( 'undo' );

expect( editor.getData() ).to.equal( '<p>Foobar.</p>' );
} );
} );

it( 'fixing empty roots should be transparent to undo - multiple roots', () => {
return VirtualTestEditor.create( {
plugins: [ Paragraph, UndoEngine ]
} )
.then( newEditor => {
const editor = newEditor;
const doc = editor.document;
const root = doc.getRoot();
const otherRoot = doc.createRoot( '$root', 'otherRoot' );
editor.editing.createRoot( 'div', 'otherRoot' );

editor.data.set( '<p>Foobar.</p>', 'main' );
editor.data.set( '<p>Foobar.</p>', 'otherRoot' );

doc.enqueueChanges( () => {
doc.batch().remove( root.getChild( 0 ) );
doc.batch().remove( otherRoot.getChild( 0 ) );
} );

expect( editor.data.get( 'main' ) ).to.equal( '<p>&nbsp;</p>' );
expect( editor.data.get( 'otherRoot' ) ).to.equal( '<p>&nbsp;</p>' );

editor.execute( 'undo' );

expect( editor.data.get( 'main' ) ).to.equal( '<p>&nbsp;</p>' );
expect( editor.data.get( 'otherRoot' ) ).to.equal( '<p>Foobar.</p>' );

editor.execute( 'undo' );

expect( editor.data.get( 'main' ) ).to.equal( '<p>Foobar.</p>' );
expect( editor.data.get( 'otherRoot' ) ).to.equal( '<p>Foobar.</p>' );
} );
} );
} );
} );
56 changes: 56 additions & 0 deletions tests/paragraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import {
import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';

import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';

import ModelDocumentFragment from '@ckeditor/ckeditor5-engine/src/model/documentfragment';
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
import ModelText from '@ckeditor/ckeditor5-engine/src/model/text';
import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';

describe( 'Paragraph feature', () => {
let editor, doc;
Expand Down Expand Up @@ -339,6 +342,59 @@ describe( 'Paragraph feature', () => {
} );
} );

describe( 'post-fixing empty roots', () => {
it( 'should fix empty roots after editor is initialised', () => {
expect( doc.getRoot().childCount ).to.equal( 1 );
expect( doc.getRoot().getChild( 0 ).is( 'paragraph' ) ).to.be.true;
} );

it( 'should fix roots if it becomes empty', () => {
editor.setData( '<p>Foobar</p>' );

// Since `setData` first removes all contents from editor and then sets content during same enqueue
// changes block, this checks whether fixing empty roots does not kick too early and does not
// fix root if it is not needed.
expect( editor.getData() ).to.equal( '<p>Foobar</p>' );

editor.setData( '' );

expect( doc.getRoot().childCount ).to.equal( 1 );
expect( doc.getRoot().getChild( 0 ).is( 'paragraph' ) ).to.be.true;
} );

it( 'should not fix root if it got content during changesDone event', () => {
// "Autoheading feature".
doc.schema.registerItem( 'heading', '$block' );

buildModelConverter().for( editor.editing.modelToView, editor.data.modelToView )
.fromElement( 'heading' )
.toElement( 'h1' );

doc.on( 'changesDone', () => {
const root = doc.getRoot();

if ( root.isEmpty ) {
doc.enqueueChanges( () => {
doc.batch().insert( ModelPosition.createAt( root ), new ModelElement( 'heading' ) );
} );
}
} );

editor.setData( '' );

expect( doc.getRoot().childCount ).to.equal( 1 );
expect( doc.getRoot().getChild( 0 ).name ).to.equal( 'heading' );
} );

it( 'should not fix root which does not allow paragraph', () => {
doc.schema.disallow( { name: 'paragraph', inside: '$root' } );

editor.setData( '' );

expect( editor.getData() ).to.equal( '' );
} );
} );

describe( 'command', () => {
it( 'should be set in the editor', () => {
expect( editor.commands.get( 'paragraph' ) ).to.be.instanceof( ParagraphCommand );
Expand Down

0 comments on commit c42d33e

Please sign in to comment.