8
0
Просмотр исходного кода

Changed model structure of title element to make it possible to controll it by schema.

Oskar Wróbel 6 лет назад
Родитель
Сommit
d9309f948d

+ 111 - 17
packages/ckeditor5-heading/src/title.js

@@ -59,24 +59,48 @@ export default class Title extends Plugin {
 		 */
 		this._bodyPlaceholder = null;
 
-		// Schema.
+		// To use Schema for disabling some features when selection is inside the title element
+		// it's needed to create the following structure:
+		// <title><title-content>The title text</title-content></title>
+		// See: https://github.com/ckeditor/ckeditor5/issues/2005.
 		model.schema.register( 'title', { inheritAllFrom: '$block' } );
+		model.schema.register( 'title-content', {
+			inheritAllFrom: '$block',
+			allowIn: 'title'
+		} );
 
-		// Allow title only directly inside a root.
 		model.schema.addChildCheck( ( context, childDefinition ) => {
-			if ( !context.endsWith( '$root' ) && childDefinition.name === 'title' ) {
+			// Allow `title` element only directly inside a root.
+			if ( childDefinition.name === 'title' && !context.endsWith( '$root' ) ) {
+				return false;
+			}
+
+			// Allow only `title-content` inside `title`.
+			if (
+				context.endsWith( 'title' ) && childDefinition.name !== 'title-content' ||
+				childDefinition.name === 'title-content' && !context.endsWith( 'title' )
+			) {
 				return false;
 			}
 		} );
 
-		// Disallow all attributes in title.
+		// Disallow all attributes in `title`.
 		model.schema.addAttributeCheck( context => {
-			if ( context.endsWith( 'title $text' ) ) {
+			if ( context.endsWith( 'title-content $text' ) ) {
 				return false;
 			}
 		} );
 
-		editor.conversion.elementToElement( { model: 'title', view: 'h1' } );
+		// Because of title is represented by two elements in the model
+		// but only one in the view it's needed to adjust Mapper.
+		editor.editing.mapper.on( 'modelToViewPosition', mapModelPositionToView( editor.editing.view ) );
+		editor.data.mapper.on( 'modelToViewPosition', mapModelPositionToView( editor.editing.view ) );
+
+		// `title-content` <-> `h1` conversion.
+		editor.conversion.elementToElement( { model: 'title-content', view: 'h1' } );
+
+		// Take care about proper title element structure.
+		model.document.registerPostFixer( writer => this._fixTitleContent( writer ) );
 
 		// Create and take care about proper position of a title element.
 		model.document.registerPostFixer( writer => this._fixTitleElement( writer ) );
@@ -98,9 +122,9 @@ export default class Title extends Plugin {
 	 */
 	setTitle( data ) {
 		const editor = this.editor;
-		const title = editor.model.document.getRoot().getChild( 0 );
+		const titleContent = editor.model.document.getRoot().getChild( 0 ).getChild( 0 );
 
-		editor.model.insertContent( editor.data.parse( data ), title, 'in' );
+		editor.model.insertContent( editor.data.parse( data, 'title-content' ), titleContent, 'in' );
 	}
 
 	/**
@@ -112,7 +136,7 @@ export default class Title extends Plugin {
 	 * @returns {String} Title of the document.
 	 */
 	getTitle() {
-		const title = this.editor.model.document.getRoot().getChild( 0 );
+		const title = this.editor.model.document.getRoot().getChild( 0 ).getChild( 0 );
 
 		return this.editor.data.stringify( title );
 	}
@@ -152,6 +176,47 @@ export default class Title extends Plugin {
 		return this.editor.data.processor.toData( viewDocumentFragment );
 	}
 
+	/**
+	 * Returns `title` element when is in the document. Returns `null` otherwise.
+	 *
+	 * @private
+	 * @returns {module:engine/model/element~Element|null}
+	 */
+	_getTitleElement() {
+		const root = this.editor.model.document.getRoot();
+
+		return Array.from( root.getChildren() ).find( element => element.is( 'title' ) );
+	}
+
+	/**
+	 * Model post-fixer callback that ensures `title` has only one `title-content` child.
+	 * All additional children should be moved after `title` element and renamed to a paragraph.
+	 *
+	 * @private
+	 * @param {module:engine/model/writer~Writer} writer
+	 * @returns {Boolean}
+	 */
+	_fixTitleContent( writer ) {
+		const title = this._getTitleElement();
+
+		if ( !title ) {
+			return false;
+		}
+
+		const titleChildren = Array.from( title.getChildren() );
+		let hasChanged = false;
+
+		titleChildren.shift();
+
+		for ( const titleChild of titleChildren ) {
+			writer.move( writer.createRangeOn( titleChild ), title, 'after' );
+			writer.rename( titleChild, 'paragraph' );
+			hasChanged = true;
+		}
+
+		return hasChanged;
+	}
+
 	/**
 	 * Model post-fixer callback that takes care about creating and keeping `title` element as a first child in a root.
 	 *
@@ -170,12 +235,18 @@ export default class Title extends Plugin {
 		for ( const rootChild of modelRoot.getChildren() ) {
 			// If the first element is not a title we need to fix it.
 			if ( index === 0 && !rootChild.is( 'title' ) ) {
-				const title = Array.from( modelRoot.getChildren() ).find( item => item.is( 'title' ) );
+				const title = this._getTitleElement();
 
 				// Change first element to the title if it can be a title.
 				if ( allowedToBeTitle.has( rootChild.name ) ) {
-					writer.rename( rootChild, 'title' );
-					// After changing element to the title is has to be filtered out from disallowed attributes.
+					const position = model.createPositionBefore( rootChild );
+					const title = writer.createElement( 'title' );
+
+					writer.insert( title, position );
+					writer.append( rootChild, title );
+					writer.rename( rootChild, 'title-content' );
+
+					// After changing element to the title it has to be filtered out from disallowed attributes.
 					model.schema.removeDisallowedAttributes( Array.from( rootChild.getChildren() ), writer );
 
 				// If the first element cannot be a title but title is already in the root
@@ -187,13 +258,22 @@ export default class Title extends Plugin {
 				// If there is no title or any element that could be a title then create an empty title.
 				} else {
 					writer.insertElement( 'title', modelRoot );
+					writer.insertElement( 'title-content', modelRoot.getChild( 0 ) );
 				}
 
 				hasChanged = true;
 
-			// If there is a title in the content then change ot to a paragraph.
+			// If there is a title somewhere in the content.
 			} else if ( index > 0 && rootChild.is( 'title' ) ) {
-				writer.rename( rootChild, 'paragraph' );
+				// Rename it to a paragraph if it has a content.
+				if ( model.hasContent( rootChild ) ) {
+					writer.rename( rootChild, 'paragraph' );
+					writer.unwrap( rootChild.getChild( 0 ) );
+				// Or remove if it's empty (it's created as a result of splitting parents by insertContent method).
+				} else {
+					writer.remove( rootChild );
+				}
+
 				hasChanged = true;
 			}
 
@@ -248,7 +328,7 @@ export default class Title extends Plugin {
 		const viewRoot = view.document.getRoot();
 
 		// Attach placeholder to the view title element.
-		editor.editing.downcastDispatcher.on( 'insert:title', ( evt, data, conversionApi ) => {
+		editor.editing.downcastDispatcher.on( 'insert:title-content', ( evt, data, conversionApi ) => {
 			enablePlaceholder( {
 				view,
 				element: conversionApi.mapper.toViewElement( data.item ),
@@ -306,7 +386,7 @@ export default class Title extends Plugin {
 				const selection = model.document.selection;
 				const selectedElements = Array.from( selection.getSelectedBlocks() );
 
-				if ( selectedElements.length === 1 && selectedElements[ 0 ].name === 'title' ) {
+				if ( selectedElements.length === 1 && selectedElements[ 0 ].name === 'title-content' ) {
 					const firstBodyElement = model.document.getRoot().getChild( 1 );
 					writer.setSelection( firstBodyElement, 0 );
 					cancel();
@@ -328,10 +408,24 @@ export default class Title extends Plugin {
 				const selectionPosition = selection.getFirstPosition();
 
 				if ( selectedElement === root.getChild( 1 ) && selectionPosition.isAtStart ) {
-					writer.setSelection( root.getChild( 0 ), 0 );
+					writer.setSelection( root.getChild( 0 ).getChild( 0 ), 0 );
 					cancel();
 				}
 			} );
 		} );
 	}
 }
+
+function mapModelPositionToView( editingView ) {
+	return ( evt, data ) => {
+		if ( !data.modelPosition.parent.is( 'title' ) ) {
+			return;
+		}
+
+		const modelParent = data.modelPosition.parent.parent;
+		const viewParent = data.mapper.toViewElement( modelParent );
+
+		data.viewPosition = editingView.createPositionAt( viewParent, 0 );
+		evt.stop();
+	};
+}

+ 3 - 2
packages/ckeditor5-heading/tests/manual/title.js

@@ -15,11 +15,12 @@ import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 import { UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
 import Image from '@ckeditor/ckeditor5-image/src/image';
 import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
+import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ Enter, Typing, Undo, Heading, Title, Clipboard, Image, ImageUpload ],
-		toolbar: [ 'heading', '|', 'undo', 'redo', 'imageUpload' ]
+		plugins: [ Enter, Typing, Undo, Heading, Title, Clipboard, Image, ImageUpload, Bold ],
+		toolbar: [ 'heading', '|', 'undo', 'redo', 'bold', 'imageUpload' ]
 	} )
 	.then( editor => {
 		window.editor = editor;

+ 2 - 10
packages/ckeditor5-heading/tests/manual/title.md

@@ -20,17 +20,9 @@ There should be no empty paragraph at the end of document, title should contains
 
 There should be no empty paragraph at the end of document, title should contains `Foo` body `Bar`.
 
-### Uploading image to the title
+### Changing title
 
 - type something in the title
 - put selection in the middle of the title
-- upload image using toolbar button
 
-Image should land after the title element and should not split the title.
-
-### Uploading image to the title (drag&drop)
-
-- type something in the title
-- try to drop image to the title or before the title
-
-Image should always land after the title element.
+Heading dropdown, upload and bold icons should be disabled as long as selection stay in the title element.

+ 393 - 178
packages/ckeditor5-heading/tests/title.js

@@ -17,8 +17,6 @@ import Image from '@ckeditor/ckeditor5-image/src/image';
 import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
 import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 
-import DataTransfer from '@ckeditor/ckeditor5-clipboard/src/datatransfer';
-import { UploadAdapterMock, createNativeFileMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
 import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
@@ -50,126 +48,361 @@ describe( 'Title', () => {
 	} );
 
 	it( 'should set proper schema rules', () => {
-		expect( model.schema.isRegistered( 'title', '$text' ) ).to.equal( true );
-		expect( model.schema.checkChild( [ 'title' ], '$text' ) ).to.equal( true );
-		expect( model.schema.checkChild( [ '$root' ], 'title' ) ).to.equal( true );
-		expect( model.schema.checkChild( [ '$root', 'blockQuote' ], 'title' ) ).to.equal( false );
-		expect( model.schema.checkChild( [ '$root', 'paragraph' ], 'title' ) ).to.equal( false );
-		expect( model.schema.checkAttribute( [ 'title', '$text' ], 'bold' ) ).to.equal( false );
-		expect( model.schema.checkAttribute( [ 'title', '$text' ], 'foo' ) ).to.equal( false );
+		expect( model.schema.isRegistered( 'title' ) ).to.equal( true );
+		expect( model.schema.isRegistered( 'title-content' ) ).to.equal( true );
+
+		expect( model.schema.checkChild( 'title', '$text' ) ).to.equal( false );
+		expect( model.schema.checkChild( 'title', 'paragraph' ) ).to.equal( false );
+		expect( model.schema.checkChild( 'title', 'title-content' ) ).to.equal( true );
+		expect( model.schema.checkChild( '$root', 'title-content' ) ).to.equal( false );
+		expect( model.schema.checkChild( 'blockQuote', 'title-content' ) ).to.equal( false );
+		expect( model.schema.checkChild( 'title-content', '$text' ) ).to.equal( true );
+		expect( model.schema.checkChild( 'title-content', 'paragraph' ) ).to.equal( false );
+		expect( model.schema.checkChild( 'title-content', 'blockQuote' ) ).to.equal( false );
+		expect( model.schema.checkChild( '$root', 'title' ) ).to.equal( true );
+		expect( model.schema.checkChild( 'blockQuote', 'title' ) ).to.equal( false );
+		expect( model.schema.checkChild( 'paragraph', 'title' ) ).to.equal( false );
+
+		model.schema.extend( '$text', { allowAttributes: [ 'bold', 'foo' ] } );
+
+		expect( model.schema.checkAttribute( [ 'title-content', '$text' ], 'bold' ) ).to.equal( false );
+		expect( model.schema.checkAttribute( [ 'paragraph', '$text' ], 'bold' ) ).to.equal( true );
+
+		expect( model.schema.checkAttribute( [ 'title-content', '$text' ], 'foo' ) ).to.equal( false );
+		expect( model.schema.checkAttribute( [ 'paragraph', '$text' ], 'foo' ) ).to.equal( true );
 	} );
 
 	it( 'should convert title to h1', () => {
-		setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
+		setData( model,
+			'<title><title-content>Foo</title-content></title>' +
+			'<paragraph>Bar</paragraph>'
+		);
 
 		expect( editor.getData() ).to.equal( '<h1>Foo</h1><p>Bar</p>' );
 	} );
 
 	describe( 'model post-fixing', () => {
 		it( 'should set title and content elements', () => {
-			setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
+			setData( model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 
-			expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph>Bar</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 		} );
 
 		it( 'should create a content element when only title has been set', () => {
-			setData( model, '<title>Foo</title>' );
+			setData( model, '<title><title-content>Foo</title-content></title>' );
 
-			expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph></paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<paragraph></paragraph>'
+			);
 		} );
 
 		it( 'should create a title and content elements when are missing', () => {
 			setData( model, '' );
 
-			expect( getData( model ) ).to.equal( '<title>[]</title><paragraph></paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]</title-content></title>' +
+				'<paragraph></paragraph>'
+			);
 		} );
 
 		it( 'should change heading1 element to title when is set as a first root child', () => {
-			setData( model, '<heading1>Foo</heading1><heading1>Bar</heading1>' );
+			setData( model,
+				'<heading1>Foo</heading1>' +
+				'<heading1>Bar</heading1>'
+			);
 
-			expect( getData( model ) ).to.equal( '<title>[]Foo</title><heading1>Bar</heading1>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<heading1>Bar</heading1>'
+			);
 		} );
 
 		it( 'should change heading2 element to title when is set as a first root child', () => {
-			setData( model, '<heading2>Foo</heading2><heading2>Bar</heading2>' );
+			setData( model,
+				'<heading2>Foo</heading2>' +
+				'<heading2>Bar</heading2>'
+			);
 
-			expect( getData( model ) ).to.equal( '<title>[]Foo</title><heading2>Bar</heading2>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<heading2>Bar</heading2>'
+			);
 		} );
 
 		it( 'should change heading3 element to title when is set as a first root child', () => {
-			setData( model, '<heading3>Foo</heading3><heading3>Bar</heading3>' );
+			setData( model,
+				'<heading3>Foo</heading3>' +
+				'<heading3>Bar</heading3>'
+			);
 
-			expect( getData( model ) ).to.equal( '<title>[]Foo</title><heading3>Bar</heading3>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<heading3>Bar</heading3>'
+			);
 		} );
 
 		it( 'should change paragraph element to title when is set as a first root child', () => {
-			setData( model, '<paragraph>Foo</paragraph><paragraph>Bar</paragraph>' );
+			setData( model,
+				'<paragraph>Foo</paragraph>' +
+				'<paragraph>Bar</paragraph>'
+			);
 
-			expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph>Bar</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 		} );
 
 		it( 'should change title element to a paragraph when is not a first root child #1', () => {
-			setData( model, '<title>Foo</title><title>Bar</title>' );
+			setData( model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<title><title-content>Bar</title-content></title>'
+			);
 
-			expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph>Bar</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 		} );
 
 		it( 'should change title element to a paragraph when is not a first root child #2', () => {
-			setData( model, '<title>Foo</title><paragraph>Bar</paragraph><title>Biz</title>' );
+			setData( model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>' +
+				'<title><title-content>Biz</title-content></title>'
+			);
 
-			expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph>Bar</paragraph><paragraph>Biz</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>' +
+				'<paragraph>Biz</paragraph>'
+			);
 		} );
 
 		it( 'should move element after a title element when is not allowed to be a title', () => {
-			setData( model, '<blockQuote><paragraph>Foo</paragraph></blockQuote><title>Bar</title>' );
+			setData( model,
+				'<blockQuote><paragraph>Foo</paragraph></blockQuote>' +
+				'<title><title-content>Bar</title-content></title>'
+			);
 
-			expect( getData( model ) ).to.equal( '<title>[]Bar</title><blockQuote><paragraph>Foo</paragraph></blockQuote>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Bar</title-content></title>' +
+				'<blockQuote><paragraph>Foo</paragraph></blockQuote>'
+			);
 		} );
 
 		it( 'should create a missing title element before an element that cannot to be a title element', () => {
 			setData( model, '<blockQuote><paragraph>Foo</paragraph></blockQuote>' );
 
-			expect( getData( model ) ).to.equal( '<title>[]</title><blockQuote><paragraph>Foo</paragraph></blockQuote>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]</title-content></title>' +
+				'<blockQuote><paragraph>Foo</paragraph></blockQuote>'
+			);
 		} );
 
 		it( 'should clear element from attributes when changing to title element', () => {
 			model.schema.extend( '$text', { allowAttributes: 'bold' } );
 
 			setData( model,
-				'<paragraph>F<$text bold="true">o</$text>o</paragraph><paragraph>B<$text bold="true">a</$text>r</paragraph>'
+				'<paragraph>F<$text bold="true">o</$text>o</paragraph>' +
+				'<paragraph>B<$text bold="true">a</$text>r</paragraph>'
 			);
 
-			expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph>B<$text bold="true">a</$text>r</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<paragraph>B<$text bold="true">a</$text>r</paragraph>'
+			);
+		} );
+	} );
+
+	describe( 'prevent extra paragraphing', () => {
+		it( 'should remove the extra paragraph when pasting to the editor with body placeholder', () => {
+			setData( model, '<title><title-content>[]</title-content></title>' );
+
+			const dataTransferMock = {
+				getData: type => {
+					if ( type === 'text/html' ) {
+						return '<h1>Title</h1><p>Body</p>';
+					}
+				},
+				types: [],
+				files: []
+			};
+
+			editor.editing.view.document.fire( 'paste', {
+				dataTransfer: dataTransferMock,
+				preventDefault() {}
+			} );
+
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>Title</title-content></title>' +
+				'<paragraph>Body[]</paragraph>'
+			);
+		} );
+
+		it( 'should not remove the extra paragraph when pasting to the editor with directly created body element', () => {
+			setData( model,
+				'<title><title-content>[]</title-content></title>' +
+				'<paragraph></paragraph>'
+			);
+
+			const dataTransferMock = {
+				getData: type => {
+					if ( type === 'text/html' ) {
+						return '<h1>Title</h1><p>Body</p>';
+					}
+				},
+				types: [],
+				files: []
+			};
+
+			editor.editing.view.document.fire( 'paste', {
+				dataTransfer: dataTransferMock,
+				preventDefault() {}
+			} );
+
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>Title</title-content></title>' +
+				'<paragraph>Body[]</paragraph>' +
+				'<paragraph></paragraph>'
+			);
+		} );
+
+		it( 'should remove the extra paragraph when pressing enter in the title', () => {
+			setData( model, '<title><title-content>fo[]o</title-content></title>' );
+
+			editor.execute( 'enter' );
+
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>fo</title-content></title>' +
+				'<paragraph>[]o</paragraph>'
+			);
+		} );
+
+		it( 'should not remove the extra paragraph when pressing enter in the title when body is created directly', () => {
+			setData( model,
+				'<title><title-content>fo[]o</title-content></title>' +
+				'<paragraph></paragraph>'
+			);
+
+			editor.execute( 'enter' );
+
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>fo</title-content></title>' +
+				'<paragraph>[]o</paragraph>' +
+				'<paragraph></paragraph>'
+			);
 		} );
 	} );
 
 	describe( 'setTitle()', () => {
 		it( 'should set new content of a title element', () => {
-			setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
+			setData( model,
+				'<title><title-content></title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 
 			editor.plugins.get( 'Title' ).setTitle( 'Biz' );
 
-			expect( getData( model ) ).to.equal( '<title>[]Biz</title><paragraph>Bar</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Biz</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
+		} );
+
+		it( 'should replace old content of a title element', () => {
+			setData( model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
+
+			editor.plugins.get( 'Title' ).setTitle( 'Biz' );
+
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Biz</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 		} );
 
 		it( 'should clear content of a title element', () => {
-			setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
+			setData( model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 
 			editor.plugins.get( 'Title' ).setTitle( '' );
 
-			expect( getData( model ) ).to.equal( '<title>[]</title><paragraph>Bar</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
+		} );
+
+		it( 'should properly handle HTML element', () => {
+			setData( model,
+				'<title><title-content></title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
+
+			editor.plugins.get( 'Title' ).setTitle( '<p>Foo</p>' );
+
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
+		} );
+
+		it( 'should properly handle multiple HTML elements', () => {
+			setData( model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
+
+			editor.plugins.get( 'Title' ).setTitle( '<p>Foo</p><p>bar</p>' );
+
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foobar</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
+		} );
+
+		it( 'should do nothing when setting empty content to the empty title', () => {
+			setData( model,
+				'<title><title-content></title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
+
+			editor.plugins.get( 'Title' ).setTitle( '' );
+
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 		} );
 	} );
 
 	describe( 'getTitle()', () => {
 		it( 'should return content of a title element', () => {
-			setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
+			setData( model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 
 			expect( editor.plugins.get( 'Title' ).getTitle() ).to.equal( 'Foo' );
 		} );
 
 		it( 'should return content of an empty title element', () => {
-			setData( model, '<title></title><paragraph>Bar</paragraph>' );
+			setData( model,
+				'<title><title-content></title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 
 			expect( editor.plugins.get( 'Title' ).getTitle() ).to.equal( '' );
 		} );
@@ -177,41 +410,62 @@ describe( 'Title', () => {
 
 	describe( 'setBody()', () => {
 		it( 'should set new content to body', () => {
-			setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
+			setData( model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 
 			editor.plugins.get( 'Title' ).setBody( 'Biz' );
 
-			expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph>Biz</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<paragraph>Biz</paragraph>'
+			);
 		} );
 
 		it( 'should set empty content to body', () => {
-			setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
+			setData( model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 
 			editor.plugins.get( 'Title' ).setBody( '' );
 
-			expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph></paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<paragraph></paragraph>'
+			);
 		} );
 
 		it( 'should set html content to body', () => {
-			setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
+			setData( model,
+				'<title><title-content>Foo</title-content>' +
+				'</title><paragraph>Bar</paragraph>'
+			);
 
 			editor.plugins.get( 'Title' ).setBody( '<blockQuote>Bar</blockQuote><p>Biz</p>' );
 
 			expect( getData( model ) ).to.equal(
-				'<title>[]Foo</title><blockQuote><paragraph>Bar</paragraph></blockQuote><paragraph>Biz</paragraph>'
+				'<title><title-content>[]Foo</title-content></title>' +
+				'<blockQuote><paragraph>Bar</paragraph></blockQuote>' +
+				'<paragraph>Biz</paragraph>'
 			);
 		} );
 	} );
 
 	describe( 'getBody()', () => {
 		it( 'should return all data except the title element', () => {
-			setData( model, '<title>Foo</title><paragraph>Bar</paragraph><paragraph>Biz</paragraph>' );
+			setData( model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>' +
+				'<paragraph>Biz</paragraph>'
+			);
 
 			expect( editor.plugins.get( 'Title' ).getBody() ).to.equal( '<p>Bar</p><p>Biz</p>' );
 		} );
 
 		it( 'should return empty paragraph when body is empty', () => {
-			setData( model, '<title>Foo</title>' );
+			setData( model, '<title><title-content>Foo</title-content></title>' );
 
 			expect( editor.plugins.get( 'Title' ).getBody() ).to.equal( '<p>&nbsp;</p>' );
 		} );
@@ -225,7 +479,10 @@ describe( 'Title', () => {
 		} );
 
 		it( 'should attach placeholder placeholder to title and body', () => {
-			setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
+			setData( model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>'
+			);
 
 			const title = viewRoot.getChild( 0 );
 			const body = viewRoot.getChild( 1 );
@@ -238,7 +495,10 @@ describe( 'Title', () => {
 		} );
 
 		it( 'should show placeholder in empty title and body', () => {
-			setData( model, '<title></title><paragraph></paragraph>' );
+			setData( model,
+				'<title><title-content></title-content></title>' +
+				'<paragraph></paragraph>'
+			);
 
 			const title = viewRoot.getChild( 0 );
 			const body = viewRoot.getChild( 1 );
@@ -251,7 +511,11 @@ describe( 'Title', () => {
 		} );
 
 		it( 'should hide placeholder from body with more than one child elements', () => {
-			setData( editor.model, '<title>Foo</title><paragraph></paragraph><paragraph></paragraph>' );
+			setData( editor.model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<paragraph></paragraph>' +
+				'<paragraph></paragraph>'
+			);
 
 			const body = viewRoot.getChild( 1 );
 
@@ -260,7 +524,10 @@ describe( 'Title', () => {
 		} );
 
 		it( 'should hide placeholder from body with element other than paragraph', () => {
-			setData( editor.model, '<title>Foo</title><heading1></heading1>' );
+			setData( editor.model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<heading1></heading1>'
+			);
 
 			const body = viewRoot.getChild( 1 );
 
@@ -269,19 +536,25 @@ describe( 'Title', () => {
 		} );
 
 		it( 'should hide placeholder when title element become not empty', () => {
-			setData( model, '<title></title><paragraph>[]</paragraph>' );
+			setData( model,
+				'<title><title-content></title-content></title>' +
+				'<paragraph>[]</paragraph>'
+			);
 
 			expect( viewRoot.getChild( 0 ).hasClass( 'ck-placeholder' ) ).to.equal( true );
 
 			model.change( writer => {
-				writer.appendText( 'Bar', null, model.document.getRoot().getChild( 0 ) );
+				writer.appendText( 'Bar', null, model.document.getRoot().getChild( 0 ).getChild( 0 ) );
 			} );
 
 			expect( viewRoot.getChild( 0 ).hasClass( 'ck-placeholder' ) ).to.equal( false );
 		} );
 
 		it( 'should hide placeholder when body element become not empty', () => {
-			setData( model, '<title>Foo</title><paragraph></paragraph>' );
+			setData( model,
+				'<title><title-content>Foo</title-content></title>' +
+				'<paragraph></paragraph>'
+			);
 
 			expect( viewRoot.getChild( 1 ).hasClass( 'ck-placeholder' ) ).to.equal( true );
 
@@ -297,7 +570,11 @@ describe( 'Title', () => {
 			const domConverter = editor.editing.view.domConverter;
 			let bodyDomElement;
 
-			setData( editor.model, '<title>[Foo</title><paragraph>Bar</paragraph><paragraph>Baz]</paragraph>' );
+			setData( editor.model,
+				'<title><title-content>[Foo</title-content></title>' +
+				'<paragraph>Bar</paragraph>' +
+				'<paragraph>Baz]</paragraph>'
+			);
 			editor.model.deleteContent( editor.model.document.selection );
 
 			bodyDomElement = domConverter.mapViewToDom( viewRoot.getChild( 1 ) );
@@ -316,7 +593,10 @@ describe( 'Title', () => {
 
 	describe( 'Tab press handling', () => {
 		it( 'should handle tab key when the selection is at the beginning of the title', () => {
-			setData( model, '<title>[]foo</title><paragraph>bar</paragraph>' );
+			setData( model,
+				'<title><title-content>[]foo</title-content></title>' +
+				'<paragraph>bar</paragraph>'
+			);
 
 			const eventData = getEventData( keyCodes.tab );
 
@@ -324,11 +604,17 @@ describe( 'Title', () => {
 
 			sinon.assert.calledOnce( eventData.preventDefault );
 			sinon.assert.calledOnce( eventData.stopPropagation );
-			expect( getData( model ) ).to.equal( '<title>foo</title><paragraph>[]bar</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>foo</title-content></title>' +
+				'<paragraph>[]bar</paragraph>'
+			);
 		} );
 
 		it( 'should handle tab key when the selection is at the end of the title', () => {
-			setData( model, '<title>foo[]</title><paragraph>bar</paragraph>' );
+			setData( model,
+				'<title><title-content>foo[]</title-content></title>' +
+				'<paragraph>bar</paragraph>'
+			);
 
 			const eventData = getEventData( keyCodes.tab );
 
@@ -336,11 +622,17 @@ describe( 'Title', () => {
 
 			sinon.assert.calledOnce( eventData.preventDefault );
 			sinon.assert.calledOnce( eventData.stopPropagation );
-			expect( getData( model ) ).to.equal( '<title>foo</title><paragraph>[]bar</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>foo</title-content></title>' +
+				'<paragraph>[]bar</paragraph>'
+			);
 		} );
 
 		it( 'should not handle tab key when the selection is in the title and body', () => {
-			setData( model, '<title>fo[o</title><paragraph>b]ar</paragraph>' );
+			setData( model,
+				'<title><title-content>fo[o</title-content></title>' +
+				'<paragraph>b]ar</paragraph>'
+			);
 
 			const eventData = getEventData( keyCodes.tab );
 
@@ -348,11 +640,17 @@ describe( 'Title', () => {
 
 			sinon.assert.notCalled( eventData.preventDefault );
 			sinon.assert.notCalled( eventData.stopPropagation );
-			expect( getData( model ) ).to.equal( '<title>fo[o</title><paragraph>b]ar</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>fo[o</title-content></title>' +
+				'<paragraph>b]ar</paragraph>'
+			);
 		} );
 
 		it( 'should not handle tab key when the selection is in the body', () => {
-			setData( model, '<title>foo</title><paragraph>[]bar</paragraph>' );
+			setData( model,
+				'<title><title-content>foo</title-content></title>' +
+				'<paragraph>[]bar</paragraph>'
+			);
 
 			const eventData = getEventData( keyCodes.tab );
 
@@ -360,13 +658,19 @@ describe( 'Title', () => {
 
 			sinon.assert.notCalled( eventData.preventDefault );
 			sinon.assert.notCalled( eventData.stopPropagation );
-			expect( getData( model ) ).to.equal( '<title>foo</title><paragraph>[]bar</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>foo</title-content></title>' +
+				'<paragraph>[]bar</paragraph>'
+			);
 		} );
 	} );
 
 	describe( 'Shift + Tab press handling', () => {
 		it( 'should handle shift + tab keys when the selection is at the beginning of the body', () => {
-			setData( model, '<title>foo</title><paragraph>[]bar</paragraph>' );
+			setData( model,
+				'<title><title-content>foo</title-content></title>' +
+				'<paragraph>[]bar</paragraph>'
+			);
 
 			const eventData = getEventData( keyCodes.tab, { shiftKey: true } );
 
@@ -374,11 +678,17 @@ describe( 'Title', () => {
 
 			sinon.assert.calledOnce( eventData.preventDefault );
 			sinon.assert.calledOnce( eventData.stopPropagation );
-			expect( getData( model ) ).to.equal( '<title>[]foo</title><paragraph>bar</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>[]foo</title-content></title>' +
+				'<paragraph>bar</paragraph>'
+			);
 		} );
 
 		it( 'should not handle shift + tab keys when the selection is not at the beginning of the body', () => {
-			setData( model, '<title>foo</title><paragraph>b[]ar</paragraph>' );
+			setData( model,
+				'<title><title-content>foo</title-content></title>' +
+				'<paragraph>b[]ar</paragraph>'
+			);
 
 			const eventData = getEventData( keyCodes.tab, { shiftKey: true } );
 
@@ -386,11 +696,17 @@ describe( 'Title', () => {
 
 			sinon.assert.notCalled( eventData.preventDefault );
 			sinon.assert.notCalled( eventData.stopPropagation );
-			expect( getData( model ) ).to.equal( '<title>foo</title><paragraph>b[]ar</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>foo</title-content></title>' +
+				'<paragraph>b[]ar</paragraph>'
+			);
 		} );
 
 		it( 'should not handle shift + tab keys when the selection is not collapsed', () => {
-			setData( model, '<title>foo</title><paragraph>[b]ar</paragraph>' );
+			setData( model,
+				'<title><title-content>foo</title-content></title>' +
+				'<paragraph>[b]ar</paragraph>'
+			);
 
 			const eventData = getEventData( keyCodes.tab, { shiftKey: true } );
 
@@ -398,11 +714,17 @@ describe( 'Title', () => {
 
 			sinon.assert.notCalled( eventData.preventDefault );
 			sinon.assert.notCalled( eventData.stopPropagation );
-			expect( getData( model ) ).to.equal( '<title>foo</title><paragraph>[b]ar</paragraph>' );
+			expect( getData( model ) ).to.equal(
+				'<title><title-content>foo</title-content></title>' +
+				'<paragraph>[b]ar</paragraph>'
+			);
 		} );
 
 		it( 'should not handle shift + tab keys when the selection is in the title', () => {
-			setData( model, '<title>[]foo</title><paragraph>bar</paragraph>' );
+			setData( model,
+				'<title><title-content>[]foo</title-content></title>' +
+				'<paragraph>bar</paragraph>'
+			);
 
 			const eventData = getEventData( keyCodes.tab, { shiftKey: true } );
 
@@ -410,116 +732,9 @@ describe( 'Title', () => {
 
 			sinon.assert.notCalled( eventData.preventDefault );
 			sinon.assert.notCalled( eventData.stopPropagation );
-			expect( getData( model ) ).to.equal( '<title>[]foo</title><paragraph>bar</paragraph>' );
-		} );
-	} );
-
-	describe( 'prevent extra paragraphing', () => {
-		it( 'should remove the extra paragraph when pasting to the editor with body placeholder', () => {
-			setData( model, '<title>[]</title>' );
-
-			const dataTransferMock = {
-				getData: type => {
-					if ( type === 'text/html' ) {
-						return '<h1>Title</h1><p>Body</p>';
-					}
-				},
-				types: [],
-				files: []
-			};
-
-			editor.editing.view.document.fire( 'paste', {
-				dataTransfer: dataTransferMock,
-				preventDefault() {}
-			} );
-
-			expect( getData( model ) ).to.equal( '<title>Title</title><paragraph>Body[]</paragraph>' );
-		} );
-
-		it( 'should not remove the extra paragraph when pasting to the editor with directly created body element', () => {
-			setData( model, '<title>[]</title><paragraph></paragraph>' );
-
-			const dataTransferMock = {
-				getData: type => {
-					if ( type === 'text/html' ) {
-						return '<h1>Title</h1><p>Body</p>';
-					}
-				},
-				types: [],
-				files: []
-			};
-
-			editor.editing.view.document.fire( 'paste', {
-				dataTransfer: dataTransferMock,
-				preventDefault() {}
-			} );
-
-			expect( getData( model ) ).to.equal( '<title>Title</title><paragraph>Body[]</paragraph><paragraph></paragraph>' );
-		} );
-
-		it( 'should remove the extra paragraph when pressing enter in the title', () => {
-			setData( model, '<title>fo[]o</title>' );
-
-			editor.execute( 'enter' );
-
-			expect( getData( model ) ).to.equal( '<title>fo</title><paragraph>[]o</paragraph>' );
-		} );
-
-		it( 'should not remove the extra paragraph when pressing enter in the title when body is created directly', () => {
-			setData( model, '<title>fo[]o</title><paragraph></paragraph>' );
-
-			editor.execute( 'enter' );
-
-			expect( getData( model ) ).to.equal( '<title>fo</title><paragraph>[]o</paragraph><paragraph></paragraph>' );
-		} );
-	} );
-
-	describe( 'upload to title - integration', () => {
-		let file, fileRepository;
-
-		beforeEach( () => {
-			fileRepository = editor.plugins.get( 'FileRepository' );
-			fileRepository.createUploadAdapter = newLoader => new UploadAdapterMock( newLoader );
-			file = createNativeFileMock();
-		} );
-
-		it( 'should move image after the title when was dropped before it', () => {
-			setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
-
-			const dataTransfer = new DataTransfer( { files: [ file ], types: [ 'Files' ] } );
-
-			const targetRange = model.createRange( model.createPositionAt( model.document.getRoot(), 0 ) );
-			const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
-
-			editor.editing.view.document.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
-
-			const { id } = fileRepository.getLoader( file );
-
-			expect( getData( model ) ).to.equal(
-				'<title>Foo</title>' +
-				`[<image uploadId="${ id }" uploadStatus="reading"></image>]` +
-				'<paragraph>Bar</paragraph>'
-			);
-		} );
-
-		it( 'should move image after the title when was dropped to it', () => {
-			setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
-
-			const dataTransfer = new DataTransfer( { files: [ file ], types: [ 'Files' ] } );
-
-			const title = model.document.getRoot().getChild( 0 );
-
-			const targetRange = model.createRange( model.createPositionAt( title, 1 ) );
-			const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
-
-			editor.editing.view.document.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
-
-			const { id } = fileRepository.getLoader( file );
-
 			expect( getData( model ) ).to.equal(
-				'<title>Foo</title>' +
-				`[<image uploadId="${ id }" uploadStatus="reading"></image>]` +
-				'<paragraph>Bar</paragraph>'
+				'<title><title-content>[]foo</title-content></title>' +
+				'<paragraph>bar</paragraph>'
 			);
 		} );
 	} );