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

Merge pull request #92 from ckeditor/t/ckeditor5-engine/1198

Enhancement: Added support for `ConverterDefinition` in the heading options configuration. Closes #72. Closes ckeditor/ckeditor5#651.

BREAKING CHANGE: `config.heading.options` format has changed. The valid `HeadingOption` syntax is now compatible with `ConverterDefinition`: `{ model: 'heading1', view: 'h2', title: 'Heading 1' }`.
Piotrek Koszuliński 8 лет назад
Родитель
Сommit
0f8ebf7911

+ 13 - 0
packages/ckeditor5-heading/docs/_snippets/features/custom-heading-elements.html

@@ -0,0 +1,13 @@
+<style>
+	h2.fancy, .ck-heading_heading2_fancy {
+		color: #ff0050;
+		font-size: 1.3em;
+	}
+</style>
+
+<div id="snippet-custom-heading-elements">
+	<h1>Heading 1</h1>
+	<h2>Heading 2</h2>
+	<h2 class="fancy">Fancy Heading 2</h2>
+	<p>This is <a href="https://ckeditor5.github.io">CKEditor 5</a>.</p>
+</div>

+ 35 - 0
packages/ckeditor5-heading/docs/_snippets/features/custom-heading-elements.js

@@ -0,0 +1,35 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-custom-heading-elements' ), {
+		heading: {
+			options: [
+				{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
+				{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
+				{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
+				{
+					model: 'headingFancy',
+					view: {
+						name: 'h2',
+						class: 'fancy',
+						priority: 'high'
+					},
+					title: 'Heading 2 (fancy)', class: 'ck-heading_heading2_fancy'
+				}
+			]
+		},
+		toolbar: {
+			viewportTopOffset: 60
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 3 - 3
packages/ckeditor5-heading/docs/_snippets/features/custom-heading-levels.js

@@ -9,9 +9,9 @@ ClassicEditor
 	.create( document.querySelector( '#snippet-custom-heading-levels' ), {
 		heading: {
 			options: [
-				{ modelElement: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
-				{ modelElement: 'heading1', viewElement: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
-				{ modelElement: 'heading2', viewElement: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' }
+				{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
+				{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
+				{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' }
 			]
 		},
 		toolbar: {

+ 55 - 3
packages/ckeditor5-heading/docs/features/headings.md

@@ -39,9 +39,9 @@ ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 		heading: {
 			options: [
-				{ modelElement: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
-				{ modelElement: 'heading1', viewElement: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
-				{ modelElement: 'heading2', viewElement: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' }
+				{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
+				{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
+				{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' }
 			]
 		}
 	} )
@@ -51,6 +51,58 @@ ClassicEditor
 
 {@snippet features/custom-heading-levels}
 
+### Configuring custom heading elements
+
+It is also possible to define fully custom elements for headings by using the {@link module:engine/view/viewelementdefinition~ViewElementDefinition advanced format} of the {@link module:heading/heading~HeadingConfig#options `heading.options`} configuration option.
+
+For example, the following editor will support the following two heading options at the same time: `<h2 class="fancy">` and `<h2>`:
+
+```html
+<style>
+	// Styles for the heading in the content and for the dropdown item.
+	h2.fancy, .ck-heading_heading2_fancy {
+		color: #ff0050;
+		font-size: 1.3em;
+	}
+</style>
+
+<div id="snippet-custom-heading-levels">
+	<h1>Heading 1</h1>
+	<h2>Heading 2</h2>
+	<h2 class="fancy">Fancy Heading 2</h2>
+	<p>This is <a href="https://ckeditor5.github.io">CKEditor 5</a>.</p>
+</div>
+```
+
+```js
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		heading: {
+			options: [
+				{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
+				{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
+				{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
+				{
+					model: 'headingFancy',
+					view: {
+						name: 'h2',
+						class: 'fancy',
+
+						// It needs to be converted before the standard 'heading2'.
+						priority: 'high'
+					},
+					title: 'Heading 2 (fancy)',
+					class: 'ck-heading_heading2_fancy'
+				}
+			]
+		}
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+{@snippet features/custom-heading-elements}
+
 ## Installation
 
 <info-box info>

+ 21 - 16
packages/ckeditor5-heading/src/heading.js

@@ -52,9 +52,9 @@ export default class Heading extends Plugin {
 		const dropdownTooltip = t( 'Heading' );
 
 		for ( const option of options ) {
-			const command = editor.commands.get( option.modelElement );
+			const command = editor.commands.get( option.model );
 			const itemModel = new Model( {
-				commandName: option.modelElement,
+				commandName: option.model,
 				label: option.title,
 				class: option.class
 			} );
@@ -161,16 +161,6 @@ function getCommandsBindingTargets( commands, attribute ) {
 	return Array.prototype.concat( ...commands.map( c => [ c, attribute ] ) );
 }
 
-/**
- * Heading option descriptor.
- *
- * @typedef {Object} module:heading/heading~HeadingOption
- * @property {String} modelElement Element's name in the model.
- * @property {String} viewElement The name of the view element that will be used to represent the model element in the view.
- * @property {String} title The user-readable title of the option.
- * @property {String} class The class which will be added to the dropdown item representing this option.
- */
-
 /**
  * The configuration of the heading feature. Introduced by the {@link module:heading/headingengine~HeadingEngine} feature.
  *
@@ -202,10 +192,10 @@ function getCommandsBindingTargets( commands, attribute ) {
  *
  *		const headingConfig = {
  *			options: [
- *				{ modelElement: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
- *				{ modelElement: 'heading1', viewElement: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
- *				{ modelElement: 'heading2', viewElement: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
- *				{ modelElement: 'heading3', viewElement: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
+ *				{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
+ *				{ model: 'heading1', view: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
+ *				{ model: 'heading2', view: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
+ *				{ model: 'heading3', view: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
  *			]
  *		};
  *
@@ -220,6 +210,9 @@ function getCommandsBindingTargets( commands, attribute ) {
  * the {@link module:paragraph/paragraph~Paragraph} feature (which is required by
  * the {@link module:heading/headingengine~HeadingEngine} feature).
  *
+ * You can **read more** about configuring heading levels and **see more examples** in
+ * the {@glink features/headings Headings} guide.
+ *
  * Note: In the model you should always start from `heading1`, regardless of how the headings are represented in the view.
  * That's assumption is used by features like {@link module:autoformat/autoformat~Autoformat} to know which element
  * they should use when applying the first level heading.
@@ -231,3 +224,15 @@ function getCommandsBindingTargets( commands, attribute ) {
  *
  * @member {Array.<module:heading/heading~HeadingOption>} module:heading/heading~HeadingConfig#options
  */
+
+/**
+ * Heading option descriptor.
+ *
+ * This format is compatible with {@link module:engine/conversion/definition-based-converters~ConverterDefinition}
+ * and adds to additional properties: `title` and `class`.
+ *
+ * @typedef {Object} module:heading/heading~HeadingOption
+ * @extends module:engine/conversion/definition-based-converters~ConverterDefinition
+ * @property {String} title The user-readable title of the option.
+ * @property {String} class The class which will be added to the dropdown item representing this option.
+ */

+ 15 - 16
packages/ckeditor5-heading/src/headingengine.js

@@ -8,9 +8,12 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
-import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import {
+	modelElementToViewContainerElement,
+	viewToModelElement
+} from '@ckeditor/ckeditor5-engine/src/conversion/definition-based-converters';
+
 import HeadingCommand from './headingcommand';
 
 const defaultModelElement = 'paragraph';
@@ -30,10 +33,10 @@ export default class HeadingEngine extends Plugin {
 
 		editor.config.define( 'heading', {
 			options: [
-				{ modelElement: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
-				{ modelElement: 'heading1', viewElement: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
-				{ modelElement: 'heading2', viewElement: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
-				{ modelElement: 'heading3', viewElement: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
+				{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
+				{ model: 'heading1', view: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
+				{ model: 'heading2', view: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
+				{ model: 'heading3', view: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
 			]
 		} );
 	}
@@ -56,24 +59,20 @@ export default class HeadingEngine extends Plugin {
 
 		for ( const option of options ) {
 			// Skip paragraph - it is defined in required Paragraph feature.
-			if ( option.modelElement !== defaultModelElement ) {
+			if ( option.model !== defaultModelElement ) {
 				// Schema.
-				editor.model.schema.register( option.modelElement, {
+				editor.model.schema.register( option.model, {
 					inheritAllFrom: '$block'
 				} );
 
 				// Build converter from model to view for data and editing pipelines.
-				buildModelConverter().for( data.modelToView, editing.modelToView )
-					.fromElement( option.modelElement )
-					.toElement( option.viewElement );
+				modelElementToViewContainerElement( option, [ data.modelToView, editing.modelToView ] );
 
 				// Build converter from view to model for data pipeline.
-				buildViewConverter().for( data.viewToModel )
-					.fromElement( option.viewElement )
-					.toElement( option.modelElement );
+				viewToModelElement( option, [ data.viewToModel ] );
 
 				// Register the heading command for this option.
-				editor.commands.add( option.modelElement, new HeadingCommand( editor, option.modelElement ) );
+				editor.commands.add( option.model, new HeadingCommand( editor, option.model ) );
 			}
 		}
 	}
@@ -91,7 +90,7 @@ export default class HeadingEngine extends Plugin {
 		if ( enterCommand ) {
 			this.listenTo( enterCommand, 'afterExecute', ( evt, data ) => {
 				const positionParent = editor.model.document.selection.getFirstPosition().parent;
-				const isHeading = options.some( option => positionParent.is( option.modelElement ) );
+				const isHeading = options.some( option => positionParent.is( option.model ) );
 
 				if ( isHeading && !positionParent.is( defaultModelElement ) && positionParent.childCount === 0 ) {
 					data.writer.rename( positionParent, defaultModelElement );

+ 13 - 13
packages/ckeditor5-heading/tests/heading.js

@@ -118,8 +118,8 @@ describe( 'Heading', () => {
 			beforeEach( () => {
 				commands = {};
 
-				editor.config.get( 'heading.options' ).forEach( ( { modelElement } ) => {
-					commands[ modelElement ] = editor.commands.get( modelElement );
+				editor.config.get( 'heading.options' ).forEach( ( { model } ) => {
+					commands[ model ] = editor.commands.get( model );
 				} );
 			} );
 
@@ -151,17 +151,17 @@ describe( 'Heading', () => {
 
 			beforeEach( () => {
 				return localizedEditor( [
-					{ modelElement: 'paragraph', title: 'Paragraph' },
-					{ modelElement: 'heading1', viewElement: 'h2', title: 'Heading 1' },
-					{ modelElement: 'heading2', viewElement: 'h3', title: 'Heading 2' }
+					{ model: 'paragraph', title: 'Paragraph' },
+					{ model: 'heading1', view: { name: 'h2' }, title: 'Heading 1' },
+					{ model: 'heading2', view: { name: 'h3' }, title: 'Heading 2' }
 				] );
 			} );
 
 			it( 'does not alter the original config', () => {
 				expect( editor.config.get( 'heading.options' ) ).to.deep.equal( [
-					{ modelElement: 'paragraph', title: 'Paragraph' },
-					{ modelElement: 'heading1', viewElement: 'h2', title: 'Heading 1' },
-					{ modelElement: 'heading2', viewElement: 'h3', title: 'Heading 2' }
+					{ model: 'paragraph', title: 'Paragraph' },
+					{ model: 'heading1', view: { name: 'h2' }, title: 'Heading 1' },
+					{ model: 'heading2', view: { name: 'h3' }, title: 'Heading 2' }
 				] );
 			} );
 
@@ -194,8 +194,8 @@ describe( 'Heading', () => {
 
 			it( 'allows custom titles', () => {
 				return localizedEditor( [
-					{ modelElement: 'paragraph', title: 'Custom paragraph title' },
-					{ modelElement: 'heading1', title: 'Custom heading1 title' }
+					{ model: 'paragraph', title: 'Custom paragraph title' },
+					{ model: 'heading1', view: { name: 'h1' }, title: 'Custom heading1 title' }
 				] ).then( () => {
 					const listView = dropdown.listView;
 
@@ -208,7 +208,7 @@ describe( 'Heading', () => {
 
 			it( 'translates default using the the locale', () => {
 				return localizedEditor( [
-					{ modelElement: 'paragraph', title: 'Paragraph' }
+					{ model: 'paragraph', title: 'Paragraph' }
 				] ).then( () => {
 					const listView = dropdown.listView;
 
@@ -236,8 +236,8 @@ describe( 'Heading', () => {
 						dropdown = editor.ui.componentFactory.create( 'headings' );
 						commands = {};
 
-						editor.config.get( 'heading.options' ).forEach( ( { modelElement } ) => {
-							commands[ modelElement ] = editor.commands.get( modelElement );
+						editor.config.get( 'heading.options' ).forEach( ( { model } ) => {
+							commands[ model ] = editor.commands.get( model );
 						} );
 
 						editorElement.remove();

+ 12 - 12
packages/ckeditor5-heading/tests/headingcommand.js

@@ -10,9 +10,9 @@ import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 const options = [
-	{ modelElement: 'heading1', viewElement: 'h2', title: 'H2' },
-	{ modelElement: 'heading2', viewElement: 'h3', title: 'H3' },
-	{ modelElement: 'heading3', viewElement: 'h4', title: 'H4' }
+	{ model: 'heading1', view: { name: 'h2' }, title: 'H2' },
+	{ model: 'heading2', view: { name: 'h3' }, title: 'H3' },
+	{ model: 'heading3', view: { name: 'h4' }, title: 'H4' }
 ];
 
 describe( 'HeadingCommand', () => {
@@ -30,8 +30,8 @@ describe( 'HeadingCommand', () => {
 			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 
 			for ( const option of options ) {
-				commands[ option.modelElement ] = new HeadingCommand( editor, option.modelElement );
-				schema.register( option.modelElement, { inheritAllFrom: '$block' } );
+				commands[ option.model ] = new HeadingCommand( editor, option.model );
+				schema.register( option.model, { inheritAllFrom: '$block' } );
 			}
 
 			schema.register( 'notBlock' );
@@ -59,7 +59,7 @@ describe( 'HeadingCommand', () => {
 			test( option );
 		}
 
-		function test( { modelElement } ) {
+		function test( { model: modelElement } ) {
 			it( `equals ${ modelElement } when collapsed selection is placed inside ${ modelElement } element`, () => {
 				setData( model, `<${ modelElement }>foobar</${ modelElement }>` );
 				const element = root.getChild( 0 );
@@ -200,11 +200,11 @@ describe( 'HeadingCommand', () => {
 			} );
 
 			function test( from, to ) {
-				it( `converts ${ from.modelElement } to ${ to.modelElement } on collapsed selection`, () => {
-					setData( model, `<${ from.modelElement }>foo[]bar</${ from.modelElement }>` );
-					commands[ to.modelElement ].execute();
+				it( `converts ${ from.model } to ${ to.model } on collapsed selection`, () => {
+					setData( model, `<${ from.model }>foo[]bar</${ from.model }>` );
+					commands[ to.model ].execute();
 
-					expect( getData( model ) ).to.equal( `<${ to.modelElement }>foo[]bar</${ to.modelElement }>` );
+					expect( getData( model ) ).to.equal( `<${ to.model }>foo[]bar</${ to.model }>` );
 				} );
 			}
 		} );
@@ -245,7 +245,7 @@ describe( 'HeadingCommand', () => {
 				);
 			} );
 
-			function test( { modelElement: fromElement }, { modelElement: toElement } ) {
+			function test( { model: fromElement }, { model: toElement } ) {
 				it( `converts ${ fromElement } to ${ toElement } on non-collapsed selection`, () => {
 					setData(
 						model,
@@ -264,7 +264,7 @@ describe( 'HeadingCommand', () => {
 
 	describe( 'isEnabled', () => {
 		for ( const option of options ) {
-			test( option.modelElement );
+			test( option.model );
 		}
 
 		function test( modelElement ) {

+ 49 - 6
packages/ckeditor5-heading/tests/headingengine.js

@@ -75,6 +75,48 @@ describe( 'HeadingEngine', () => {
 		expect( editor.getData() ).to.equal( '<h4>foobar</h4>' );
 	} );
 
+	describe( 'user defined', () => {
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ HeadingEngine ],
+					heading: {
+						options: [
+							{ model: 'paragraph', title: 'paragraph' },
+							{
+								model: 'heading1',
+								view: 'h1',
+								acceptsAlso: [
+									{ name: 'p', attribute: { 'data-heading': 'h1' }, priority: 'high' }
+								],
+								title: 'User H1'
+							}
+						]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+				} );
+		} );
+
+		it( 'should convert from defined h element', () => {
+			editor.setData( '<h1>foobar</h1>' );
+
+			expect( getData( model, { withoutSelection: true } ) ).to.equal( '<heading1>foobar</heading1>' );
+			expect( editor.getData() ).to.equal( '<h1>foobar</h1>' );
+		} );
+
+		it( 'should convert from defined paragraph with attributes', () => {
+			editor.setData( '<p data-heading="h1">foobar</p><p>Normal paragraph</p>' );
+
+			expect( getData( model, { withoutSelection: true } ) )
+				.to.equal( '<heading1>foobar</heading1><paragraph>Normal paragraph</paragraph>' );
+
+			expect( editor.getData() ).to.equal( '<h1>foobar</h1><p>Normal paragraph</p>' );
+		} );
+	} );
+
 	it( 'should not blow up if there\'s no enter command in the editor', () => {
 		return VirtualTestEditor
 			.create( {
@@ -87,18 +129,18 @@ describe( 'HeadingEngine', () => {
 			describe( 'default value', () => {
 				it( 'should be set', () => {
 					expect( editor.config.get( 'heading.options' ) ).to.deep.equal( [
-						{ modelElement: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
-						{ modelElement: 'heading1', viewElement: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
-						{ modelElement: 'heading2', viewElement: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
-						{ modelElement: 'heading3', viewElement: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
+						{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
+						{ model: 'heading1', view: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
+						{ model: 'heading2', view: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
+						{ model: 'heading3', view: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
 					] );
 				} );
 			} );
 
 			it( 'should customize options', () => {
 				const options = [
-					{ modelElement: 'paragraph', title: 'Paragraph' },
-					{ modelElement: 'h4', viewElement: 'h4', title: 'H4' }
+					{ model: 'paragraph', title: 'Paragraph' },
+					{ model: 'h4', view: { name: 'h4' }, title: 'H4' }
 				];
 
 				return VirtualTestEditor
@@ -125,3 +167,4 @@ describe( 'HeadingEngine', () => {
 		} );
 	} );
 } );
+