-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
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.
|
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 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.
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 ) |
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.
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.
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.
blocks/editable/index.js
Outdated
|
||
function getFormatProperties( formatName, parents ) { | ||
return formatName === 'link' ? getLinkProperties( find( parents, node => node.nodeName.toLowerCase() === 'a' ) ) : {}; | ||
} |
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.
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.
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.
👍
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.
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
?
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.
reduce!
blocks/editable/index.js
Outdated
@@ -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' ] ) ) } |
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 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(); |
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.
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.
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...
blocks/library/paragraph/index.js
Outdated
@@ -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' } ), |
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.
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.
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.
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 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
this.footnoteFormatter = { |
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.
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.
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.
@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.
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.
blocks/editable/index.js
Outdated
@@ -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 ) => { |
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.
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.
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?
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.
We do in this instance as Tiny mutates the formatter and then we can't compare this.props.formatters
with nextProps.formatters
in componentWillReceiveProps
.
blocks/editable/index.js
Outdated
} ); | ||
|
||
return accFormats; | ||
}, {} ); |
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.
👍
blocks/editable/index.js
Outdated
@@ -612,6 +621,12 @@ export default class Editable extends Component { | |||
this.editor.setDirty( true ); | |||
} | |||
|
|||
getToolbarCustomControls() { |
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.
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.
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)
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 looking good for me. After a rebase and probably removal of the example, this should be good to go
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.
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: