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 #92 from ckeditor/t/ckeditor5-engine/1198
Browse files Browse the repository at this point in the history
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' }`.
  • Loading branch information
Reinmar authored Jan 16, 2018
2 parents 45c13e6 + dc12732 commit afeddd0
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 69 deletions.
13 changes: 13 additions & 0 deletions docs/_snippets/features/custom-heading-elements.html
Original file line number Diff line number Diff line change
@@ -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 changes: 35 additions & 0 deletions docs/_snippets/features/custom-heading-elements.js
Original file line number Diff line number Diff line change
@@ -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 );
} );
6 changes: 3 additions & 3 deletions docs/_snippets/features/custom-heading-levels.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
58 changes: 55 additions & 3 deletions docs/features/headings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
]
}
} )
Expand All @@ -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>
Expand Down
37 changes: 21 additions & 16 deletions src/heading.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
} );
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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' }
* ]
* };
*
Expand All @@ -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.
Expand All @@ -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.
*/
31 changes: 15 additions & 16 deletions src/headingengine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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' }
]
} );
}
Expand All @@ -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 ) );
}
}
}
Expand All @@ -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 );
Expand Down
26 changes: 13 additions & 13 deletions tests/heading.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
} );
} );

Expand Down Expand Up @@ -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' }
] );
} );

Expand Down Expand Up @@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
Loading

0 comments on commit afeddd0

Please sign in to comment.