Skip to content

Commit b4c68a5

Browse files
ellatrixyouknowriad
authored andcommitted
RichText: List: fix indentation (#13563)
* fix list indentation * Add more tests * Guard against negative lineIndex * Fix outdent error
1 parent 2c01107 commit b4c68a5

File tree

4 files changed

+113
-37
lines changed

4 files changed

+113
-37
lines changed

packages/rich-text/src/indent-list-items.js

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,36 @@ import { LINE_SEPARATOR } from './special-characters';
66
import { normaliseFormats } from './normalise-formats';
77
import { getLineIndex } from './get-line-index';
88

9+
/**
10+
* Gets the line index of the first previous list item with higher indentation.
11+
*
12+
* @param {Object} value Value to search.
13+
* @param {number} lineIndex Line index of the list item to compare with.
14+
*
15+
* @return {boolean} The line index.
16+
*/
17+
function getTargetLevelLineIndex( { text, formats }, lineIndex ) {
18+
const startFormats = formats[ lineIndex ] || [];
19+
20+
let index = lineIndex;
21+
22+
while ( index-- >= 0 ) {
23+
if ( text[ index ] !== LINE_SEPARATOR ) {
24+
continue;
25+
}
26+
27+
const formatsAtIndex = formats[ index ] || [];
28+
29+
// Return the first line index that is one level higher. If the level is
30+
// lower or equal, there is no result.
31+
if ( formatsAtIndex.length === startFormats.length + 1 ) {
32+
return index;
33+
} else if ( formatsAtIndex.length <= startFormats.length ) {
34+
return;
35+
}
36+
}
37+
}
38+
939
/**
1040
* Indents any selected list items if possible.
1141
*
@@ -23,62 +53,39 @@ export function indentListItems( value, rootFormat ) {
2353
}
2454

2555
const { text, formats, start, end } = value;
56+
const previousLineIndex = getLineIndex( value, lineIndex );
2657
const formatsAtLineIndex = formats[ lineIndex ] || [];
27-
const targetFormats = formats[ getLineIndex( value, lineIndex ) ] || [];
58+
const formatsAtPreviousLineIndex = formats[ previousLineIndex ] || [];
2859

2960
// The the indentation of the current line is greater than previous line,
3061
// then the line cannot be furter indented.
31-
if ( formatsAtLineIndex.length > targetFormats.length ) {
62+
if ( formatsAtLineIndex.length > formatsAtPreviousLineIndex.length ) {
3263
return value;
3364
}
3465

3566
const newFormats = formats.slice();
67+
const targetLevelLineIndex = getTargetLevelLineIndex( value, lineIndex );
3668

3769
for ( let index = lineIndex; index < end; index++ ) {
3870
if ( text[ index ] !== LINE_SEPARATOR ) {
3971
continue;
4072
}
4173

42-
// If the indentation of the previous line is the same as the current
43-
// line, then duplicate the type and append all current types. E.g.
44-
//
45-
// 1. one
46-
// 2. two <= Selected
47-
// * three <= Selected
48-
//
49-
// should become:
50-
//
51-
// 1. one
52-
// 1. two <= Selected
53-
// * three <= Selected
54-
//
55-
// ^ Inserted list
56-
//
57-
// Otherwise take the target formats and append traling lists. E.g.
58-
//
59-
// 1. one
60-
// * target
61-
// 2. two <= Selected
62-
// * three <= Selected
63-
//
64-
// should become:
65-
//
66-
// 1. one
67-
// * target
68-
// * two <= Selected
69-
// * three <= Selected
70-
//
71-
if ( targetFormats.length === formatsAtLineIndex.length ) {
74+
// Get the previous list, and if there's a child list, take over the
75+
// formats. If not, duplicate the last level and create a new level.
76+
if ( targetLevelLineIndex ) {
77+
const targetFormats = formats[ targetLevelLineIndex ] || [];
78+
newFormats[ index ] = targetFormats.concat(
79+
( newFormats[ index ] || [] ).slice( targetFormats.length - 1 )
80+
);
81+
} else {
82+
const targetFormats = formats[ previousLineIndex ] || [];
7283
const lastformat = targetFormats[ targetFormats.length - 1 ] || rootFormat;
7384

7485
newFormats[ index ] = targetFormats.concat(
7586
[ lastformat ],
7687
( newFormats[ index ] || [] ).slice( targetFormats.length )
7788
);
78-
} else {
79-
newFormats[ index ] = targetFormats.concat(
80-
( newFormats[ index ] || [] ).slice( targetFormats.length - 1 )
81-
);
8289
}
8390
}
8491

packages/rich-text/src/outdent-list-items.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ export function outdentListItems( value ) {
3838
continue;
3939
}
4040

41+
// In the case of level 0, the formats at the index are undefined.
42+
const currentFormats = newFormats[ index ] || [];
43+
4144
// Omit the indentation level where the selection starts.
4245
newFormats[ index ] = parentFormats.concat(
43-
newFormats[ index ].slice( parentFormats.length + 1 )
46+
currentFormats.slice( parentFormats.length + 1 )
4447
);
4548

4649
if ( newFormats[ index ].length === 0 ) {

packages/rich-text/src/test/indent-list-items.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,48 @@ describe( 'indentListItems', () => {
130130
expect( result ).not.toBe( record );
131131
expect( getSparseArrayLength( result.formats ) ).toBe( 2 );
132132
} );
133+
134+
it( 'should indent one level at a time', () => {
135+
// As we're testing list formats, the text should remain the same.
136+
const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4`;
137+
const record = {
138+
formats: [ , [ ul ], , [ ul, ul ], , , , ],
139+
text,
140+
start: 6,
141+
end: 6,
142+
};
143+
144+
const result1 = indentListItems( deepFreeze( record ), ul );
145+
146+
expect( result1 ).not.toBe( record );
147+
expect( getSparseArrayLength( result1.formats ) ).toBe( 3 );
148+
expect( result1 ).toEqual( {
149+
formats: [ , [ ul ], , [ ul, ul ], , [ ul ], , ],
150+
text,
151+
start: 6,
152+
end: 6,
153+
} );
154+
155+
const result2 = indentListItems( deepFreeze( result1 ), ul );
156+
157+
expect( result2 ).not.toBe( result1 );
158+
expect( getSparseArrayLength( result2.formats ) ).toBe( 3 );
159+
expect( result2 ).toEqual( {
160+
formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul ], , ],
161+
text,
162+
start: 6,
163+
end: 6,
164+
} );
165+
166+
const result3 = indentListItems( deepFreeze( result2 ), ul );
167+
168+
expect( result3 ).not.toBe( result2 );
169+
expect( getSparseArrayLength( result3.formats ) ).toBe( 3 );
170+
expect( result3 ).toEqual( {
171+
formats: [ , [ ul ], , [ ul, ul ], , [ ul, ul, ul ], , ],
172+
text,
173+
start: 6,
174+
end: 6,
175+
} );
176+
} );
133177
} );

packages/rich-text/src/test/outdent-list-items.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,26 @@ describe( 'outdentListItems', () => {
137137
expect( result ).not.toBe( record );
138138
expect( getSparseArrayLength( result.formats ) ).toBe( 2 );
139139
} );
140+
141+
it( 'should outdent when a selected item is at level 0', () => {
142+
// As we're testing list formats, the text should remain the same.
143+
const text = `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }3`;
144+
const record = {
145+
formats: [ , [ ul ], , , , ],
146+
text,
147+
start: 2,
148+
end: 5,
149+
};
150+
const expected = {
151+
formats: [ , , , , , ],
152+
text,
153+
start: 2,
154+
end: 5,
155+
};
156+
const result = outdentListItems( deepFreeze( record ) );
157+
158+
expect( result ).toEqual( expected );
159+
expect( result ).not.toBe( record );
160+
expect( getSparseArrayLength( result.formats ) ).toBe( 0 );
161+
} );
140162
} );

0 commit comments

Comments
 (0)