From 32547ddcb3114962667ab3ec263aae3c939c2c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Durpe?= Date: Mon, 28 Jan 2019 17:24:19 +0100 Subject: [PATCH] RichText: List: Fix indent/outdent (#12667) * RichText: List: use value to indent/outdent * Add outdent * Support multi outdent * Hold one reference per format * Keep list formats when indenting to new index * Remove unneeded parameter * Rename * Add logic to change list type * Add tests * Add e2e tests * Add some basic docs. Clean up. * Remove duplicate wp-tinymce dependency * Clean up * Add more docs, fix getSelectedListNode * Put duplicate text values in constant --- lib/packages-dependencies.php | 1 - .../blocks/__snapshots__/list.test.js.snap | 42 ++++++ packages/e2e-tests/specs/blocks/list.test.js | 59 ++++++++ .../editor/src/components/rich-text/index.js | 17 +-- .../src/components/rich-text/list-edit.js | 122 +++++++++++----- .../src/components/rich-text/tinymce.js | 5 - packages/rich-text/src/change-list-type.js | 62 ++++++++ packages/rich-text/src/get-line-index.js | 26 ++++ .../rich-text/src/get-parent-line-index.js | 37 +++++ packages/rich-text/src/indent-list-items.js | 91 ++++++++++++ packages/rich-text/src/index.js | 3 + packages/rich-text/src/normalise-formats.js | 41 +++--- packages/rich-text/src/outdent-list-items.js | 50 +++++++ .../rich-text/src/test/change-list-type.js | 74 ++++++++++ .../rich-text/src/test/indent-list-items.js | 133 ++++++++++++++++++ .../rich-text/src/test/normalise-formats.js | 5 +- .../rich-text/src/test/outdent-list-items.js | 96 +++++++++++++ 17 files changed, 785 insertions(+), 79 deletions(-) create mode 100644 packages/rich-text/src/change-list-type.js create mode 100644 packages/rich-text/src/get-line-index.js create mode 100644 packages/rich-text/src/get-parent-line-index.js create mode 100644 packages/rich-text/src/indent-list-items.js create mode 100644 packages/rich-text/src/outdent-list-items.js create mode 100644 packages/rich-text/src/test/change-list-type.js create mode 100644 packages/rich-text/src/test/indent-list-items.js create mode 100644 packages/rich-text/src/test/outdent-list-items.js 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 ); + } ); +} );