API to add custom formats to Editable#3060
Conversation
|
I've added support for a simple inline style editor so that the API can be discussed. The idea is that |
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
youknowriad
left a comment
There was a problem hiding this comment.
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 ), | ||
| }; |
There was a problem hiding this comment.
Nice refactoring to merge the "link" control to the same loop 👍
| onClick: this.toggleFormat( control.format ), | ||
| isActive: !! formats[ control.format ], | ||
| } ) ); | ||
| .concat( customControls ) |
There was a problem hiding this comment.
Should we concat before filtering? Thinking we could define customControls but decide to disable temporarily for some reason.
There was a problem hiding this comment.
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' ) ) : {}; | ||
| } |
There was a problem hiding this comment.
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.
blocks/editable/index.js
Outdated
| activeFormats.forEach( ( activeFormat ) => formats[ activeFormat ] = true ); | ||
| const formatNames = this.props.formattingControls || FORMATS; | ||
|
|
||
| forEach( this.editor.formatter.matchAll( formatNames ), ( activeFormat ) => { |
There was a problem hiding this comment.
Should this be a map instead of forEach?
blocks/editable/index.js
Outdated
| formats={ this.state.formats } | ||
| onChange={ this.changeFormats } | ||
| enabledControls={ formattingControls } | ||
| customControls={ map( this.props.formatters, ( formatter ) => pick( formatter, [ 'format', 'title', 'icon' ] ) ) } |
There was a problem hiding this comment.
Is it really necessary to loop and pick only some properties?
|
|
||
| onInit() { | ||
| this.updateFocus(); | ||
| this.registerCustomFormatters(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...
blocks/library/paragraph/index.js
Outdated
| placeholder={ placeholder || __( 'New Paragraph' ) } | ||
| formattingControls={ [ 'bold', 'italic', 'strikethrough', 'link', 'red' ] } | ||
| formatters={ [ | ||
| createInlineStyleFormatter( 'red', 'hammer', 'Red', { color: 'red' } ), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Maybe we can pass the args as an object? I foresee the formatting object getting a lot more complex like in
gutenberg/blocks/library/paragraph/index.js
Line 109 in ece64ee
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
No problems, changed this to a simple object.
|
@youknowriad that sounds like a good idea. Should I focus on the formatter API in this issue and make another PR for global formatters? |
blocks/editable/index.js
Outdated
| function getFormatProperties( formatName, parents ) { | ||
| return formatName === 'link' ? getLinkProperties( find( parents, node => node.nodeName.toLowerCase() === 'a' ) ) : {}; | ||
| switch ( formatName ) { | ||
| case 'link' : |
There was a problem hiding this comment.
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.
blocks/editable/index.js
Outdated
| registerCustomFormatters() { | ||
| forEach( this.props.formatters, ( formatter ) => { | ||
| this.editor.formatter.register( formatter.format, formatter.formatter ); | ||
| forEach( this.props.initialFormatters, ( formatter ) => { |
There was a problem hiding this comment.
Minor: Actually, I'd prefer leaving the prop as "formatters", coupled with the console.error but I'm fine with both
blocks/editable/index.js
Outdated
| 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 } ); |
There was a problem hiding this comment.
Why do we need a new instance? Also the formatter.formatter bothers me a bit, maybe we could deconstruct instead?
There was a problem hiding this comment.
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; | ||
| }, {} ); |
blocks/editable/index.js
Outdated
| this.editor.setDirty( true ); | ||
| } | ||
|
|
||
| getToolbarCustomControls() { |
There was a problem hiding this comment.
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?
@tg-ephox yep, exploring the global registering of the custom formatters could be explored separately 👍 |
blocks/library/paragraph/index.js
Outdated
| icon: 'hammer', | ||
| style: { color: 'red' }, | ||
| } ] | ||
| } |
There was a problem hiding this comment.
Minor: this should probably be } ] } on the same line (like the opening side)
youknowriad
left a comment
There was a problem hiding this comment.
This is looking good for me. After a rebase and probably removal of the example, this should be good to go
youknowriad
left a comment
There was a problem hiding this comment.
Nice work, I'm approving this but please hold on merging. Let's merge after the 1.5 release.
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: