-
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
Quote v2: retain selection after transform #39838
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,14 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { findKey } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useRef } from '@wordpress/element'; | ||
import { useRefEffect } from '@wordpress/compose'; | ||
import { slice, toHTMLString } from '@wordpress/rich-text'; | ||
import { insert, toHTMLString } from '@wordpress/rich-text'; | ||
import { getBlockTransforms, findTransform } from '@wordpress/blocks'; | ||
import { useDispatch } from '@wordpress/data'; | ||
|
||
|
@@ -13,6 +18,34 @@ import { useDispatch } from '@wordpress/data'; | |
import { store as blockEditorStore } from '../../store'; | ||
import { preventEventDiscovery } from './prevent-event-discovery'; | ||
|
||
// A robust way to retain selection position through various | ||
// transforms is to insert a special character at the position and | ||
// then recover it. | ||
const START_OF_SELECTED_AREA = '\u0086'; | ||
|
||
function findSelection( blocks ) { | ||
let i = blocks.length; | ||
|
||
while ( i-- ) { | ||
const attributeKey = findKey( | ||
blocks[ i ].attributes, | ||
( v ) => | ||
typeof v === 'string' && | ||
v.indexOf( START_OF_SELECTED_AREA ) !== -1 | ||
); | ||
|
||
if ( attributeKey ) { | ||
return [ blocks[ i ].clientId, attributeKey, 0 ]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I'm not wrong this returns the attribute name as "identifier". AFAIK, this has never been enforced anyway. A block can use an "identifier" in a RichText that is completely different than the RichText attribute name. In fact, I think assuming there's a 1-1 relationship between attributes and RichText is a wrong assumptions. One could use multiple RichText in a single attribute... or transform RichText values... Maybe a better solution here would be to only update the selection (and focus the RichText) when the RichText rerenders internally and detects that the invisible character is in the received value prop. It's also a solution that is block-independent (can be bundled inside the raw RichText). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right, it's hard to enforce, but it should be the same as the attribute key. If you use a
Sure, you can do that. But if you're using merge functions you can't :)
My fear with that is that the character ends up in the block editor store. So far we've always taken it out before syncing to the store. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @youknowriad We can also just return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Just looked at the "merge" functions and it seem they make that assumption too but it seems wrong there as well. I think ideally even merge functions should just put the character in the right position instead of making that assumptions. It seems to me we have two options: 1- Be opinionated and assume "merging" is only for full text blocks and enforce that the rule "identifier <=> attribute name" is enforced somehow (document, warn block authors...) 2- Be non-opinionated and let block authors define the desired behavior, basically there's two options here: a- Use an invisible character that can end up in attributes... My idealist mind think 2.B is the best option here conceptually but the pragmatist in me knows that it might be a big change with some backward compatibility challenges. So I'd be ok with 1 for now (this PR basically) but I think we need to be clear and explicit about the requirements here. The block author need to be aware of these more clearly and not something that is assumed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
But then if a block has two RichText this breaks. If we're sticking with the opinionated approach, enforcing the attribute <=> identifier seems better but we should document and fail properly if not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, if we do keep the opinionated approach, why do we have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, agreed about failing properly. What I like about (1) is that its works right now and these merge/transform functions remain simple. I'm wondering if there could be a way for RichText (the I also don't like the name There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. Right, this is a block-editor specific RichText :) We have two: the one in the rich text package is block agnostic and the one in the block-editor package (used by block) has a lot of block context for merging and splitting and syncing all state to the block editor. |
||
} | ||
|
||
const nestedSelection = findSelection( blocks[ i ].innerBlocks ); | ||
|
||
if ( nestedSelection ) { | ||
return nestedSelection; | ||
} | ||
} | ||
} | ||
|
||
export function useInputRules( props ) { | ||
const { | ||
__unstableMarkLastChangeAsPersistent, | ||
|
@@ -22,7 +55,7 @@ export function useInputRules( props ) { | |
propsRef.current = props; | ||
return useRefEffect( ( element ) => { | ||
function inputRule() { | ||
const { value, onReplace } = propsRef.current; | ||
const { value, onReplace, selectionChange } = propsRef.current; | ||
|
||
if ( ! onReplace ) { | ||
return; | ||
|
@@ -52,11 +85,12 @@ export function useInputRules( props ) { | |
} | ||
|
||
const content = toHTMLString( { | ||
value: slice( value, start, text.length ), | ||
value: insert( value, START_OF_SELECTED_AREA, 0, start ), | ||
} ); | ||
const block = transformation.transform( content ); | ||
|
||
onReplace( [ block ] ); | ||
selectionChange( ...findSelection( [ block ] ) ); | ||
__unstableMarkAutomaticChange(); | ||
} | ||
|
||
|
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.
While this is the only use case for nested blocks, there may be value in reusing this function in other places where we use the
START_OF_SELECTED_AREA
trick.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 took advantage of this and abstracted it into a reusable function while removing Lodash in #41806. Would love to get your feedback there when you get a chance @ellatrix.