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

API to add custom formats to Editable #3060

Merged
merged 5 commits into from
Oct 26, 2017
Merged

Conversation

tg-ephox
Copy link
Contributor

Description

Pulled this out of Footnotes to as might be useful to block implementers.

Fixes #3059

How Has This Been Tested?

Screenshots (jpeg or gifs if applicable):

Types of changes

New Feature

Checklist:

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

@tg-ephox tg-ephox added the [Status] In Progress Tracking issues with work in progress label Oct 19, 2017
@tg-ephox tg-ephox self-assigned this Oct 19, 2017
@tg-ephox tg-ephox requested a review from aduth October 19, 2017 07:32
@tg-ephox tg-ephox changed the title Created simple inline style formatter API to add custom formats to Editable Oct 19, 2017
@tg-ephox
Copy link
Contributor Author

I've added support for a simple inline style editor so that the API can be discussed. The idea is that formatters are added to Editable and registered with TinyMCE. Registration currently only happens once on editor init, but display/usage is controlled via formattingControls.

@codecov
Copy link

codecov bot commented Oct 19, 2017

Codecov Report

Merging #3060 into master will decrease coverage by 0.03%.
The diff coverage is 8.33%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master   #3060      +/-   ##
=========================================
- Coverage   32.23%   32.2%   -0.04%     
=========================================
  Files         216     216              
  Lines        6151    6163      +12     
  Branches     1081    1085       +4     
=========================================
+ Hits         1983    1985       +2     
- Misses       3517    3523       +6     
- Partials      651     655       +4
Impacted Files Coverage Δ
blocks/editable/format-toolbar/index.js 6.38% <0%> (ø) ⬆️
blocks/editable/index.js 10.7% <10.52%> (+0.27%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 653e555...e76eb2d. Read the comment docs.

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.

I like this, nice work here.

Do you think the custom formatters should be registered globally and applied to all blocks using Editable or maybe with a special opt-in our opt-out prop? (which means we could use withEditorSettings to retrieve this formatters instead of a prop passed to Editable).

Keeping them as a prop has the advantage of flexibility but may add a bit of burden to support the custom formatters for block authors?

--
Also as a follow-up, I can see us providing a choice to add a formatter as a separate control (like implemented here) or as an item in a "Custom Formats" control with a dropdown.

...control,
onClick: isLink ? this.addLink : this.toggleFormat( control.format ),
isActive: this.isFormatActive( control.format ) || ( isLink && isAddingLink ),
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice refactoring to merge the "link" control to the same loop 👍

onClick: this.toggleFormat( control.format ),
isActive: !! formats[ control.format ],
} ) );
.concat( customControls )
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we concat before filtering? Thinking we could define customControls but decide to disable temporarily for some reason.

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 changed this to only pass formatters that have been filtered through formattingControls on editable.


function getFormatProperties( formatName, parents ) {
return formatName === 'link' ? getLinkProperties( find( parents, node => node.nodeName.toLowerCase() === 'a' ) ) : {};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor and personal preference: I would have used a single function and a switch on formatName. It makes it easy to extend if needed but it's not very important.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

activeFormats.forEach( ( activeFormat ) => formats[ activeFormat ] = true );
const formatNames = this.props.formattingControls || FORMATS;

forEach( this.editor.formatter.matchAll( formatNames ), ( activeFormat ) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be a map instead of forEach?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

reduce!

@@ -620,6 +641,7 @@ export default class Editable extends Component {
formats={ this.state.formats }
onChange={ this.changeFormats }
enabledControls={ formattingControls }
customControls={ map( this.props.formatters, ( formatter ) => pick( formatter, [ 'format', 'title', 'icon' ] ) ) }
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it really necessary to loop and pick only some properties?

@@ -143,6 +155,13 @@ export default class Editable extends Component {

onInit() {
this.updateFocus();
this.registerCustomFormatters();
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if the formatters prop change, should we resync the custom formatters in TinyMCE? If it's not possible, maybe we should just guard against updating this prop in componentWillReceiveProps?

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 changed the prop from formatters to initialFormatters and added a console.error into componentWillReceiveProps, I forsee passing functions through this in future so it will be hard to determine if props have changed...

@@ -183,6 +184,10 @@ registerBlockType( 'core/paragraph', {
onMerge={ mergeBlocks }
onReplace={ onReplace }
placeholder={ placeholder || __( 'New Paragraph' ) }
formattingControls={ [ 'bold', 'italic', 'strikethrough', 'link', 'red' ] }
formatters={ [
createInlineStyleFormatter( 'red', 'hammer', 'Red', { color: 'red' } ),
Copy link
Member

Choose a reason for hiding this comment

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

Without looking at the function definition, it is difficult to know at a glance what these arguments represent. Could we not just pass this as an array of objects? Why do we need createInlineStyleFormatter at all if we can provide a plain object representation that satisfies the need?

Copy link
Member

Choose a reason for hiding this comment

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

Or if formatters need to be keyed by a name (which we're passing as an argument), then an object of key => setting pairs.

Could we generate a name on behalf of the implementer, based on the other settings provided? Something like a json-stable-stringify on the other options to create a unique and deterministic key. Might be overkill.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe we can pass the args as an object? I foresee the formatting object getting a lot more complex like in

this.footnoteFormatter = {
to deal with formats that involve other settings, so thought it might be better to always use a function to create a formatter?

Copy link
Member

Choose a reason for hiding this comment

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

If the formatting object becomes complex, how does the function remedy this? If it's the case that it generates many of the values of the objects based on the arguments, then I expect we could still do similar by generating those values based on the incoming formatters prop objects when they come time to be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No problems, changed this to a simple object.

@tg-ephox
Copy link
Contributor Author

@youknowriad that sounds like a good idea. Should I focus on the formatter API in this issue and make another PR for global formatters?

function getFormatProperties( formatName, parents ) {
return formatName === 'link' ? getLinkProperties( find( parents, node => node.nodeName.toLowerCase() === 'a' ) ) : {};
switch ( formatName ) {
case 'link' :
Copy link
Contributor

Choose a reason for hiding this comment

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

could we add scoping here:

case 'link': {
     // code here
  }

Switch cases do not scope let/const variables by default, it could create confusion with other "cases", it's a good practice to scope the "case" whenever we declare a variable there.

@@ -159,8 +159,8 @@ export default class Editable extends Component {
}

registerCustomFormatters() {
forEach( this.props.formatters, ( formatter ) => {
this.editor.formatter.register( formatter.format, formatter.formatter );
forEach( this.props.initialFormatters, ( formatter ) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: Actually, I'd prefer leaving the prop as "formatters", coupled with the console.error but I'm fine with both

forEach( this.props.formatters, ( formatter ) => {
this.editor.formatter.register( formatter.format, formatter.formatter );
forEach( this.props.initialFormatters, ( formatter ) => {
this.editor.formatter.register( formatter.format, { ...formatter.formatter } );
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need a new instance? Also the formatter.formatter bothers me a bit, maybe we could deconstruct instead?

Copy link
Contributor Author

@tg-ephox tg-ephox Oct 23, 2017

Choose a reason for hiding this comment

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

We do in this instance as Tiny mutates the formatter and then we can't compare this.props.formatters with nextProps.formatters in componentWillReceiveProps.

} );

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

Choose a reason for hiding this comment

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

👍

@@ -612,6 +621,12 @@ export default class Editable extends Component {
this.editor.setDirty( true );
}

getToolbarCustomControls() {
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the reasoning for moving this filter to Editable? Since there's a filter already done in the formatting toolbar, I'd have expected it to be done there?

@youknowriad
Copy link
Contributor

Should I focus on the formatter API in this issue and make another PR for global formatters?

@tg-ephox yep, exploring the global registering of the custom formatters could be explored separately 👍

icon: 'hammer',
style: { color: 'red' },
} ]
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: this should probably be } ] } on the same line (like the opening side)

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.

This is looking good for me. After a rebase and probably removal of the example, this should be good to go

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.

Nice work, I'm approving this but please hold on merging. Let's merge after the 1.5 release.

@tg-ephox tg-ephox merged commit 21258ac into master Oct 26, 2017
@tg-ephox tg-ephox deleted the try/3059-custom-formatter-api branch October 26, 2017 01:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Status] In Progress Tracking issues with work in progress
Projects
None yet
Development

Successfully merging this pull request may close these issues.

API to add custom formats to Editable
3 participants