-
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
Text formatting UI #460
Text formatting UI #460
Conversation
Tested and is working pretty well but I have several questions/remarks here.
Could we pass the
|
As discussed earlier in Slack, I think we should mark some areas as the main text area or a field that needs controls repositioned. We'll need a way to distinguish between the two.
Suggestions welcome! If we use |
Yep, it's probably a bad name, the most important for me is having an object containing all the inline controls data. const inlineState = {
bold: true,
italic: false,
link: { url: 'http://google.com', target: '_blank' }
} |
Sure, that's the same but a different name. What if the state changes by changing the selection, not by clicking the button. How does Editable know what happened? |
that's the |
2a1b0c4
to
fd4e667
Compare
@youknowriad Changed some things. I'll try to get rid of |
Can we do this by passing |
Yeah, we can cache the path and compare it, so we're not executing the commands on selection changes. |
I was thinking of comparing |
Like this? Any other thoughts? |
blocks/components/editable/index.js
Outdated
|
||
forEach( nextProps.formats, ( state, format ) => { | ||
if ( state !== this.props.formats[ format ] ) { | ||
this.editor.formatter.toggle( formatMap[ format ] ); |
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.
Thanks for the changes @iseulde. This starts to look good for me. But I think we should not use toggle
here. I'm seeing some bugs while the format is changed when I click in different places of the Editable. I suspect this is caused by the toggle
because we're not taking into account the value of this.props.formats[ format ]
whether it's true or false. I don't know maybe something like this.editor.execCommand( formatMap[ format ], true, this.props.formats[ format ] )
would fix it?
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 toggle
is the right way to do it, we should probably check whether the current path is already equivalent to the this.props.formats[ format ]
value before calling toggle
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.
Does the new state check make sense?
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.
(And explicitly calling apply or remove.)
blocks/components/editable/index.js
Outdated
const formatMap = { | ||
strong: 'bold', | ||
em: 'italic' | ||
}; |
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 wonder if we should do the mapping before calling the onFormatChange
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.
Yep, we should.
Works great! Can we get link and strikethrough in also? |
Sure strikethrough is the same. Link will need more UI. Maybe best a separate PR. |
We can also probably get rid for the format map by checking TinyMCE settings for that... |
@@ -127,6 +146,22 @@ export default class Editable extends wp.element.Component { | |||
} | |||
} | |||
|
|||
componentWillReceiveProps( nextProps ) { | |||
forEach( nextProps.formats, ( state, format ) => { | |||
const currentState = this.formats[ format ] || false; |
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 would prefer if we find a way to avoid the cache this.formats
but it's ok for now.
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 this.formats
here any different than this.props.formats
? Like Riad, I also instinctively cringe at the sight of instance variables, though they're sometimes justified.
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.
Yes, It can be different than this.props.formats
right after we trigger the onFormatChange
callback.
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.
Any suggestions here are welcome.
blocks/library/text/index.js
Outdated
@@ -47,7 +47,25 @@ registerBlock( 'core/text', { | |||
} | |||
], | |||
|
|||
edit( { attributes, setAttributes, insertBlockAfter } ) { | |||
formatting: [ |
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.
Just pinging @aduth maybe he has some thoughts on the block API part of this PR. The formatting
attribute here and the two props onFormatChange
and formats
241e2b6
to
869f34b
Compare
blocks/components/editable/index.js
Outdated
@@ -47,6 +54,17 @@ export default class Editable extends wp.element.Component { | |||
editor.on( 'focusout', this.onChange ); | |||
editor.on( 'NewBlock', this.onNewBlock ); | |||
editor.on( 'focusin', this.onFocus ); | |||
|
|||
if ( this.props.onFormatChange ) { | |||
editor.on( 'nodechange', ( { parents } ) => { |
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.
Can we move the callback logic to a separate named function on the class? Just in an effort to avoid a sprawling setup
method.
blocks/components/editable/index.js
Outdated
formatMap[ node.nodeName.toLowerCase() ] | ||
) ); | ||
|
||
this.formats = zipObject( path, path.map( () => true ) ); |
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.
Suggestion for O(4n) to O(n) optimization:
const formats = parents.reduce( ( memo, node ) => {
const tagName = node.nodeName.toLowerCase();
if ( formatMap.hasOwnProperty( tagName ) ) {
memo[ formatMap[ tagName ] ] = true;
}
return memo;
}, {} );
blocks/library/text/index.js
Outdated
} | ||
], | ||
|
||
edit( { attributes, setAttributes, insertBlockAfter, focus, setFocus, onFormatChange, formats } ) { |
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.
Not specific to this pull request, but I'm concerned about how many props an implementer is expected to be aware of just to use an Editable
component. Previously I'd suggested considering context
as a transparent pass-through. I don't really know whether it's a perfect solution, but we'll need to think of something. Maybe also something in the global state instead of VisualBlock
's state.
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.
Same here, also since these are probably things the block implementor doesn't need to know anything about.
blocks/library/text/index.js
Outdated
@@ -47,7 +47,25 @@ registerBlock( 'core/text', { | |||
} | |||
], | |||
|
|||
edit( { attributes, setAttributes, insertBlockAfter, focus, setFocus } ) { | |||
formatting: [ |
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.
In API docs I'd remarked that "Inline controls for Editable elements are identical for every block and cannot be modified." The thinking being that it'd be quite rare for an editable to not need these standard formatting controls, even to the point that we'd want to discourage it for the break in consistency. Do you have any feelings on this? Or could we at least have a standard set and make the intent an opt-out instead of opt-in?
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.
Also, if we're introducing changes to the block API, we should probably document them at the same time. Specifically thinking about available format
options.
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 agree with this. I don't think a plugin should be able to change these (would love to see examples where they should). From earlier conversations I thought we wanted it like this.
blocks/components/editable/index.js
Outdated
|
||
if ( state !== currentState ) { | ||
if ( state ) { | ||
this.editor.focus(); |
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: If the focus
applies first to both if
and else
, might as well move it above the condition (single occurrence).
editor/modes/visual-editor/block.js
Outdated
} | ||
|
||
bindBlockNode( node ) { | ||
this.node = node; | ||
} | ||
|
||
onFormatChange( formats ) { | ||
if ( ! isEqual( formats, this.state.formats ) ) { |
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 would we allow onFormatChange
to be invoked if a change actually hasn't occurred? Or in any case, why would we optimize for it? Thinking we can drop the isEqual
check altogether.
editor/modes/visual-editor/block.js
Outdated
} | ||
|
||
bindBlockNode( node ) { | ||
this.node = node; | ||
} | ||
|
||
onFormatChange( formats ) { | ||
if ( ! isEqual( formats, this.state.formats ) ) { | ||
this.setState( ( prevState ) => { |
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.
Curious about the choice of callback signature. Why not:
onFormatChange( formats ) {
this.setState( { formats } };
}
editor/modes/visual-editor/block.js
Outdated
} | ||
|
||
toggleFormat( format ) { | ||
this.setState( ( prevState ) => { |
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.
Similar to above, maybe more concise:
const { formats } = this.state;
this.setState( {
formats: {
...formats,
[ format ]: ! formats[ format ]
}
} );
editor/modes/visual-editor/block.js
Outdated
controls={ settings.formatting.map( ( control ) => ( { | ||
...control, | ||
onClick: () => this.toggleFormat( control.format ), | ||
isActive: () => !! this.state.formats[ control.format ] |
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.
Hmm, reconsidering whether isActive
really makes sense as a function. We can save this for a separate pull request, but seems like a boolean would be a better substitute
@@ -127,6 +146,22 @@ export default class Editable extends wp.element.Component { | |||
} | |||
} | |||
|
|||
componentWillReceiveProps( nextProps ) { | |||
forEach( nextProps.formats, ( state, format ) => { | |||
const currentState = this.formats[ format ] || false; |
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 this.formats
here any different than this.props.formats
? Like Riad, I also instinctively cringe at the sight of instance variables, though they're sometimes justified.
0b8cf56
to
a8c964d
Compare
} | ||
|
||
bindBlockNode( node ) { | ||
this.node = node; | ||
} | ||
|
||
onFormatChange( formats ) { | ||
if ( ! this.state.hasEditable ) { |
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 feels a bit like a hack :) but in the same time I can't find a better alternative
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.
It does :)
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.
An alternative would be an extra setting for it, but not sure if that's any better.
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.
Really good job on this @iseulde. This is good to be merged for me 👍
I'm still trying to narrow in on what's causing it, but specific to this branch I don't have a text cursor (I-beam) when hovering the text block and can't select any text. |
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.
Sure enough, that was it @youknowriad . Works well after a local rebase. LGTM 👍
See #452
Still needs a styling fix and a look into Range errors, but wanted to push it here to see what others think.