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
Delete empty paragraphs when backspacing #3732
Delete empty paragraphs when backspacing #3732
Conversation
Codecov Report
@@ Coverage Diff @@
## master #3732 +/- ##
==========================================
- Coverage 37.7% 37.58% -0.12%
==========================================
Files 279 278 -1
Lines 6737 6726 -11
Branches 1226 1227 +1
==========================================
- Hits 2540 2528 -12
- Misses 3536 3537 +1
Partials 661 661
Continue to review full report at Codecov.
|
editor/effects.js
Outdated
@@ -217,6 +218,9 @@ export default { | |||
// Only focus the previous block if it's not mergeable | |||
if ( ! blockType.merge ) { | |||
dispatch( focusBlock( blockA.uid ) ); | |||
if ( blockB.name === 'core/paragraph' && ! blockB.attributes.content.length ) { |
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 instinct here is to invent a isBlockEmpty
helper which we would use instead of this very very specific logic.
I'm not sure, though, since it probably is only paragraphs that we want this behaviour for...
Thoughts, @aduth?
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 could also introduce a flag that marks all other removable blocks like heading or list.
blockB.attributes.content.length
- is it safe to assume that content is there and it's a string?
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 yes, you can at least introduce a local variable which better explains what is happening in this conditional statement :)
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 could introduce a isEmpty
property to the block API and if not defined the block is never empty. Only the block knows how to read its attributes.
This is another API to maintain but it makes sense to me. cc @aduth
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 added an isEmpty
property and implemented it for core/paragraph
. I agree that extra API isn't ideal, but this seems necessary.
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 love the isEmpty
property (more to a general concern of limiting number of properties on the block API). It seems to me if Editable is smart enough to know that it should merge its own content into previous, it can also be smart enough to know that it has no content to merge and should just remove. That said, I also don't love an onRemove
being added to Editable. Wondering if we could enhance one of the existing like onReplace
to also encompass behavior of "replacing with nothing" ?
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.
8d9cab7
to
e5383ce
Compare
Seriously cool work. In my testing, behavior wise, this works exactly like it needs to! 👍 👍 from an experience point of view. |
blocks/library/list/index.js
Outdated
values: value.map( ( item, index ) => <li key={ index } >{ get( item, 'children.props.children', '' ) } </li> ) | ||
.concat( citation ? <li key="citation">{ citation }</li> : [] ), | ||
values: value.map( item => get( item, 'children.props.children' ) ).concat( citation ) | ||
.filter( text => typeof text === 'string' ) |
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 was a nice find that we are creating a list with an empty <li>
and this affects some functionality 👍
Unfortunately, we cannot be that strict and force the text to be a string. For example, if we have a quote with items in bold they are ignored in the transformation because these items are not a string.
If the user adds a quote with a line then 3 empty lines and another line, I'm not certain if we should just transform to a list with two items or to a list with one item 3 empty items and one last item.
Maybe we can just add a special case that verifies if value and citation are both empty/unset and if yes we just return with a list with no items. e.g: if ( ( ! value || ! value.length ) && ! citation ) {
.
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 fiddled with this in e2257f5. There's a surprising amount of edge cases!
The logic I opted for is:
'Quote' / 'Citation'
→<li>Quote</li><li>Citation</li>
'' / 'Citation'
→<li></li><li>Citation</li>
'Quote' / ''
→<li>Quote</li>
'' / ''
→ ``
e5383ce
to
e2257f5
Compare
A small thing that I really appreciate from this change: Clicking into the "Write your story" and hitting Backspace resets to remove the inserted paragraph block. |
blocks/editable/index.js
Outdated
const isEmpty = dom.isEmpty( rootNode ); | ||
if ( this.props.onReplace && isEmpty ) { | ||
// Remove this block if the editor is empty | ||
this.props.onReplace( [] ); |
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.
One odd thing about the logic flow here: If I render an <Editable />
with onReplace
, but not onMerge
, I won't have this emptiness replace behavior, since this is only reached if onMerge
is assigned, even though it's not called.
I should note to my review, functionally it works great and I'm glad to see it handled in Editable entirely. |
Hmm, a potential issue is that some blocks may have multiple Editables, and one of them being empty is not a sufficient metric to determine that the block should be replaced. Example: Pressing backspace from a quote citation, when the quote itself has text, shouldn't delete the block. |
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 point... there's a lot of This approach kind of works right now because in practice it's only Unfortunately that means that the bug that this PR attempts to fix is only fixed for paragraph blocks and not quotes (which has If we give BUT doing this means we now have the problem you describe where a quote that contains only a citation can be incorrectly deleted: Long story short, I think that patching I have two ideas:
|
Could we add a new callback |
e2257f5
to
07e051e
Compare
Nice suggestion. I've done this and it seems to work pretty well. I also fixed a similar bug to what @gziolo found where if one presses backspace in an empty paragraph block that follows a list block, an empty |
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 seems to work quite nicely.
blocks/editable/index.js
Outdated
event.preventDefault(); | ||
event.stopImmediatePropagation(); | ||
|
||
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.
Is the return;
necessary?
blocks/editable/index.js
Outdated
} | ||
|
||
const isEmpty = dom.isEmpty( rootNode ); | ||
if ( isEmpty && this.props.onRemove ) { |
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 dom.isEmpty
is non-trivial (sorta appears so), we could achieve a minor optimization here with conditional short-circuiting by reordering the condition to test the truthy onRemove
first:
if ( this.props.onRemove && dom.isEmpty( rootNode ) ) {
When backspacing from an empty paragraph into a block that has no merge function (e.g. an image), the empty paragraph should be deleted. Empty lists, headings and quotes are treated similarly.
Reduce the amount of times that empty <li>s are created when converting a core/quote to a core/list. The conversion logic is: * 'Quote' / 'Citation' -> `<li>Quote</li><li>Citation</li>` * '' / 'Citation' -> `<li></li><li>Citation</li>` * 'Quote' / '' -> `<li>Quote</li>` * '' / '' -> ``
Prevent empty paragraphs from being converted to an empty <li>.
07e051e
to
18e281a
Compare
Nice work here. |
This is a rough attempt to fix #3624.
When backspacing from an empty paragraph into a block that has no merge function (e.g. an image), the empty paragraph should be deleted.
Out with the old:
In with the new:
How to test