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

Rich Text: Fix Format Type Assignment During Parsing #11488

Merged
merged 2 commits into from
Nov 6, 2018

Conversation

ellatrix
Copy link
Member

@ellatrix ellatrix commented Nov 5, 2018

Description

Fixes e.g. #6648, generally that no format type can use the same tag name, so e.g. you cannot have two format types handling a or span elements.

Currently format types are assigned based on the provided tagName. This was a temporary solution, intended to be expanded with classes and other attributes, to find a format type match for a given element.

This is a proposal to instead normally serialise a class name. Only in cases where it is wanted that the format type can handle any element with the tag name, you can pass null for the class name.

So say you want to register a custom link format type, you can do so by providing a as the tagName and a className, without clashing with any other format types that want to use the same tag, including the core link format.

Additionally, it enables us to still handle e.g. links if a plugin using the anchor tag gets deactivated.

This is an important change to the Format API that would be good to include before the API even gets shipped, in 4.2, so I'm moving it to the milestone for discussion.

How has this been tested?

Screenshots

Types of changes

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.

@ellatrix ellatrix added [Feature] Rich Text Related to the Rich Text component that allows developers to render a contenteditable [Priority] High Used to indicate top priority items that need quick attention labels Nov 5, 2018
@ellatrix ellatrix requested review from a team and gziolo November 5, 2018 11:33
@ellatrix ellatrix mentioned this pull request Nov 5, 2018
4 tasks
@ellatrix ellatrix added this to the 4.2 milestone Nov 5, 2018
@ellatrix
Copy link
Member Author

ellatrix commented Nov 5, 2018

Changed it so that we use classes instead of data attributes with the format name. @gziolo The only thing I wonder now is, since the class has to be unique, maybe we can just use at at the name/unique ID as well? tagName/className or something like that.

@ellatrix
Copy link
Member Author

ellatrix commented Nov 6, 2018

Test format type:

wp.richText.registerFormatType( 'my-plugin/link', {
	title: 'Custom Link',
	tagName: 'a',
	attributes: {
		url: 'href',
	},
	className: 'my-custum-format',
	edit: function( props ) {
		return wp.element.createElement( props.ToolbarButton, {
			icon: 'editor-strikethrough',
			title: 'Custom Link',
			onClick: function() {
				props.onChange( wp.richText.toggleFormat( props.value, { type: 'my-plugin/link', attributes: {
					url: '#test',
				} } ) );
			},
			isActive: props.isActive,
		} );
	},
} );

tagName: 'em',
},
tagName: 'em',
className: null,
edit( { isActive, value, onChange, ToolbarButton, Shortcut } ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We're receiving components as props? That's really weird, I guess it's out of scope of this PR, but I'm interested in revisiting this pattern.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we should change it. Maybe even before releasing?

@ellatrix ellatrix requested a review from a team November 6, 2018 09:44
let formatType;

if ( attributes && attributes.class ) {
formatType = select( 'core/rich-text' ).getFormatTypeForClassName( attributes.class );
Copy link
Contributor

@youknowriad youknowriad Nov 6, 2018

Choose a reason for hiding this comment

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

Are these two places here the only places where we rely globally on the data module?
I'd like us to avoid this pattern in the future. And only use the data module in withSelect/withDispatch or provide the registry as an argument somehow (or the available formats in this case)

The issue with this pattern is that it relies on a singleton which is not great if you have the same npm module installed twice for instance.

I'm fine keeping them for now, if we already do the same elsewhere in rich-text.

Copy link
Member Author

Choose a reason for hiding this comment

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

It got the data the same way before, jut through a wrapper function. Sounds good to revisit separately.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we just change the signature of the function to toFormat({ type, attributes }, availableFormats)? In my testing we do this in this function and in fromFormat. I'd prefer if we can avoid breaking changes here by adding this extra argument, but if you think it's a big refactoring, let's delay.

Copy link
Member Author

Choose a reason for hiding this comment

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

Where should the available formats be given? in the RichText component? And then we pass it down all the way toHTMLString( { availableFormats } ) etc.?

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, yes I would prefer this or rewrite all these APIs as selectors. It does feel more impactful that I first thought so you have my 👍 for shipping as is.

Copy link
Member

Choose a reason for hiding this comment

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

This would also make writing tests easier as you don't have to include stores in tests. This issue also exists in other places. I agree that we should keep such usage to the absolute minimum. We can iterate on the codebase later.

Copy link
Member

Choose a reason for hiding this comment

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

Thinking a bit more about it, I think that it might be better to avoid introducing more specialized selectors getFormatTypeForClassName and `getFormatTypeForBareElement. Instead, we can perform filtering using utility helpers in this file. I don't think we will need this logic anywhere else.

) {
window.console.error(
'Format tag names must be a string.'
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Not for this PR: Maybe we could leverage yup at some point for this and block registration, a validation schema seems like a good enhancement.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, we need to improve error handling a lot and built some abstraction on top of it.

@gziolo
Copy link
Member

gziolo commented Nov 6, 2018

We discussed two solutions here. This PR implements:

className - mandatory, provide the name which is string to be included as a class name to identify your format type in HTML. Advanced usage: if there is no bare element handler, you can define your own by setting className value to null.

the other approach we discussed would describe as follows:

canHandleBareElement - optional, defaults to false. You will need to provide className unless value is set to true.
className - optional, provides the name which is string to be included as a class name to identify your format type in HTML when canHandleBareElement is set to false. Omit it otherwise or it will throw a validation error.

Copy link
Contributor

@youknowriad youknowriad left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@gziolo
Copy link
Member

gziolo commented Nov 6, 2018

We should add e2e tests as a follow-up. Examples to look at:

and tests that use those plugins.

Using the provided example:
#11488 (comment)

Basically, you put it into a plugin and add some simple test which verifies it shows up in UI. In this case, it should also verify whether block updates properly.

formatType = select( 'core/rich-text' ).getFormatTypeForClassName( attributes.class );

if ( formatType ) {
attributes.class = ` ${ attributes.class } `.replace( ` ${ formatType.className } `, ' ' ).trim();
Copy link
Member

Choose a reason for hiding this comment

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

What is happening here, can you explain? It's mostly me trying to understand why we need it. Maybe we should add an inline comment.

Copy link
Member Author

Choose a reason for hiding this comment

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

We need the preserve any other classes. E.g. a plugin may add extra classes, or if the the plugin doesn't use the class attribute, user added classes should be preserved.

Copy link
Member

Choose a reason for hiding this comment

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

Right, make sense now.

) {
window.console.error(
'Format tag names must be a string.'
);
Copy link
Member

Choose a reason for hiding this comment

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

Yes, we need to improve error handling a lot and built some abstraction on top of it.

}
} else {
const formatTypeForClassName = select( 'core/rich-text' )
.getFormatTypeForClassName( settings.className );
Copy link
Member

Choose a reason for hiding this comment

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

In theory, you could allow to reuse the same class name with different tag names. We can relax it later though. Not sure about it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, easier to relax than be more strict.

typeof settings.tagName !== 'string' ||
settings.tagName === ''
) {
window.console.error(
Copy link
Member

Choose a reason for hiding this comment

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

Minor: When no tag name is provided, it could show a different error.

return false;
}

return ` ${ elementClassName } `.indexOf( ` ${ className } ` ) >= 0;
Copy link
Member

Choose a reason for hiding this comment

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

I think I get why we add wrapping spaces :)

value,
} ) => {
it( description, () => {
if ( formatName ) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: for simplicity, it might be easier to add the test without formatName in their own block.

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you mean?

formatName,
formatType,
html,
value,
Copy link
Member

Choose a reason for hiding this comment

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

Can we rename value to expectedValue? It's hard to follow spec data without a context.

import { registerFormatType } from '../register-format-type';
import { unregisterFormatType } from '../unregister-format-type';

describe( 'registerFormatType', () => {
Copy link
Member

Choose a reason for hiding this comment

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

Nice one. I personally prefer more code but to have all test pieces put together in one place :)

}

const elementAttributes = {};
const elementAttributes = unregisteredAttributes ? { ...unregisteredAttributes } : {};
Copy link
Member

Choose a reason for hiding this comment

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

Skip it! I'm guessing, but it might also work as:

const elementAttributes = { ...unregisteredAttributes }

I tried with

{ ...null } // {}
{ ...undefined } // {}

Just sharing how JS tries to be smart :)

const parent = getParent( pointer );
const newNode = append( parent, fromFormat( { type, attributes, object } ) );
const newNode = append( parent, fromFormat( format ) );
Copy link
Member

Choose a reason for hiding this comment

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

Do we no longer need to clone it? Just making sure it was done on purpose.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, the cloning is not needed. I think it's a remnant, also to destruct object.

@gziolo
Copy link
Member

gziolo commented Nov 6, 2018

Update example after recent changes in master:

wp.richText.registerFormatType( 'my-plugin/link', {
	title: 'Custom Link',
	tagName: 'a',
	attributes: {
		url: 'href',
	},
	className: 'my-custum-format',
	edit: function( props ) {
console.log( props ); 
		return wp.element.createElement( wp.editor.RichTextToolbarButton, {
			icon: 'editor-strikethrough',
			title: 'Custom Link',
			onClick: function() {
				props.onChange( wp.richText.toggleFormat( props.value, { type: 'my-plugin/link', attributes: {
					url: '#test',
				} } ) );
			},
			isActive: props.isActive,
		} );
	},
} );```

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

It works as advertised in my testing. Let's move forward and address anything that is valid as a follow up. I will work on e2e tests tomorrow.

Nice work 👍

@@ -61,7 +61,7 @@ describe( 'create', () => {
formatName,
formatType,
html,
value,
value: expectedValue,
Copy link
Member

Choose a reason for hiding this comment

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

I thought we could update it inside the definition,too :)

I can do it myself when working on e2e tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh. It is used for other tests as well where value is the input.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Rich Text Related to the Rich Text component that allows developers to render a contenteditable [Priority] High Used to indicate top priority items that need quick attention
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants