diff --git a/.eslintrc.js b/.eslintrc.js index 4b354fe2d986ff..2f47ada5001739 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,6 +87,7 @@ module.exports = { 'dropRight', 'each', 'findIndex', + 'findKey', 'flatten', 'isArray', 'isFinite', diff --git a/packages/block-editor/src/components/rich-text/use-input-rules.js b/packages/block-editor/src/components/rich-text/use-input-rules.js index 719bba1caa2740..b2b1800ef037d4 100644 --- a/packages/block-editor/src/components/rich-text/use-input-rules.js +++ b/packages/block-editor/src/components/rich-text/use-input-rules.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { findKey } from 'lodash'; - /** * WordPress dependencies */ @@ -17,21 +12,17 @@ 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'; +import { + retrieveSelectedAttribute, + START_OF_SELECTED_AREA, +} from '../../utils/selection'; 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 + const attributeKey = retrieveSelectedAttribute( + blocks[ i ].attributes ); if ( attributeKey ) { diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 213b9824636591..e20872ca78a817 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { castArray, findKey, first, isObject, last, some } from 'lodash'; +import { castArray, first, isObject, last, some } from 'lodash'; /** * WordPress dependencies @@ -26,6 +26,10 @@ import deprecated from '@wordpress/deprecated'; * Internal dependencies */ import { mapRichTextSettings } from './utils'; +import { + retrieveSelectedAttribute, + START_OF_SELECTED_AREA, +} from '../utils/selection'; /** * Action which will insert a default block insert action if there @@ -771,10 +775,6 @@ export const __unstableDeleteSelection = ...mapRichTextSettings( attributeDefinitionB ), } ); - // 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'; - valueA = remove( valueA, selectionA.offset, valueA.text.length ); valueB = insert( valueB, START_OF_SELECTED_AREA, 0, selectionB.offset ); @@ -822,12 +822,7 @@ export const __unstableDeleteSelection = ); } - const newAttributeKey = findKey( - updatedAttributes, - ( v ) => - typeof v === 'string' && - v.indexOf( START_OF_SELECTED_AREA ) !== -1 - ); + const newAttributeKey = retrieveSelectedAttribute( updatedAttributes ); const convertedHtml = updatedAttributes[ newAttributeKey ]; const convertedValue = create( { @@ -1052,10 +1047,6 @@ export const mergeBlocks = } } - // 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'; - // Clone the blocks so we don't insert the character in a "live" block. const cloneA = cloneBlock( blockA ); const cloneB = cloneBlock( blockB ); @@ -1098,12 +1089,8 @@ export const mergeBlocks = ); if ( canRestoreTextSelection ) { - const newAttributeKey = findKey( - updatedAttributes, - ( v ) => - typeof v === 'string' && - v.indexOf( START_OF_SELECTED_AREA ) !== -1 - ); + const newAttributeKey = + retrieveSelectedAttribute( updatedAttributes ); const convertedHtml = updatedAttributes[ newAttributeKey ]; const convertedValue = create( { html: convertedHtml, diff --git a/packages/block-editor/src/utils/selection.js b/packages/block-editor/src/utils/selection.js new file mode 100644 index 00000000000000..68c634d591c5e9 --- /dev/null +++ b/packages/block-editor/src/utils/selection.js @@ -0,0 +1,26 @@ +/** + * A robust way to retain selection position through various + * transforms is to insert a special character at the position and + * then recover it. + */ +export const START_OF_SELECTED_AREA = '\u0086'; + +/** + * Retrieve the block attribute that contains the selection position. + * + * @param {Object} blockAttributes Block attributes. + * @return {string|void} The name of the block attribute that was previously selected. + */ +export function retrieveSelectedAttribute( blockAttributes ) { + if ( ! blockAttributes ) { + return; + } + + return Object.keys( blockAttributes ).find( ( name ) => { + const value = blockAttributes[ name ]; + return ( + typeof value === 'string' && + value.indexOf( START_OF_SELECTED_AREA ) !== -1 + ); + } ); +} diff --git a/packages/block-editor/src/utils/test/selection.js b/packages/block-editor/src/utils/test/selection.js new file mode 100644 index 00000000000000..484ad201b14fff --- /dev/null +++ b/packages/block-editor/src/utils/test/selection.js @@ -0,0 +1,39 @@ +/** + * Internal dependencies + */ +import { + retrieveSelectedAttribute, + START_OF_SELECTED_AREA, +} from '../selection'; + +describe( 'retrieveSelectedAttribute', () => { + it( 'returns undefined if block attributes are not an object', () => { + expect( retrieveSelectedAttribute( undefined ) ).toBeUndefined(); + expect( retrieveSelectedAttribute( null ) ).toBeUndefined(); + } ); + + it( 'returns the block attribute name if it contains the selection position character', () => { + const blockAttributes = { + foo: `this is not selected`, + bar: `this${ START_OF_SELECTED_AREA }is selected`, + }; + expect( retrieveSelectedAttribute( blockAttributes ) ).toBe( 'bar' ); + } ); + + it( 'returns the first block attribute that contains the selection position character', () => { + const blockAttributes = { + foo: `this is not selected`, + bar: `this${ START_OF_SELECTED_AREA }is selected`, + baz: `this${ START_OF_SELECTED_AREA }is selected`, + }; + expect( retrieveSelectedAttribute( blockAttributes ) ).toBe( 'bar' ); + } ); + + it( 'returns undefined if no block attribute contains the selection position character', () => { + const blockAttributes = { + foo: `this is not selected`, + bar: `this is not selected either`, + }; + expect( retrieveSelectedAttribute( blockAttributes ) ).toBeUndefined(); + } ); +} );