-
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
Blocks: Handle pressing backspace at the beginning of blocks #461
Conversation
caae7d8
to
5f405c4
Compare
I could use some 👀 here |
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.
Seems #466 will conflict with some of the ideas here.
|
||
// If the block types can not match, do nothing | ||
if ( ! blockWithTheSameType ) { | ||
return; |
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.
Do we want to remove the previous block here?
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.
Good question. I don't really know the right answer. I think this use-case might not happen as often as we think because I'm assuming the majority of textual blocks could be transformed in between'em but It's a possibility.
I preferred to avoid deletion and do nothing but I don't have strong arguments for one or another.
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.
My main reason for bringing it up was merely as a point of consistency, since it seemed from the above condition that the "unhandleable" case should simply be removed.
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'm not considering the "merge not defined" as "unhandleable" but more like, if you want this block to be removed when we hit backspace, don't define the merge
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'm not considering the "merge not defined" as "unhandleable" but more like, if you want this block to be removed when we hit backspace, don't define the
merge
Isn't that the condition above though, not this one?
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?
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.
Oh, I misread your previous message as "if you don't want this block to be removed". Disregard.
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 fine, but we should probably document the behavior somewhere.
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'd very much prefer that we don't silently delete content here. Submitted a PR to change this behavior - #530
Might not be that bad actually, basically turns from concatenating strings to creating an array of two |
5f405c4
to
188ef3f
Compare
( dispatch, ownProps ) => ( { | ||
onChange( updates ) { | ||
onChange( uid, updates ) { |
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: I don't love that we have some props that accept a uid
and others that assume it from the other props. I understand the need for it and don't have a great 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.
Do you think making the uid
mandatory even for callbacks we're using only for the current block (and using _.partial
) is a good move?
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 that idea because it promotes consistency, but I don't like that idea because it loses the sense that the component is meant to be specific to a single block.
} | ||
|
||
onKeyDown( event ) { | ||
if ( this.props.onMerge && event.keyCode === KEYCODE_BACKSPACE && this.isStartOfEditor() ) { |
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 don't know if it was intentional, but nice optimization having the heaviest part of the condition performed last and taking advantage of short-circuiting 👍
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.
Intentional :)
Do you think this could qualify as a third |
I don't have a preference. Merging is not really a transformation but it returns |
I hadn't thought of it 'til today, and only came to mind as an attempt to consider if we could eliminate the |
188ef3f
to
3a03903
Compare
element/index.js
Outdated
@@ -65,3 +66,21 @@ export function renderToString( element ) { | |||
|
|||
return renderToStaticMarkup( element ); | |||
} | |||
|
|||
|
|||
export function concatValues( value1, value2 ) { |
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.
@aduth Concatenating children values is not that easy, can you think of a simpler alternative?
blocks/components/editable/index.js
Outdated
@@ -10,6 +10,7 @@ import { Parser as HtmlToReactParser } from 'html-to-react'; | |||
*/ | |||
import './style.scss'; | |||
|
|||
const KEYCODE_BACKSPACE = 8; |
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.
There's also window.tinymce.util.VK
. It's okay like this I guess, but at some point we may have to use other things from it anyway, like metaKeyPressed
.
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.
At some point in the near future we may also consider leveraging the now-preferred event.key
property which, funny enough, is better supported in IE back to IE11 than it is in Safari (only available in most recent versions).
3a03903
to
37cd1e0
Compare
blocks/components/editable/index.js
Outdated
do { | ||
const child = element; | ||
element = element.parentNode; | ||
if ( element.childNodes[ 0 ] !== child ) { |
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.
element.firstChild
? (It's faster)
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.
👍
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.
element.firstChild
? (It's faster)
I agree with the suggestion but now I'm intrigued in what way it would be faster? Seems like the difference is array access at an index, which I wouldn't expect to be slow.
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.
@aduth I honestly have no idea :) If you find out, please let me know. :)
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's also not just array access?
element/index.js
Outdated
@@ -65,3 +66,20 @@ export function renderToString( element ) { | |||
|
|||
return renderToStaticMarkup( element ); | |||
} | |||
|
|||
export function concatValues( value1, value2 ) { |
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 we are to drop the element React abstraction, what's to happen to these utility functions?
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 function effectively concatenating two element trees? Could the name more accurately reflect this? "values" is not a term that means much to me in a React 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.
concatChildren
?
If we are to drop the element React abstraction, what's to happen to these utility functions?
Yep, we should consider moving those to a utils inside the editor
folder maybe.
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.
concatChildren
?
I like this, as it seems like if this were a function native to React, it'd be called React.Children.concat
element/index.js
Outdated
@@ -65,3 +66,20 @@ export function renderToString( element ) { | |||
|
|||
return renderToStaticMarkup( element ); | |||
} | |||
|
|||
export function concatValues( value1, value2 ) { |
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 function effectively concatenating two element trees? Could the name more accurately reflect this? "values" is not a term that means much to me in a React sense.
element/index.js
Outdated
@@ -65,3 +66,20 @@ export function renderToString( element ) { | |||
|
|||
return renderToStaticMarkup( element ); | |||
} | |||
|
|||
export function concatValues( value1, value2 ) { | |||
const toArray = value => Array.isArray( value ) ? Children.toArray( value ) : [ value ]; |
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 not enough to use Children.toArray
in all cases here?
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.
That was my first guess, but it's not working as expected on string values
element/index.js
Outdated
export function concatValues( value1, value2 ) { | ||
const toArray = value => Array.isArray( value ) ? Children.toArray( value ) : [ value ]; | ||
|
||
return toArray( value1 ) |
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 this function be simplified to...
return [
...Children.toArray( value1 ),
...Children.toArray( value2 )
];
?
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, Children.toArray
is not enough, and I figured out, I'd reset the keys to avoid any confusion/duplication
element/index.js
Outdated
key: index | ||
}; | ||
} ); | ||
} |
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.
Evidenced by my confusion on the behavior of this function, would be good to add some JSDoc and test cases.
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, and unit tests too :)
editor/modes/visual-editor/block.js
Outdated
( state, ownProps ) => { | ||
const order = state.blocks.order.indexOf( ownProps.uid ); | ||
return { | ||
previousBlock: order === 0 ? null : state.blocks.byUid[ state.blocks.order[ order - 1 ] ], |
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 this should be <= 0
to account for the chance that uid
is not present in the order
array?
Alternatively perhaps the fallback could occur as an ||
case:
previousBlock: state.blocks.byUid[ state.blocks.order[ state.blocks.order.indexOf( ownProps.uid ) - 1 ] ] || null
(with better formatting)
c2ff037
to
78b6c9a
Compare
Any other concern here? |
Do we want to address focus issues separately? I'd hoped to be able to split and restore a block cleanly, but appears focus is lost after the merge:
Expected: Text block to have same text as in step 1 |
@aduth Yeah, I noticed and the fix for this would be to implement the focus |
I cannot merge two blocks if the second block has no content. |
Fine to handle separately (though maybe good to create an issue), but we should also handle the forward deletion merge. |
@aduth This is working for me. Can you detail your use case (what kind of blocks etc...) |
Error when backspacing on new block paragraph:
Unable to backspace second of two empty headings added to post:
|
Thanks for the details @aduth
This was caused because TinyMCE is handling the
Oh thanks! I was trying to reproduce, by splitting and backspace which was working. Fixed in fbef18c |
TinyMCE's internal event handling respects propagation being stopped, so in 5a32acf I changed to test whether the editor was removed after the merge to determine whether to stop. |
element/index.js
Outdated
* | ||
* @return {Array} The concatenation | ||
*/ | ||
export function concatChildren( children1, children2 ) { |
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 was poking around at this function, and had a couple observations:
- I think it'd be easy enough and a nice feature to support a spread value of
...childrens
, so two or more set of children can be concatenated - I'm still unclear why
toArray
need exist. You'd mentioned different values, but in my testingChildren.toArray
seems to handle most everything thrown at it. - I think we might be able to drop an iteration by creating the return value while iterating children (instead of
toArray
thenmap
)
Do you think something like this would work?
/**
* Concatenate two or more React children objects
*
* @param {...?Object} childrens Set of children to concatenate
* @return {Array} The concatenated value
*/
export function concatChildren( ...childrens ) {
return childrens.reduce( ( memo, children, i ) => {
Children.forEach( children, ( child, l ) => {
memo.push( cloneElement( child, {
key: [ i, l ].join()
} ) );
} );
return memo;
}, [] );
}
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 think it'd be easy enough and a nice feature to support a spread value of ...childrens, so two or more set of children can be concatenated
Great idea!
I've tried your version but it's not working. I'm finding the toArray
to be necessary. I'm unsure if it may be a problem coming from another place (parser or anything), but you can easily reproduce a bug by backspacing from the start of the second test block "I imagine..."
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.
Huh, yeah, I can reproduce that. Seems to be an issue at the point of calling cloneElement
on the string though. This variation seems to work okay for me:
/**
* Concatenate two or more React children objects
*
* @param {...?Object} childrens Set of children to concatenate
* @return {Array} The concatenated value
*/
export function concatChildren( ...childrens ) {
return childrens.reduce( ( memo, children, i ) => {
Children.forEach( children, ( child, j ) => {
if ( 'string' !== typeof child ) {
child = cloneElement( child, {
key: [ i, j ].join()
} );
}
memo.push( child );
} );
return memo;
}, [] );
}
- Adds a `merge` function to the block API
ff4ee99
to
1b360dc
Compare
@aduth Updated the |
In this PR, I'm handling the backspace behaviour. I've tried to keep the API needed for this as minimal as possible:
mergeWithPrevious
to be called when we hit backspace at the beginning of a block.merge
function responsible of merging attributes of a block with the same type.The different use cases are:
If the
merge
function is not defined in a block, this means the blocks is an "object" block that can not be merged (think image, embed, gallery), thus, the block is removed when we hit backspace in a following block.If the
merge
function is defined, we can merge this block with the following one but before doing that we need to match the block types of these two blocks. We transform the following block to the same block type and then we call the merge function to merge them. If the following block can not be switched to the current block, this means these two blocks are not mergeable.