Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle alignment with classes #8996

Merged
merged 26 commits into from
Feb 26, 2021
Merged

Handle alignment with classes #8996

merged 26 commits into from
Feb 26, 2021

Conversation

maxbarnas
Copy link
Contributor

@maxbarnas maxbarnas commented Feb 5, 2021

Suggested merge commit message (convention)

Feature (alignment): Add option to use classes instead of inline styles. Closes #8516.


Additional information

For example – encountered issues, assumptions you had to make, other affected tickets, etc.

Comment on lines 65 to 66
options: [ 'left', 'right' ],
classNames: [ 'my-align-left', 'my-align-right' ]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could make this configuration as we do for image styles? https://ckeditor.com/docs/ckeditor5/latest/api/module_image_image-ImageConfig.html#member-styles

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 133 to 142
const view = {
key: 'style',
value: {
'text-align': option
}
};
const model = {
key: 'alignment',
value: option
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would inline those into the object below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

for ( const option of options ) {
definition.view[ option ] = {
key: 'class',
value: [ alignmentClassNames[ option ] ]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this value should be an array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we find it needed on the live review?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was not needed after all. Thanks for pointing that out.


setModelData( model, '<paragraph>[]x</paragraph>' );

expect( newEditor.getData() ).to.equal( '<p>x</p>' );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this line is not needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

@@ -32,7 +32,8 @@ export default class AlignmentEditing extends Plugin {
super( editor );

editor.config.define( 'alignment', {
options: [ ...supportedOptions ]
options: [ ...defaultOptions ],
classNames: []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is not needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 51 to 52
const enabledOptions = alignmentOptions.map( option => option.name ).filter( isSupported );
const classNameConfig = alignmentOptions.map( option => option.className );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should not stick to the list of objects and transform those to some helping maps.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 66 to 70
const alignmentClassNames = classNameConfig.reduce( ( classNameMap, className, index ) => {
classNameMap[ enabledOptions[ index ] ] = className;

return classNameMap;
}, {} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be more readable if we would use Object.fromEntries()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 22 to 24
return Object.assign( {}, {
name: option
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return Object.assign( {}, {
name: option
} );
return {
name: option
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* @returns {Array.<Object>} Normalized object holding the configuration.
*/
export function normalizeAlignmentOptions( configuredOptions = [] ) {
return configuredOptions.map( normalizeOption );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would inline normalizeOption.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

let optionObj;

if ( typeof option == 'string' ) {
optionObj = Object.assign( {}, { name: option } );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
optionObj = Object.assign( {}, { name: option } );
optionObj = { name: option };

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if ( typeof option == 'string' ) {
optionObj = Object.assign( {}, { name: option } );
} else {
optionObj = Object.assign( {}, defaultOptions[ index ], option );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that those arrays are equal in length and have the same logic on the same indexes?

Also, I would suggest using:

optionObj = {
	...someObject,
	...someOverride,
	name: 'foo'
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 95 to 99
const classNameAlreadyExists = optionObj.className && succeedingOptions.some(
// The `item.className` can be undefined. We shouldn't count it as a duplicate.
item => item.className &&
item.className == optionObj.className
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make it look cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a notch more readable.

Comment on lines 76 to 81
const nameAlreadyExists = succeedingOptions.some(
item => {
const optionName = item.name || item;

return optionName == optionObj.name;
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const nameAlreadyExists = succeedingOptions.some(
item => {
const optionName = item.name || item;
return optionName == optionObj.name;
} );
const nameAlreadyExists = succeedingOptions.some( item => {
const optionName = item.name || item;
return optionName == optionObj.name;
} );

Maybe we could do normalization from `"string" to { name: "string" }` in the first loop and then in the next loop normalize it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@maxbarnas
Copy link
Contributor Author

The PR is ready for another round of review.

@Mgsy
Copy link
Member

Mgsy commented Feb 23, 2021

Ready for testing, @FilipTokarski @LukaszGudel please take a look at it :)

packages/ckeditor5-alignment/src/alignment.js Outdated Show resolved Hide resolved
Comment on lines 66 to 68
const definition = buildClassDefinition( optionsToConvert );

editor.conversion.attributeToAttribute( definition );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be in one line.

Comment on lines 71 to 73
const definition = buildDowncastInlineDefinition( optionNamesToConvert );

editor.conversion.attributeToAttribute( definition );
editor.conversion.for( 'downcast' ).attributeToAttribute( definition );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here - one line.

// @private
function _buildDefinition( options ) {
function buildDowncastInlineDefinition( optionNames ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could use options as in other definition builders (this way we will not need optionNamesToConvert)


// Prepare upcast definitions for inline alignment styles.
// @private
function buildUpcastInlineDefinitions( optionNames ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this is needed now and wasn't needed previously.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because downcasting to inline styles is now optional. That's why I had to divide upcast and downcast.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh ok, and DowncastHelpers does not support that short notation for the definition

* @param {Array.<String|Object>} configuredOptions Alignment plugin configuration.
* @returns {Array.<Object>} Normalized object holding the configuration.
*/
export function normalizeAlignmentOptions( configuredOptions = [] ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this param optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not anymore.

if ( typeof option == 'string' ) {
optionObj = { name: option };
} else {
optionObj = { ...optionNameToOptionMap[ option ], ...option };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is confusing, what is it supposed to do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default value from optionNameToOptionMap[ option ] gets overwritten by option.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but option is not a string in this case so what it will take from the optionNameToOptionMap?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! Fixed!

Comment on lines 80 to 88
/**
* 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|Object>} allOptions Contents of `alignment.options`.
*/
throw new CKEditorError( 'alignment-config-name-already-defined', { option, allOptions } );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix the indentation.

Comment on lines 96 to 103
/**
* 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|Object>} allOptions Contents of `alignment.options`.
*/
throw new CKEditorError( 'alignment-config-classname-already-defined', { option, allOptions } );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix the indentation.

@@ -12,19 +12,19 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest
import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';

import AlignmentCommand from '../src/alignmentcommand';
import { CKEditorError } from '../../../src/utils';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to be an invalid import path.

@@ -63,17 +60,13 @@ export default class AlignmentEditing extends Plugin {

if ( shouldUseClasses ) {
// Downcast to only to classes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is not reflecting what the code does - it's converting both ways, not only downcasting.

@@ -86,11 +79,12 @@ export default class AlignmentEditing extends Plugin {

// Prepare downcast conversion definition for inline alignment styling.
// @private
function buildDowncastInlineDefinition( optionNames ) {
function buildDowncastInlineDefinition( options ) {
const optionNames = options.map( option => option.name );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could inline it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two places in this function using it. Does it make sense to inline it and repeat the same operation twice?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first one uses a list of names, and the second one is iterating itself

@@ -109,7 +103,8 @@ function buildDowncastInlineDefinition( optionNames ) {

// Prepare upcast definitions for inline alignment styles.
// @private
function buildUpcastInlineDefinitions( optionNames ) {
function buildUpcastInlineDefinitions( options ) {
const optionNames = options.map( option => option.name );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed, you could access the option name in the loop below.

@LukaszGudel
Copy link
Contributor

While testing this feature I've added this alignment config:

		alignment: {
			options: [
				'left',
				'right',
				{ name: 'center', className: 'AlignCenter' },
			]
		},

I know that it is incorrect to mix inlines and classes. Editor is opening with the content but it seems like there are some side effects. Every block element has class right and every table/widget has inline style float: right. If I change to RTL then every block element has class 'left' and tables are floating to the left. I've tested this on all-features manual test.

IMO even warning in console about mixed config should help any developer who is using these config options incorrectly.

beforeEach( () => {
return VirtualTestEditor
beforeEach( async () => {
const newEditor = await VirtualTestEditor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newEditor const is not needed, you could assign it directly to editor.

@@ -175,6 +225,34 @@ describe( 'AlignmentEditing', () => {

expect( editor.getData() ).to.equal( '<p style="text-align:center;">x</p>' );
} );

it( 'uses class when classNames is set', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to have a data upcast also covered.

@@ -3,7 +3,8 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

import { isDefault, isSupported, supportedOptions } from '../src/utils';
import { CKEditorError } from '../../../src/utils';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import seems to be invalid.

} );
} );

describe( 'className property', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe those tests should be in the other test file where we test normalization?

@LukaszGudel
Copy link
Contributor

  1. Use config to use classes instead of inline styles.
  2. Set data by calling:
editor.setData('<p style="text-align:center">Hello world</p><table><tr><td style="text-align:center"><p>Hello</p></td></tr></table>')

Result:

<p class="AlignCenter">Hello world</p>
<figure class="table"><table><tbody><tr><td style="text-align:center;">Hello</td></tr></tbody></table></figure>

Paragraph's style will convert to class but <td> style will not change.

@LukaszGudel
Copy link
Contributor

LukaszGudel commented Feb 23, 2021

CKEditor4 can output this html:

<table border="1" cellpadding="1" cellspacing="1" style="width:500px">
	<tbody>
		<tr>
			<td>Hello</td>
			<td class="AlignRight">World</td>
		</tr>
		<tr>
			<td class="AlignCenter">Foo</td>
			<td class="AlignJustify">Bar</td>
		</tr>
	</tbody>
</table>

but it looks like <td> classes are ignored in CKEditor5.

@maxbarnas
Copy link
Contributor Author

maxbarnas commented Feb 23, 2021

editor.setData('<p style="text-align:center">Hello world</p><table><tr><td style="text-align:center"><p>Hello</p></td></tr></table>')

This is interesting. If you do the same with the inline styles (so no alignment.options), you'll get the same thing. Seems like our implementation (and I mean even pre-className version) does not accept alignment directly on <td>.

If you manually align the contents of the table cell, the cell will get a paragraph, and that paragraph will be styled. And this kind of contents will be correctly handled by setData() and getData().

@niegowski
Copy link
Contributor

This is interesting. If you do the same with the inline styles (so no alignment.options), you'll get the same thing. Seems like our implementation (and I mean even pre-className version) does not accept alignment directly on <td>.

If you manually align the contents of the table cell, the cell will get a paragraph, and that paragraph will be styled. And this kind of contents will be correctly handled by setData() and getData().

Alignment in table cells is handled by the table cell styles feature.

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`}.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no validation for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

@LukaszGudel
Copy link
Contributor

Overall, this feature seems to be fine 👍 I did not find weird behaviors, other than cases with alignments.

@FilipTokarski
Copy link
Member

FilipTokarski commented Feb 24, 2021

Same here, seems to be fine 👍

@niegowski niegowski merged commit 638543b into master Feb 26, 2021
@niegowski niegowski deleted the i/8516 branch February 26, 2021 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for classes in the alignment plugin
5 participants