Skip to content

Commit

Permalink
Merge pull request #8996 from ckeditor/i/8516
Browse files Browse the repository at this point in the history
Feature (alignment): Option to use classes instead of inline styles. Closes #8516.
  • Loading branch information
niegowski committed Feb 26, 2021
2 parents eed13fb + f9ee5f4 commit 638543b
Show file tree
Hide file tree
Showing 8 changed files with 702 additions and 71 deletions.
29 changes: 29 additions & 0 deletions packages/ckeditor5-alignment/docs/features/text-alignment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'`.

<info-box>
Expand All @@ -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.

<info-box>
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`}.
</info-box>

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:
Expand Down
19 changes: 18 additions & 1 deletion packages/ckeditor5-alignment/src/alignment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<String>} module:alignment/alignment~AlignmentConfig#options
* @member {Array.<String|module:alignment/alignmentediting~AlignmentFormat>} module:alignment/alignment~AlignmentConfig#options
*/
101 changes: 89 additions & 12 deletions packages/ckeditor5-alignment/src/alignmentediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,7 +32,7 @@ export default class AlignmentEditing extends Plugin {
super( editor );

editor.config.define( 'alignment', {
options: [ ...supportedOptions ]
options: [ ...supportedOptions.map( option => ( { name: option } ) ) ]
} );
}

Expand All @@ -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.
*/
7 changes: 4 additions & 3 deletions packages/ckeditor5-alignment/src/alignmentui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ],
Expand Down Expand Up @@ -67,17 +67,18 @@ 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 ) );

componentFactory.add( 'alignment', locale => {
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.
Expand Down
89 changes: 89 additions & 0 deletions packages/ckeditor5-alignment/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.<String|module:alignment/alignmentediting~AlignmentFormat>} configuredOptions Alignment plugin configuration.
* @returns {Array.<module:alignment/alignmentediting~AlignmentFormat>} 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.<String|module:alignment/alignmentediting~AlignmentFormat>} 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.<String|module:alignment/alignmentediting~AlignmentFormat>} 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.<String|module:alignment/alignmentediting~AlignmentFormat>} configuredOptions
* Contents of `alignment.options`.
*/
throw new CKEditorError( 'alignment-config-classname-already-defined', { option, configuredOptions } );
}
}
} );

return normalizedOptions;
}
Loading

0 comments on commit 638543b

Please sign in to comment.