diff --git a/lib/packages-dependencies.php b/lib/packages-dependencies.php
index ece2a6ae2bb867..60a125997f3bcf 100644
--- a/lib/packages-dependencies.php
+++ b/lib/packages-dependencies.php
@@ -133,7 +133,6 @@
),
'wp-editor' => array(
'lodash',
- 'wp-tinymce-lists',
'wp-a11y',
'wp-api-fetch',
'wp-blob',
diff --git a/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap b/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap
index 19a5186156809e..a4cb6cdb75be57 100644
--- a/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap
+++ b/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap
@@ -86,6 +86,18 @@ exports[`List should be immeadiately saved on indentation 1`] = `
"
`;
+exports[`List should change the base list type 1`] = `
+"
+
+"
+`;
+
+exports[`List should change the indented list type 1`] = `
+"
+
+"
+`;
+
exports[`List should create paragraph on split at end and merge back with content 1`] = `
"
@@ -102,6 +114,36 @@ exports[`List should create paragraph on split at end and merge back with conten
"
`;
+exports[`List should indent and outdent level 1 1`] = `
+"
+
+"
+`;
+
+exports[`List should indent and outdent level 1 2`] = `
+"
+
+"
+`;
+
+exports[`List should indent and outdent level 2 1`] = `
+"
+
+"
+`;
+
+exports[`List should indent and outdent level 2 2`] = `
+"
+
+"
+`;
+
+exports[`List should indent and outdent level 2 3`] = `
+"
+
+"
+`;
+
exports[`List should split indented list item 1`] = `
"
diff --git a/packages/e2e-tests/specs/blocks/list.test.js b/packages/e2e-tests/specs/blocks/list.test.js
index 1401698c43a110..022c0b88b13362 100644
--- a/packages/e2e-tests/specs/blocks/list.test.js
+++ b/packages/e2e-tests/specs/blocks/list.test.js
@@ -203,4 +203,63 @@ describe( 'List', () => {
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
+
+ it( 'should change the base list type', async () => {
+ await insertBlock( 'List' );
+ await page.click( 'button[aria-label="Convert to ordered list"]' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ it( 'should change the indented list type', async () => {
+ await insertBlock( 'List' );
+ await page.keyboard.type( 'a' );
+ await page.keyboard.press( 'Enter' );
+ await pressKeyWithModifier( 'primary', 'm' );
+ await page.keyboard.type( '1' );
+
+ // Pointer device is needed. Shift+Tab won't focus the toolbar.
+ // To do: fix so Shift+Tab works.
+ await page.mouse.move( 200, 300, { steps: 10 } );
+ await page.mouse.move( 250, 350, { steps: 10 } );
+
+ await page.click( 'button[aria-label="Convert to ordered list"]' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ it( 'should indent and outdent level 1', async () => {
+ await insertBlock( 'List' );
+ await page.keyboard.type( 'a' );
+ await page.keyboard.press( 'Enter' );
+ await pressKeyWithModifier( 'primary', 'm' );
+ await page.keyboard.type( '1' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+
+ await pressKeyWithModifier( 'primaryShift', 'm' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ it( 'should indent and outdent level 2', async () => {
+ await insertBlock( 'List' );
+ await page.keyboard.type( 'a' );
+ await page.keyboard.press( 'Enter' );
+ await pressKeyWithModifier( 'primary', 'm' );
+ await page.keyboard.type( '1' );
+ await page.keyboard.press( 'Enter' );
+ await pressKeyWithModifier( 'primary', 'm' );
+ await page.keyboard.type( 'i' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+
+ await pressKeyWithModifier( 'primaryShift', 'm' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+
+ await pressKeyWithModifier( 'primaryShift', 'm' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
} );
diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js
index 72d86b84415566..21ae5a91b5cac3 100644
--- a/packages/editor/src/components/rich-text/index.js
+++ b/packages/editor/src/components/rich-text/index.js
@@ -91,7 +91,6 @@ export class RichText extends Component {
this.onSplit = this.props.unstableOnSplit;
}
- this.onSetup = this.onSetup.bind( this );
this.onFocus = this.onFocus.bind( this );
this.onBlur = this.onBlur.bind( this );
this.onChange = this.onChange.bind( this );
@@ -137,15 +136,6 @@ export class RichText extends Component {
this.editableRef = node;
}
- /**
- * Sets a reference to the TinyMCE editor instance.
- *
- * @param {Editor} editor The editor instance as passed by TinyMCE.
- */
- onSetup( editor ) {
- this.editor = editor;
- }
-
setFocusedElement() {
if ( this.props.setFocusedElement ) {
this.props.setFocusedElement( this.props.instanceId );
@@ -837,12 +827,12 @@ export class RichText extends Component {
- { isSelected && this.editor && this.multilineTag === 'li' && (
+ { isSelected && this.multilineTag === 'li' && (
this.onChange( this.createRecord() ) }
+ value={ record }
+ onChange={ this.onChange }
/>
) }
{ isSelected && ! inlineToolbar && (
@@ -865,7 +855,6 @@ export class RichText extends Component {
(
+/**
+ * Whether or not the root list is selected.
+ *
+ * @return {boolean} True if the root list or nothing is selected, false if an
+ * inner list is selected.
+ */
+function isListRootSelected() {
+ const listNode = getSelectedListNode();
+
+ // Consider the root list selected if nothing is selected.
+ return ! listNode || listNode.contentEditable === 'true';
+}
+
+/**
+ * Wether or not the selected list has the given tag name.
+ *
+ * @param {string} tagName The tag name the list should have.
+ * @param {string} rootTagName The current root tag name, to compare with in
+ * case nothing is selected.
+ *
+ * @return {boolean} [description]
+ */
+function isActiveListType( tagName, rootTagName ) {
+ const listNode = getSelectedListNode();
+
+ if ( ! listNode ) {
+ return tagName === rootTagName;
+ }
+
+ return listNode.nodeName.toLowerCase() === tagName;
+}
+
+export const ListEdit = ( {
+ onTagNameChange,
+ tagName,
+ value,
+ onChange,
+} ) => (
{
- editor.execCommand( 'Outdent' );
- onSyncDOM();
+ onChange( outdentListItems( value ) );
} }
/>
{
- editor.execCommand( 'Indent' );
- onSyncDOM();
+ onChange( indentListItems( value, { type: tagName } ) );
} }
/>
{
- editor.execCommand( 'Indent' );
- onSyncDOM();
+ onChange( indentListItems( value, { type: tagName } ) );
} }
/>
{
- editor.execCommand( 'Outdent' );
- onSyncDOM();
+ onChange( outdentListItems( value ) );
} }
/>
@@ -75,43 +125,39 @@ export const ListEdit = ( { editor, onTagNameChange, tagName, onSyncDOM } ) => (
onTagNameChange && {
icon: 'editor-ul',
title: __( 'Convert to unordered list' ),
- isActive: isActiveListType( editor, 'ul', tagName ),
+ isActive: isActiveListType( 'ul', tagName ),
onClick() {
- if ( isListRootSelected( editor ) ) {
+ onChange( changeListType( value, { type: 'ul' } ) );
+
+ if ( isListRootSelected() ) {
onTagNameChange( 'ul' );
- } else {
- editor.execCommand( 'InsertUnorderedList' );
- onSyncDOM();
}
},
},
onTagNameChange && {
icon: 'editor-ol',
title: __( 'Convert to ordered list' ),
- isActive: isActiveListType( editor, 'ol', tagName ),
+ isActive: isActiveListType( 'ol', tagName ),
onClick() {
- if ( isListRootSelected( editor ) ) {
+ onChange( changeListType( value, { type: 'ol' } ) );
+
+ if ( isListRootSelected() ) {
onTagNameChange( 'ol' );
- } else {
- editor.execCommand( 'InsertOrderedList' );
- onSyncDOM();
}
},
},
{
icon: 'editor-outdent',
title: __( 'Outdent list item' ),
- onClick() {
- editor.execCommand( 'Outdent' );
- onSyncDOM();
+ onClick: () => {
+ onChange( outdentListItems( value ) );
},
},
{
icon: 'editor-indent',
title: __( 'Indent list item' ),
- onClick() {
- editor.execCommand( 'Indent' );
- onSyncDOM();
+ onClick: () => {
+ onChange( indentListItems( value, { type: tagName } ) );
},
},
].filter( Boolean ) }
diff --git a/packages/editor/src/components/rich-text/tinymce.js b/packages/editor/src/components/rich-text/tinymce.js
index a29117bdce24d8..529fa15c1a54ce 100644
--- a/packages/editor/src/components/rich-text/tinymce.js
+++ b/packages/editor/src/components/rich-text/tinymce.js
@@ -200,16 +200,11 @@ export default class TinyMCE extends Component {
lists_indent_on_tab: false,
};
- if ( multilineTag === 'li' ) {
- settings.plugins.push( 'lists' );
- }
-
tinymce.init( {
...settings,
target: this.editorNode,
setup: ( editor ) => {
this.editor = editor;
- this.props.onSetup( editor );
// TinyMCE resets the element content on initialization, even
// when it's already identical to what exists currently. This
diff --git a/packages/rich-text/src/change-list-type.js b/packages/rich-text/src/change-list-type.js
new file mode 100644
index 00000000000000..2b92dda0ab92ce
--- /dev/null
+++ b/packages/rich-text/src/change-list-type.js
@@ -0,0 +1,62 @@
+/**
+ * Internal dependencies
+ */
+
+import { LINE_SEPARATOR } from './special-characters';
+import { normaliseFormats } from './normalise-formats';
+import { getLineIndex } from './get-line-index';
+import { getParentLineIndex } from './get-parent-line-index';
+
+/**
+ * Changes the list type of the selected indented list, if any. Looks at the
+ * currently selected list item and takes the parent list, then changes the list
+ * type of this list. When multiple lines are selected, the parent lists are
+ * takes and changed.
+ *
+ * @param {Object} value Value to change.
+ * @param {Object} newFormat The new list format object. Choose between
+ * `{ type: 'ol' }` and `{ type: 'ul' }`.
+ *
+ * @return {Object} The changed value.
+ */
+export function changeListType( value, newFormat ) {
+ const { text, formats, start, end } = value;
+ const startLineFormats = formats[ getLineIndex( value, start ) ] || [];
+ const endLineFormats = formats[ getLineIndex( value, end ) ] || [];
+ const startIndex = getParentLineIndex( value, start );
+ const newFormats = formats.slice( 0 );
+ const startCount = startLineFormats.length - 1;
+ const endCount = endLineFormats.length - 1;
+
+ let changed;
+
+ for ( let index = startIndex + 1 || 0; index < text.length; index++ ) {
+ if ( text[ index ] !== LINE_SEPARATOR ) {
+ continue;
+ }
+
+ if ( ( newFormats[ index ] || [] ).length <= startCount ) {
+ break;
+ }
+
+ if ( ! newFormats[ index ] ) {
+ continue;
+ }
+
+ changed = true;
+ newFormats[ index ] = newFormats[ index ].map( ( format, i ) => {
+ return i < startCount || i > endCount ? format : newFormat;
+ } );
+ }
+
+ if ( ! changed ) {
+ return value;
+ }
+
+ return normaliseFormats( {
+ text,
+ formats: newFormats,
+ start,
+ end,
+ } );
+}
diff --git a/packages/rich-text/src/get-line-index.js b/packages/rich-text/src/get-line-index.js
new file mode 100644
index 00000000000000..ff44efb98fceba
--- /dev/null
+++ b/packages/rich-text/src/get-line-index.js
@@ -0,0 +1,26 @@
+/**
+ * Internal dependencies
+ */
+
+import { LINE_SEPARATOR } from './special-characters';
+
+/**
+ * Gets the currently selected line index, or the first line index if the
+ * selection spans over multiple items.
+ *
+ * @param {Object} value Value to get the line index from.
+ * @param {boolean} startIndex Optional index that should be contained by the
+ * line. Defaults to the selection start of the
+ * value.
+ *
+ * @return {?boolean} The line index. Undefined if not found.
+ */
+export function getLineIndex( { start, text }, startIndex = start ) {
+ let index = startIndex;
+
+ while ( index-- ) {
+ if ( text[ index ] === LINE_SEPARATOR ) {
+ return index;
+ }
+ }
+}
diff --git a/packages/rich-text/src/get-parent-line-index.js b/packages/rich-text/src/get-parent-line-index.js
new file mode 100644
index 00000000000000..332764dc45bd30
--- /dev/null
+++ b/packages/rich-text/src/get-parent-line-index.js
@@ -0,0 +1,37 @@
+/**
+ * Internal dependencies
+ */
+
+import { LINE_SEPARATOR } from './special-characters';
+
+/**
+ * Gets the index of the first parent list. To get the parent list formats, we
+ * go through every list item until we find one with exactly one format type
+ * less.
+ *
+ * @param {Object} value Value to search.
+ * @param {number} startIndex Index to start search at.
+ *
+ * @return {Array} The parent list line index.
+ */
+export function getParentLineIndex( { text, formats }, startIndex ) {
+ let index = startIndex;
+ let startFormats;
+
+ while ( index-- ) {
+ if ( text[ index ] !== LINE_SEPARATOR ) {
+ continue;
+ }
+
+ const formatsAtIndex = formats[ index ] || [];
+
+ if ( ! startFormats ) {
+ startFormats = formatsAtIndex;
+ continue;
+ }
+
+ if ( formatsAtIndex.length === startFormats.length - 1 ) {
+ return index;
+ }
+ }
+}
diff --git a/packages/rich-text/src/indent-list-items.js b/packages/rich-text/src/indent-list-items.js
new file mode 100644
index 00000000000000..39226de4722dc4
--- /dev/null
+++ b/packages/rich-text/src/indent-list-items.js
@@ -0,0 +1,91 @@
+/**
+ * Internal dependencies
+ */
+
+import { LINE_SEPARATOR } from './special-characters';
+import { normaliseFormats } from './normalise-formats';
+import { getLineIndex } from './get-line-index';
+
+/**
+ * Indents any selected list items if possible.
+ *
+ * @param {Object} value Value to change.
+ * @param {Object} rootFormat
+ *
+ * @return {Object} The changed value.
+ */
+export function indentListItems( value, rootFormat ) {
+ const lineIndex = getLineIndex( value );
+
+ // There is only one line, so the line cannot be indented.
+ if ( lineIndex === undefined ) {
+ return value;
+ }
+
+ const { text, formats, start, end } = value;
+ const formatsAtLineIndex = formats[ lineIndex ] || [];
+ const targetFormats = formats[ getLineIndex( value, lineIndex ) ] || [];
+
+ // The the indentation of the current line is greater than previous line,
+ // then the line cannot be furter indented.
+ if ( formatsAtLineIndex.length > targetFormats.length ) {
+ return value;
+ }
+
+ const newFormats = formats.slice();
+
+ 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 ) {
+ 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 )
+ );
+ }
+ }
+
+ return normaliseFormats( {
+ text,
+ formats: newFormats,
+ start,
+ end,
+ } );
+}
diff --git a/packages/rich-text/src/index.js b/packages/rich-text/src/index.js
index 61e6475dc778a6..86f7cd1b29ae1d 100644
--- a/packages/rich-text/src/index.js
+++ b/packages/rich-text/src/index.js
@@ -25,3 +25,6 @@ export { toHTMLString } from './to-html-string';
export { toggleFormat } from './toggle-format';
export { LINE_SEPARATOR } from './special-characters';
export { unregisterFormatType } from './unregister-format-type';
+export { indentListItems } from './indent-list-items';
+export { outdentListItems } from './outdent-list-items';
+export { changeListType } from './change-list-type';
diff --git a/packages/rich-text/src/normalise-formats.js b/packages/rich-text/src/normalise-formats.js
index 349540df9dc256..533df66933886a 100644
--- a/packages/rich-text/src/normalise-formats.js
+++ b/packages/rich-text/src/normalise-formats.js
@@ -1,3 +1,9 @@
+/**
+ * External dependencies
+ */
+
+import { find } from 'lodash';
+
/**
* Internal dependencies
*/
@@ -12,25 +18,22 @@ import { isFormatEqual } from './is-format-equal';
* @return {Object} New value with normalised formats.
*/
export function normaliseFormats( { formats, text, start, end } ) {
- const newFormats = formats.slice( 0 );
-
- newFormats.forEach( ( formatsAtIndex, index ) => {
- const lastFormatsAtIndex = newFormats[ index - 1 ];
-
- if ( lastFormatsAtIndex ) {
- const newFormatsAtIndex = formatsAtIndex.slice( 0 );
-
- newFormatsAtIndex.forEach( ( format, formatIndex ) => {
- const lastFormat = lastFormatsAtIndex[ formatIndex ];
-
- if ( isFormatEqual( format, lastFormat ) ) {
- newFormatsAtIndex[ formatIndex ] = lastFormat;
- }
- } );
-
- newFormats[ index ] = newFormatsAtIndex;
- }
- } );
+ const refs = [];
+ const newFormats = formats.map( ( formatsAtIndex ) =>
+ formatsAtIndex.map( ( format ) => {
+ const equalRef = find( refs, ( ref ) =>
+ isFormatEqual( ref, format )
+ );
+
+ if ( equalRef ) {
+ return equalRef;
+ }
+
+ refs.push( format );
+
+ return format;
+ } )
+ );
return { formats: newFormats, text, start, end };
}
diff --git a/packages/rich-text/src/outdent-list-items.js b/packages/rich-text/src/outdent-list-items.js
new file mode 100644
index 00000000000000..78036aae165e1b
--- /dev/null
+++ b/packages/rich-text/src/outdent-list-items.js
@@ -0,0 +1,50 @@
+/**
+ * Internal dependencies
+ */
+
+import { LINE_SEPARATOR } from './special-characters';
+import { normaliseFormats } from './normalise-formats';
+import { getLineIndex } from './get-line-index';
+import { getParentLineIndex } from './get-parent-line-index';
+
+/**
+ * Outdents any selected list items if possible.
+ *
+ * @param {Object} value Value to change.
+ *
+ * @return {Object} The changed value.
+ */
+export function outdentListItems( value ) {
+ const { text, formats, start, end } = value;
+ const lineIndex = getLineIndex( value );
+ const lineFormats = formats[ lineIndex ];
+
+ if ( lineFormats === undefined ) {
+ return value;
+ }
+
+ const newFormats = formats.slice( 0 );
+ const parentFormats = formats[ getParentLineIndex( value, lineIndex ) ] || [];
+
+ for ( let index = lineIndex; index < end; index++ ) {
+ if ( text[ index ] !== LINE_SEPARATOR ) {
+ continue;
+ }
+
+ // Omit the indentation level where the selection starts.
+ newFormats[ index ] = parentFormats.concat(
+ newFormats[ index ].slice( parentFormats.length + 1 )
+ );
+
+ if ( newFormats[ index ].length === 0 ) {
+ delete newFormats[ lineIndex ];
+ }
+ }
+
+ return normaliseFormats( {
+ text,
+ formats: newFormats,
+ start,
+ end,
+ } );
+}
diff --git a/packages/rich-text/src/test/change-list-type.js b/packages/rich-text/src/test/change-list-type.js
new file mode 100644
index 00000000000000..3817c66ba7977e
--- /dev/null
+++ b/packages/rich-text/src/test/change-list-type.js
@@ -0,0 +1,74 @@
+/**
+ * External dependencies
+ */
+import deepFreeze from 'deep-freeze';
+
+/**
+ * Internal dependencies
+ */
+
+import { changeListType } from '../change-list-type';
+import { getSparseArrayLength } from './helpers';
+import { LINE_SEPARATOR } from '../special-characters';
+
+describe( 'changeListType', () => {
+ const ul = { type: 'ul' };
+ const ol = { type: 'ol' };
+
+ it( 'should only change list type if list item is indented', () => {
+ const record = {
+ formats: [ , ],
+ text: '1',
+ start: 1,
+ end: 1,
+ };
+ const result = changeListType( deepFreeze( record ), ul );
+
+ expect( result ).toEqual( record );
+ expect( result ).toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 0 );
+ } );
+
+ it( 'should change list type', () => {
+ const record = {
+ formats: [ , [ ul ] ],
+ text: `1${ LINE_SEPARATOR }`,
+ start: 2,
+ end: 2,
+ };
+ const expected = {
+ formats: [ , [ ol ] ],
+ text: `1${ LINE_SEPARATOR }`,
+ start: 2,
+ end: 2,
+ };
+ const result = changeListType( deepFreeze( record ), ol );
+
+ expect( result ).toEqual( expected );
+ expect( result ).not.toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 1 );
+ } );
+
+ it( 'should outdent with multiple lines selected', () => {
+ // As we're testing list formats, the text should remain the same.
+ const text = `a${ LINE_SEPARATOR }1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }i${ LINE_SEPARATOR }3${ LINE_SEPARATOR }4${ LINE_SEPARATOR }b`;
+
+ const record = {
+ formats: [ , [ ul ], , [ ul ], , [ ul, ul ], , [ ul ], , [ ul ], , , , [ ul ], , ],
+ text,
+ start: 4,
+ end: 9,
+ };
+ const expected = {
+ formats: [ , [ ol ], , [ ol ], , [ ol, ul ], , [ ol ], , [ ol ], , , , [ ul ], , ],
+ text,
+ start: 4,
+ end: 9,
+ };
+ const result = changeListType( deepFreeze( record ), ol );
+
+ expect( result ).toEqual( expected );
+ expect( result ).not.toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 6 );
+ } );
+} );
diff --git a/packages/rich-text/src/test/indent-list-items.js b/packages/rich-text/src/test/indent-list-items.js
new file mode 100644
index 00000000000000..c55d5063d21a8b
--- /dev/null
+++ b/packages/rich-text/src/test/indent-list-items.js
@@ -0,0 +1,133 @@
+/**
+ * External dependencies
+ */
+import deepFreeze from 'deep-freeze';
+
+/**
+ * Internal dependencies
+ */
+
+import { indentListItems } from '../indent-list-items';
+import { getSparseArrayLength } from './helpers';
+import { LINE_SEPARATOR } from '../special-characters';
+
+describe( 'indentListItems', () => {
+ const ul = { type: 'ul' };
+ const ol = { type: 'ol' };
+
+ it( 'should not indent only item', () => {
+ const record = {
+ formats: [ , ],
+ text: '1',
+ start: 1,
+ end: 1,
+ };
+ const result = indentListItems( deepFreeze( record ), ul );
+
+ expect( result ).toEqual( record );
+ expect( result ).toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 0 );
+ } );
+
+ it( 'should indent', () => {
+ // As we're testing list formats, the text should remain the same.
+ const text = `1${ LINE_SEPARATOR }`;
+ const record = {
+ formats: [ , , ],
+ text,
+ start: 2,
+ end: 2,
+ };
+ const expected = {
+ formats: [ , [ ul ] ],
+ text,
+ start: 2,
+ end: 2,
+ };
+ const result = indentListItems( deepFreeze( record ), ul );
+
+ expect( result ).toEqual( expected );
+ expect( result ).not.toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 1 );
+ } );
+
+ it( 'should not indent without target list', () => {
+ const record = {
+ formats: [ , [ ul ] ],
+ text: `1${ LINE_SEPARATOR }`,
+ start: 2,
+ end: 2,
+ };
+ const result = indentListItems( deepFreeze( record ), ul );
+
+ expect( result ).toEqual( record );
+ expect( result ).toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 1 );
+ } );
+
+ it( 'should indent and merge with previous list', () => {
+ // As we're testing list formats, the text should remain the same.
+ const text = `1${ LINE_SEPARATOR }${ LINE_SEPARATOR }`;
+ const record = {
+ formats: [ , [ ol ], , ],
+ text,
+ start: 3,
+ end: 3,
+ };
+ const expected = {
+ formats: [ , [ ol ], [ ol ] ],
+ text,
+ start: 3,
+ end: 3,
+ };
+ const result = indentListItems( deepFreeze( record ), ul );
+
+ expect( result ).toEqual( expected );
+ expect( result ).not.toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 2 );
+ } );
+
+ it( 'should indent already indented item', () => {
+ // 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 ], , [ ul ], , ],
+ text,
+ start: 5,
+ end: 5,
+ };
+ const expected = {
+ formats: [ , [ ul ], , [ ul, ul ], , ],
+ text,
+ start: 5,
+ end: 5,
+ };
+ const result = indentListItems( deepFreeze( record ), ul );
+
+ expect( result ).toEqual( expected );
+ expect( result ).not.toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 2 );
+ } );
+
+ it( 'should indent with multiple lines selected', () => {
+ // 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: [ , [ ul ], , [ ul, ul ], , ],
+ text,
+ start: 2,
+ end: 5,
+ };
+ const result = indentListItems( deepFreeze( record ), ul );
+
+ expect( result ).toEqual( expected );
+ expect( result ).not.toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 2 );
+ } );
+} );
diff --git a/packages/rich-text/src/test/normalise-formats.js b/packages/rich-text/src/test/normalise-formats.js
index 8984bcb17771b8..25525625f18b5d 100644
--- a/packages/rich-text/src/test/normalise-formats.js
+++ b/packages/rich-text/src/test/normalise-formats.js
@@ -16,16 +16,17 @@ describe( 'normaliseFormats', () => {
it( 'should normalise formats', () => {
const record = {
- formats: [ , [ em ], [ { ...em }, { ...strong } ], [ em, strong ] ],
+ formats: [ , [ em ], [ { ...em }, { ...strong } ], [ em, strong ], , [ { ...em } ] ],
text: 'one two three',
};
const result = normaliseFormats( deepFreeze( record ) );
expect( result ).toEqual( record );
expect( result ).not.toBe( record );
- expect( getSparseArrayLength( result.formats ) ).toBe( 3 );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 4 );
expect( result.formats[ 1 ][ 0 ] ).toBe( result.formats[ 2 ][ 0 ] );
expect( result.formats[ 1 ][ 0 ] ).toBe( result.formats[ 3 ][ 0 ] );
+ expect( result.formats[ 1 ][ 0 ] ).toBe( result.formats[ 5 ][ 0 ] );
expect( result.formats[ 2 ][ 1 ] ).toBe( result.formats[ 3 ][ 1 ] );
} );
} );
diff --git a/packages/rich-text/src/test/outdent-list-items.js b/packages/rich-text/src/test/outdent-list-items.js
new file mode 100644
index 00000000000000..d321a4c02ffe83
--- /dev/null
+++ b/packages/rich-text/src/test/outdent-list-items.js
@@ -0,0 +1,96 @@
+/**
+ * External dependencies
+ */
+import deepFreeze from 'deep-freeze';
+
+/**
+ * Internal dependencies
+ */
+
+import { outdentListItems } from '../outdent-list-items';
+import { getSparseArrayLength } from './helpers';
+import { LINE_SEPARATOR } from '../special-characters';
+
+describe( 'outdentListItems', () => {
+ const ul = { type: 'ul' };
+
+ it( 'should not outdent only item', () => {
+ const record = {
+ formats: [ , ],
+ text: '1',
+ start: 1,
+ end: 1,
+ };
+ const result = outdentListItems( deepFreeze( record ), ul );
+
+ expect( result ).toEqual( record );
+ expect( result ).toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 0 );
+ } );
+
+ it( 'should indent', () => {
+ // As we're testing list formats, the text should remain the same.
+ const text = `1${ LINE_SEPARATOR }`;
+ const record = {
+ formats: [ , [ ul ] ],
+ text,
+ start: 2,
+ end: 2,
+ };
+ const expected = {
+ formats: [ , , ],
+ text,
+ start: 2,
+ end: 2,
+ };
+ const result = outdentListItems( deepFreeze( record ), ul );
+
+ expect( result ).toEqual( expected );
+ expect( result ).not.toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 0 );
+ } );
+
+ it( 'should outdent two levels deep', () => {
+ // 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 ], , [ ul, ul ], , ],
+ text,
+ start: 5,
+ end: 5,
+ };
+ const expected = {
+ formats: [ , [ ul ], , [ ul ], , ],
+ text,
+ start: 5,
+ end: 5,
+ };
+ const result = outdentListItems( deepFreeze( record ), ul );
+
+ expect( result ).toEqual( expected );
+ expect( result ).not.toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 2 );
+ } );
+
+ it( 'should outdent with multiple lines selected', () => {
+ // 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 ], , [ ul, ul ], , ],
+ text,
+ start: 2,
+ end: 5,
+ };
+ const expected = {
+ formats: [ , , , [ ul ], , ],
+ text,
+ start: 2,
+ end: 5,
+ };
+ const result = outdentListItems( deepFreeze( record ), ul );
+
+ expect( result ).toEqual( expected );
+ expect( result ).not.toBe( record );
+ expect( getSparseArrayLength( result.formats ) ).toBe( 1 );
+ } );
+} );