diff --git a/packages/ckeditor5-alignment/docs/features/text-alignment.md b/packages/ckeditor5-alignment/docs/features/text-alignment.md index 625e94a7f75..7e67892de00 100644 --- a/packages/ckeditor5-alignment/docs/features/text-alignment.md +++ b/packages/ckeditor5-alignment/docs/features/text-alignment.md @@ -22,6 +22,8 @@ There are more CKEditor 5 features that can help you organize your content: ## Configuring alignment options +### Defining available options + It is possible to configure which alignment options are available in the editor by setting the {@link module:alignment/alignment~AlignmentConfig#options `alignment.options`} configuration option. You can choose from `'left'`, `'right'`, `'center'` and `'justify'`. @@ -46,6 +48,33 @@ ClassicEditor {@snippet features/custom-text-alignment-options} +### Using classes instead of inline style + +By default alignment is set inline using `text-align` CSS property. If you wish the feature to output more semantic content that uses classes instead of inline styles, you can specify class names by using the `className` property in `config.alignment.options` and style them by using a stylesheet. + + + Once you decide to use classes for the alignment, you must define `className` for **all** alignment entries in {@link module:alignment/alignment~AlignmentConfig#options `config.alignment.options`}. + + +The following configuration will set `.my-align-left` and `.my-align-right` to left and right alignment, respectively. + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + alignment: { + options: [ + { name: 'left', className: 'my-align-left' }, + { name: 'right', className: 'my-align-right' } + ] + }, + toolbar: [ + 'heading', '|', 'bulletedList', 'numberedList', 'alignment', 'undo', 'redo' + ] + } ) + .then( ... ) + .catch( ... ); +``` + ## Configuring the toolbar You can choose to use the alignment dropdown (`'alignment'`) or configure the toolbar to use separate buttons for each of the options: diff --git a/packages/ckeditor5-alignment/src/alignment.js b/packages/ckeditor5-alignment/src/alignment.js index 6012d44ce7e..054c653fcb5 100644 --- a/packages/ckeditor5-alignment/src/alignment.js +++ b/packages/ckeditor5-alignment/src/alignment.js @@ -82,7 +82,24 @@ export default class Alignment extends Plugin { * .then( ... ) * .catch( ... ); * + * By default the alignment is set inline using `text-align` CSS property. To further customize the alignment you can + * provide names of classes for each alignment option using `className` property. + * + * **Note:** Once you define `className` property for one option, you need to specify it for all other options. + * + * ClassicEditor + * .create( editorElement, { + * alignment: { + * options: [ + * { name: 'left', className: 'my-align-left' }, + * { name: 'right', className: 'my-align-right' } + * ] + * } + * } ) + * .then( ... ) + * .catch( ... ); + * * See the demo of {@glink features/text-alignment#configuring-alignment-options custom alignment options}. * - * @member {Array.} module:alignment/alignment~AlignmentConfig#options + * @member {Array.} module:alignment/alignment~AlignmentConfig#options */ diff --git a/packages/ckeditor5-alignment/src/alignmentediting.js b/packages/ckeditor5-alignment/src/alignmentediting.js index b44320e5428..6da914b9de2 100644 --- a/packages/ckeditor5-alignment/src/alignmentediting.js +++ b/packages/ckeditor5-alignment/src/alignmentediting.js @@ -10,7 +10,7 @@ import { Plugin } from 'ckeditor5/src/core'; import AlignmentCommand from './alignmentcommand'; -import { isDefault, isSupported, supportedOptions } from './utils'; +import { isDefault, isSupported, normalizeAlignmentOptions, supportedOptions } from './utils'; /** * The alignment editing feature. It introduces the {@link module:alignment/alignmentcommand~AlignmentCommand command} and adds @@ -32,7 +32,7 @@ export default class AlignmentEditing extends Plugin { super( editor ); editor.config.define( 'alignment', { - options: [ ...supportedOptions ] + options: [ ...supportedOptions.map( option => ( { name: option } ) ) ] } ); } @@ -44,40 +44,117 @@ export default class AlignmentEditing extends Plugin { const locale = editor.locale; const schema = editor.model.schema; - // Filter out unsupported options. - const enabledOptions = editor.config.get( 'alignment.options' ).filter( isSupported ); + const options = normalizeAlignmentOptions( editor.config.get( 'alignment.options' ) ); + + // Filter out unsupported options and those that are redundant, e.g. `left` in LTR / `right` in RTL mode. + const optionsToConvert = options.filter( + option => isSupported( option.name ) && !isDefault( option.name, locale ) + ); + + // Once there is at least one `className` defined, we switch to alignment with classes. + const shouldUseClasses = optionsToConvert.some( option => !!option.className ); // Allow alignment attribute on all blocks. schema.extend( '$block', { allowAttributes: 'alignment' } ); editor.model.schema.setAttributeProperties( 'alignment', { isFormatting: true } ); - const definition = _buildDefinition( enabledOptions.filter( option => !isDefault( option, locale ) ) ); + if ( shouldUseClasses ) { + editor.conversion.attributeToAttribute( buildClassDefinition( optionsToConvert ) ); + } else { + // Downcast inline styles. + editor.conversion.for( 'downcast' ).attributeToAttribute( buildDowncastInlineDefinition( optionsToConvert ) ); + } - editor.conversion.attributeToAttribute( definition ); + const upcastInlineDefinitions = buildUpcastInlineDefinitions( optionsToConvert ); + + // Always upcast from inline styles. + for ( const definition of upcastInlineDefinitions ) { + editor.conversion.for( 'upcast' ).attributeToAttribute( definition ); + } editor.commands.add( 'alignment', new AlignmentCommand( editor ) ); } } -// Utility function responsible for building converter definition. +// Prepare downcast conversion definition for inline alignment styling. // @private -function _buildDefinition( options ) { +function buildDowncastInlineDefinition( options ) { const definition = { model: { key: 'alignment', - values: options.slice() + values: options.map( option => option.name ) }, view: {} }; - for ( const option of options ) { - definition.view[ option ] = { + for ( const { name } of options ) { + definition.view[ name ] = { key: 'style', value: { - 'text-align': option + 'text-align': name } }; } return definition; } + +// Prepare upcast definitions for inline alignment styles. +// @private +function buildUpcastInlineDefinitions( options ) { + const definitions = []; + + for ( const { name } of options ) { + definitions.push( { + view: { + key: 'style', + value: { + 'text-align': name + } + }, + model: { + key: 'alignment', + value: name + } + } ); + } + + return definitions; +} + +// Prepare conversion definitions for upcast and downcast alignment with classes. +// @private +function buildClassDefinition( options ) { + const definition = { + model: { + key: 'alignment', + values: options.map( option => option.name ) + }, + view: {} + }; + + for ( const option of options ) { + definition.view[ option.name ] = { + key: 'class', + value: option.className + }; + } + + return definition; +} + +/** + * The alignment configuration format descriptor. + * + * const alignmentFormat = { + * name: 'right', + * className: 'my-align-right-class' + * } + * + * @typedef {Object} module:alignment/alignmentediting~AlignmentFormat + * + * @property {'left'|'right'|'center'|'justify'} name One of the alignment names options. + * + * @property {String} className The CSS class used to represent the style in the view. + * Used to override default, inline styling for alignment. + */ diff --git a/packages/ckeditor5-alignment/src/alignmentui.js b/packages/ckeditor5-alignment/src/alignmentui.js index 483345fd68e..0b43b4d2663 100644 --- a/packages/ckeditor5-alignment/src/alignmentui.js +++ b/packages/ckeditor5-alignment/src/alignmentui.js @@ -10,7 +10,7 @@ import { Plugin, icons } from 'ckeditor5/src/core'; import { ButtonView, createDropdown, addToolbarToDropdown } from 'ckeditor5/src/ui'; -import { isSupported } from './utils'; +import { isSupported, normalizeAlignmentOptions } from './utils'; const iconsMap = new Map( [ [ 'left', icons.alignLeft ], @@ -67,9 +67,10 @@ export default class AlignmentUI extends Plugin { const editor = this.editor; const componentFactory = editor.ui.componentFactory; const t = editor.t; - const options = editor.config.get( 'alignment.options' ); + const options = normalizeAlignmentOptions( editor.config.get( 'alignment.options' ) ); options + .map( option => option.name ) .filter( isSupported ) .forEach( option => this._addButton( option ) ); @@ -77,7 +78,7 @@ export default class AlignmentUI extends Plugin { const dropdownView = createDropdown( locale ); // Add existing alignment buttons to dropdown's toolbar. - const buttons = options.map( option => componentFactory.create( `alignment:${ option }` ) ); + const buttons = options.map( option => componentFactory.create( `alignment:${ option.name }` ) ); addToolbarToDropdown( dropdownView, buttons ); // Configure dropdown properties an behavior. diff --git a/packages/ckeditor5-alignment/src/utils.js b/packages/ckeditor5-alignment/src/utils.js index a7881b91581..75883d864a9 100644 --- a/packages/ckeditor5-alignment/src/utils.js +++ b/packages/ckeditor5-alignment/src/utils.js @@ -3,6 +3,8 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +import { CKEditorError, logWarning } from 'ckeditor5/src/utils'; + /** * @module alignment/utils */ @@ -44,3 +46,90 @@ export function isDefault( alignment, locale ) { return alignment === 'left'; } } + +/** + * Brings the configuration to the common form, an array of objects. + * + * @param {Array.} configuredOptions Alignment plugin configuration. + * @returns {Array.} Normalized object holding the configuration. + */ +export function normalizeAlignmentOptions( configuredOptions ) { + const normalizedOptions = configuredOptions + .map( option => { + let result; + + if ( typeof option == 'string' ) { + result = { name: option }; + } else { + result = option; + } + + return result; + } ) + // Remove all unknown options. + .filter( option => { + const isNameValid = !!supportedOptions.includes( option.name ); + if ( !isNameValid ) { + /** + * The `name` in one of the `alignment.options` is not recognized. + * The available options are: `'left'`, `'right'`, `'center'` and `'justify'`. + * + * @error alignment-config-name-not-recognized + * @param {Object} option Options with unknown value of the `name` property. + */ + logWarning( 'alignment-config-name-not-recognized', { option } ); + } + + return isNameValid; + } ); + + const classNameCount = normalizedOptions.filter( option => !!option.className ).length; + + // We either use classes for all styling options or for none. + if ( classNameCount && classNameCount < normalizedOptions.length ) { + /** + * The `className` property has to be defined for all options once at least one option declares `className`. + * + * @error alignment-config-classnames-are-missing + * @param {Array.} configuredOptions Contents of `alignment.options`. + */ + throw new CKEditorError( 'alignment-config-classnames-are-missing', { configuredOptions } ); + } + + // Validate resulting config. + normalizedOptions.forEach( ( option, index, allOptions ) => { + const succeedingOptions = allOptions.slice( index + 1 ); + const nameAlreadyExists = succeedingOptions.some( item => item.name == option.name ); + + if ( nameAlreadyExists ) { + /** + * The same `name` in one of the `alignment.options` was already declared. + * Each `name` representing one alignment option can be set exactly once. + * + * @error alignment-config-name-already-defined + * @param {Object} option First option that declares given `name`. + * @param {Array.} configuredOptions Contents of `alignment.options`. + */ + throw new CKEditorError( 'alignment-config-name-already-defined', { option, configuredOptions } ); + } + + // The `className` property is present. Check for duplicates then. + if ( option.className ) { + const classNameAlreadyExists = succeedingOptions.some( item => item.className == option.className ); + + if ( classNameAlreadyExists ) { + /** + * The same `className` in one of the `alignment.options` was already declared. + * + * @error alignment-config-classname-already-defined + * @param {Object} option First option that declares given `className`. + * @param {Array.} configuredOptions + * Contents of `alignment.options`. + */ + throw new CKEditorError( 'alignment-config-classname-already-defined', { option, configuredOptions } ); + } + } + } ); + + return normalizedOptions; +} diff --git a/packages/ckeditor5-alignment/tests/alignmentediting.js b/packages/ckeditor5-alignment/tests/alignmentediting.js index 59ed6bde282..3eacd9dbe65 100644 --- a/packages/ckeditor5-alignment/tests/alignmentediting.js +++ b/packages/ckeditor5-alignment/tests/alignmentediting.js @@ -16,15 +16,13 @@ import AlignmentCommand from '../src/alignmentcommand'; describe( 'AlignmentEditing', () => { let editor, model; - beforeEach( () => { - return VirtualTestEditor + beforeEach( async () => { + editor = await VirtualTestEditor .create( { plugins: [ AlignmentEditing, Paragraph ] - } ) - .then( newEditor => { - editor = newEditor; - model = editor.model; } ); + + model = editor.model; } ); afterEach( () => { @@ -50,15 +48,13 @@ describe( 'AlignmentEditing', () => { } ); describe( 'integration', () => { - beforeEach( () => { - return VirtualTestEditor + beforeEach( async () => { + const editor = await VirtualTestEditor .create( { plugins: [ AlignmentEditing, ImageCaptionEditing, Paragraph, ListEditing, HeadingEditing ] - } ) - .then( newEditor => { - editor = newEditor; - model = editor.model; } ); + + model = editor.model; } ); it( 'is allowed on paragraph', () => { @@ -96,46 +92,150 @@ describe( 'AlignmentEditing', () => { expect( editor.getData() ).to.equal( '

x

' ); } ); + + describe( 'className', () => { + it( 'adds converters to the data pipeline', async () => { + const newEditor = await VirtualTestEditor + .create( { + plugins: [ AlignmentEditing, Paragraph ], + alignment: { + options: [ + { name: 'left', className: 'foo-left' }, + { name: 'right', className: 'foo-right' }, + { name: 'center', className: 'foo-center' }, + { name: 'justify', className: 'foo-justify' } + ] + } + } ); + const model = newEditor.model; + const data = '

x

'; + + newEditor.setData( data ); + + expect( getModelData( model ) ).to.equal( '[]x' ); + + return newEditor.destroy(); + } ); + + it( 'adds a converter to the view pipeline', async () => { + const newEditor = await VirtualTestEditor + .create( { + plugins: [ AlignmentEditing, Paragraph ], + alignment: { + options: [ + { name: 'left', className: 'foo-left' }, + { name: 'right', className: 'foo-right' }, + { name: 'center', className: 'foo-center' }, + { name: 'justify', className: 'foo-justify' } + ] + } + } ); + const model = newEditor.model; + + setModelData( model, '[]x' ); + + expect( newEditor.getData() ).to.equal( '

x

' ); + + newEditor.execute( 'alignment', { value: 'left' } ); + + expect( getModelData( model ) ).to.equal( '[]x' ); + expect( newEditor.getData() ).to.equal( '

x

' ); + + return newEditor.destroy(); + } ); + } ); } ); describe( 'RTL content', () => { - it( 'adds converters to the data pipeline', () => { - return VirtualTestEditor + it( 'adds converters to the data pipeline', async () => { + const newEditor = await VirtualTestEditor .create( { language: { content: 'ar' }, plugins: [ AlignmentEditing, Paragraph ] - } ) - .then( newEditor => { - const model = newEditor.model; - const data = '

x

'; + } ); + const model = newEditor.model; + const data = '

x

'; - newEditor.setData( data ); + newEditor.setData( data ); - expect( getModelData( model ) ).to.equal( '[]x' ); - expect( newEditor.getData() ).to.equal( '

x

' ); + expect( getModelData( model ) ).to.equal( '[]x' ); + expect( newEditor.getData() ).to.equal( '

x

' ); - return newEditor.destroy(); - } ); + return newEditor.destroy(); } ); - it( 'adds a converter to the view pipeline', () => { - return VirtualTestEditor + it( 'adds a converter to the view pipeline', async () => { + const newEditor = await VirtualTestEditor .create( { language: { content: 'ar' }, plugins: [ AlignmentEditing, Paragraph ] - } ) - .then( newEditor => { - const model = newEditor.model; + } ); + const model = newEditor.model; + + setModelData( model, '[]x' ); + expect( newEditor.getData() ).to.equal( '

x

' ); - setModelData( model, '[]x' ); - expect( newEditor.getData() ).to.equal( '

x

' ); + return newEditor.destroy(); + } ); - return newEditor.destroy(); - } ); + describe( 'className', () => { + it( 'adds a converter to the view pipeline', async () => { + const newEditor = await VirtualTestEditor + .create( { + language: { + content: 'ar' + }, + plugins: [ AlignmentEditing, Paragraph ], + alignment: { + options: [ + { name: 'left', className: 'foo-left' }, + { name: 'right', className: 'foo-right' }, + { name: 'center', className: 'foo-center' }, + { name: 'justify', className: 'foo-justify' } + ] + } + } ); + const model = newEditor.model; + + setModelData( model, '[]x' ); + + newEditor.execute( 'alignment', { value: 'left' } ); + + expect( getModelData( model ) ).to.equal( '[]x' ); + expect( newEditor.getData() ).to.equal( '

x

' ); + + return newEditor.destroy(); + } ); + + it( 'adds converters to the data pipeline', async () => { + const newEditor = await VirtualTestEditor + .create( { + language: { + content: 'ar' + }, + plugins: [ AlignmentEditing, Paragraph ], + alignment: { + options: [ + { name: 'left', className: 'foo-left' }, + { name: 'right', className: 'foo-right' }, + { name: 'center', className: 'foo-center' }, + { name: 'justify', className: 'foo-justify' } + ] + } + } ); + const model = newEditor.model; + const data = '

x

'; + + newEditor.setData( data ); + + expect( getModelData( model ) ).to.equal( '[]x' ); + + return newEditor.destroy(); + } ); } ); } ); @@ -175,6 +275,56 @@ describe( 'AlignmentEditing', () => { expect( editor.getData() ).to.equal( '

x

' ); } ); + + describe( 'className', () => { + it( 'adds a converter to the view pipeline', async () => { + const newEditor = await VirtualTestEditor + .create( { + plugins: [ AlignmentEditing, Paragraph ], + alignment: { + options: [ + { name: 'left', className: 'foo-left' }, + { name: 'right', className: 'foo-right' }, + { name: 'center', className: 'foo-center' }, + { name: 'justify', className: 'foo-justify' } + ] + } + } ); + const model = newEditor.model; + + setModelData( model, '[]x' ); + + newEditor.execute( 'alignment', { value: 'center' } ); + + expect( getModelData( model ) ).to.equal( '[]x' ); + expect( newEditor.getData() ).to.equal( '

x

' ); + + return newEditor.destroy(); + } ); + + it( 'adds converters to the data pipeline', async () => { + const newEditor = await VirtualTestEditor + .create( { + plugins: [ AlignmentEditing, Paragraph ], + alignment: { + options: [ + { name: 'left', className: 'foo-left' }, + { name: 'right', className: 'foo-right' }, + { name: 'center', className: 'foo-center' }, + { name: 'justify', className: 'foo-justify' } + ] + } + } ); + const model = newEditor.model; + const data = '

x

'; + + newEditor.setData( data ); + + expect( getModelData( model ) ).to.equal( '[]x' ); + + return newEditor.destroy(); + } ); + } ); } ); describe( 'right alignment', () => { @@ -193,46 +343,148 @@ describe( 'AlignmentEditing', () => { expect( editor.getData() ).to.equal( '

x

' ); } ); + + describe( 'className', () => { + it( 'adds a converter to the view pipeline', async () => { + const newEditor = await VirtualTestEditor + .create( { + plugins: [ AlignmentEditing, Paragraph ], + alignment: { + options: [ + { name: 'left', className: 'foo-left' }, + { name: 'right', className: 'foo-right' }, + { name: 'center', className: 'foo-center' }, + { name: 'justify', className: 'foo-justify' } + ] + } + } ); + const model = newEditor.model; + + setModelData( model, '[]x' ); + + newEditor.execute( 'alignment', { value: 'right' } ); + + expect( getModelData( model ) ).to.equal( '[]x' ); + expect( newEditor.getData() ).to.equal( '

x

' ); + + return newEditor.destroy(); + } ); + + it( 'adds converters to the data pipeline', async () => { + const newEditor = await VirtualTestEditor + .create( { + plugins: [ AlignmentEditing, Paragraph ], + alignment: { + options: [ + { name: 'left', className: 'foo-left' }, + { name: 'right', className: 'foo-right' }, + { name: 'center', className: 'foo-center' }, + { name: 'justify', className: 'foo-justify' } + ] + } + } ); + const model = newEditor.model; + const data = '

x

'; + + newEditor.setData( data ); + + expect( getModelData( model ) ).to.equal( '[]x' ); + + return newEditor.destroy(); + } ); + } ); } ); describe( 'RTL content', () => { - it( 'adds converters to the data pipeline', () => { - return VirtualTestEditor + it( 'adds converters to the data pipeline', async () => { + const newEditor = await VirtualTestEditor .create( { language: { content: 'ar' }, plugins: [ AlignmentEditing, Paragraph ] - } ) - .then( newEditor => { - const model = newEditor.model; - const data = '

x

'; + } ); + const model = newEditor.model; + const data = '

x

'; - newEditor.setData( data ); + newEditor.setData( data ); - expect( getModelData( model ) ).to.equal( '[]x' ); - expect( newEditor.getData() ).to.equal( '

x

' ); + expect( getModelData( model ) ).to.equal( '[]x' ); + expect( newEditor.getData() ).to.equal( '

x

' ); - return newEditor.destroy(); - } ); + return newEditor.destroy(); } ); - it( 'adds a converter to the view pipeline', () => { - return VirtualTestEditor + it( 'adds a converter to the view pipeline', async () => { + const newEditor = await VirtualTestEditor .create( { language: { content: 'ar' }, plugins: [ AlignmentEditing, Paragraph ] - } ) - .then( newEditor => { - const model = newEditor.model; + } ); + const model = newEditor.model; - setModelData( model, '[]x' ); - expect( newEditor.getData() ).to.equal( '

x

' ); + setModelData( model, '[]x' ); + expect( newEditor.getData() ).to.equal( '

x

' ); - return newEditor.destroy(); - } ); + return newEditor.destroy(); + } ); + + describe( 'className', () => { + it( 'adds a converter to the view pipeline', async () => { + const newEditor = await VirtualTestEditor + .create( { + language: { + content: 'ar' + }, + plugins: [ AlignmentEditing, Paragraph ], + alignment: { + options: [ + { name: 'left', className: 'foo-left' }, + { name: 'right', className: 'foo-right' }, + { name: 'center', className: 'foo-center' }, + { name: 'justify', className: 'foo-justify' } + ] + } + } ); + const model = newEditor.model; + + setModelData( model, '[]x' ); + + newEditor.execute( 'alignment', { value: 'right' } ); + + expect( getModelData( model ) ).to.equal( '[]x' ); + expect( newEditor.getData() ).to.equal( '

x

' ); + + return newEditor.destroy(); + } ); + + it( 'adds converters to the data pipeline', async () => { + const newEditor = await VirtualTestEditor + .create( { + language: { + content: 'ar' + }, + plugins: [ AlignmentEditing, Paragraph ], + alignment: { + options: [ + { name: 'left', className: 'foo-left' }, + { name: 'right', className: 'foo-right' }, + { name: 'center', className: 'foo-center' }, + { name: 'justify', className: 'foo-justify' } + ] + } + } ); + const model = newEditor.model; + const data = '

x

'; + + newEditor.setData( data ); + + expect( getModelData( model ) ).to.equal( '[]x' ); + + return newEditor.destroy(); + } ); } ); } ); } ); @@ -252,6 +504,56 @@ describe( 'AlignmentEditing', () => { expect( editor.getData() ).to.equal( '

x

' ); } ); + + describe( 'className', () => { + it( 'adds a converter to the view pipeline', async () => { + const newEditor = await VirtualTestEditor + .create( { + plugins: [ AlignmentEditing, Paragraph ], + alignment: { + options: [ + { name: 'left', className: 'foo-left' }, + { name: 'right', className: 'foo-right' }, + { name: 'center', className: 'foo-center' }, + { name: 'justify', className: 'foo-justify' } + ] + } + } ); + const model = newEditor.model; + + setModelData( model, '[]x' ); + + newEditor.execute( 'alignment', { value: 'justify' } ); + + expect( getModelData( model ) ).to.equal( '[]x' ); + expect( newEditor.getData() ).to.equal( '

x

' ); + + return newEditor.destroy(); + } ); + + it( 'adds converters to the data pipeline', async () => { + const newEditor = await VirtualTestEditor + .create( { + plugins: [ AlignmentEditing, Paragraph ], + alignment: { + options: [ + { name: 'left', className: 'foo-left' }, + { name: 'right', className: 'foo-right' }, + { name: 'center', className: 'foo-center' }, + { name: 'justify', className: 'foo-justify' } + ] + } + } ); + const model = newEditor.model; + const data = '

x

'; + + newEditor.setData( data ); + + expect( getModelData( model ) ).to.equal( '[]x' ); + + return newEditor.destroy(); + } ); + } ); } ); describe( 'should be extensible', () => { @@ -297,7 +599,14 @@ describe( 'AlignmentEditing', () => { describe( 'options', () => { describe( 'default value', () => { it( 'should be set', () => { - expect( editor.config.get( 'alignment.options' ) ).to.deep.equal( [ 'left', 'right', 'center', 'justify' ] ); + expect( editor.config.get( 'alignment.options' ) ).to.deep.equal( + [ + { name: 'left' }, + { name: 'right' }, + { name: 'center' }, + { name: 'justify' } + ] + ); } ); } ); } ); diff --git a/packages/ckeditor5-alignment/tests/utils.js b/packages/ckeditor5-alignment/tests/utils.js index d02436d912d..2b6fb9ed974 100644 --- a/packages/ckeditor5-alignment/tests/utils.js +++ b/packages/ckeditor5-alignment/tests/utils.js @@ -3,9 +3,15 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -import { isDefault, isSupported, supportedOptions } from '../src/utils'; +/* globals console */ + +import { CKEditorError } from 'ckeditor5/src/utils'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { isDefault, isSupported, supportedOptions, normalizeAlignmentOptions } from '../src/utils'; describe( 'utils', () => { + testUtils.createSinonSandbox(); + describe( 'isDefault()', () => { it( 'should return true for "left" alignment only (LTR)', () => { const locale = { @@ -47,4 +53,107 @@ describe( 'utils', () => { expect( supportedOptions ).to.deep.equal( [ 'left', 'right', 'center', 'justify' ] ); } ); } ); + + describe( 'normalizeAlignmentOptions', () => { + it( 'normalizes mixed input into an config array of objects', () => { + const config = [ + 'left', + { name: 'right' }, + 'center', + { name: 'justify' } + ]; + + const result = normalizeAlignmentOptions( config ); + + expect( result ).to.deep.equal( + [ + { 'name': 'left' }, + { 'name': 'right' }, + { 'name': 'center' }, + { 'name': 'justify' } + ] + ); + } ); + + it( 'warns if the name is not recognized', () => { + testUtils.sinon.stub( console, 'warn' ); + + const config = [ + 'left', + { name: 'center1' } + ]; + + expect( normalizeAlignmentOptions( config ) ).to.deep.equal( [ + { name: 'left' } + ] ); + + const params = { + option: { name: 'center1' } + }; + + sinon.assert.calledOnce( console.warn ); + sinon.assert.calledWithExactly( console.warn, + sinon.match( /^alignment-config-name-not-recognized/ ), + params, + sinon.match.string // Link to the documentation + ); + } ); + + it( 'throws when the className is not defined for all options', () => { + const config = [ + 'left', + { name: 'center', className: 'foo-center' } + ]; + let error; + + try { + normalizeAlignmentOptions( config ); + } catch ( err ) { + error = err; + } + + expect( error.constructor ).to.equal( CKEditorError ); + expect( error ).to.match( /alignment-config-classnames-are-missing/ ); + } ); + + it( 'throws when the name already exists', () => { + const config = [ + 'center', + { name: 'center' } + ]; + let error; + + try { + normalizeAlignmentOptions( config ); + } catch ( err ) { + error = err; + } + + expect( error.constructor ).to.equal( CKEditorError ); + expect( error ).to.match( /alignment-config-name-already-defined/ ); + } ); + + it( 'throws when the className already exists', () => { + const config = [ + { + name: 'center', + className: 'foo-center' + }, + { + name: 'justify', + className: 'foo-center' + } + ]; + let error; + + try { + normalizeAlignmentOptions( config ); + } catch ( err ) { + error = err; + } + + expect( error.constructor ).to.equal( CKEditorError ); + expect( error ).to.match( /alignment-config-classname-already-defined/ ); + } ); + } ); } ); diff --git a/packages/ckeditor5-engine/src/conversion/conversion.js b/packages/ckeditor5-engine/src/conversion/conversion.js index 3dd6393b6bc..7f93d4dc5d3 100644 --- a/packages/ckeditor5-engine/src/conversion/conversion.js +++ b/packages/ckeditor5-engine/src/conversion/conversion.js @@ -227,7 +227,7 @@ export default class Conversion { * editor.conversion.elementToElement( { * model: 'heading', * view: 'h2', - * // Convert "headling-like" paragraphs to headings. + * // Convert "heading-like" paragraphs to headings. * upcastAlso: viewElement => { * const fontSize = viewElement.getStyle( 'font-size' ); *