-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Conversation
options: [ 'left', 'right' ], | ||
classNames: [ 'my-align-left', 'my-align-right' ] |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
const view = { | ||
key: 'style', | ||
value: { | ||
'text-align': option | ||
} | ||
}; | ||
const model = { | ||
key: 'alignment', | ||
value: option | ||
}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ] ] |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>' ); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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: [] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
const enabledOptions = alignmentOptions.map( option => option.name ).filter( isSupported ); | ||
const classNameConfig = alignmentOptions.map( option => option.className ); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
const alignmentClassNames = classNameConfig.reduce( ( classNameMap, className, index ) => { | ||
classNameMap[ enabledOptions[ index ] ] = className; | ||
|
||
return classNameMap; | ||
}, {} ); |
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
return Object.assign( {}, { | ||
name: option | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return Object.assign( {}, { | |
name: option | |
} ); | |
return { | |
name: option | |
}; |
There was a problem hiding this comment.
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 ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would inline normalizeOption
.
There was a problem hiding this comment.
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 } ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optionObj = Object.assign( {}, { name: option } ); | |
optionObj = { name: option }; |
There was a problem hiding this comment.
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 ); |
There was a problem hiding this comment.
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'
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
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 | ||
); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
const nameAlreadyExists = succeedingOptions.some( | ||
item => { | ||
const optionName = item.name || item; | ||
|
||
return optionName == optionObj.name; | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
The PR is ready for another round of review. |
Ready for testing, @FilipTokarski @LukaszGudel please take a look at it :) |
const definition = buildClassDefinition( optionsToConvert ); | ||
|
||
editor.conversion.attributeToAttribute( definition ); |
There was a problem hiding this comment.
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.
const definition = buildDowncastInlineDefinition( optionNamesToConvert ); | ||
|
||
editor.conversion.attributeToAttribute( definition ); | ||
editor.conversion.for( 'downcast' ).attributeToAttribute( definition ); |
There was a problem hiding this comment.
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 ) { |
There was a problem hiding this comment.
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 ) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = [] ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this param optional?
There was a problem hiding this comment.
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 }; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! Fixed!
/** | ||
* 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 } ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix the indentation.
/** | ||
* 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 } ); |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 ); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ); |
There was a problem hiding this comment.
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.
While testing this feature I've added this alignment config:
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 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 |
There was a problem hiding this comment.
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 () => { |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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?
Result:
Paragraph's style will convert to class but |
CKEditor4 can output this html:
but it looks like |
This is interesting. If you do the same with the inline styles (so no 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 |
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`}. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
Overall, this feature seems to be fine 👍 I did not find weird behaviors, other than cases with alignments. |
Same here, seems to be fine 👍 |
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.