Explorar el Código

Base infrastructure and tests for the insertContent() function.

Piotrek Koszuliński hace 9 años
padre
commit
e26381c970

+ 8 - 15
packages/ckeditor5-engine/src/datacontroller.js

@@ -111,16 +111,7 @@ export default class DataController {
 		this.viewToModel.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
 		this.viewToModel.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
 
-		this.on( 'insertContent', ( evt, data ) => insertContent.call( this, data.batch, data.selection, data.content ) );
-
-		// TMP!
-		// Create an "all allowed" context in the schema for processing the pasted content.
-		// Read: https://github.com/ckeditor/ckeditor5-engine/issues/638#issuecomment-255086588
-
-		const schema = model.schema;
-
-		schema.registerItem( '$clipboardHolder', '$root' );
-		schema.allow( { name: '$text', inside: '$clipboardHolder' } );
+		this.on( 'insertContent', ( evt, data ) => insertContent( this, data.batch, data.selection, data.content ) );
 	}
 
 	/**
@@ -241,21 +232,23 @@ mix( DataController, EmitterMixin );
  * TODO
  *
  * @method engine.dataController.insertContent
- * @context engine.DataController
+ * @param {engine.DataController} dataController
  * @param {engine.model.Batch} batch Batch to which deltas will be added.
  * @param {engine.model.Selection} selection Selection into which the content should be inserted.
  * The selection should be collapsed.
  * @param {engine.model.DocumentFragment} content The content to insert.
  */
-export default function insertContent( batch, selection, content ) {
+export function insertContent( dataController, batch, selection, content ) {
 	if ( !selection.isCollapsed ) {
-		this.model.composer.deleteContents( batch, selection );
+		dataController.model.composer.deleteContents( batch, selection, {
+			merge: true
+		} );
 	}
 
 	// Convert the pasted content to a model document fragment.
 	// Convertion is contextual, but in this case we need an "all allowed" context and for that
-	// we use the $clipboardHolder item. See the comment in the constructor.
-	const modelFragment = this.viewToModel.convert( content, {
+	// we use the $clipboardHolder item.
+	const modelFragment = dataController.viewToModel.convert( content, {
 		context: [ '$clipboardHolder' ]
 	} );
 

+ 14 - 0
packages/ckeditor5-engine/src/model/schema.js

@@ -250,6 +250,11 @@ export default class Schema {
 	 * Creates Schema instance.
 	 */
 	constructor() {
+		/**
+		 * TODO
+		 */
+		this.objects = new Set();
+
 		/**
 		 * Schema items registered in the schema.
 		 *
@@ -274,6 +279,15 @@ export default class Schema {
 
 		this.allow( { name: '$block', inside: '$root' } );
 		this.allow( { name: '$inline', inside: '$block' } );
+
+		// TMP!
+		// Create an "all allowed" context in the schema for processing the pasted content.
+		// Read: https://github.com/ckeditor/ckeditor5-engine/issues/638#issuecomment-255086588
+
+		if ( !this.hasItem( '$clipboardHolder' ) ) {
+			this.registerItem( '$clipboardHolder', '$root' );
+			this.allow( { name: '$text', inside: '$clipboardHolder' } );
+		}
 	}
 
 	/**

+ 34 - 0
packages/ckeditor5-engine/tests/datacontroller.js

@@ -12,6 +12,9 @@ import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor
 import buildViewConverter  from '/ckeditor5/engine/conversion/buildviewconverter.js';
 import buildModelConverter  from '/ckeditor5/engine/conversion/buildmodelconverter.js';
 
+import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
+import ViewText from '/ckeditor5/engine/view/text.js';
+
 import { getData, setData, stringify, parse } from '/ckeditor5/engine/dev-utils/model.js';
 
 import count from '/ckeditor5/utils/count.js';
@@ -37,6 +40,19 @@ describe( 'DataController', () => {
 
 			expect( data.processor ).to.be.undefined;
 		} );
+
+		it( 'should add insertContent listener', () => {
+			const batch = modelDocument.batch();
+			const content = new ViewDocumentFragment( [ new ViewText( 'x' ) ] );
+
+			schema.registerItem( 'paragraph', '$block' );
+
+			setData( modelDocument, '<paragraph>a[]b</paragraph>' );
+
+			data.fire( 'insertContent', { batch, content, selection: modelDocument.selection } );
+
+			expect( getData( modelDocument ) ).to.equal( '<paragraph>ax[]b</paragraph>' );
+		} );
 	} );
 
 	describe( 'parse', () => {
@@ -252,4 +268,22 @@ describe( 'DataController', () => {
 			expect( data ).to.respondTo( 'destroy' );
 		} );
 	} );
+
+	describe( 'insertContent', () => {
+		it( 'should fire the insertContent event', () => {
+			const spy = sinon.spy();
+			const batch = modelDocument.batch();
+			const content = new ViewDocumentFragment( [ new ViewText( 'x' ) ] );
+
+			data.on( 'insertContent', spy );
+
+			data.insertContent( batch, modelDocument.selection, content );
+
+			expect( spy.args[ 0 ][ 1 ] ).to.deep.equal( {
+				batch: batch,
+				selection: modelDocument.selection,
+				content: content
+			} );
+		} );
+	} );
 } );

+ 477 - 0
packages/ckeditor5-engine/tests/datacontroller/insertcontent.js

@@ -0,0 +1,477 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: model */
+
+import Document from '/ckeditor5/engine/model/document.js';
+import { default as DataController, insertContent } from '/ckeditor5/engine/datacontroller.js';
+
+import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
+import ModelDocumentFragment from '/ckeditor5/engine/model/documentfragment.js';
+import Text from '/ckeditor5/engine/model/text.js';
+
+import { setData, getData, parse } from '/ckeditor5/engine/dev-utils/model.js';
+
+describe( 'DataController', () => {
+	let doc, dataController;
+
+	describe( 'insertContent', () => {
+		describe( 'in simple scenarios', () => {
+			beforeEach( () => {
+				doc = new Document();
+				doc.createRoot();
+
+				dataController = new DataController( doc );
+
+				const schema = doc.schema;
+
+				schema.registerItem( 'image', '$inline' );
+				schema.registerItem( 'disallowedElement' );
+
+				schema.allow( { name: '$text', inside: '$root' } );
+				schema.allow( { name: 'image', inside: '$root' } );
+				// Otherwise it won't be passed to the temporary model fragment used inside insertContent().
+				schema.allow( { name: 'disallowedElement', inside: '$clipboardHolder' } );
+
+				schema.objects.add( 'image' );
+			} );
+
+			test(
+				'inserts one text node',
+				'xyz',
+				'f[]oo',
+				'fxyz[]oo'
+			);
+
+			test(
+				'inserts one text node (at the end)',
+				'xyz',
+				'foo[]',
+				'fooxyz[]'
+			);
+
+			test(
+				'inserts an element',
+				'<image></image>',
+				'f[]oo',
+				'f<image></image>[]oo'
+			);
+
+			test(
+				'inserts a text and an element',
+				'xyz<image></image>',
+				'f[]oo',
+				'fxyz<image></image>[]oo'
+			);
+
+			test(
+				'strips a disallowed element',
+				'<disallowedElement>xyz</disallowedElement>',
+				'f[]oo',
+				'fxyz[]oo'
+			);
+
+			test(
+				'deletes selection before inserting the content',
+				'x',
+				'f[abc]oo',
+				'fx[]oo'
+			);
+
+			describe( 'spaces handling', () => {
+				// Note: spaces in the view are not encoded like in the DOM, so subsequent spaces must be
+				// inserted into the model as is. The conversion to nbsps happen on view<=>DOM conversion.
+
+				test(
+					'inserts one space',
+					new Text( ' ' ),
+					'f[]oo',
+					'f []oo'
+				);
+
+				test(
+					'inserts three spaces',
+					new Text( '   ' ),
+					'f[]oo',
+					'f   []oo'
+				);
+
+				test(
+					'inserts spaces at the end',
+					new Text( '   ' ),
+					'foo[]',
+					'foo   []'
+				);
+
+				test(
+					'inserts one nbsp',
+					new Text( '\u200a' ),
+					'f[]oo',
+					'f\u200a[]oo'
+				);
+
+				test(
+					'inserts word surrounded by spaces',
+					new Text( ' xyz  ' ),
+					'f[]oo',
+					'f xyz  []oo'
+				);
+			} );
+		} );
+
+		describe( 'in blocks', () => {
+			beforeEach( () => {
+				doc = new Document();
+				doc.createRoot();
+
+				dataController = new DataController( doc );
+
+				const schema = doc.schema;
+
+				schema.registerItem( 'paragraph', '$block' );
+				schema.registerItem( 'heading1', '$block' );
+				schema.registerItem( 'heading2', '$block' );
+				schema.registerItem( 'blockWidget' );
+				schema.registerItem( 'inlineWidget' );
+
+				schema.allow( { name: 'blockWidget', inside: '$root' } );
+				schema.allow( { name: 'inlineWidget', inside: '$block' } );
+				schema.allow( { name: 'inlineWidget', inside: '$clipboardHolder' } );
+
+				schema.objects.add( 'blockWidget' );
+				schema.objects.add( 'inlineWidget' );
+			} );
+
+			test(
+				'inserts one text node',
+				'xyz',
+				'<paragraph>f[]oo</paragraph>',
+				'<paragraph>fxyz[]oo</paragraph>'
+			);
+
+			test(
+				'inserts one text node to fully selected paragraph',
+				'xyz',
+				'<paragraph>[foo]</paragraph>',
+				'<paragraph>xyz[]</paragraph>'
+			);
+
+			test(
+				'inserts one text node to fully selected paragraphs (from outside)',
+				'xyz',
+				'[<paragraph>foo</paragraph><paragraph>bar</paragraph>]',
+				'<paragraph>xyz[]</paragraph>'
+			);
+
+			test(
+				'merges two blocks before inserting content (p+p)',
+				'xyz',
+				'<paragraph>fo[o</paragraph><paragraph>b]ar</paragraph>',
+				'<paragraph>foxyz[]ar</paragraph>'
+			);
+
+			test(
+				'inserts inline widget and text',
+				'xyz<inlineWidget></inlineWidget>',
+				'<paragraph>f[]oo</paragraph>',
+				'<paragraph>fxyz<inlineWidget></inlineWidget>[]oo</paragraph>'
+			);
+
+			// Note: In CKEditor 4 the blocks are not merged, but to KISS we're merging here
+			// because that's what deleteContent() does.
+			test(
+				'merges two blocks before inserting content (h+p)',
+				'xyz',
+				'<heading1>fo[o</heading1><paragraph>b]ar</paragraph>',
+				'<heading1>foxyz[]ar</heading1>'
+			);
+
+			describe( 'block to block handling', () => {
+				// Note: This is temporary implementation which gives a quite poor UX.
+				// See https://github.com/ckeditor/ckeditor5-engine/issues/652
+
+				test(
+					'inserts one paragraph',
+					'<paragraph>xyz</paragraph>',
+					'<paragraph>f[]oo</paragraph>',
+					'<paragraph>fxyz[]oo</paragraph>'
+				);
+
+				test(
+					'inserts one paragraph (at the end)',
+					'<paragraph>xyz</paragraph>',
+					'<paragraph>foo[]</paragraph>',
+					'<paragraph>fooxyz[]</paragraph>'
+				);
+
+				test(
+					'inserts one paragraph into an empty paragraph',
+					'<paragraph>xyz</paragraph>',
+					'<paragraph>[]</paragraph>',
+					'<paragraph>xyz</paragraph>'
+				);
+
+				test(
+					'inserts one block into a fully selected content',
+					'<heading2>xyz</heading2>',
+					'<heading1>[foo</heading1><paragraph>bar]</paragraph>',
+					'<heading2>xyz</heading2>'
+				);
+
+				test(
+					'inserts one heading',
+					'<heading1>xyz</heading1>',
+					'<paragraph>f[]oo</paragraph>',
+					'<paragraph>f</paragraph><heading1>xyz</heading1><paragraph>[]oo</paragraph>'
+				);
+
+				test(
+					'inserts two headings',
+					'<heading1>xyz1</heading1><heading1>xyz2</heading1>',
+					'<paragraph>f[]oo</paragraph>',
+					'<paragraph>f</paragraph><heading1>xyz1</heading1><heading1>xyz2</heading1><paragraph>[]oo</paragraph>'
+				);
+
+				test(
+					'inserts one object',
+					'<blockWidget></blockWidget>',
+					'<paragraph>f[]oo</paragraph>',
+					'<paragraph>f</paragraph>[<blockWidget></blockWidget>]<paragraph>oo</paragraph>'
+				);
+
+				test(
+					'inserts one object (at the end)',
+					'<blockWidget></blockWidget>',
+					'<paragraph>foo[]</paragraph>',
+					'<paragraph>foo</paragraph>[<blockWidget></blockWidget>]'
+				);
+
+				test(
+					'inserts one object (at the beginning)',
+					'<blockWidget></blockWidget>',
+					'<paragraph>[]bar</paragraph>',
+					'[<blockWidget></blockWidget>]<paragraph>bar</paragraph>'
+				);
+			} );
+
+			describe( 'mixed content to block', () => {
+				test(
+					'inserts text + paragraph',
+					'xxx<paragraph>yyy</paragraph>',
+					'<paragraph>f[]oo</paragraph>',
+					'<paragraph>fxxx</paragraph><paragraph>yyy</paragraph><paragraph>[]oo</paragraph>'
+				);
+
+				test(
+					'inserts text + paragraph (at the beginning)',
+					'xxx<paragraph>yyy</paragraph>',
+					'<paragraph>[]foo</paragraph>',
+					'<paragraph>xxx</paragraph><paragraph>yyy</paragraph><paragraph>[]foo</paragraph>'
+				);
+
+				test(
+					'inserts text + paragraph (at the end)',
+					'xxx<paragraph>yyy</paragraph>',
+					'<paragraph>foo[]</paragraph>',
+					'<paragraph>fooxxx</paragraph><paragraph>yyy</paragraph><paragraph>[]</paragraph>'
+				);
+
+				test(
+					'inserts paragraph + text',
+					'<paragraph>yyy</paragraph>xxx',
+					'<paragraph>f[]oo</paragraph>',
+					'<paragraph>f</paragraph><paragraph>xyz</paragraph><paragraph>[]oo</paragraph>'
+				);
+
+				test(
+					'inserts paragraph + text (at the beginning)',
+					'<paragraph>yyy</paragraph>xxx',
+					'<paragraph>f[]oo</paragraph>',
+					'<paragraph>f</paragraph><paragraph>xyz</paragraph><paragraph>[]oo</paragraph>'
+				);
+
+				test(
+					'inserts paragraph + text (at the end)',
+					'<paragraph>yyy</paragraph>xxx',
+					'<paragraph>f[]oo</paragraph>',
+					'<paragraph>f</paragraph><paragraph>xyz</paragraph><paragraph>[]oo</paragraph>'
+				);
+
+				test(
+					'inserts text + heading',
+					'xxx<heading1>yyy</heading1>',
+					'<paragraph>f[]oo</paragraph>',
+					'<paragraph>fxxx</paragraph><heading1>yyy</heading1><paragraph>[]oo</paragraph>'
+				);
+
+				test(
+					'inserts paragraph + object',
+					'<paragraph>xxx</paragraph><blockWidget></blockWidget>',
+					'<paragraph>f[]oo</paragraph>',
+					'<paragraph>fxxx</paragraph>[<blockWidget></blockWidget>]<paragraph>oo</paragraph>'
+				);
+
+				test(
+					'inserts object + paragraph',
+					'<blockWidget></blockWidget><paragraph>xxx</paragraph>',
+					'<paragraph>f[]oo</paragraph>',
+					'<paragraph>f</paragraph><blockWidget></blockWidget><paragraph>xxx[]oo</paragraph>'
+				);
+			} );
+
+			describe( 'content over a block object', () => {
+				test(
+					'inserts text',
+					'xxx',
+					'<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>',
+					'<paragraph>foo</paragraph><paragraph>xxx[]</paragraph><paragraph>bar</paragraph>'
+				);
+
+				test(
+					'inserts paragraph',
+					'<paragraph>xxx</paragraph>',
+					'<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>',
+					'<paragraph>foo</paragraph><paragraph>xxx[]</paragraph><paragraph>bar</paragraph>'
+				);
+
+				test(
+					'inserts text + paragraph',
+					'yyy<paragraph>xxx</paragraph>',
+					'<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>',
+					'<paragraph>foo</paragraph><paragraph>yyy</paragraph><paragraph>xxx[]</paragraph><paragraph>bar</paragraph>'
+				);
+
+				test(
+					'inserts two blocks',
+					'<heading1>xxx</heading1><paragraph>yyy</paragraph>',
+					'<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>',
+					'<paragraph>foo</paragraph><heading1>xxx</heading1><paragraph>yyy[]</paragraph><paragraph>bar</paragraph>'
+				);
+
+				test(
+					'inserts block object',
+					'<blockWidget></blockWidget>',
+					'<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>',
+					'<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' // It's enough, don't worry.
+				);
+
+				test(
+					'inserts inline object',
+					'<inlineWidget></inlineWidget>',
+					'<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>',
+					'<paragraph>foo</paragraph><paragraph>[<inlineWidget></inlineWidget>]</paragraph><paragraph>bar</paragraph>'
+				);
+			} );
+
+			describe( 'content over an inline object', () => {
+				test(
+					'inserts text',
+					'xxx',
+					'<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>',
+					'<paragraph>fooxxx[]bar</paragraph>'
+				);
+
+				test(
+					'inserts paragraph',
+					'<paragraph>xxx</paragraph>',
+					'<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>',
+					'<paragraph>fooxxx[]bar</paragraph>'
+				);
+
+				test(
+					'inserts text + paragraph',
+					'yyy<paragraph>xxx</paragraph>',
+					'<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>',
+					'<paragraph>fooyyy</paragraph><paragraph>xxx[]bar</paragraph>'
+				);
+
+				test(
+					'inserts two blocks',
+					'<heading1>xxx</heading1><paragraph>yyy</paragraph>',
+					'<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>',
+					'<paragraph>fooxxx</paragraph><paragraph>yyy[]bar</paragraph>'
+				);
+
+				test(
+					'inserts inline object',
+					'<inlineWidget></inlineWidget>',
+					'<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>',
+					'<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>'
+				);
+
+				test(
+					'inserts block object',
+					'<blockWidget></blockWidget>',
+					'<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>',
+					'<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>'
+				);
+			} );
+		} );
+
+		describe( 'filtering out', () => {
+			beforeEach( () => {
+				doc = new Document();
+				doc.createRoot();
+
+				dataController = new DataController( doc );
+
+				const schema = doc.schema;
+
+				schema.registerItem( 'paragraph', '$block' );
+
+				// Let's use table as an example of content which needs to be filtered out.
+				schema.registerItem( 'table' );
+				schema.registerItem( 'td' );
+
+				schema.allow( { name: 'table', inside: '$clipboardHolder' } );
+				schema.allow( { name: 'td', inside: '$clipboardHolder' } );
+				schema.allow( { name: '$block', inside: 'td' } );
+				schema.allow( { name: '$text', inside: 'td' } );
+			} );
+
+			test(
+				'filters out disallowed elements and leaves out the text',
+				'<table><td>xxx</td><td>yyy</td></table>',
+				'<paragraph>f[]oo</paragraph>',
+				'<paragraph>fxxxyyy[]oo</paragraph>'
+			);
+
+			test(
+				'filters out disallowed elements and leaves out the paragraphs',
+				'<table><td><paragraph>xxx</paragraph><paragraph>yyy</paragraph><paragraph>zzz</paragraph></td></table>',
+				'<paragraph>f[]oo</paragraph>',
+				'<paragraph>fxxx</paragraph><paragraph>yyy</paragraph><paragraph>zzz[]oo</paragraph>'
+			);
+		} );
+	} );
+
+	// @param {String} title
+	// @param {engine.model.Item|String} content
+	function test( title, content, initialData, expectedData ) {
+		it( title, () => {
+			setData( doc, initialData );
+
+			if ( typeof content == 'string' ) {
+				content = parse( content, doc.schema, {
+					context: [ '$clipboardHolder' ]
+				} );
+			}
+
+			if ( !( content instanceof ModelDocumentFragment ) ) {
+				content = new ModelDocumentFragment( [ content ] );
+			}
+
+			// Override the convertion so we get exactly the model that we defined in the content param.
+			// This way we avoid the need to write converters for everything we want to test.
+			dataController.viewToModel.convert = () => {
+				return content;
+			};
+
+			insertContent( dataController, doc.batch(), doc.selection, new ViewDocumentFragment() );
+
+			expect( getData( doc ) ).to.equal( expectedData );
+		} );
+	}
+} );