diff --git a/packages/block-editor/src/components/rich-text/format-edit.js b/packages/block-editor/src/components/rich-text/format-edit.js index 3ab3f47d226aec..75b077ab321d43 100644 --- a/packages/block-editor/src/components/rich-text/format-edit.js +++ b/packages/block-editor/src/components/rich-text/format-edit.js @@ -1,11 +1,7 @@ /** * WordPress dependencies */ -import { - getActiveFormat, - getActiveObject, - isCollapsed, -} from '@wordpress/rich-text'; +import { getActiveFormat, getActiveObject } from '@wordpress/rich-text'; export default function FormatEdit( { formatTypes, @@ -22,37 +18,11 @@ export default function FormatEdit( { } const activeFormat = getActiveFormat( value, name ); - let isActive = activeFormat !== undefined; + const isActive = activeFormat !== undefined; const activeObject = getActiveObject( value ); const isObjectActive = activeObject !== undefined && activeObject.type === name; - // Edge case: un-collapsed link formats. - // If there is a missing link format at either end of the selection - // then we shouldn't show the Edit UI because the selection has exceeded - // the bounds of the link format. - // Also if the format objects don't match then we're dealing with two separate - // links so we should not allow the link to be modified over the top. - if ( name === 'core/link' && ! isCollapsed( value ) ) { - const formats = value.formats; - - const linkFormatAtStart = formats[ value.start ]?.find( - ( { type } ) => type === 'core/link' - ); - - const linkFormatAtEnd = formats[ value.end - 1 ]?.find( - ( { type } ) => type === 'core/link' - ); - - if ( - ! linkFormatAtStart || - ! linkFormatAtEnd || - linkFormatAtStart !== linkFormatAtEnd - ) { - isActive = false; - } - } - return ( type === formatType ); } diff --git a/packages/rich-text/src/get-active-formats.js b/packages/rich-text/src/get-active-formats.js index 256c95eca01431..09bbef0cf2d6cf 100644 --- a/packages/rich-text/src/get-active-formats.js +++ b/packages/rich-text/src/get-active-formats.js @@ -1,6 +1,11 @@ /** @typedef {import('./create').RichTextValue} RichTextValue */ /** @typedef {import('./create').RichTextFormatList} RichTextFormatList */ +/** + * Internal dependencies + */ +import { isFormatEqual } from './is-format-equal'; + /** * Gets the all format objects at the start of the selection. * @@ -10,10 +15,8 @@ * * @return {RichTextFormatList} Active format objects. */ -export function getActiveFormats( - { formats, start, end, activeFormats }, - EMPTY_ACTIVE_FORMATS = [] -) { +export function getActiveFormats( value, EMPTY_ACTIVE_FORMATS = [] ) { + const { formats, start, end, activeFormats } = value; if ( start === undefined ) { return EMPTY_ACTIVE_FORMATS; } @@ -37,5 +40,49 @@ export function getActiveFormats( return formatsAfter; } - return formats[ start ] || EMPTY_ACTIVE_FORMATS; + // If there's no formats at the start index, there are not active formats. + if ( ! formats[ start ] ) { + return EMPTY_ACTIVE_FORMATS; + } + + const selectedFormats = formats.slice( start, end ); + + // Clone the formats so we're not mutating the live value. + const _activeFormats = [ ...selectedFormats[ 0 ] ]; + let i = selectedFormats.length; + + // For performance reasons, start from the end where it's much quicker to + // realise that there are no active formats. + while ( i-- ) { + const formatsAtIndex = selectedFormats[ i ]; + + // If we run into any index without formats, we're sure that there's no + // active formats. + if ( ! formatsAtIndex ) { + return EMPTY_ACTIVE_FORMATS; + } + + let ii = _activeFormats.length; + + // Loop over the active formats and remove any that are not present at + // the current index. + while ( ii-- ) { + const format = _activeFormats[ ii ]; + + if ( + ! formatsAtIndex.find( ( _format ) => + isFormatEqual( format, _format ) + ) + ) { + _activeFormats.splice( ii, 1 ); + } + } + + // If there are no active formats, we can stop. + if ( _activeFormats.length === 0 ) { + return EMPTY_ACTIVE_FORMATS; + } + } + + return _activeFormats || EMPTY_ACTIVE_FORMATS; } diff --git a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap index 756d06885ea14d..a0eb7b3cb0a3ce 100644 --- a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap +++ b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap @@ -61,9 +61,7 @@ exports[`recordToDom should create a value with image object and formatting 1`] exports[`recordToDom should create a value with image object and text after 1`] = ` - + { expect( getActiveFormat( record, 'em' ) ).toBe( undefined ); } ); - it( 'should return format at first character for uncollapsed selection', () => { + it( 'should return format when active over whole selection', () => { const record = { formats: [ [ em ], [ strong ], , ], text: 'one', start: 0, - end: 2, + end: 1, }; expect( getActiveFormat( record, 'em' ) ).toBe( em ); } ); + it( 'should return not return format when not active over whole selection', () => { + const record = { + formats: [ [ em ], [ strong ], , ], + text: 'one', + start: 0, + end: 2, + }; + + expect( getActiveFormat( record, 'em' ) ).toBe( undefined ); + } ); + it( 'should return undefined if at the boundary before', () => { const record = { formats: [ [ em ], , [ em ] ], diff --git a/packages/rich-text/src/test/toggle-format.js b/packages/rich-text/src/test/toggle-format.js index 6a71412c413fb2..95f03947fa667d 100644 --- a/packages/rich-text/src/test/toggle-format.js +++ b/packages/rich-text/src/test/toggle-format.js @@ -14,15 +14,18 @@ describe( 'toggleFormat', () => { const strong = { type: 'strong' }; const em = { type: 'em' }; - it( 'should remove format if it exists at start of selection', () => { + it( 'should remove format if it is active', () => { const record = { formats: [ , , , - [ strong ], + // In reality, formats at a different index are never the same + // value. Only formats that create the same tag are the same + // value. + [ { type: 'strong' } ], + [ em, strong ], [ em, strong ], - [ em ], [ em ], , ,