Skip to content

Commit

Permalink
RichText: stablize onSplit
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed Apr 19, 2019
1 parent a1e56bb commit 7eca736
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 148 deletions.
6 changes: 5 additions & 1 deletion packages/block-editor/src/components/rich-text/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ Render a rich [`contenteditable` input](https://developer.mozilla.org/en-US/docs

*Optional.* By default, a line break will be inserted on <kbd>Enter</kbd>. If the editable field can contain multiple paragraphs, this property can be set to create new paragraphs on <kbd>Enter</kbd>.

### `onSplit( value: String ): Function`

*Optional.* Called when the content can be split, where `value` is a piece of content being split off. Here you should create a new block with that content and return it. Note that you also need to provide `onReplace` in order for this to take any effect.

### `onReplace( blocks: Array ): Function`

*Optional.* Called when the `RichText` instance is empty and it can be replaced with the given blocks.
*Optional.* Called when the `RichText` instance can be replaced with the given blocks.

### `onMerge( forward: Boolean ): Function`

Expand Down
119 changes: 49 additions & 70 deletions packages/block-editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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 );
Expand All @@ -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 ),
Expand Down Expand Up @@ -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( {
Expand All @@ -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();
Expand All @@ -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';
}

Expand All @@ -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 );
}
}
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 );
}
}
}
Expand Down Expand Up @@ -824,51 +820,40 @@ 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.
*
* @param {Array} blocks The blocks to add after the split point.
* @param {Object} context The context for splitting.
*/
splitContent( blocks = [], context = {} ) {
if ( ! this.onSplit ) {
onSplit( record, pastedBlocks = [] ) {
const { onReplace, onSplit, 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 );
onReplace( blocks );
}

/**
Expand Down Expand Up @@ -963,12 +948,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;
}

Expand Down
23 changes: 11 additions & 12 deletions packages/block-library/src/heading/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export default function HeadingEdit( {
attributes,
setAttributes,
mergeBlocks,
insertBlocksAfter,
onReplace,
className,
} ) {
Expand Down Expand Up @@ -53,17 +52,17 @@ export default function HeadingEdit( {
value={ content }
onChange={ ( value ) => setAttributes( { content: value } ) }
onMerge={ mergeBlocks }
unstableOnSplit={
insertBlocksAfter ?
( before, after, ...blocks ) => {
setAttributes( { content: before } );
insertBlocksAfter( [
...blocks,
createBlock( 'core/paragraph', { content: after } ),
] );
} :
undefined
}
onSplit={ ( value ) => {
if ( ! value ) {
return createBlock( 'core/paragraph' );
}

return createBlock( 'core/heading', {
...attributes,
content: value,
} );
} }
onReplace={ onReplace }
onRemove={ () => onReplace( [] ) }
style={ { textAlign: align } }
className={ className }
Expand Down
28 changes: 8 additions & 20 deletions packages/block-library/src/list/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import { __ } from '@wordpress/i18n';
import { createBlock } from '@wordpress/blocks';
import { RichText } from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import { name } from './';

export default function ListEdit( {
attributes,
insertBlocksAfter,
setAttributes,
mergeBlocks,
onReplace,
Expand All @@ -26,25 +30,9 @@ export default function ListEdit( {
className={ className }
placeholder={ __( 'Write list…' ) }
onMerge={ mergeBlocks }
unstableOnSplit={
insertBlocksAfter ?
( before, after, ...blocks ) => {
if ( ! blocks.length ) {
blocks.push( createBlock( 'core/paragraph' ) );
}

if ( after !== '<li></li>' ) {
blocks.push( createBlock( 'core/list', {
ordered,
values: after,
} ) );
}

setAttributes( { values: before } );
insertBlocksAfter( blocks );
} :
undefined
}
onSplit={ ( value ) => createBlock( name, { values: value } ) }
onSplitMiddle={ () => createBlock( 'core/paragraph' ) }
onReplace={ onReplace }
onRemove={ () => onReplace( [] ) }
onTagNameChange={ ( tag ) => setAttributes( { ordered: tag === 'ol' } ) }
/>
Expand Down
46 changes: 1 addition & 45 deletions packages/block-library/src/paragraph/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ class ParagraphBlock extends Component {

this.onReplace = this.onReplace.bind( this );
this.toggleDropCap = this.toggleDropCap.bind( this );
this.splitBlock = this.splitBlock.bind( this );
}

onReplace( blocks ) {
Expand All @@ -80,49 +79,6 @@ class ParagraphBlock extends Component {
return checked ? __( 'Showing large initial letter.' ) : __( 'Toggle to show a large initial letter.' );
}

/**
* Split handler for RichText value, namely when content is pasted or the
* user presses the Enter key.
*
* @param {?Array} before Optional before value, to be used as content
* in place of what exists currently for the
* block. If undefined, the block is deleted.
* @param {?Array} after Optional after value, to be appended in a new
* paragraph block to the set of blocks passed
* as spread.
* @param {...WPBlock} blocks Optional blocks inserted between the before
* and after value blocks.
*/
splitBlock( before, after, ...blocks ) {
const {
attributes,
insertBlocksAfter,
setAttributes,
onReplace,
} = this.props;

if ( after !== null ) {
// Append "After" content as a new paragraph block to the end of
// any other blocks being inserted after the current paragraph.
blocks.push( createBlock( name, { content: after } ) );
}

if ( blocks.length && insertBlocksAfter ) {
insertBlocksAfter( blocks );
}

const { content } = attributes;
if ( before === null ) {
// If before content is omitted, treat as intent to delete block.
onReplace( [] );
} else if ( content !== before ) {
// Only update content if it has in-fact changed. In case that user
// has created a new paragraph at end of an existing one, the value
// of before will be strictly equal to the current content.
setAttributes( { content: before } );
}
}

render() {
const {
attributes,
Expand Down Expand Up @@ -242,7 +198,7 @@ class ParagraphBlock extends Component {
content: nextContent,
} );
} }
unstableOnSplit={ this.splitBlock }
onSplit={ ( value ) => createBlock( name, { content: value } ) }
onMerge={ mergeBlocks }
onReplace={ this.onReplace }
onRemove={ () => onReplace( [] ) }
Expand Down

0 comments on commit 7eca736

Please sign in to comment.