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

Adding inline styles and attributes to widget with applyStyle #1681

Closed
wants to merge 33 commits into from

Conversation

engineering-this
Copy link
Contributor

What is the purpose of this pull request?

Feature

Does your PR contain necessary tests?

All patches which change the editor code must include tests. You can always read more
on PR testing,
how to set the testing environment and
how to create tests
in the official CKEditor documentation.

This PR contains

  • Unit tests
  • Manual tests

What changes did you make?

I have extended widget.applyRemoveStyle method so when passed CKEDITOR.style instance has property 'style' or 'attribute' it will add or remove by those properties to widget.

Closes #1674

@engineering-this
Copy link
Contributor Author

Initially I was supposed to work on adding/removing styles and attributes as separate tickets, but since doing them both looks almost the same I squashed it into one PR.

if ( !updatedClasses[ cl ] )
changed = updatedClasses[ cl ] = 1;
} else {
if ( updatedClasses[ cl ] ) {
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 part of my code, although I see now that I can change it to else if { with other changes requested by reviewer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I would take this opportunity to refactor it, as you mentioned it should look something like:

while ( ( cl = classes.pop() ) ) {
	if ( apply ) {
		if ( !updatedClasses[ cl ] ) {
			changed = updatedClasses[ cl ] = 1;
		}
	} else if ( updatedClasses[ cl ] ) {
		delete updatedClasses[ cl ];
		changed = 1;
	}
}

@mlewand mlewand requested a review from f1ames February 19, 2018 11:42
Copy link
Contributor

@f1ames f1ames left a comment

Choose a reason for hiding this comment

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

This issue is a little more complex as modifications in applyRemoveStyle method affects 3 other methods:

CHANGES.md Outdated

New Features:

* [#1674](https://github.com/ckeditor/ckeditor-dev/issues/1674): The [Widget](https://ckeditor.com/cke4/addon/widget) plugin accepts inline styles and HTML attributes from [`CKEDITOR.style.customHandlers.widget`](https://docs.ckeditor.com/ckeditor4/latest/api/CKEDITOR_style_customHandlers_widget.html).
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't it be more clear to say that now widgets are fully compatible or fully apply CKEDITOR.style when applied via applyStyle/removeStyle method? (Previously only classes were added, now all attributes and styles are added)

if ( !updatedClasses[ cl ] )
changed = updatedClasses[ cl ] = 1;
} else {
if ( updatedClasses[ cl ] ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I would take this opportunity to refactor it, as you mentioned it should look something like:

while ( ( cl = classes.pop() ) ) {
	if ( apply ) {
		if ( !updatedClasses[ cl ] ) {
			changed = updatedClasses[ cl ] = 1;
		}
	} else if ( updatedClasses[ cl ] ) {
		delete updatedClasses[ cl ];
		changed = 1;
	}
}


// Ee... Something is wrong with this style.
if ( !classes )
if ( !classes && !styles && !attributes )
Copy link
Contributor

Choose a reason for hiding this comment

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

If you could also fix the codestyle here as we rather use if wtih {.

}
}
}
if ( changed )
Copy link
Contributor

Choose a reason for hiding this comment

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

And here (if { ... }).

widget.setData( 'classes', updatedClasses );
}

function toggleStyleAttribute ( element, property, StyleOrAttr, apply ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is defined/created every time applyRemoveStyle is executed. I would move it outside as a helper same as getStyleClasses.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also i don't see a reason why StyleOrAttr starts with uppercase. It is a standard param like any other in this function.

function toggleStyleAttribute ( element, property, StyleOrAttr, apply ) {
apply = apply ? 'set' : 'remove';
StyleOrAttr = StyleOrAttr ? 'Style' : 'Attribute';
StyleOrAttr = apply + StyleOrAttr;
Copy link
Contributor

Choose a reason for hiding this comment

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

You can just make one-liner out of it:

StyleOrAttr = apply + ( StyleOrAttr ? 'Style' : 'Attribute' );

StyleOrAttr = StyleOrAttr ? 'Style' : 'Attribute';
StyleOrAttr = apply + StyleOrAttr;

for ( var key in property ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of iterating you may use apply/removeStyles and apply/removeAttributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. There is no method removeStyles only removeStyle.
  2. Method setAttributes takes object containing pairs as attribute, while removeAttributes takes array as argument.

Applying all that will make our logic more complicated than it is right now so I will stay with current approach.

Copy link
Contributor

@f1ames f1ames left a comment

Choose a reason for hiding this comment

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

Remember to check if other unit tests are passing after modifying behaviour of existing methods. It is likely that some tests checked behaviour which was changed. I run some widget unit tests and see some are failing:

image

Also as checkStyleActive was modified, we need new unit tests covering new behaviour.

CHANGES.md Outdated

New Features:

* [#1674](https://github.com/ckeditor/ckeditor-dev/issues/1674): The [Widget](https://ckeditor.com/cke4/addon/widget) plugin now fully applies [`CKEDITOR.style.customHandlers.widget`](https://docs.ckeditor.com/ckeditor4/latest/api/CKEDITOR_style_customHandlers_widget.html) excepts for `element` when using methods `applyStyle` or `removeStyle`.
Copy link
Contributor

Choose a reason for hiding this comment

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

The Widget plugin now fully supports CKEDITOR.style's attibuttes/styles via applyStyle, removeStyle and checkStyleActive methods.

☝️ Maybe something like this?

Please remember about proper linking to docs (so also method names should be linked).

*
* By default this method handles only classes defined in the style. It clones existing
* classes which are stored in the {@link #property-data widget data}'s `classes` property,
* Since 4.10 this method applies all attributes styles and classes from {@link CKEDITOR.style}.
Copy link
Contributor

Choose a reason for hiding this comment

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

all attributes and styles from {@link CKEDITOR.style}.

As class is basically attribute maybe it may be skipped here (also CKEDITOR.style does not have separate class property so mentioning it may be misleading - in opposite to styles which are fine here).

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you apply to all 3 methods descriptions.

* classes which are stored in the {@link #property-data widget data}'s `classes` property,
* Since 4.10 this method applies all attributes styles and classes from {@link CKEDITOR.style}.
*
* Previously this method handled only classes defined in the style.
Copy link
Contributor

Choose a reason for hiding this comment

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

classes defined in {@link CKEDITOR.style} attributes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you apply to all 3 methods descriptions.

@@ -1026,20 +1026,19 @@
},

/**
* Applies the specified style to the widget. It is highly recommended to use the
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 can preserve this info about recommended methods?

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you apply to all 3 methods descriptions.

Copy link
Member

Choose a reason for hiding this comment

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

I second this: info about recommended methods should stay.

*
* Previously this method handled only classes defined in the style.
*
* This method adds classes by cloning existing ones which are stored in the {@link #property-data widget data}'s `classes` property,
Copy link
Contributor

Choose a reason for hiding this comment

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

Now it sounds somehow misleading, that classes are added by cloning existing ones. I would stay with previous version, like:

This method clones existing classes ( ... ), adds new classes, and calls...

return false;
}
return true;
return CKEDITOR.style.prototype.checkElementMatch.call( style, this.element );
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just return style.checkElementMatch( this.element );

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm using style.prototype because using it like that would refer to style.customHandler.widget.checkElementMatch instead. But after reworking my PR I won't call it.

return false;
style = CKEDITOR.tools.copy( style );
style.element = this.element.getName();
style._.definition.ignoreReadonly = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Here is already an example the modifying copy (style._.definition.ignoreReadonly), will also modify source object. See below:

image


if ( !classes )
return false;
style = CKEDITOR.tools.copy( style );
Copy link
Contributor

Choose a reason for hiding this comment

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

CKEDITOR.tools.copy does a shallow copy so it is not completely safe to use it (as changing copy may change source object). There are two solutions here:

  • Use CKEDITOR.tools.clone which does deep copy.
  • Create new style using definition from source one: new CKEDITOR.style( CKEDITOR.tools.clone( style._.definition ) ) (still we need to clone the definition).

I'm for the second one as cloning whole CKEDITOR.style object may be expensive. And if you look on CKEDITOR.style constructor it doesn't perform anything fancy or computational heavy so it should be faster than cloning whole object.

toggleStyleAttribute( widget.element, attributes, 0, apply );
}

function toggleStyleAttribute ( element, property, styleOrAttr, apply ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Having a Boolean property with name like styleOrAttr is misleading. Without looking at function code is not possible to assume what styleOrAttr=false or styleOrAttr=true really means. But, it is also quite difficult to come up with proper name in such cases 🤔

You may go same way as apply param - if true it means apply/set if not - remove. So here it would be attributes - true means apply attributes, false - styles (far from perfect IMHO).

The more readable way (but a little bit "inflated" too) would be something like:

function toogleAttributes( element, properties, apply ) { 
	toogleProperties( element, properties, apply, 'Attribute' );
}

function toogleStyles( element, properties, apply ) {
	toogleProperties( element, properties, apply, 'Style' );
}

function toogleProperties( element, properties, apply, propertyType ) { ... }

or actually, you may just stay with toogleProperties (skipping toogleAttributes and toggleStyles) and call it with proper params in the first place.

Copy link
Contributor

Choose a reason for hiding this comment

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

One more thing, property param, should be properties as it is an object containing multiple properties.

};

bender.test( {
'test apply remove inline style': function() {
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 add few tests here checking how it behaves when widget already has some attributes, styles defined. You may check if different attributes are preserved and same ones are replaced, if styles are merged with existing once and same values replaced, etc.

@f1ames
Copy link
Contributor

f1ames commented Feb 22, 2018

After F2F talk, we came up with two conclusions:

  • Style object (it may contain styles and attributes) should be applied to widget wrapper (the outermost wrapper element). Main reason for this is that styles applied to widget element may not work as expected (take float for example, which will reposition element inside widget instead of the whole widget).
  • When downcasting the widget all styles should be applied to widget element (as wrapper is removed). In order to do so, one idea is to store the merged definition of all applied styles so it can be easily applied. On upcast, definition should be created based on existing styles/attributes. When merging styles definitions, keep in mind there might be conflicts so styles applied later have priority as they overwrites what was before in case of conflicts.

@f1ames
Copy link
Contributor

f1ames commented Feb 22, 2018

As there were some doubts if we should store all applied styles as a one, merged object or just as an array of all applied styles, after looking how CKEDITOR.style works, going with one object with all applied/removed styles properties merged seems to be a proper approach.

First of all it works the same way as just applying styles to an element. If a style contains property which is already present in the element, the new value overwrites old one of existing property (same will happen when merging newly applied style object to cached style definition).

Two short scenarios just to illustrate how it should work (again based on CKEDITOR.style):

Example 1

var style1 = new CKEDITOR.style( { element: 'p', styles: { color: 'red' } } ),
    style2 = new CKEDITOR.style( { element: 'p', styles: { color: 'blue' } } )
    style3 = new CKEDITOR.style( { element: 'p', styles: { color: 'blue', 'font-size': '10px' } } );

// Here we assume that the styles were applied (in order of declaration)
// to current selection/element so we can operate on it.
// It has the following styles: 'style="color: blue; font-size: 10px;"'
editor.removeStyle( s1 ); // Does nothing
editor.removeStyle( s2 ); // Would remove "color: blue" leaving 'style="font-size: 10px"'
editor.removeStyle( s3 ); // Would remove whole styles

Of course if we run editor.removeStyle( s2 ); and then editor.removeStyle( s3 );, the second one will have no effect as the style (as a whole) is not present on the selection/element.

Example 2

Having styles with some common property, like:

var style1 = new CKEDITOR.style( { element: 'p', styles: { color: 'blue', 'font-size': '10px' } } ),
    style2 = new CKEDITOR.style( { element: 'p', styles: { color: 'blue', float: 'left' } } );

It is possible to only remove one style:

editor.removeStyle( s1 ); // Leaves 'style="float: left"'.
editor.removeStyle( s2 ); // Does nothing.

// Or in the reverse order.
editor.removeStyle( s2 ); // Leaves 'style="font-size: 10px"'.
editor.removeStyle( s1 ); // Does nothing.

For quick experiments with styles (especially removing) you may use this codpen.


One more thing, after F2F talk with @engineering-this, we decide to not reuse CKEDITOR.style.checkElementMatch when checking if style is applied to widget, but go with simpler approch whcih will just compare given style to widget current style cache (which holds current "styling state").

@engineering-this
Copy link
Contributor Author

engineering-this commented Feb 23, 2018

Currently I'm working on getting styles from widget.element and passing them to widget.wrapper on upcast and reverse on downcast. There is a case when the widget is generated from img and then its linked. When downcasting widget.element is now a, so I can't pass styles back to widget that way, because our source output will be different than input. Also when such widget would be unlinked it would lose all its styles because of that.

@f1ames
Copy link
Contributor

f1ames commented Feb 23, 2018

The problem @engineering-this mentioned in #1681 (comment) is quite tricky. We were thinking about storing somehow (in e.g. styleDefiniton.element) element name (so when it gets linked we still now what was the element), but it won't work in some cases as you can have linked widget on start or paste and then you don't have any element name stored from earlier.

I see two solutions:

  1. Find a reliable way to determine which element is a proper widget.element which should have styles applied (skipping a is not an option ofc as widget can have it in its structure by default).
  2. Use styles in a way that utilizes styleDefinition.element so even if widget gets linked (or its HTML changes somehow) styles will be applied to the element pointed by styles (btw. it looks like it should work like this as styleDefiniton.element plays important role in other places where styles are used).

@@ -1055,9 +1054,10 @@
* {@link CKEDITOR.style#checkActive} method instead of using this method directly,
* because unlike style's method, this one does not perform any checks.
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 sure if this part should stay or not. Now this method checks much more than just classes.

if ( attributes ) {
for ( item in attributes ) {
if ( item !== 'class' && item !== 'style' ) {
if ( !( item in styleDefinition.attributes ) ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could make two if's as one, but it is easier to read this way.

Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it wouldn't be more readable to create helper function and make it one if.

// If there are no styles remove it to make sure we don't have `styles=''` in our output.
delete styleDefinition.styles;
}
wrapper.attributes[ 'data-style-definition' ] = JSON.stringify( styleDefinition );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This might be considered as 'ugly' solution, but I couldn't find better way to store widget.styleDefinition before widget is initialized.

Copy link
Member

Choose a reason for hiding this comment

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

At least it should be internal CKE attribute, [data**-cke**-style-definition].

What's more, information about partial styles (classes) are inside widget's data. Maybe we should reuse this mechanism and include all styles there.

@engineering-this
Copy link
Contributor Author

This could possibly close #1566 too. After review I'd like other devs to discuss if we need any further integration between widget and CKEDITOR.style.

Copy link
Member

@Comandeer Comandeer left a comment

Choose a reason for hiding this comment

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

Additionally there are no tests for integration with Undo, which does not work correctly (part of styles are lost after undoing/redoing).

if ( attributes ) {
for ( item in attributes ) {
if ( item !== 'class' && item !== 'style' ) {
if ( !( item in styleDefinition.attributes ) ) {
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it wouldn't be more readable to create helper function and make it one if.

// @param {CKEDITOR.style} style Custom widget style.
// @param {Boolean} apply Whether to apply or remove style.
// @param {Boolean} whenever to apply or remove style.
Copy link
Member

Choose a reason for hiding this comment

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

These API docs are incorrect/incomplete.

}

// function setTest( bot, testStyles, testAttributes, config ) {
function setTest( bot, config ) {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe it's just me, but I'll rewrite this function to replace ifs with action dictionary:

function setTest() {
	var tests = {
		apply: function() {
			[]
		},
		remove: function() {
			[]
		}
	};

	while ( index < config.length ) {
		var conf = config[ index ],
			action = CKEDITOR.tools.objectKeys( conf )[ 0 ];

		tests[ action ]( conf[ action ] );
	}
}

More complex, but IMO also more readable.

var styleDefinition = JSON.parse( wrapper.$.getAttribute( 'data-style-definition' ) );
delete wrapper.$.attributes[ 'data-style-definition' ];

wrapper.removeAttribute( 'data-style-definition' );
Copy link
Member

Choose a reason for hiding this comment

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

Why there are doubled removal of this attribute?

@@ -830,7 +834,7 @@
* @param [startupData] Initial widget data. This data object will overwrite the default data and
* the data loaded from the DOM.
*/
function Widget( widgetsRepo, id, element, widgetDef, startupData ) {
function Widget( widgetsRepo, id, element, widgetDef, startupData, styleDefinition ) {
Copy link
Member

Choose a reason for hiding this comment

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

There is no API docs for the last parameter.

*
* @readonly
* @since: 4.10.0
* @property {CKEDITOR.plugins.widget.styleDefinition}
Copy link
Member

Choose a reason for hiding this comment

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

There is no such type.

// If there are no styles remove it to make sure we don't have `styles=''` in our output.
delete styleDefinition.styles;
}
wrapper.attributes[ 'data-style-definition' ] = JSON.stringify( styleDefinition );
Copy link
Member

Choose a reason for hiding this comment

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

At least it should be internal CKE attribute, [data**-cke**-style-definition].

What's more, information about partial styles (classes) are inside widget's data. Maybe we should reuse this mechanism and include all styles there.

@@ -932,6 +938,10 @@

// Image can be wrapped in link <a><img/></a>.
image = el.getFirst( 'img' ) || el.getFirst( 'a' ).getFirst( 'img' );

el.lockedStyle = el.lockedStyle || {};
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't it be bound to the widget, not the element itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At this point widget hasn't been initialized and there is no instance to which I could bind this attribute.

Copy link
Member

Choose a reason for hiding this comment

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

But upcast method takes data as a second parameter. These locked styles could be treated as a part of widget's data.

Copy link
Member

@Comandeer Comandeer left a comment

Choose a reason for hiding this comment

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

Additionally there are no unit tests for undo integration.

if ( CKEDITOR.tools.objectKeys( styles ).length ) {
wrapper.setStyles( styleDefinition.styles );
} else {
delete styleDefinition.styles
Copy link
Member

Choose a reason for hiding this comment

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

Missing semicolon.

var classes = widget.styleDefinition ? widget.styleDefinition.attributes[ 'class' ] : undefined;
classes = classes && classes.split( /\s+/ ) ;
if ( classes ) {
if ( !!remove ) {
Copy link
Member

Choose a reason for hiding this comment

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

This logic seems a little bit reversed.

@@ -932,6 +938,10 @@

// Image can be wrapped in link <a><img/></a>.
image = el.getFirst( 'img' ) || el.getFirst( 'a' ).getFirst( 'img' );

el.lockedStyle = el.lockedStyle || {};
Copy link
Member

Choose a reason for hiding this comment

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

But upcast method takes data as a second parameter. These locked styles could be treated as a part of widget's data.


var wrapper = widgetsRepo.wrapElement( toBeWrapped[ 0 ], toBeWrapped[ 1 ] );
handleStylesOnUpcast( toBeWrapped[ 0 ], wrapper );
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it shouldn't be moved into upcast iterator, constructed in createUpcastIterator. Thanks to that we'd be able to replace some magic properties (e.g. lockedStyles) with reusing widget's data.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've moved lockedStyle to data and accessed it from here.

Copy link
Member

Choose a reason for hiding this comment

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

But now parsing data is done manually. If we move handleStylesOnUpcast to mentioned iterator, widget system would parse it for us

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can't access widgets wrapper from there, because its not yet created. I could split this logic into two steps. First in iterator create styleDefinition object and save it in data. Then inside initOn add styles to actual html elements.

Copy link
Member

@Comandeer Comandeer left a comment

Choose a reason for hiding this comment

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

I wonder if styleDefinition also couldn't be moved to widget data, similar to lockedStyle, and whole style handling logic would then be placed inside initial data handler. WDYT, @engineering-this and @mlewand?

var classes = widget.styleDefinition ? widget.styleDefinition.attributes[ 'class' ] : undefined;
if ( classes ) {
classes = classes && classes.split( /\s+/ ) ;
if ( !!remove ) {
Copy link
Member

Choose a reason for hiding this comment

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

That logic is still reversed…

@engineering-this
Copy link
Contributor Author

@Comandeer @mlewand

I wonder if styleDefinition also couldn't be moved to widget data, similar to lockedStyle, and whole style handling logic would then be placed inside initial data handler. WDYT

Doing so results in many tests failing
http://localhost:1030/#tests/is:unit,path:/tests/plugins/widget,path:/tests/plugins/image2,path:/tests/plugins/imagebase,path:/tests/plugins/easyimage
I'd rather avoid changing that. Currently I'm removing this attribute right when I don't use it. Putting it in data would affect tests that have hardcoded expected html output.


if ( attributes ) {
for ( item in attributes ) {
if ( isNotInAttributes( item, styleDefinition.attributes ) ) {
Copy link
Member

Choose a reason for hiding this comment

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

It seems a little bit odd to have negative function. IMO !isInAttributes is more readable.

Copy link
Member

Choose a reason for hiding this comment

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

What's more: why only attrbitues have appropriate helper function and every other styles have just inline conditional?

* Removes the specified style from the widget. It is highly recommended to use the
* {@link CKEDITOR.editor#removeStyle} or {@link CKEDITOR.style#remove} methods instead of
* using this method directly, because unlike editor's and style's methods, this one
* does not perform any checks.
Copy link
Member

Choose a reason for hiding this comment

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

IMO it's better to leave this comment as this method still does not perform any checks.

@@ -1026,20 +1026,19 @@
},

/**
* Applies the specified style to the widget. It is highly recommended to use the
Copy link
Member

Choose a reason for hiding this comment

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

I second this: info about recommended methods should stay.

@@ -1873,6 +1914,10 @@
// REPOSITORY helpers -----------------------------------------------------
//

function isNotInAttributes( item, attributes ) {
return item !== 'class' && item !== 'style' && !( item in attributes );
Copy link
Member

Choose a reason for hiding this comment

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

As I said, it's rather odd to have negative function.

} );
classes = classes.join( ' ' );
if ( classes ) {
styleDefinition.attributes[ 'class' ] = classes;
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it a duplication of the existing classes logic (storing them inside widget's data)?

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 is, but as styleDefinition is meant to hold all widgets styles it should contain also classes. While we can't get rid of data.classes it will be duplicated...


var wrapper = widgetsRepo.wrapElement( toBeWrapped[ 0 ], toBeWrapped[ 1 ] );
handleStylesOnUpcast( toBeWrapped[ 0 ], wrapper );
Copy link
Member

Choose a reason for hiding this comment

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

But now parsing data is done manually. If we move handleStylesOnUpcast to mentioned iterator, widget system would parse it for us

delete updatedClasses[ cl ];
changed = 1;
}
}
if ( changed ) {
widget.setData( 'classes', updatedClasses );
Copy link
Member

Choose a reason for hiding this comment

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

Is it really needed now? We already store such information in styleDefinition.

Copy link
Member

Choose a reason for hiding this comment

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

I mean: we have now two sources of truth – our new styleDefinition object and legacy classes data in widget's data.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Getting rid of data.classes wouldn't break users custom plugins that extend widget? I'm not sure how to approach this.

Copy link
Member

Choose a reason for hiding this comment

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

I'm quite sure that this data is used only internally by our plugin to know, which styles are applied to the widget. I don't even see any info about data.classes in our docs. @mlewand, am I right or I'm missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

@mlewand mlewand Apr 12, 2018

Choose a reason for hiding this comment

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

@Comandeer @engineering-this We can't get rid of data.classes it has been already exposed. Among all it's used by the image2 plugin.

Community had already a good time on picking it up, and relying on that. Because of that we should keep it for compatibility reasons.

// @param {CKEDITOR.plugins.widget} Widget instance.
// @param {Object} Pairs of inline styles or attributes.
// @param {Boolean} whenever to apply or remove style.
function toggleStyleAttribute( widget, properties, attribute, apply ) {
Copy link
Member

Choose a reason for hiding this comment

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

Parameters names and use of this function is rather unclear. toggleStyleAttribute should… well, toggle [style] attribute. But it seems that this function is used to apply/remove attributes/styles.

Copy link
Member

@Comandeer Comandeer left a comment

Choose a reason for hiding this comment

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

  • Easy Image has the same issue as image2: data.classes in newly created widget does not contain .easyimage class, yet styleDefinition – does. Maybe such behavior is hardcoded into widget plugin itself?
  • This situation exists, so it means that we don't have tests for synchronisation between data.classes and styleDefinition :P
  • The fact that captioned image inserted via image2 plugin is aligned couldn't be derived from inspecting styleDefinition. It does not contain any information about it. Yet text-align: <align> is added to the widget wrapper and data.align is set. I'm not sure how to deal with this situation, because in getData such image is transformed into:
<div style="text-align:center">
<figure class="image" style="display:inline-block"><img alt="Comandeer" height="96" src="https://www.comandeer.pl/images/custom/comandeer.jpg" width="96" />
<figcaption>Podpis</figcaption>
</figure>
</div>

Therefore image is not aligned in output HTML. Yet I feel it still a mismatch between old style system and new one.

  • What's even more interesting, for aligned (not captioned) inline image2, styleDefinition contains styles for p element. The output HTML for this widget is:
<p style="text-align:center"><img alt="Comandeer" height="96" src="https://www.comandeer.pl/images/custom/comandeer.jpg" width="96" /></p>

What's more interesting, styleableElements for this widget are img figure, which makes such styleDefinition even more inexplicable.

  • If it's not enough, after switching to source mode and back to WYSIWYG one, styleDefiniton still contains p element, but without any attributes and styles.
  • I even wonder if we could make styleDefiniton a dictionary of style definitions, with styles dedicated to elements (parts?) of the widget and styles dedicated for widget's wrapper. In case of aligned image2 it could look like:
{
	wrapper: {
		styles: {
			'text-align': 'center'
		}
	},

	figure: {
		styles: {
			'display': 'inline-block'
		}
	}
}

It would eliminate styleDefinition.lockedStyle and carry far more information about actual styles. Downside: we'd need to rewrite nearly everything we already have…

  • There is no manual test for undo integration.
  • We could also add styleDefinition inspector to tests, because inspecting it in browser's console is a little bit troublesome.

styleDefinition.styles = CKEDITOR.tools.parseCssText( style );
} else {
// If there are no styles remove it to make sure we don't have `styles=''` in our output.
// delete styleDefinition.styles;
Copy link
Member

Choose a reason for hiding this comment

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

Some dead code here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, I cheated myself here. I was looking for this line in my code and couldn't find it... It took me a minute to remember. I fixed this and amended, but didn't push 😁

@engineering-this
Copy link
Contributor Author

engineering-this commented Apr 24, 2018

I even wonder if we could make styleDefiniton a dictionary of style definitions, with styles dedicated to elements (parts?) of the widget and styles dedicated for widget's wrapper. In case of aligned image2 it could look like:

I though about it, but then what in case of two elements with same html tag in widget. For that case I am thinking about something like that:

wrapper: {
	name: 'div',
	styleDefinition: {},
	0: {
		name: 'p',
		styleDefinition: {}
	},
	1: {
		name: 'p',
		styleDefinition: {},
		0: { some other nested child },
		1: { some other nested child },
	}
}

But that looks way too over complicated, and we are basically creating some kind copy of DOM tree.
@mlewand WDYT?

@engineering-this
Copy link
Contributor Author

I've updated manual test and added new test. So @mlewand you can take a look on this. In the meantime I will fix this one:

Easy Image has the same issue as image2: data.classes in newly created widget does not contain .easyimage class, yet styleDefinition – does. Maybe such behavior is hardcoded into widget plugin itself?

@f1ames f1ames closed this Mar 20, 2020
@f1ames f1ames added the pr:frozen ❄ The pull request on which work was suspended due to various reasons. label Mar 20, 2020
@awasson
Copy link

awasson commented Aug 4, 2020

I see that this issue has been frozen.

Does that mean that the issue of image tags not being able to take classes from the StylesCombo because IMG becomes IMAGE in the editor has been resolved? It has been an issue with the CKEditor that is bundled with Drupal 8 from day one and I'm keen about resolving it.

Cheers,
Andrew

@Dumluregn
Copy link
Contributor

Hello!

Unfortunately, the issue itself is still valid (#1674). We've just frozen this PR as the work done here wasn't finished two years ago and our codebase changed a lot since then, so it would take a lot of time to just rebase this PR and resolve all the conflicts.

@awasson
Copy link

awasson commented Aug 6, 2020

Hello @Dumluregn,

Thanks for the information. I've been following the issue from the point of view coming from Drupal 8 where CKEditor is bundled. There is an issue in the Drupal Issue Queue (2642808) that goes back to 2015 and it has stalled in anticipation of a resolution from the CKEditor side.

On Drupal we can preprocess the stylesSet array before rendering the editor so we are able to loop through the img classes and create a duplicate set for the image widget, formatted in such a way that CKEditor can work with them. The following example of this takes a class for an img tag and duplicates it for an image widget.

Original stylesSet

"stylesSet":[ { "name":"This Class", "element":"img", "attributes":{ "class":"this-class" } } ]

StylesSet rendered in CKEditor.
"stylesSet":[ { "name":"This Class", "element":"img", "attributes":{ "class":"this-class" } }, { "name":"This Class Widget", "type":"widget", "widget":"image", "attributes":{ "class":"this-class" } } ]

I'm not sure if this will help with the native CKEditor but this works within Drupal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr:frozen ❄ The pull request on which work was suspended due to various reasons.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Adding inline styles and attributes to widget with applyStyle
6 participants