diff --git a/packages/rich-text/src/apply-format.js b/packages/rich-text/src/apply-format.js index 14402735b1e60..c2b96af10d600 100644 --- a/packages/rich-text/src/apply-format.js +++ b/packages/rich-text/src/apply-format.js @@ -10,6 +10,12 @@ import { find } from 'lodash'; import { normaliseFormats } from './normalise-formats'; +function replace( array, index, value ) { + array = array.slice(); + array[ index ] = value; + return array; +} + /** * Apply a format object to a Rich Text value from the given `startIndex` to the * given `endIndex`. Indices are retrieved from the selection if none are @@ -38,15 +44,19 @@ export function applyFormat( // If the caret is at a format of the same type, expand start and end to // the edges of the format. This is useful to apply new attributes. if ( startFormat ) { - while ( find( newFormats[ startIndex ], startFormat ) ) { - applyFormats( newFormats, startIndex, format ); + const index = newFormats[ startIndex ].indexOf( startFormat ); + + while ( newFormats[ startIndex ] && newFormats[ startIndex ][ index ] === startFormat ) { + newFormats[ startIndex ] = + replace( newFormats[ startIndex ], index, format ); startIndex--; } endIndex++; - while ( find( newFormats[ endIndex ], startFormat ) ) { - applyFormats( newFormats, endIndex, format ); + while ( newFormats[ endIndex ] && newFormats[ endIndex ][ index ] === startFormat ) { + newFormats[ endIndex ] = + replace( newFormats[ endIndex ], index, format ); endIndex++; } // Otherwise, insert a placeholder with the format so new input appears @@ -58,20 +68,29 @@ export function applyFormat( }; } } else { + // Determine the highest position the new format can be inserted at. + let position = +Infinity; + + for ( let index = startIndex; index < endIndex; index++ ) { + if ( newFormats[ index ] ) { + newFormats[ index ] = newFormats[ index ] + .filter( ( { type } ) => type !== format.type ); + + const length = newFormats[ index ].length; + + if ( length < position ) { + position = length; + } + } else { + newFormats[ index ] = []; + position = 0; + } + } + for ( let index = startIndex; index < endIndex; index++ ) { - applyFormats( newFormats, index, format ); + newFormats[ index ].splice( position, 0, format ); } } return normaliseFormats( { ...value, formats: newFormats } ); } - -function applyFormats( formats, index, format ) { - if ( formats[ index ] ) { - const newFormatsAtIndex = formats[ index ].filter( ( { type } ) => type !== format.type ); - newFormatsAtIndex.push( format ); - formats[ index ] = newFormatsAtIndex; - } else { - formats[ index ] = [ format ]; - } -} diff --git a/packages/rich-text/src/test/apply-format.js b/packages/rich-text/src/test/apply-format.js index b75dc7ee5de00..67d2c3ff08297 100644 --- a/packages/rich-text/src/test/apply-format.js +++ b/packages/rich-text/src/test/apply-format.js @@ -17,12 +17,124 @@ describe( 'applyFormat', () => { const a2 = { type: 'a', attributes: { href: '#test' } }; it( 'should apply format', () => { + const record = { + formats: [ , , , , ], + text: 'test', + }; + const expected = { + ...record, + formats: [ [ em ], [ em ], [ em ], [ em ] ], + }; + const result = applyFormat( deepFreeze( record ), em, 0, 4 ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 4 ); + } ); + + it( 'should apply format on top of existing format', () => { + const record = { + formats: [ [ strong ], [ strong ], [ strong ], [ strong ] ], + text: 'test', + }; + const expected = { + ...record, + formats: [ [ strong, em ], [ strong, em ], [ strong, em ], [ strong, em ] ], + }; + const result = applyFormat( deepFreeze( record ), em, 0, 4 ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 4 ); + } ); + + it( 'should apply format and remove same format type', () => { + const record = { + formats: [ [ strong ], [ em, strong ], [ em, strong ], [ strong ] ], + text: 'test', + }; + const expected = { + ...record, + formats: [ [ strong, em ], [ strong, em ], [ strong, em ], [ strong, em ] ], + }; + const result = applyFormat( deepFreeze( record ), em, 0, 4 ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 4 ); + } ); + + it( 'should apply format around existing format', () => { + const record = { + formats: [ , [ em ], [ em ], , ], + text: 'test', + }; + const expected = { + ...record, + formats: [ [ strong ], [ strong, em ], [ strong, em ], [ strong ] ], + }; + const result = applyFormat( deepFreeze( record ), strong, 0, 4 ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 4 ); + } ); + + it( 'should apply format around existing format with edge right', () => { + const record = { + formats: [ , [ em ], [ em ], , ], + text: 'test', + }; + const expected = { + ...record, + formats: [ [ strong ], [ strong, em ], [ strong, em ], , ], + }; + const result = applyFormat( deepFreeze( record ), strong, 0, 3 ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 3 ); + } ); + + it( 'should apply format around existing format with edge left', () => { + const record = { + formats: [ , [ em ], [ em ], , ], + text: 'test', + }; + const expected = { + ...record, + formats: [ , [ strong, em ], [ strong, em ], [ strong ] ], + }; + const result = applyFormat( deepFreeze( record ), strong, 1, 4 ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 3 ); + } ); + + it( 'should apply format around existing format with break', () => { + const record = { + formats: [ , [ em ], , [ em ] ], + text: 'test', + }; + const expected = { + ...record, + formats: [ , [ strong, em ], [ strong ], [ strong, em ] ], + }; + const result = applyFormat( deepFreeze( record ), strong, 1, 4 ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 3 ); + } ); + + it( 'should apply format crossing existing format', () => { const record = { formats: [ , , , , [ em ], [ em ], [ em ], , , , , , , ], text: 'one two three', }; const expected = { - formats: [ , , , [ strong ], [ em, strong ], [ em, strong ], [ em ], , , , , , , ], + formats: [ , , , [ strong ], [ strong, em ], [ strong, em ], [ em ], , , , , , , ], text: 'one two three', }; const result = applyFormat( deepFreeze( record ), strong, 3, 6 ); @@ -40,7 +152,7 @@ describe( 'applyFormat', () => { end: 6, }; const expected = { - formats: [ , , , [ strong ], [ em, strong ], [ em, strong ], [ em ], , , , , , , ], + formats: [ , , , [ strong ], [ strong, em ], [ strong, em ], [ em ], , , , , , , ], text: 'one two three', start: 3, end: 6, diff --git a/packages/rich-text/src/test/toggle-format.js b/packages/rich-text/src/test/toggle-format.js index 4a890a43bfae5..28762a37b1603 100644 --- a/packages/rich-text/src/test/toggle-format.js +++ b/packages/rich-text/src/test/toggle-format.js @@ -42,7 +42,7 @@ describe( 'toggleFormat', () => { end: 6, }; const expected = { - formats: [ , , , [ strong ], [ em, strong ], [ em, strong ], [ em ], , , , , , , ], + formats: [ , , , [ strong ], [ strong, em ], [ strong, em ], [ em ], , , , , , , ], text: 'one two three', start: 3, end: 6,