Skip to content
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

Merged
merged 8 commits into from
May 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,7 @@ _Parameters_

- _clientIds_ `(string|Array<string>)`: Block client ID(s) to replace.
- _blocks_ `(Object|Array<Object>)`: Replacement block(s).
- _indexToSelect_ `number`: Index of replacement block to select.

<a name="replaceInnerBlocks" href="#replaceInnerBlocks">#</a> **replaceInnerBlocks**

Expand Down
4 changes: 2 additions & 2 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -708,8 +708,8 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => {
}
}
},
onReplace( blocks ) {
replaceBlocks( [ ownProps.clientId ], blocks );
onReplace( blocks, indexToSelect ) {
replaceBlocks( [ ownProps.clientId ], blocks, indexToSelect );
},
onShiftSelection() {
if ( ! ownProps.isSelectionEnabled ) {
Expand Down
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
130 changes: 63 additions & 67 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 @@ -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 = [] ) {
Copy link
Contributor

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:

  • putting the caret in the middle of a RichText and pasting
  • click "Enter" in the middle of the text
  • other use-cases I'm not aware of?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 199a1e9.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 means the second block. :) After pressing enter, the caret goes in the second block (there are only two)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively this could be written as blocks.length I guess.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean the index is always blocks.length - 1 (the last block)


onReplace( blocks, indexToSelect );
}

/**
Expand Down Expand Up @@ -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;
}

Expand Down
9 changes: 6 additions & 3 deletions packages/block-editor/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,14 @@ export function toggleSelection( isSelectionEnabled = true ) {
* Returns an action object signalling that a blocks should be replaced with
* one or more replacement blocks.
*
* @param {(string|string[])} clientIds Block client ID(s) to replace.
* @param {(Object|Object[])} blocks Replacement block(s).
* @param {(string|string[])} clientIds Block client ID(s) to replace.
* @param {(Object|Object[])} blocks Replacement block(s).
* @param {number} indexToSelect Index of replacement block to
* select.
*
* @yields {Object} Action object.
*/
export function* replaceBlocks( clientIds, blocks ) {
export function* replaceBlocks( clientIds, blocks, indexToSelect ) {
clientIds = castArray( clientIds );
blocks = castArray( blocks );
const rootClientId = yield select(
Expand All @@ -249,6 +251,7 @@ export function* replaceBlocks( clientIds, blocks ) {
clientIds,
blocks,
time: Date.now(),
indexToSelect,
};
yield* ensureDefaultBlock();
}
Expand Down
15 changes: 7 additions & 8 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -787,25 +787,24 @@ export function blockSelection( state = BLOCK_SELECTION_INITIAL_STATE, action )
return state;
}

// If there are replacement blocks, assign last block as the next
// selected block, otherwise set to null.
const lastBlock = last( action.blocks );
const indexToSelect = action.indexToSelect || action.blocks.length - 1;
const blockToSelect = action.blocks[ indexToSelect ];

if ( ! lastBlock ) {
if ( ! blockToSelect ) {
return BLOCK_SELECTION_INITIAL_STATE;
}

if (
lastBlock.clientId === state.start.clientId &&
lastBlock.clientId === state.end.clientId
blockToSelect.clientId === state.start.clientId &&
blockToSelect.clientId === state.end.clientId
) {
return state;
}

return {
...BLOCK_SELECTION_INITIAL_STATE,
start: { clientId: lastBlock.clientId },
end: { clientId: lastBlock.clientId },
start: { clientId: blockToSelect.clientId },
end: { clientId: blockToSelect.clientId },
};
}
case 'TOGGLE_SELECTION':
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 @@ -20,7 +20,6 @@ export default function HeadingEdit( {
attributes,
setAttributes,
mergeBlocks,
insertBlocksAfter,
onReplace,
className,
} ) {
Expand Down Expand Up @@ -52,17 +51,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
Loading