-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
RichText: stabilize onSplit #14765
RichText: stabilize onSplit #14765
Changes from all commits
0003241
a38e416
6745325
8cd03fc
110a13f
7af898f
199a1e9
331e054
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 |
---|---|---|
|
@@ -40,10 +40,10 @@ import { | |
__unstableIndentListItems as indentListItems, | ||
__unstableGetActiveFormats as getActiveFormats, | ||
__unstableUpdateFormats as updateFormats, | ||
replace, | ||
} from '@wordpress/rich-text'; | ||
import { decodeEntities } from '@wordpress/html-entities'; | ||
import { withFilters, IsolatedEventContainer } from '@wordpress/components'; | ||
import deprecated from '@wordpress/deprecated'; | ||
import isShallowEqual from '@wordpress/is-shallow-equal'; | ||
|
||
/** | ||
|
@@ -114,17 +114,6 @@ export class RichText extends Component { | |
this.multilineWrapperTags = [ 'ul', 'ol' ]; | ||
} | ||
|
||
if ( this.props.onSplit ) { | ||
this.onSplit = this.props.onSplit; | ||
|
||
deprecated( 'wp.editor.RichText onSplit prop', { | ||
plugin: 'Gutenberg', | ||
alternative: 'wp.editor.RichText unstableOnSplit prop', | ||
} ); | ||
} else if ( this.props.unstableOnSplit ) { | ||
this.onSplit = this.props.unstableOnSplit; | ||
} | ||
|
||
this.onFocus = this.onFocus.bind( this ); | ||
this.onBlur = this.onBlur.bind( this ); | ||
this.onChange = this.onChange.bind( this ); | ||
|
@@ -144,6 +133,7 @@ export class RichText extends Component { | |
this.valueToEditableHTML = this.valueToEditableHTML.bind( this ); | ||
this.handleHorizontalNavigation = this.handleHorizontalNavigation.bind( this ); | ||
this.onPointerDown = this.onPointerDown.bind( this ); | ||
this.onSplit = this.onSplit.bind( this ); | ||
|
||
this.formatToValue = memize( | ||
this.formatToValue.bind( this ), | ||
|
@@ -276,6 +266,8 @@ export class RichText extends Component { | |
// Only process file if no HTML is present. | ||
// Note: a pasted file may have the URL as plain text. | ||
const item = find( [ ...items, ...files ], ( { type } ) => /^image\/(?:jpe?g|png|gif)$/.test( type ) ); | ||
const record = this.getRecord(); | ||
|
||
if ( item && ! html ) { | ||
const file = item.getAsFile ? item.getAsFile() : item; | ||
const content = pasteHandler( { | ||
|
@@ -291,14 +283,12 @@ export class RichText extends Component { | |
if ( shouldReplace ) { | ||
this.props.onReplace( content ); | ||
} else if ( this.onSplit ) { | ||
this.splitContent( content ); | ||
this.onSplit( record, content ); | ||
} | ||
|
||
return; | ||
} | ||
|
||
const record = this.getRecord(); | ||
|
||
// There is a selection, check if a URL is pasted. | ||
if ( ! isCollapsed( record ) ) { | ||
const pastedText = ( html || plainText ).replace( /<[^>]+>/g, '' ).trim(); | ||
|
@@ -319,13 +309,14 @@ export class RichText extends Component { | |
} | ||
} | ||
|
||
const shouldReplace = this.props.onReplace && this.isEmpty(); | ||
const canReplace = this.props.onReplace && this.isEmpty(); | ||
const canSplit = this.props.onReplace && this.props.onSplit; | ||
|
||
let mode = 'INLINE'; | ||
|
||
if ( shouldReplace ) { | ||
if ( canReplace ) { | ||
mode = 'BLOCKS'; | ||
} else if ( this.onSplit ) { | ||
} else if ( canSplit ) { | ||
mode = 'AUTO'; | ||
} | ||
|
||
|
@@ -338,17 +329,20 @@ export class RichText extends Component { | |
} ); | ||
|
||
if ( typeof content === 'string' ) { | ||
const recordToInsert = create( { html: content } ); | ||
this.onChange( insert( record, recordToInsert ) ); | ||
} else if ( this.onSplit ) { | ||
if ( ! content.length ) { | ||
return; | ||
let valueToInsert = create( { html: content } ); | ||
|
||
// If the content should be multiline, we should process text | ||
// separated by a line break as separate lines. | ||
if ( this.multilineTag ) { | ||
valueToInsert = replace( valueToInsert, /\n+/g, LINE_SEPARATOR ); | ||
} | ||
|
||
if ( shouldReplace ) { | ||
this.onChange( insert( record, valueToInsert ) ); | ||
} else if ( content.length > 0 ) { | ||
if ( canReplace ) { | ||
this.props.onReplace( content ); | ||
} else { | ||
this.splitContent( content, { paste: true } ); | ||
this.onSplit( record, content ); | ||
} | ||
} | ||
} | ||
|
@@ -599,6 +593,8 @@ export class RichText extends Component { | |
*/ | ||
onKeyDown( event ) { | ||
const { keyCode, shiftKey, altKey, metaKey, ctrlKey } = event; | ||
const { onReplace, onSplit } = this.props; | ||
const canSplit = onReplace && onSplit; | ||
|
||
if ( | ||
// Only override left and right keys without modifiers pressed. | ||
|
@@ -718,15 +714,15 @@ export class RichText extends Component { | |
if ( this.multilineTag ) { | ||
if ( event.shiftKey ) { | ||
this.onChange( insert( record, '\n' ) ); | ||
} else if ( this.onSplit && isEmptyLine( record ) ) { | ||
this.onSplit( ...split( record ).map( this.valueToFormat ) ); | ||
} else if ( canSplit && isEmptyLine( record ) ) { | ||
this.onSplit( record ); | ||
} else { | ||
this.onChange( insertLineSeparator( record ) ); | ||
} | ||
} else if ( event.shiftKey || ! this.onSplit ) { | ||
} else if ( event.shiftKey || ! canSplit ) { | ||
this.onChange( insert( record, '\n' ) ); | ||
} else { | ||
this.splitContent(); | ||
this.onSplit( record ); | ||
} | ||
} | ||
} | ||
|
@@ -825,50 +821,56 @@ export class RichText extends Component { | |
} | ||
|
||
/** | ||
* Splits the content at the location of the selection. | ||
* | ||
* Replaces the content of the editor inside this element with the contents | ||
* before the selection. Sends the elements after the selection to the `onSplit` | ||
* handler. | ||
* Signals to the RichText owner that the block can be replaced with two | ||
* blocks as a result of splitting the block by pressing enter, or with | ||
* blocks as a result of splitting the block by pasting block content in the | ||
* instance. | ||
* | ||
* @param {Array} blocks The blocks to add after the split point. | ||
* @param {Object} context The context for splitting. | ||
* @param {Object} record The rich text value to split. | ||
* @param {Array} pastedBlocks The pasted blocks to insert, if any. | ||
*/ | ||
splitContent( blocks = [], context = {} ) { | ||
if ( ! this.onSplit ) { | ||
onSplit( record, pastedBlocks = [] ) { | ||
const { | ||
onReplace, | ||
onSplit, | ||
__unstableOnSplitMiddle: onSplitMiddle, | ||
} = this.props; | ||
|
||
if ( ! onReplace || ! onSplit ) { | ||
return; | ||
} | ||
|
||
const record = this.createRecord(); | ||
let [ before, after ] = split( record ); | ||
|
||
// In case split occurs at the trailing or leading edge of the field, | ||
// assume that the before/after values respectively reflect the current | ||
// value. This also provides an opportunity for the parent component to | ||
// determine whether the before/after value has changed using a trivial | ||
// strict equality operation. | ||
if ( isEmpty( after ) ) { | ||
before = record; | ||
} else if ( isEmpty( before ) ) { | ||
after = record; | ||
} | ||
const blocks = []; | ||
const [ before, after ] = split( record ); | ||
const hasPastedBlocks = pastedBlocks.length > 0; | ||
|
||
// If pasting and the split would result in no content other than the | ||
// pasted blocks, remove the before and after blocks. | ||
if ( context.paste ) { | ||
before = isEmpty( before ) ? null : before; | ||
after = isEmpty( after ) ? null : after; | ||
// Create a block with the content before the caret if there's no pasted | ||
// blocks, or if there are pasted blocks and the value is not empty. | ||
// We do not want a leading empty block on paste, but we do if split | ||
// with e.g. the enter key. | ||
if ( ! hasPastedBlocks || ! isEmpty( before ) ) { | ||
blocks.push( onSplit( this.valueToFormat( before ) ) ); | ||
} | ||
|
||
if ( before ) { | ||
before = this.valueToFormat( before ); | ||
if ( hasPastedBlocks ) { | ||
blocks.push( ...pastedBlocks ); | ||
} else if ( onSplitMiddle ) { | ||
blocks.push( onSplitMiddle() ); | ||
} | ||
|
||
if ( after ) { | ||
after = this.valueToFormat( after ); | ||
// If there's pasted blocks, append a block with the content after the | ||
// caret. Otherwise, do append and empty block if there is no | ||
// `onSplitMiddle` prop, but if there is and the content is empty, the | ||
// middle block is enough to set focus in. | ||
if ( hasPastedBlocks || ! onSplitMiddle || ! isEmpty( after ) ) { | ||
blocks.push( onSplit( this.valueToFormat( after ) ) ); | ||
} | ||
|
||
this.onSplit( before, after, ...blocks ); | ||
// If there are pasted blocks, set the selection to the last one. | ||
// Otherwise, set the selection to the second block. | ||
const indexToSelect = hasPastedBlocks ? blocks.length - 1 : 1; | ||
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. It's not immediately clear to me why should we put the caret int the first block if there's no pasted blocks? 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. Alternatively this could be written as 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. Does this mean the index is always |
||
|
||
onReplace( blocks, indexToSelect ); | ||
} | ||
|
||
/** | ||
|
@@ -963,12 +965,6 @@ export class RichText extends Component { | |
} ); | ||
} | ||
|
||
// Guard for blocks passing `null` in onSplit callbacks. May be removed | ||
// if onSplit is revised to not pass a `null` value. | ||
if ( value === null ) { | ||
return create(); | ||
} | ||
|
||
return 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.
I think it would be good to clarify the different use cases this function is called (sorry If it's somewhere else and I missed it), something like:
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.
Done in 199a1e9.