diff --git a/packages/rich-text/src/get-last-child-index.js b/packages/rich-text/src/get-last-child-index.js new file mode 100644 index 00000000000000..976051b10c0a3e --- /dev/null +++ b/packages/rich-text/src/get-last-child-index.js @@ -0,0 +1,40 @@ +/** + * Internal dependencies + */ + +import { LINE_SEPARATOR } from './special-characters'; + +/** + * Gets the line index of the last child in the list. + * + * @param {Object} value Value to search. + * @param {number} lineIndex Line index of a list item in the list. + * + * @return {Array} The index of the last child. + */ +export function getLastChildIndex( { text, formats }, lineIndex ) { + const lineFormats = formats[ lineIndex ] || []; + // Use the given line index in case there are no next children. + let childIndex = lineIndex; + + // `lineIndex` could be `undefined` if it's the first line. + for ( let index = lineIndex || 0; index < text.length; index++ ) { + // We're only interested in line indices. + if ( text[ index ] !== LINE_SEPARATOR ) { + continue; + } + + const formatsAtIndex = formats[ index ] || []; + + // If the amout of formats is equal or more, store it, then return the + // last one if the amount of formats is less. + if ( formatsAtIndex.length >= lineFormats.length ) { + childIndex = index; + } else { + return childIndex; + } + } + + // If the end of the text is reached, return the last child index. + return childIndex; +} diff --git a/packages/rich-text/src/outdent-list-items.js b/packages/rich-text/src/outdent-list-items.js index 78036aae165e1b..b26587f3a0bd13 100644 --- a/packages/rich-text/src/outdent-list-items.js +++ b/packages/rich-text/src/outdent-list-items.js @@ -6,6 +6,7 @@ import { LINE_SEPARATOR } from './special-characters'; import { normaliseFormats } from './normalise-formats'; import { getLineIndex } from './get-line-index'; import { getParentLineIndex } from './get-parent-line-index'; +import { getLastChildIndex } from './get-last-child-index'; /** * Outdents any selected list items if possible. @@ -16,17 +17,23 @@ import { getParentLineIndex } from './get-parent-line-index'; */ export function outdentListItems( value ) { const { text, formats, start, end } = value; - const lineIndex = getLineIndex( value ); - const lineFormats = formats[ lineIndex ]; + const startingLineIndex = getLineIndex( value, start ); - if ( lineFormats === undefined ) { + // Return early if the starting line index cannot be further outdented. + if ( formats[ startingLineIndex ] === undefined ) { return value; } const newFormats = formats.slice( 0 ); - const parentFormats = formats[ getParentLineIndex( value, lineIndex ) ] || []; - - for ( let index = lineIndex; index < end; index++ ) { + const parentFormats = formats[ getParentLineIndex( value, startingLineIndex ) ] || []; + const endingLineIndex = getLineIndex( value, end ); + const lastChildIndex = getLastChildIndex( value, endingLineIndex ); + + // Outdent all list items from the starting line index until the last child + // index of the ending list. All children of the ending list need to be + // outdented, otherwise they'll be orphaned. + for ( let index = startingLineIndex; index <= lastChildIndex; index++ ) { + // Skip indices that are not line separators. if ( text[ index ] !== LINE_SEPARATOR ) { continue; } @@ -37,7 +44,7 @@ export function outdentListItems( value ) { ); if ( newFormats[ index ].length === 0 ) { - delete newFormats[ lineIndex ]; + delete newFormats[ index ]; } } diff --git a/packages/rich-text/src/test/get-last-child-index.js b/packages/rich-text/src/test/get-last-child-index.js new file mode 100644 index 00000000000000..55c881d356555c --- /dev/null +++ b/packages/rich-text/src/test/get-last-child-index.js @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ + +import { getLastChildIndex } from '../get-last-child-index'; +import { LINE_SEPARATOR } from '../special-characters'; + +describe( 'outdentListItems', () => { + const ul = { type: 'ul' }; + + it( 'should return undefined if there is only one line', () => { + expect( getLastChildIndex( deepFreeze( { + formats: [ , ], + text: '1', + } ), undefined ) ).toBe( undefined ); + } ); + + it( 'should return the last line if no line is indented', () => { + expect( getLastChildIndex( deepFreeze( { + formats: [ , ], + text: `1${ LINE_SEPARATOR }`, + } ), undefined ) ).toBe( 1 ); + } ); + + it( 'should return the last child index', () => { + expect( getLastChildIndex( deepFreeze( { + formats: [ , [ ul ], , [ ul ], , ], + text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, + } ), undefined ) ).toBe( 3 ); + } ); + + it( 'should return the last child index by sibling', () => { + expect( getLastChildIndex( deepFreeze( { + formats: [ , [ ul ], , [ ul ], , ], + text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, + } ), 1 ) ).toBe( 3 ); + } ); + + it( 'should return the last child index (with further lower indented items)', () => { + expect( getLastChildIndex( deepFreeze( { + formats: [ , [ ul ], , , , ], + text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`, + } ), 1 ) ).toBe( 1 ); + } ); +} ); diff --git a/packages/rich-text/src/test/outdent-list-items.js b/packages/rich-text/src/test/outdent-list-items.js index d321a4c02ffe83..afe1c09d03a314 100644 --- a/packages/rich-text/src/test/outdent-list-items.js +++ b/packages/rich-text/src/test/outdent-list-items.js @@ -21,7 +21,7 @@ describe( 'outdentListItems', () => { start: 1, end: 1, }; - const result = outdentListItems( deepFreeze( record ), ul ); + const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( record ); expect( result ).toBe( record ); @@ -43,7 +43,7 @@ describe( 'outdentListItems', () => { start: 2, end: 2, }; - const result = outdentListItems( deepFreeze( record ), ul ); + const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); @@ -65,7 +65,7 @@ describe( 'outdentListItems', () => { start: 5, end: 5, }; - const result = outdentListItems( deepFreeze( record ), ul ); + const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); @@ -87,10 +87,32 @@ describe( 'outdentListItems', () => { start: 2, end: 5, }; - const result = outdentListItems( deepFreeze( record ), ul ); + const result = outdentListItems( deepFreeze( record ) ); expect( result ).toEqual( expected ); expect( result ).not.toBe( record ); expect( getSparseArrayLength( result.formats ) ).toBe( 1 ); } ); + + it( 'should outdent list item with children', () => { + // 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 ], , [ ul, ul ], , ], + text, + start: 2, + end: 2, + }; + const expected = { + formats: [ , , , [ ul ], , [ ul ], , ], + text, + start: 2, + end: 2, + }; + const result = outdentListItems( deepFreeze( record ) ); + + expect( result ).toEqual( expected ); + expect( result ).not.toBe( record ); + expect( getSparseArrayLength( result.formats ) ).toBe( 2 ); + } ); } ); diff --git a/test/e2e/specs/blocks/__snapshots__/list.test.js.snap b/test/e2e/specs/blocks/__snapshots__/list.test.js.snap index a4cb6cdb75be57..71118e8d65118c 100644 --- a/test/e2e/specs/blocks/__snapshots__/list.test.js.snap +++ b/test/e2e/specs/blocks/__snapshots__/list.test.js.snap @@ -144,6 +144,18 @@ exports[`List should indent and outdent level 2 3`] = ` " `; +exports[`List should outdent with children 1`] = ` +" + +" +`; + +exports[`List should outdent with children 2`] = ` +" + +" +`; + exports[`List should split indented list item 1`] = ` " diff --git a/test/e2e/specs/blocks/list.test.js b/test/e2e/specs/blocks/list.test.js index 23fb5ff010c4bb..95bc283d32a65f 100644 --- a/test/e2e/specs/blocks/list.test.js +++ b/test/e2e/specs/blocks/list.test.js @@ -262,4 +262,22 @@ describe( 'List', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'should outdent with children', async () => { + await insertBlock( 'List' ); + await page.keyboard.type( 'a' ); + await page.keyboard.press( 'Enter' ); + await pressKeyWithModifier( 'primary', 'm' ); + await page.keyboard.type( 'b' ); + await page.keyboard.press( 'Enter' ); + await pressKeyWithModifier( 'primary', 'm' ); + await page.keyboard.type( 'c' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'ArrowUp' ); + await pressKeyWithModifier( 'primaryShift', 'm' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } );