浏览代码

Added: introduced post-fixers which result in adding paragraph element to all empty roots on `model.Document#event:changesDone`.

Szymon Cofalik 8 年之前
父节点
当前提交
2937a7161a

+ 44 - 0
packages/ckeditor5-paragraph/src/paragraph.js

@@ -75,6 +75,14 @@ export default class Paragraph extends Plugin {
 		}, { priority: 'low' } );
 		}, { priority: 'low' } );
 
 
 		editor.commands.set( 'paragraph', new ParagraphCommand( editor ) );
 		editor.commands.set( 'paragraph', new ParagraphCommand( editor ) );
+
+		// Post-fixer that takes care of adding empty paragraph elements to empty roots.
+		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' } );
 	}
 	}
 }
 }
 
 
@@ -234,3 +242,39 @@ function hasParagraphLikeContent( element ) {
 
 
 	return false;
 	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 ) {
+			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();
+}

+ 38 - 0
packages/ckeditor5-paragraph/tests/paragraph-intergration.js

@@ -5,6 +5,7 @@
 
 
 import Paragraph from '../src/paragraph';
 import Paragraph from '../src/paragraph';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 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 HeadingEngine from '@ckeditor/ckeditor5-heading/src/headingengine';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import {
 import {
@@ -145,4 +146,41 @@ 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();
+
+					// Selection is in <p>, hence &nbsp;
+					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>' );
+				} );
+		} );
+	} );
 } );
 } );

+ 56 - 0
packages/ckeditor5-paragraph/tests/paragraph.js

@@ -14,9 +14,12 @@ import {
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
 
 import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
 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 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 ModelText from '@ckeditor/ckeditor5-engine/src/model/text';
+import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
 
 
 describe( 'Paragraph feature', () => {
 describe( 'Paragraph feature', () => {
 	let editor, doc;
 	let editor, doc;
@@ -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', () => {
 	describe( 'command', () => {
 		it( 'should be set in the editor', () => {
 		it( 'should be set in the editor', () => {
 			expect( editor.commands.get( 'paragraph' ) ).to.be.instanceof( ParagraphCommand );
 			expect( editor.commands.get( 'paragraph' ) ).to.be.instanceof( ParagraphCommand );