diff --git a/packages/rich-text/src/indent-list-items.js b/packages/rich-text/src/indent-list-items.js index 39226de4722dc4..516b3c85f96ea2 100644 --- a/packages/rich-text/src/indent-list-items.js +++ b/packages/rich-text/src/indent-list-items.js @@ -6,6 +6,36 @@ import { LINE_SEPARATOR } from './special-characters'; import { normaliseFormats } from './normalise-formats'; import { getLineIndex } from './get-line-index'; +/** + * Gets the line index of the first previous list item with higher indentation. + * + * @param {Object} value Value to search. + * @param {number} lineIndex Line index of the list item to compare with. + * + * @return {boolean} The line index. + */ +function getTargetLevelLineIndex( { text, formats }, lineIndex ) { + const startFormats = formats[ lineIndex ] || []; + + let index = lineIndex; + + while ( index-- >= 0 ) { + if ( text[ index ] !== LINE_SEPARATOR ) { + continue; + } + + const formatsAtIndex = formats[ index ] || []; + + // Return the first line index that is one level higher. If the level is + // lower or equal, there is no result. + if ( formatsAtIndex.length === startFormats.length + 1 ) { + return index; + } else if ( formatsAtIndex.length <= startFormats.length ) { + return; + } + } +} + /** * Indents any selected list items if possible. * @@ -23,62 +53,39 @@ export function indentListItems( value, rootFormat ) { } const { text, formats, start, end } = value; + const previousLineIndex = getLineIndex( value, lineIndex ); const formatsAtLineIndex = formats[ lineIndex ] || []; - const targetFormats = formats[ getLineIndex( value, lineIndex ) ] || []; + const formatsAtPreviousLineIndex = formats[ previousLineIndex ] || []; // The the indentation of the current line is greater than previous line, // then the line cannot be furter indented. - if ( formatsAtLineIndex.length > targetFormats.length ) { + if ( formatsAtLineIndex.length > formatsAtPreviousLineIndex.length ) { return value; } const newFormats = formats.slice(); + const targetLevelLineIndex = getTargetLevelLineIndex( value, lineIndex ); for ( let index = lineIndex; index < end; index++ ) { if ( text[ index ] !== LINE_SEPARATOR ) { continue; } - // If the indentation of the previous line is the same as the current - // line, then duplicate the type and append all current types. E.g. - // - // 1. one - // 2. two <= Selected - // * three <= Selected - // - // should become: - // - // 1. one - // 1. two <= Selected - // * three <= Selected - // - // ^ Inserted list - // - // Otherwise take the target formats and append traling lists. E.g. - // - // 1. one - // * target - // 2. two <= Selected - // * three <= Selected - // - // should become: - // - // 1. one - // * target - // * two <= Selected - // * three <= Selected - // - if ( targetFormats.length === formatsAtLineIndex.length ) { + // Get the previous list, and if there's a child list, take over the + // formats. If not, duplicate the last level and create a new level. + if ( targetLevelLineIndex ) { + const targetFormats = formats[ targetLevelLineIndex ] || []; + newFormats[ index ] = targetFormats.concat( + ( newFormats[ index ] || [] ).slice( targetFormats.length - 1 ) + ); + } else { + const targetFormats = formats[ previousLineIndex ] || []; const lastformat = targetFormats[ targetFormats.length - 1 ] || rootFormat; newFormats[ index ] = targetFormats.concat( [ lastformat ], ( newFormats[ index ] || [] ).slice( targetFormats.length ) ); - } else { - newFormats[ index ] = targetFormats.concat( - ( newFormats[ index ] || [] ).slice( targetFormats.length - 1 ) - ); } } diff --git a/packages/rich-text/src/outdent-list-items.js b/packages/rich-text/src/outdent-list-items.js index b26587f3a0bd13..3a493caa9b03a8 100644 --- a/packages/rich-text/src/outdent-list-items.js +++ b/packages/rich-text/src/outdent-list-items.js @@ -38,9 +38,12 @@ export function outdentListItems( value ) { continue; } + // In the case of level 0, the formats at the index are undefined. + const currentFormats = newFormats[ index ] || []; + // Omit the indentation level where the selection starts. newFormats[ index ] = parentFormats.concat( - newFormats[ index ].slice( parentFormats.length + 1 ) + currentFormats.slice( parentFormats.length + 1 ) ); if ( newFormats[ index ].length === 0 ) { diff --git a/packages/rich-text/src/test/indent-list-items.js b/packages/rich-text/src/test/indent-list-items.js index c55d5063d21a8b..e7f631e5fa8f94 100644 --- a/packages/rich-text/src/test/indent-list-items.js +++ b/packages/rich-text/src/test/indent-list-items.js @@ -130,4 +130,48 @@ describe( 'indentListItems', () => { expect( result ).not.toBe( record ); expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); } ); + + it( 'should indent one level at a time', () => { + // As we're testing list formats, the text should remain the same. + const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`; + const record = { + formats: [ , [ ul ], , [ ul, ul ], , , , ], + text, + start: 6, + end: 6, + }; + + const result1 = indentListItems( deepFreeze( record ), ul ); + + expect( result1 ).not.toBe( record ); + expect( getSparseArrayLength( result1.formats ) ).toBe( 3 ); + expect( result1 ).toEqual( { + formats: [ , [ ul ], , [ ul, ul ], , [ ul ], , ], + text, + start: 6, + end: 6, + } ); + + const result2 = indentListItems( deepFreeze( result1 ), ul ); + + expect( result2 ).not.toBe( result1 ); + expect( getSparseArrayLength( result2.formats ) ).toBe( 3 ); + expect( result2 ).toEqual( { + formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul ], , ], + text, + start: 6, + end: 6, + } ); + + const result3 = indentListItems( deepFreeze( result2 ), ul ); + + expect( result3 ).not.toBe( result2 ); + expect( getSparseArrayLength( result3.formats ) ).toBe( 3 ); + expect( result3 ).toEqual( { + formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul, ul ], , ], + text, + start: 6, + end: 6, + } ); + } ); } ); diff --git a/packages/rich-text/src/test/outdent-list-items.js b/packages/rich-text/src/test/outdent-list-items.js index c5b75d5d391886..c2bf8b30e4766b 100644 --- a/packages/rich-text/src/test/outdent-list-items.js +++ b/packages/rich-text/src/test/outdent-list-items.js @@ -137,4 +137,26 @@ describe( 'outdentListItems', () => { expect( result ).not.toBe( record ); expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); } ); + + it( 'should outdent when a selected item is at level 0', () => { + // As we're testing list formats, the text should remain the same. + const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`; + const record = { + formats: [ , [ ul ], , , , ], + text, + start: 2, + end: 5, + }; + const expected = { + formats: [ , , , , , ], + text, + start: 2, + end: 5, + }; + const result = outdentListItems( deepFreeze( record ) ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 0 ); + } ); } );