diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index fa326f04905460..38b8bc8ff96ae0 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -2,18 +2,12 @@ * External dependencies */ import classnames from 'classnames'; -import { first, last } from 'lodash'; import { animated } from 'react-spring/web.cjs'; /** * WordPress dependencies */ import { useRef, useEffect, useLayoutEffect, useState, useContext } from '@wordpress/element'; -import { - focus, - isTextField, - placeCaretAtHorizontalEdge, -} from '@wordpress/dom'; import { BACKSPACE, DELETE, ENTER } from '@wordpress/keycodes'; import { getBlockType, @@ -41,7 +35,6 @@ import BlockInvalidWarning from './block-invalid-warning'; import BlockCrashWarning from './block-crash-warning'; import BlockCrashBoundary from './block-crash-boundary'; import BlockHtml from './block-html'; -import { isInsideRootBlock } from '../../utils/dom'; import useMovingAnimation from './moving-animation'; import { Context } from './root-container'; @@ -90,7 +83,6 @@ function BlockListBlock( { index, isValid, attributes, - initialPosition, wrapperProps, setAttributes, onReplace, @@ -136,49 +128,19 @@ function BlockListBlock( { const blockType = getBlockType( name ); const blockAriaLabel = useDebouncedAccessibleBlockLabel( blockType, attributes, index, moverDirection, 400 ); - // Handing the focus of the block on creation and update - - /** - * When a block becomes selected, transition focus to an inner tabbable. - * - * @param {boolean} ignoreInnerBlocks Should not focus inner blocks. - */ - const focusTabbable = ( ignoreInnerBlocks ) => { - // Focus is captured by the wrapper node, so while focus transition - // should only consider tabbables within editable display, since it - // may be the wrapper itself or a side control which triggered the - // focus event, don't unnecessary transition to an inner tabbable. - if ( wrapper.current.contains( document.activeElement ) ) { - return; - } - - // Find all tabbables within node. - const textInputs = focus.tabbable - .find( wrapper.current ) - .filter( isTextField ) - // Exclude inner blocks - .filter( ( node ) => ! ignoreInnerBlocks || isInsideRootBlock( wrapper.current, node ) ); - - // If reversed (e.g. merge via backspace), use the last in the set of - // tabbables. - const isReverse = -1 === initialPosition; - const target = ( isReverse ? last : first )( textInputs ); - - if ( ! target ) { - wrapper.current.focus(); - return; - } - - placeCaretAtHorizontalEdge( target, isReverse ); - }; - // Focus the selected block's wrapper or inner input on mount and update const isMounting = useRef( true ); useEffect( () => { if ( ! isMultiSelecting && ! isNavigationMode ) { if ( isSelected ) { - focusTabbable( ! isMounting.current ); + // Focus is captured by the wrapper node, so while focus transition + // should only consider tabbables within editable display, since it + // may be the wrapper itself or a side control which triggered the + // focus event, don't unnecessary transition to an inner tabbable. + if ( ! wrapper.current.contains( document.activeElement ) ) { + wrapper.current.focus(); + } } else if ( isFirstMultiSelected ) { wrapper.current.focus(); } @@ -477,14 +439,14 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { } } }, - onReplace( blocks, indexToSelect ) { + onReplace( blocks, ...props ) { if ( blocks.length && ! isUnmodifiedDefaultBlock( blocks[ blocks.length - 1 ] ) ) { __unstableMarkLastChangeAsPersistent(); } - replaceBlocks( [ ownProps.clientId ], blocks, indexToSelect ); + replaceBlocks( [ ownProps.clientId ], blocks, ...props ); }, toggleSelection( selectionEnabled ) { toggleSelection( selectionEnabled ); diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 78ac7b4dfa4b2f..8612ef38a010a9 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { omit } from 'lodash'; +import { omit, findKey } from 'lodash'; /** * WordPress dependencies @@ -244,22 +244,34 @@ class RichTextWrapper extends Component { blocks.push( onSplitMiddle() ); } + let newAttributeKey; + // 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( toHTMLString( { - value: after, + const START_OF_SELECTED_AREA = '\u0086'; + const afterBlock = onSplit( toHTMLString( { + value: insert( after, START_OF_SELECTED_AREA, 0, 0 ), multilineTag, - } ) ) ); + } ) ); + + newAttributeKey = findKey( afterBlock.attributes, ( v ) => + typeof v === 'string' && v.indexOf( START_OF_SELECTED_AREA ) !== -1 + ); + + afterBlock.attributes[ newAttributeKey ] = + afterBlock.attributes[ newAttributeKey ].replace( START_OF_SELECTED_AREA, '' ); + + blocks.push( afterBlock ); } // 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; - onReplace( blocks, indexToSelect ); + onReplace( blocks, indexToSelect, newAttributeKey, 0 ); } inputRule( value, valueToFormat ) { diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index ad430ca773f5db..34d6cf7a6921cb 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -286,7 +286,7 @@ function getBlocksWithDefaultStylesApplied( blocks, blockEditorSettings ) { * * @yield {Object} Action object. */ -export function* replaceBlocks( clientIds, blocks, indexToSelect ) { +export function* replaceBlocks( clientIds, blocks, indexToSelect, attributeKey, offset ) { clientIds = castArray( clientIds ); blocks = getBlocksWithDefaultStylesApplied( castArray( blocks ), @@ -319,6 +319,8 @@ export function* replaceBlocks( clientIds, blocks, indexToSelect ) { blocks, time: Date.now(), indexToSelect, + attributeKey, + offset, }; yield* ensureDefaultBlock(); } diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 892712d372394a..05ecba34cc6163 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1000,7 +1000,11 @@ function selection( state = {}, action ) { return state; } - return { clientId: blockToSelect.clientId }; + return { + clientId: blockToSelect.clientId, + attributeKey: action.attributeKey, + offset: action.offset, + }; } } diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index 9984a7ac97695f..908cd2cb4c5fb9 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -189,7 +189,7 @@ class RichText extends Component { } componentDidMount() { - this.applyRecord( this.record, { domOnly: true } ); + this.applyRecord( this.record ); } createRecord() {