diff --git a/packages/ckeditor5-list/src/converters.js b/packages/ckeditor5-list/src/converters.js index dab6e4e3204..7203cb9daba 100644 --- a/packages/ckeditor5-list/src/converters.js +++ b/packages/ckeditor5-list/src/converters.js @@ -605,6 +605,18 @@ export function modelChangePostFixer( model, writer ) { applied = true; } + if ( item.hasAttribute( 'listReversed' ) ) { + writer.removeAttribute( 'listReversed', item ); + + applied = true; + } + + if ( item.hasAttribute( 'listStart' ) ) { + writer.removeAttribute( 'listStart', item ); + + applied = true; + } + for ( const innerItem of Array.from( model.createRangeIn( item ) ).filter( e => e.item.is( 'element', 'listItem' ) ) ) { _addListToFix( innerItem.previousPosition ); } diff --git a/packages/ckeditor5-list/src/list.js b/packages/ckeditor5-list/src/list.js index b4a5e46e091..f4ffe2b82cd 100644 --- a/packages/ckeditor5-list/src/list.js +++ b/packages/ckeditor5-list/src/list.js @@ -35,3 +35,26 @@ export default class List extends Plugin { return 'List'; } } + +/** + * The configuration of the {@link module:list/list~List list} feature. + * + * ClassicEditor + * .create( editorElement, { + * list: ... // The list feature configuration. + * } ) + * .then( ... ) + * .catch( ... ); + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + * + * @interface ListConfig + */ + +/** + * The configuration of the {@link module:list/list~List} feature. + * + * Read more in {@link module:list/list~ListConfig}. + * + * @member {module:module:list/list~ListConfig} module:core/editor/editorconfig~EditorConfig#list + */ diff --git a/packages/ckeditor5-list/src/listreversedcommand.js b/packages/ckeditor5-list/src/listreversedcommand.js new file mode 100644 index 00000000000..1a39dfb594b --- /dev/null +++ b/packages/ckeditor5-list/src/listreversedcommand.js @@ -0,0 +1,63 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module list/listreversedcommand + */ + +import { Command } from 'ckeditor5/src/core'; +import { getSelectedListItems } from './utils'; + +/** + * The list reversed command. It changes `listReversed` attribute of the selected list items. + * It is used by the {@link module:list/liststyle~ListStyle list style feature}. + * + * @extends module:core/command~Command + */ +export default class ListReversedCommand extends Command { + /** + * @inheritDoc + */ + refresh() { + const value = this._getValue(); + this.value = value; + this.isEnabled = value != null; + } + + /** + * Executes the command. + * + * @param {Object} options + * @param {Boolean} [options.reversed=false] Whether the list should be reversed. + * @protected + */ + execute( options = {} ) { + const model = this.editor.model; + const listItems = getSelectedListItems( model ) + .filter( item => item.getAttribute( 'listType' ) == 'numbered' ); + + model.change( writer => { + for ( const item of listItems ) { + writer.setAttribute( 'listReversed', !!options.reversed, item ); + } + } ); + } + + /** + * Checks the command's {@link #value}. + * + * @private + * @returns {Boolean|null} The current value. + */ + _getValue() { + const listItem = this.editor.model.document.selection.getFirstPosition().parent; + + if ( listItem && listItem.is( 'element', 'listItem' ) && listItem.getAttribute( 'listType' ) == 'numbered' ) { + return listItem.getAttribute( 'listReversed' ); + } + + return null; + } +} diff --git a/packages/ckeditor5-list/src/liststartcommand.js b/packages/ckeditor5-list/src/liststartcommand.js new file mode 100644 index 00000000000..396662c6cac --- /dev/null +++ b/packages/ckeditor5-list/src/liststartcommand.js @@ -0,0 +1,63 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module list/liststartcommand + */ + +import { Command } from 'ckeditor5/src/core'; +import { getSelectedListItems } from './utils'; + +/** + * The list start index command. It changes `listStart` attribute of the selected list items. + * It is used by the {@link module:list/liststyle~ListStyle list style feature}. + * + * @extends module:core/command~Command + */ +export default class ListStartCommand extends Command { + /** + * @inheritDoc + */ + refresh() { + const value = this._getValue(); + this.value = value; + this.isEnabled = value != null; + } + + /** + * Executes the command. + * + * @param {Object} options + * @param {Number} [options.startIndex=1] Whether the list should be reversed. + * @protected + */ + execute( options = {} ) { + const model = this.editor.model; + const listItems = getSelectedListItems( model ) + .filter( item => item.getAttribute( 'listType' ) == 'numbered' ); + + model.change( writer => { + for ( const item of listItems ) { + writer.setAttribute( 'listStart', options.startIndex || 1, item ); + } + } ); + } + + /** + * Checks the command's {@link #value}. + * + * @private + * @returns {Boolean|null} The current value. + */ + _getValue() { + const listItem = this.editor.model.document.selection.getFirstPosition().parent; + + if ( listItem && listItem.is( 'element', 'listItem' ) && listItem.getAttribute( 'listType' ) == 'numbered' ) { + return listItem.getAttribute( 'listStart' ); + } + + return null; + } +} diff --git a/packages/ckeditor5-list/src/liststyle.js b/packages/ckeditor5-list/src/liststyle.js index 796332fe47c..c318f711069 100644 --- a/packages/ckeditor5-list/src/liststyle.js +++ b/packages/ckeditor5-list/src/liststyle.js @@ -34,3 +34,61 @@ export default class ListStyle extends Plugin { return 'ListStyle'; } } + +/** + * The configuration of the {@link module:list/liststyle~ListStyle list properties} feature. + * + * This configuration controls the individual list properties. For instance, it enables or disables specific editor commands + * operating on lists ({@link module:list/liststylecommand~ListStyleCommand `'listStyle'`}, + * {@link module:list/liststartcommand~ListStartCommand `'listStart'`}, + * {@link module:list/listreversedcommand~ListReversedCommand `'listReversed'`}), the look of the UI + * (`'numberedList'` and `'bulletedList'` dropdowns), and editor data pipeline (allowed HTML attributes). + * + * ClassicEditor + * .create( editorElement, { + * list: { + * properties: { + * styles: true, + * startIndex: true, + * reversed: true + * } + * } + * } ) + * .then( ... ) + * .catch( ... ); + * + * @interface ListPropertiesConfig + */ + +/** + * When set, the list style feature will be enabled. It allows changing the `list-style-type` HTML attribute of the lists. + * + * @default true + * @member {Boolean} module:list/liststyle~ListPropertiesConfig#styles + */ + +/** + * When set, the list start index feature will be enabled. It allows changing the `start` HTML attribute of the numbered lists. + * + * **Note**: This configuration doesn't affect bulleted and todo lists. + * + * @default false + * @member {Boolean} module:list/liststyle~ListPropertiesConfig#startIndex + */ + +/** + * When set, the list reversed feature will be enabled. It allows changing the `reversed` HTML attribute of the numbered lists. + * + * **Note**: This configuration doesn't affect bulleted and todo lists. + * + * @default false + * @member {Boolean} module:list/liststyle~ListPropertiesConfig#reversed + */ + +/** + * The configuration of the {@link module:list/liststyle~ListStyle} feature. + * + * Read more in {@link module:list/liststyle~ListPropertiesConfig}. + * + * @member {module:list/liststyle~ListPropertiesConfig} module:list/list~ListConfig#properties + */ diff --git a/packages/ckeditor5-list/src/liststylecommand.js b/packages/ckeditor5-list/src/liststylecommand.js index a2ce7c7e91b..39abbf43da6 100644 --- a/packages/ckeditor5-list/src/liststylecommand.js +++ b/packages/ckeditor5-list/src/liststylecommand.js @@ -8,10 +8,11 @@ */ import { Command } from 'ckeditor5/src/core'; -import { getSiblingNodes } from './utils'; +import { getSelectedListItems } from './utils'; /** - * The list style command. It is used by the {@link module:list/liststyle~ListStyle list style feature}. + * The list style command. It changes `listStyle` attribute of the selected list items. + * It is used by the {@link module:list/liststyle~ListStyle list style feature}. * * @extends module:core/command~Command */ @@ -53,25 +54,7 @@ export default class ListStyleCommand extends Command { */ execute( options = {} ) { const model = this.editor.model; - const document = model.document; - - // For all selected blocks find all list items that are being selected - // and update the `listStyle` attribute in those lists. - let listItems = [ ...document.selection.getSelectedBlocks() ] - .filter( element => element.is( 'element', 'listItem' ) ) - .map( element => { - const position = model.change( writer => writer.createPositionAt( element, 0 ) ); - - return [ - ...getSiblingNodes( position, 'backward' ), - ...getSiblingNodes( position, 'forward' ) - ]; - } ) - .flat(); - - // Since `getSelectedBlocks()` can return items that belong to the same list, and - // `getSiblingNodes()` returns the entire list, we need to remove duplicated items. - listItems = [ ...new Set( listItems ) ]; + const listItems = getSelectedListItems( model ); if ( !listItems.length ) { return; diff --git a/packages/ckeditor5-list/src/liststyleediting.js b/packages/ckeditor5-list/src/liststyleediting.js index 9c1557d1634..7f094de2e6c 100644 --- a/packages/ckeditor5-list/src/liststyleediting.js +++ b/packages/ckeditor5-list/src/liststyleediting.js @@ -10,6 +10,8 @@ import { Plugin } from 'ckeditor5/src/core'; import ListEditing from './listediting'; import ListStyleCommand from './liststylecommand'; +import ListReversedCommand from './listreversedcommand'; +import ListStartCommand from './liststartcommand'; import { getSiblingListItem, getSiblingNodes } from './utils'; const DEFAULT_LIST_TYPE = 'default'; @@ -20,7 +22,8 @@ const DEFAULT_LIST_TYPE = 'default'; * It sets the value for the `listItem` attribute of the {@link module:list/list~List ``} element that * allows modifying the list style type. * - * It registers the `'listStyle'` command. + * It registers the `'listStyle'`, `'listReversed'` and `'listStart'` commands if they're enabled in config. + * Read more in {@link module:list/liststyle~ListPropertiesConfig}. * * @extends module:core/plugin~Plugin */ @@ -39,6 +42,21 @@ export default class ListStyleEditing extends Plugin { return 'ListStyleEditing'; } + /** + * @inheritDoc + */ + constructor( editor ) { + super( editor ); + + editor.config.define( 'list', { + properties: { + styles: true, + startIndex: false, + reversed: false + } + } ); + } + /** * @inheritDoc */ @@ -46,29 +64,34 @@ export default class ListStyleEditing extends Plugin { const editor = this.editor; const model = editor.model; + const enabledProperties = editor.config.get( 'list.properties' ); + const strategies = createAttributeStrategies( enabledProperties ); + // Extend schema. model.schema.extend( 'listItem', { - allowAttributes: [ 'listStyle' ] + allowAttributes: strategies.map( s => s.attributeName ) } ); - editor.commands.add( 'listStyle', new ListStyleCommand( editor, DEFAULT_LIST_TYPE ) ); + for ( const strategy of strategies ) { + strategy.addCommand( editor ); + } // Fix list attributes when modifying their nesting levels (the `listIndent` attribute). - this.listenTo( editor.commands.get( 'indentList' ), '_executeCleanup', fixListAfterIndentListCommand( editor ) ); - this.listenTo( editor.commands.get( 'outdentList' ), '_executeCleanup', fixListAfterOutdentListCommand( editor ) ); + this.listenTo( editor.commands.get( 'indentList' ), '_executeCleanup', fixListAfterIndentListCommand( editor, strategies ) ); + this.listenTo( editor.commands.get( 'outdentList' ), '_executeCleanup', fixListAfterOutdentListCommand( editor, strategies ) ); this.listenTo( editor.commands.get( 'bulletedList' ), '_executeCleanup', restoreDefaultListStyle( editor ) ); this.listenTo( editor.commands.get( 'numberedList' ), '_executeCleanup', restoreDefaultListStyle( editor ) ); - // Register a post-fixer that ensures that the `listStyle` attribute is specified in each `listItem` element. - model.document.registerPostFixer( fixListStyleAttributeOnListItemElements( editor ) ); + // Register a post-fixer that ensures that the attributes is specified in each `listItem` element. + model.document.registerPostFixer( fixListAttributesOnListItemElements( editor, strategies ) ); // Set up conversion. - editor.conversion.for( 'upcast' ).add( upcastListItemStyle() ); - editor.conversion.for( 'downcast' ).add( downcastListStyleAttribute() ); + editor.conversion.for( 'upcast' ).add( upcastListItemAttributes( strategies ) ); + editor.conversion.for( 'downcast' ).add( downcastListItemAttributes( strategies ) ); // Handle merging two separated lists into the single one. - this._mergeListStyleAttributeWhileMergingLists(); + this._mergeListStyleAttributeWhileMergingLists( strategies ); } /** @@ -77,10 +100,10 @@ export default class ListStyleEditing extends Plugin { afterInit() { const editor = this.editor; - // Enable post-fixer that removes the `listStyle` attribute from to-do list items only if the "TodoList" plugin is on. + // Enable post-fixer that removes the attributes from to-do list items only if the "TodoList" plugin is on. // We need to registry the hook here since the `TodoList` plugin can be added after the `ListStyleEditing`. if ( editor.commands.get( 'todoList' ) ) { - editor.model.document.registerPostFixer( removeListStyleAttributeFromTodoList( editor ) ); + editor.model.document.registerPostFixer( removeListItemAttributesFromTodoList( editor ) ); } } @@ -88,7 +111,8 @@ export default class ListStyleEditing extends Plugin { * Starts listening to {@link module:engine/model/model~Model#deleteContent} checks whether two lists will be merged into a single one * after deleting the content. * - * The purpose of this action is to adjust the `listStyle` value for the list that was merged. + * The purpose of this action is to adjust the `listStyle`, `listReversed` and `listStart` values + * for the list that was merged. * * Consider the following model's content: * @@ -109,13 +133,14 @@ export default class ListStyleEditing extends Plugin { * See https://github.com/ckeditor/ckeditor5/issues/7879. * * @private + * @param {Array.} attributeStrategies Strategies for the enabled attributes. */ - _mergeListStyleAttributeWhileMergingLists() { + _mergeListStyleAttributeWhileMergingLists( attributeStrategies ) { const editor = this.editor; const model = editor.model; // First the outer-most`listItem` in the first list reference. - // If found, the lists should be merged and this `listItem` provides the `listStyle` attribute + // If found, the lists should be merged and this `listItem` provides the attributes // and it is also a starting point when searching for items in the second list. let firstMostOuterItem; @@ -195,7 +220,14 @@ export default class ListStyleEditing extends Plugin { ]; for ( const listItem of items ) { - writer.setAttribute( 'listStyle', firstMostOuterItem.getAttribute( 'listStyle' ), listItem ); + for ( const strategy of attributeStrategies ) { + if ( strategy.appliesToListItem( listItem ) ) { + const attributeName = strategy.attributeName; + const value = firstMostOuterItem.getAttribute( attributeName ); + + writer.setAttribute( attributeName, value, listItem ); + } + } } } ); @@ -204,11 +236,120 @@ export default class ListStyleEditing extends Plugin { } } -// Returns a converter that consumes the `style` attribute and searches for the `list-style-type` definition. +/** + * Strategy for dealing with `listItem` attributes supported by this plugin. + * + * @typedef {Object} AttributeStrategy + * @private + * @property {String} #attributeName + * @property {*} #defaultValue + * @property {Function} #addCommand + * @property {Function} #appliesToListItem + * @property {Function} #setAttributeOnDowncast + * @property {Function} #getAttributeOnUpcast +*/ + +// Creates an array of strategies for dealing with enabled listItem attributes. +// +// @param {Object} enabledProperties +// @param {Boolean} enabledProperties.styles +// @param {Boolean} enabledProperties.reversed +// @param {Boolean} enabledProperties.startIndex +// @returns {Array.} +function createAttributeStrategies( enabledProperties ) { + const strategies = []; + + if ( enabledProperties.styles ) { + strategies.push( { + attributeName: 'listStyle', + defaultValue: DEFAULT_LIST_TYPE, + + addCommand( editor ) { + editor.commands.add( 'listStyle', new ListStyleCommand( editor, DEFAULT_LIST_TYPE ) ); + }, + + appliesToListItem() { + return true; + }, + + setAttributeOnDowncast( writer, listStyle, element ) { + if ( listStyle && listStyle !== DEFAULT_LIST_TYPE ) { + writer.setStyle( 'list-style-type', listStyle, element ); + } else { + writer.removeStyle( 'list-style-type', element ); + } + }, + + getAttributeOnUpcast( listParent ) { + return listParent.getStyle( 'list-style-type' ) || DEFAULT_LIST_TYPE; + } + } ); + } + + if ( enabledProperties.reversed ) { + strategies.push( { + attributeName: 'listReversed', + defaultValue: false, + + addCommand( editor ) { + editor.commands.add( 'listReversed', new ListReversedCommand( editor ) ); + }, + + appliesToListItem( item ) { + return item.getAttribute( 'listType' ) == 'numbered'; + }, + + setAttributeOnDowncast( writer, listReversed, element ) { + if ( listReversed ) { + writer.setAttribute( 'reversed', 'reversed', element ); + } else { + writer.removeAttribute( 'reversed', element ); + } + }, + + getAttributeOnUpcast( listParent ) { + return listParent.hasAttribute( 'reversed' ); + } + } ); + } + + if ( enabledProperties.startIndex ) { + strategies.push( { + attributeName: 'listStart', + defaultValue: 1, + + addCommand( editor ) { + editor.commands.add( 'listStart', new ListStartCommand( editor ) ); + }, + + appliesToListItem( item ) { + return item.getAttribute( 'listType' ) == 'numbered'; + }, + + setAttributeOnDowncast( writer, listStart, element ) { + if ( listStart != 1 ) { + writer.setAttribute( 'start', listStart, element ); + } else { + writer.removeAttribute( 'start', element ); + } + }, + + getAttributeOnUpcast( listParent ) { + return listParent.getAttribute( 'start' ) || 1; + } + } ); + } + + return strategies; +} + +// Returns a converter consumes the `style`, `reversed` and `start` attribute. +// In `style` it searches for the `list-style-type` definition. // If not found, the `"default"` value will be used. // +// @param {Array.} attributeStrategies // @returns {Function} -function upcastListItemStyle() { +function upcastListItemAttributes( attributeStrategies ) { return dispatcher => { dispatcher.on( 'element:li', ( evt, data, conversionApi ) => { const listParent = data.viewItem.parent; @@ -219,39 +360,45 @@ function upcastListItemStyle() { return; } - const listStyle = listParent.getStyle( 'list-style-type' ) || DEFAULT_LIST_TYPE; const listItem = data.modelRange.start.nodeAfter || data.modelRange.end.nodeBefore; - conversionApi.writer.setAttribute( 'listStyle', listStyle, listItem ); + for ( const strategy of attributeStrategies ) { + if ( strategy.appliesToListItem( listItem ) ) { + const listStyle = strategy.getAttributeOnUpcast( listParent ); + conversionApi.writer.setAttribute( strategy.attributeName, listStyle, listItem ); + } + } }, { priority: 'low' } ); }; } -// Returns a converter that adds the `list-style-type` definition as a value for the `style` attribute. -// The `"default"` value is removed and not present in the view/data. +// Returns a converter that adds `reversed`, `start` attributes and adds `list-style-type` definition as a value for the `style` attribute. +// The `"default"` values are removed and not present in the view/data. // +// @param {Array.} attributeStrategies // @returns {Function} -function downcastListStyleAttribute() { +function downcastListItemAttributes( attributeStrategies ) { return dispatcher => { - dispatcher.on( 'attribute:listStyle:listItem', ( evt, data, conversionApi ) => { - const viewWriter = conversionApi.writer; - const currentElement = data.item; - - const previousElement = getSiblingListItem( currentElement.previousSibling, { - sameIndent: true, - listIndent: currentElement.getAttribute( 'listIndent' ), - direction: 'backward' - } ); + for ( const strategy of attributeStrategies ) { + dispatcher.on( `attribute:${ strategy.attributeName }:listItem`, ( evt, data, conversionApi ) => { + const viewWriter = conversionApi.writer; + const currentElement = data.item; - const viewItem = conversionApi.mapper.toViewElement( currentElement ); + const previousElement = getSiblingListItem( currentElement.previousSibling, { + sameIndent: true, + listIndent: currentElement.getAttribute( 'listIndent' ), + direction: 'backward' + } ); - // A case when elements represent different lists. We need to separate their container. - if ( !areRepresentingSameList( currentElement, previousElement ) ) { - viewWriter.breakContainer( viewWriter.createPositionBefore( viewItem ) ); - } + const viewItem = conversionApi.mapper.toViewElement( currentElement ); - setListStyle( viewWriter, data.attributeNewValue, viewItem.parent ); - }, { priority: 'low' } ); + // A case when elements represent different lists. We need to separate their container. + if ( !areRepresentingSameList( currentElement, previousElement ) ) { + viewWriter.breakContainer( viewWriter.createPositionBefore( viewItem ) ); + } + strategy.setAttributeOnDowncast( viewWriter, data.attributeNewValue, viewItem.parent ); + }, { priority: 'low' } ); + } }; // Checks whether specified list items belong to the same list. @@ -263,24 +410,13 @@ function downcastListStyleAttribute() { return listItem2 && listItem1.getAttribute( 'listType' ) === listItem2.getAttribute( 'listType' ) && listItem1.getAttribute( 'listIndent' ) === listItem2.getAttribute( 'listIndent' ) && - listItem1.getAttribute( 'listStyle' ) === listItem2.getAttribute( 'listStyle' ); - } - - // Updates or removes the `list-style-type` from the `element`. - // - // @param {module:engine/view/downcastwriter~DowncastWriter} writer - // @param {String} listStyle - // @param {module:engine/view/element~Element} element - function setListStyle( writer, listStyle, element ) { - if ( listStyle && listStyle !== DEFAULT_LIST_TYPE ) { - writer.setStyle( 'list-style-type', listStyle, element ); - } else { - writer.removeStyle( 'list-style-type', element ); - } + listItem1.getAttribute( 'listStyle' ) === listItem2.getAttribute( 'listStyle' ) && + listItem1.getAttribute( 'listReversed' ) === listItem2.getAttribute( 'listReversed' ) && + listItem1.getAttribute( 'listStart' ) === listItem2.getAttribute( 'listStart' ); } } -// When indenting list, nested list should clear its value for the `listStyle` attribute or inherit from nested lists. +// When indenting list, nested list should clear its value for the attributes or inherit from nested lists. // // ■ List item 1. // ■ List item 2.[] @@ -292,11 +428,10 @@ function downcastListStyleAttribute() { // ■ List item 3. // // @param {module:core/editor/editor~Editor} editor +// @param {Array.} attributeStrategies // @returns {Function} -function fixListAfterIndentListCommand( editor ) { +function fixListAfterIndentListCommand( editor, attributeStrategies ) { return ( evt, changedItems ) => { - let valueToSet; - const root = changedItems[ 0 ]; const rootIndent = root.getAttribute( 'listIndent' ); @@ -310,26 +445,31 @@ function fixListAfterIndentListCommand( editor ) { // ■ List item 4. // // List items: `2` and `3` should be adjusted. - if ( root.previousSibling.getAttribute( 'listIndent' ) + 1 === rootIndent ) { - // valueToSet = root.previousSibling.getAttribute( 'listStyle' ) || DEFAULT_LIST_TYPE; - valueToSet = DEFAULT_LIST_TYPE; - } else { - const previousSibling = getSiblingListItem( root.previousSibling, { + let previousSibling = null; + + if ( root.previousSibling.getAttribute( 'listIndent' ) + 1 !== rootIndent ) { + previousSibling = getSiblingListItem( root.previousSibling, { sameIndent: true, direction: 'backward', listIndent: rootIndent } ); - - valueToSet = previousSibling.getAttribute( 'listStyle' ); } editor.model.change( writer => { for ( const item of itemsToUpdate ) { - writer.setAttribute( 'listStyle', valueToSet, item ); + for ( const strategy of attributeStrategies ) { + if ( strategy.appliesToListItem( item ) ) { + const valueToSet = previousSibling == null ? + strategy.defaultValue : + previousSibling.getAttribute( strategy.attributeName ); + + writer.setAttribute( strategy.attributeName, valueToSet, item ); + } + } } } ); }; } -// When outdenting a list, a nested list should copy its value for the `listStyle` attribute +// When outdenting a list, a nested list should copy attribute values // from the previous sibling list item including the same value for the `listIndent` value. // // ■ List item 1. @@ -343,8 +483,9 @@ function fixListAfterIndentListCommand( editor ) { // ■ List item 3. // // @param {module:core/editor/editor~Editor} editor +// @param {Array.} attributeStrategies // @returns {Function} -function fixListAfterOutdentListCommand( editor ) { +function fixListAfterOutdentListCommand( editor, attributeStrategies ) { return ( evt, changedItems ) => { changedItems = changedItems.reverse().filter( item => item.is( 'element', 'listItem' ) ); @@ -403,15 +544,23 @@ function fixListAfterOutdentListCommand( editor ) { const itemsToUpdate = changedItems.filter( item => item.getAttribute( 'listIndent' ) === indent ); for ( const item of itemsToUpdate ) { - writer.setAttribute( 'listStyle', listItem.getAttribute( 'listStyle' ), item ); + for ( const strategy of attributeStrategies ) { + if ( strategy.appliesToListItem( item ) ) { + const attributeName = strategy.attributeName; + const valueToSet = listItem.getAttribute( attributeName ); + + writer.setAttribute( attributeName, valueToSet, item ); + } + } } } ); }; } -// Each `listItem` element must have specified the `listStyle` attribute. -// This post-fixer checks whether inserted elements `listItem` elements should inherit the `listStyle` value from -// their sibling nodes or should use the default value. +// Each `listItem` element must have specified the `listStyle`, `listReversed` and `listStart` attributes +// if they are enabled and supported by its `listType`. +// This post-fixer checks whether inserted elements `listItem` elements should inherit the attribute values from +// their sibling nodes or should use the default values. // // Paragraph[] // ■ List item 1. // [listStyle="square", listType="bulleted"] @@ -442,8 +591,9 @@ function fixListAfterOutdentListCommand( editor ) { // ■ List item 3. // ... // // @param {module:core/editor/editor~Editor} editor +// @param {Array.} attributeStrategies // @returns {Function} -function fixListStyleAttributeOnListItemElements( editor ) { +function fixListAttributesOnListItemElements( editor, attributeStrategies ) { return writer => { let wasFixed = false; @@ -490,39 +640,50 @@ function fixListStyleAttributeOnListItemElements( editor ) { } } - for ( const item of insertedListItems ) { - if ( !item.hasAttribute( 'listStyle' ) ) { - if ( shouldInheritListType( existingListItem, item ) ) { - writer.setAttribute( 'listStyle', existingListItem.getAttribute( 'listStyle' ), item ); - } else { - writer.setAttribute( 'listStyle', DEFAULT_LIST_TYPE, item ); - } - wasFixed = true; - } else { - // Adjust the `listStyle` attribute for inserted (pasted) items. See #8160. - // - // ■ List item 1. // [listStyle="square", listType="bulleted"] - // ○ List item 1.1. // [listStyle="circle", listType="bulleted"] - // ○ [] (selection is here) - // - // Then, pasting a list with different attributes (listStyle, listType): - // - // 1. First. // [listStyle="decimal", listType="numbered"] - // 2. Second // [listStyle="decimal", listType="numbered"] - // - // The `listType` attribute will be corrected by the `ListEditing` converters. - // We need to adjust the `listStyle` attribute. Expected structure: - // - // ■ List item 1. // [listStyle="square", listType="bulleted"] - // ○ List item 1.1. // [listStyle="circle", listType="bulleted"] - // ○ First. // [listStyle="circle", listType="bulleted"] - // ○ Second // [listStyle="circle", listType="bulleted"] - const previousSibling = item.previousSibling; + for ( const strategy of attributeStrategies ) { + const attributeName = strategy.attributeName; - if ( shouldInheritListTypeFromPreviousItem( previousSibling, item ) ) { - writer.setAttribute( 'listStyle', previousSibling.getAttribute( 'listStyle' ), item ); + for ( const item of insertedListItems ) { + if ( !strategy.appliesToListItem( item ) ) { + writer.removeAttribute( attributeName, item ); + continue; + } + + if ( !item.hasAttribute( attributeName ) ) { + if ( shouldInheritListType( existingListItem, item, strategy ) ) { + writer.setAttribute( attributeName, existingListItem.getAttribute( attributeName ), item ); + } else { + writer.setAttribute( attributeName, strategy.defaultValue, item ); + } wasFixed = true; + } else { + // Adjust the `listStyle`, `listReversed` and `listStart` + // attributes for inserted (pasted) items. See #8160. + // + // ■ List item 1. // [listStyle="square", listType="bulleted"] + // ○ List item 1.1. // [listStyle="circle", listType="bulleted"] + // ○ [] (selection is here) + // + // Then, pasting a list with different attributes (listStyle, listType): + // + // 1. First. // [listStyle="decimal", listType="numbered"] + // 2. Second // [listStyle="decimal", listType="numbered"] + // + // The `listType` attribute will be corrected by the `ListEditing` converters. + // We need to adjust the `listStyle` attribute. Expected structure: + // + // ■ List item 1. // [listStyle="square", listType="bulleted"] + // ○ List item 1.1. // [listStyle="circle", listType="bulleted"] + // ○ First. // [listStyle="circle", listType="bulleted"] + // ○ Second // [listStyle="circle", listType="bulleted"] + const previousSibling = item.previousSibling; + + if ( shouldInheritListTypeFromPreviousItem( previousSibling, item, strategy.attributeName ) ) { + writer.setAttribute( attributeName, previousSibling.getAttribute( attributeName ), item ); + + wasFixed = true; + } } } } @@ -531,26 +692,28 @@ function fixListStyleAttributeOnListItemElements( editor ) { }; } -// Checks whether the `listStyle` attribute should be copied from the `baseItem` element. +// Checks whether the `listStyle`, `listReversed` and `listStart` attributes +// should be copied from the `baseItem` element. // // The attribute should be copied if the inserted element does not have defined it and // the value for the element is other than default in the base element. // // @param {module:engine/model/element~Element|null} baseItem // @param {module:engine/model/element~Element} itemToChange +// @param {module:list/liststyleediting~AttributeStrategy} attributeStrategy // @returns {Boolean} -function shouldInheritListType( baseItem, itemToChange ) { +function shouldInheritListType( baseItem, itemToChange, attributeStrategy ) { if ( !baseItem ) { return false; } - const baseListStyle = baseItem.getAttribute( 'listStyle' ); + const baseListAttribute = baseItem.getAttribute( attributeStrategy.attributeName ); - if ( !baseListStyle ) { + if ( !baseListAttribute ) { return false; } - if ( baseListStyle === DEFAULT_LIST_TYPE ) { + if ( baseListAttribute == attributeStrategy.defaultValue ) { return false; } @@ -561,7 +724,8 @@ function shouldInheritListType( baseItem, itemToChange ) { return true; } -// Checks whether the `listStyle` attribute should be copied from previous list item. +// Checks whether the `listStyle`, `listReversed` and `listStart` attributes +// should be copied from previous list item. // // The attribute should be copied if there's a mismatch of styles of the pasted list into a nested list. // Top-level lists are not normalized as we allow side-by-side list of different types. @@ -569,7 +733,7 @@ function shouldInheritListType( baseItem, itemToChange ) { // @param {module:engine/model/element~Element|null} previousItem // @param {module:engine/model/element~Element} itemToChange // @returns {Boolean} -function shouldInheritListTypeFromPreviousItem( previousItem, itemToChange ) { +function shouldInheritListTypeFromPreviousItem( previousItem, itemToChange, attributeName ) { if ( !previousItem || !previousItem.is( 'element', 'listItem' ) ) { return false; } @@ -584,25 +748,29 @@ function shouldInheritListTypeFromPreviousItem( previousItem, itemToChange ) { return false; } - const previousItemListStyle = previousItem.getAttribute( 'listStyle' ); + const previousItemListAttribute = previousItem.getAttribute( attributeName ); - if ( !previousItemListStyle || previousItemListStyle === itemToChange.getAttribute( 'listStyle' ) ) { + if ( !previousItemListAttribute || previousItemListAttribute === itemToChange.getAttribute( attributeName ) ) { return false; } return true; } -// Removes the `listStyle` attribute from "todo" list items. +// Removes the `listStyle`, `listReversed` and `listStart` attributes from "todo" list items. // // @param {module:core/editor/editor~Editor} editor // @returns {Function} -function removeListStyleAttributeFromTodoList( editor ) { +function removeListItemAttributesFromTodoList( editor ) { return writer => { const todoListItems = getChangedListItems( editor.model.document.differ.getChanges() ) .filter( item => { // Handle the todo lists only. The rest is handled in another post-fixer. - return item.getAttribute( 'listType' ) === 'todo' && item.hasAttribute( 'listStyle' ); + return item.getAttribute( 'listType' ) === 'todo' && ( + item.hasAttribute( 'listStyle' ) || + item.hasAttribute( 'listReversed' ) || + item.hasAttribute( 'listStart' ) + ); } ); if ( !todoListItems.length ) { @@ -611,6 +779,8 @@ function removeListStyleAttributeFromTodoList( editor ) { for ( const item of todoListItems ) { writer.removeAttribute( 'listStyle', item ); + writer.removeAttribute( 'listReversed', item ); + writer.removeAttribute( 'listStart', item ); } return true; diff --git a/packages/ckeditor5-list/src/utils.js b/packages/ckeditor5-list/src/utils.js index e4db7c1e668..cbe80818bfc 100644 --- a/packages/ckeditor5-list/src/utils.js +++ b/packages/ckeditor5-list/src/utils.js @@ -287,7 +287,7 @@ export function findNestedList( viewElement ) { /** * Returns an array with all `listItem` elements that represents the same list. * - * It means that values for `listIndent`, `listType`, and `listStyle` for all items are equal. + * It means that values of `listIndent`, `listType`, `listStyle`, `listReversed` and `listStart` for all items are equal. * * @param {module:engine/model/position~Position} position Starting position. * @param {'forward'|'backward'} direction Walking direction. @@ -350,11 +350,21 @@ export function getSiblingNodes( position, direction ) { // ○ List item 3. [listType=bulleted] // ○ List item 4. [listType=bulleted] // - // Abort searching when found a different list style. + // Abort searching when found a different list style, if ( element.getAttribute( 'listStyle' ) !== listItem.getAttribute( 'listStyle' ) ) { break; } + // ... different direction + if ( element.getAttribute( 'listReversed' ) !== listItem.getAttribute( 'listReversed' ) ) { + break; + } + + // ... and different start index + if ( element.getAttribute( 'listStart' ) !== listItem.getAttribute( 'listStart' ) ) { + break; + } + if ( direction === 'backward' ) { items.unshift( element ); } else { @@ -365,6 +375,41 @@ export function getSiblingNodes( position, direction ) { return items; } +/** + * Returns an array with all `listItem` elements in the model selection. + * + * It returns all the items even if only part of the list is selected, including items that belong to nested lists. + * If no list is selected, returns an empty array. + * The order of elements is not specified. + * + * @protected + * @param {module:engine/model/model~Model} model + * @returns {Array.} + */ +export function getSelectedListItems( model ) { + const document = model.document; + + // For all selected blocks find all list items that are being selected + // and update the `listStyle` attribute in those lists. + let listItems = [ ...document.selection.getSelectedBlocks() ] + .filter( element => element.is( 'element', 'listItem' ) ) + .map( element => { + const position = model.change( writer => writer.createPositionAt( element, 0 ) ); + + return [ + ...getSiblingNodes( position, 'backward' ), + ...getSiblingNodes( position, 'forward' ) + ]; + } ) + .flat(); + + // Since `getSelectedBlocks()` can return items that belong to the same list, and + // `getSiblingNodes()` returns the entire list, we need to remove duplicated items. + listItems = [ ...new Set( listItems ) ]; + + return listItems; +} + // Implementation of getFillerOffset for view list item element. // // @returns {Number|null} Block filler offset or `null` if block filler is not needed. diff --git a/packages/ckeditor5-list/tests/listreversedcommand.js b/packages/ckeditor5-list/tests/listreversedcommand.js new file mode 100644 index 00000000000..a3863d1f94a --- /dev/null +++ b/packages/ckeditor5-list/tests/listreversedcommand.js @@ -0,0 +1,376 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import ListStyleEditing from '../src/liststyleediting'; + +describe( 'ListReversedCommand', () => { + let editor, model, listReversedCommand; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing ], + list: { + properties: { styles: false, startIndex: false, reversed: true } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + + listReversedCommand = editor.commands.get( 'listReversed' ); + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + describe( '#isEnabled', () => { + it( 'should be false if selected a paragraph', () => { + setData( model, 'Foo[]' ); + + expect( listReversedCommand.isEnabled ).to.be.false; + } ); + + it( 'should be false if selection starts in a paragraph and ends in a list item', () => { + setData( model, + 'Fo[o' + + 'Foo]' + ); + + expect( listReversedCommand.isEnabled ).to.be.false; + } ); + + it( 'should be false if selection is inside a listItem (listType: bulleted)', () => { + setData( model, 'Foo[]' ); + + expect( listReversedCommand.isEnabled ).to.be.false; + } ); + + it( 'should be true if selection is inside a listItem (collapsed selection)', () => { + setData( model, 'Foo[]' ); + + expect( listReversedCommand.isEnabled ).to.be.true; + } ); + + it( 'should be true if selection is inside a listItem (non-collapsed selection)', () => { + setData( model, '[Foo]' ); + + expect( listReversedCommand.isEnabled ).to.be.true; + } ); + + it( 'should be true attribute if selected more elements in the same list', () => { + setData( model, + '[1.' + + '2.]' + + '3.' + ); + + expect( listReversedCommand.isEnabled ).to.be.true; + } ); + } ); + + describe( '#value', () => { + it( 'should return null if selected a paragraph', () => { + setData( model, 'Foo[]' ); + + expect( listReversedCommand.value ).to.be.null; + } ); + + it( 'should return null if selection starts in a paragraph and ends in a list item', () => { + setData( model, + 'Fo[o' + + 'Foo]' + ); + + expect( listReversedCommand.value ).to.be.null; + } ); + + it( 'should return null if selection is inside a listItem (listType: bulleted)', () => { + setData( model, 'Foo[]' ); + + expect( listReversedCommand.value ).to.be.null; + } ); + + it( 'should return the value of `listReversed` attribute if selection is inside a listItem (collapsed selection)', () => { + setData( model, 'Foo[]' ); + + expect( listReversedCommand.value ).to.be.true; + } ); + + it( 'should return the value of `listReversed` attribute if selection is inside a listItem (non-collapsed selection)', () => { + setData( model, '[Foo]' ); + + expect( listReversedCommand.value ).to.be.false; + } ); + + it( 'should return the value of `listReversed` attribute if selected more elements in the same list', () => { + setData( model, + '[1.' + + '2.]' + + '3.' + ); + + expect( listReversedCommand.value ).to.be.true; + } ); + + it( 'should return the value of `listReversed` attribute for the selection inside a nested list', () => { + setData( model, + '1.' + + '1.1.[]' + + '2.' + ); + + expect( listReversedCommand.value ).to.be.true; + } ); + + it( + 'should return the value of `listReversed` attribute from a list where the selection starts (selection over nested list)', + () => { + setData( model, + '1.' + + '1.1.[' + + '2.]' + ); + + expect( listReversedCommand.value ).to.be.true; + } + ); + } ); + + describe( 'execute()', () => { + it( 'should set the `listReversed` attribute for collapsed selection', () => { + setData( model, + '1.[]' + ); + + listReversedCommand.execute( { reversed: true } ); + + expect( getData( model ) ).to.equal( + '1.[]' + ); + } ); + + it( 'should set the `listReversed` attribute for non-collapsed selection', () => { + setData( model, + '[1.]' + ); + + listReversedCommand.execute( { reversed: false } ); + + expect( getData( model ) ).to.equal( + '[1.]' + ); + } ); + + it( 'should set the `listReversed` attribute for all the same list items (collapsed selection)', () => { + setData( model, + '1.[]' + + '2.' + + '3.' + ); + + listReversedCommand.execute( { reversed: true } ); + + expect( getData( model ) ).to.equal( + '1.[]' + + '2.' + + '3.' + ); + } ); + + it( 'should set the `listReversed` attribute for all the same list items and ignores nested lists', () => { + setData( model, + '1.[]' + + '2.' + + '2.1.' + + '2.2.' + + '3.' + + '3.1.' + ); + + listReversedCommand.execute( { reversed: true } ); + + expect( getData( model ) ).to.equal( + '1.[]' + + '2.' + + '2.1.' + + '2.2.' + + '3.' + + '3.1.' + ); + } ); + + it( + 'should set the `listReversed` attribute for all the same list items and ignores "parent" list (selection in nested list)', + () => { + setData( model, + '1.' + + '2.' + + '2.1.[]' + + '2.2.' + + '3.' + + '3.1.' + ); + + listReversedCommand.execute( { reversed: true } ); + + expect( getData( model ) ).to.equal( + '1.' + + '2.' + + '2.1.[]' + + '2.2.' + + '3.' + + '3.1.' + ); + } + ); + + it( 'should stop searching for the list items when spotted non-listItem element', () => { + setData( model, + 'Foo.' + + '1.[]' + + '2.' + + '3.' + ); + + listReversedCommand.execute( { reversed: true } ); + + expect( getData( model ) ).to.equal( + 'Foo.' + + '1.[]' + + '2.' + + '3.' + ); + } ); + + it( 'should stop searching for the list items when spotted listItem with different listType attribute', () => { + setData( model, + 'Foo.' + + '1.[]' + + '2.' + + '1.' + ); + + listReversedCommand.execute( { reversed: true } ); + + expect( getData( model ) ).to.equal( + 'Foo.' + + '1.[]' + + '2.' + + '1.' + ); + } ); + + it( 'should stop searching for the list items when spotted listItem with different `listReversed` attribute', () => { + setData( model, + 'Foo.' + + '1.[]' + + '2.' + + '1.' + ); + + listReversedCommand.execute( { reversed: true } ); + + expect( getData( model ) ).to.equal( + 'Foo.' + + '1.[]' + + '2.' + + '1.' + ); + } ); + + it( 'should start searching for the list items from starting position (collapsed selection)', () => { + setData( model, + '1.' + + '2.' + + '[3.' + + 'Foo.]' + ); + + listReversedCommand.execute( { reversed: true } ); + + expect( getData( model ) ).to.equal( + '1.' + + '2.' + + '[3.' + + 'Foo.]' + ); + } ); + + it( 'should use `false` value if not specified (no options passed)', () => { + setData( model, + '1.[]' + ); + + listReversedCommand.execute(); + + expect( getData( model ) ).to.equal( + '1.[]' + ); + } ); + + it( 'should use `false` value if not specified (passed an empty object)', () => { + setData( model, + '1.[]' + ); + + listReversedCommand.execute( {} ); + + expect( getData( model ) ).to.equal( + '1.[]' + ); + } ); + + it( 'should update all items that belong to selected elements', () => { + // [x] = items that should be updated. + // All list items that belong to the same lists that selected items should be updated. + // "2." is the most outer list (listIndent=0) + // "2.1" a child list of the "2." element (listIndent=1) + // "2.1.1" a child list of the "2.1" element (listIndent=2) + // + // [x] ■ 1. + // [x] ■ [2. + // [x] ○ 2.1. + // [x] ▶ 2.1.1.] + // [x] ▶ 2.1.2. + // [x] ○ 2.2. + // [x] ■ 3. + // [ ] ○ 3.1. + // [ ] ▶ 3.1.1. + // + // "3.1" is not selected and this list should not be updated. + setData( model, + '1.' + + '[2.' + + '2.1.' + + '2.1.1.]' + + '2.1.2.' + + '2.2.' + + '3.' + + '3.1.' + + '3.1.1.' + ); + + listReversedCommand.execute( { reversed: true } ); + + expect( getData( model ) ).to.equal( + '1.' + + '[2.' + + '2.1.' + + '2.1.1.]' + + '2.1.2.' + + '2.2.' + + '3.' + + '3.1.' + + '3.1.1.' + ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-list/tests/liststartcommand.js b/packages/ckeditor5-list/tests/liststartcommand.js new file mode 100644 index 00000000000..b8d03970a79 --- /dev/null +++ b/packages/ckeditor5-list/tests/liststartcommand.js @@ -0,0 +1,376 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import ListStyleEditing from '../src/liststyleediting'; + +describe( 'ListStartCommand', () => { + let editor, model, listStartCommand; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing ], + list: { + properties: { styles: false, startIndex: true, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + + listStartCommand = editor.commands.get( 'listStart' ); + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + describe( '#isEnabled', () => { + it( 'should be false if selected a paragraph', () => { + setData( model, 'Foo[]' ); + + expect( listStartCommand.isEnabled ).to.be.false; + } ); + + it( 'should be false if selection starts in a paragraph and ends in a list item', () => { + setData( model, + 'Fo[o' + + 'Foo]' + ); + + expect( listStartCommand.isEnabled ).to.be.false; + } ); + + it( 'should be false if selection is inside a listItem (listType: bulleted)', () => { + setData( model, 'Foo[]' ); + + expect( listStartCommand.isEnabled ).to.be.false; + } ); + + it( 'should be true if selection is inside a listItem (collapsed selection)', () => { + setData( model, 'Foo[]' ); + + expect( listStartCommand.isEnabled ).to.be.true; + } ); + + it( 'should be true if selection is inside a listItem (non-collapsed selection)', () => { + setData( model, '[Foo]' ); + + expect( listStartCommand.isEnabled ).to.be.true; + } ); + + it( 'should be true attribute if selected more elements in the same list', () => { + setData( model, + '[1.' + + '2.]' + + '3.' + ); + + expect( listStartCommand.isEnabled ).to.be.true; + } ); + } ); + + describe( '#value', () => { + it( 'should return null if selected a paragraph', () => { + setData( model, 'Foo[]' ); + + expect( listStartCommand.value ).to.be.null; + } ); + + it( 'should return null if selection starts in a paragraph and ends in a list item', () => { + setData( model, + 'Fo[o' + + 'Foo]' + ); + + expect( listStartCommand.value ).to.be.null; + } ); + + it( 'should return null if selection is inside a listItem (listType: bulleted)', () => { + setData( model, 'Foo[]' ); + + expect( listStartCommand.value ).to.be.null; + } ); + + it( 'should return the value of `listStart` attribute if selection is inside a listItem (collapsed selection)', () => { + setData( model, 'Foo[]' ); + + expect( listStartCommand.value ).to.equal( 2 ); + } ); + + it( 'should return the value of `listStart` attribute if selection is inside a listItem (non-collapsed selection)', () => { + setData( model, '[Foo]' ); + + expect( listStartCommand.value ).to.equal( 3 ); + } ); + + it( 'should return the value of `listStart` attribute if selected more elements in the same list', () => { + setData( model, + '[1.' + + '2.]' + + '3.' + ); + + expect( listStartCommand.value ).to.equal( 3 ); + } ); + + it( 'should return the value of `listStart` attribute for the selection inside a nested list', () => { + setData( model, + '1.' + + '1.1.[]' + + '2.' + ); + + expect( listStartCommand.value ).to.equal( 3 ); + } ); + + it( + 'should return the value of `listStart` attribute from a list where the selection starts (selection over nested list)', + () => { + setData( model, + '1.' + + '1.1.[' + + '2.]' + ); + + expect( listStartCommand.value ).to.equal( 3 ); + } + ); + } ); + + describe( 'execute()', () => { + it( 'should set the `listStart` attribute for collapsed selection', () => { + setData( model, + '1.[]' + ); + + listStartCommand.execute( { startIndex: 5 } ); + + expect( getData( model ) ).to.equal( + '1.[]' + ); + } ); + + it( 'should set the `listStart` attribute for non-collapsed selection', () => { + setData( model, + '[1.]' + ); + + listStartCommand.execute( { startIndex: 5 } ); + + expect( getData( model ) ).to.equal( + '[1.]' + ); + } ); + + it( 'should set the `listStart` attribute for all the same list items (collapsed selection)', () => { + setData( model, + '1.[]' + + '2.' + + '3.' + ); + + listStartCommand.execute( { startIndex: 3 } ); + + expect( getData( model ) ).to.equal( + '1.[]' + + '2.' + + '3.' + ); + } ); + + it( 'should set the `listStart` attribute for all the same list items and ignores nested lists', () => { + setData( model, + '1.[]' + + '2.' + + '2.1.' + + '2.2.' + + '3.' + + '3.1.' + ); + + listStartCommand.execute( { startIndex: 2 } ); + + expect( getData( model ) ).to.equal( + '1.[]' + + '2.' + + '2.1.' + + '2.2.' + + '3.' + + '3.1.' + ); + } ); + + it( + 'should set the `listStart` attribute for all the same list items and ignores "parent" list (selection in nested list)', + () => { + setData( model, + '1.' + + '2.' + + '2.1.[]' + + '2.2.' + + '3.' + + '3.1.' + ); + + listStartCommand.execute( { startIndex: 2 } ); + + expect( getData( model ) ).to.equal( + '1.' + + '2.' + + '2.1.[]' + + '2.2.' + + '3.' + + '3.1.' + ); + } + ); + + it( 'should stop searching for the list items when spotted non-listItem element', () => { + setData( model, + 'Foo.' + + '1.[]' + + '2.' + + '3.' + ); + + listStartCommand.execute( { startIndex: 2 } ); + + expect( getData( model ) ).to.equal( + 'Foo.' + + '1.[]' + + '2.' + + '3.' + ); + } ); + + it( 'should stop searching for the list items when spotted listItem with different listType attribute', () => { + setData( model, + 'Foo.' + + '1.[]' + + '2.' + + '1.' + ); + + listStartCommand.execute( { startIndex: 2 } ); + + expect( getData( model ) ).to.equal( + 'Foo.' + + '1.[]' + + '2.' + + '1.' + ); + } ); + + it( 'should stop searching for the list items when spotted listItem with different `listStart` attribute', () => { + setData( model, + 'Foo.' + + '1.[]' + + '2.' + + '1.' + ); + + listStartCommand.execute( { startIndex: 3 } ); + + expect( getData( model ) ).to.equal( + 'Foo.' + + '1.[]' + + '2.' + + '1.' + ); + } ); + + it( 'should start searching for the list items from starting position (collapsed selection)', () => { + setData( model, + '1.' + + '2.' + + '[3.' + + 'Foo.]' + ); + + listStartCommand.execute( { startIndex: 3 } ); + + expect( getData( model ) ).to.equal( + '1.' + + '2.' + + '[3.' + + 'Foo.]' + ); + } ); + + it( 'should use `1` value if not specified (no options passed)', () => { + setData( model, + '1.[]' + ); + + listStartCommand.execute(); + + expect( getData( model ) ).to.equal( + '1.[]' + ); + } ); + + it( 'should use `1` value if not specified (passed an empty object)', () => { + setData( model, + '1.[]' + ); + + listStartCommand.execute( {} ); + + expect( getData( model ) ).to.equal( + '1.[]' + ); + } ); + + it( 'should update all items that belong to selected elements', () => { + // [x] = items that should be updated. + // All list items that belong to the same lists that selected items should be updated. + // "2." is the most outer list (listIndent=0) + // "2.1" a child list of the "2." element (listIndent=1) + // "2.1.1" a child list of the "2.1" element (listIndent=2) + // + // [x] ■ 1. + // [x] ■ [2. + // [x] ○ 2.1. + // [x] ▶ 2.1.1.] + // [x] ▶ 2.1.2. + // [x] ○ 2.2. + // [x] ■ 3. + // [ ] ○ 3.1. + // [ ] ▶ 3.1.1. + // + // "3.1" is not selected and this list should not be updated. + setData( model, + '1.' + + '[2.' + + '2.1.' + + '2.1.1.]' + + '2.1.2.' + + '2.2.' + + '3.' + + '3.1.' + + '3.1.1.' + ); + + listStartCommand.execute( { startIndex: 7 } ); + + expect( getData( model ) ).to.equal( + '1.' + + '[2.' + + '2.1.' + + '2.1.1.]' + + '2.1.2.' + + '2.2.' + + '3.' + + '3.1.' + + '3.1.1.' + ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-list/tests/liststyleediting.js b/packages/ckeditor5-list/tests/liststyleediting.js index f1038b4e09f..12b368dda09 100644 --- a/packages/ckeditor5-list/tests/liststyleediting.js +++ b/packages/ckeditor5-list/tests/liststyleediting.js @@ -17,116 +17,168 @@ import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils import ListStyleEditing from '../src/liststyleediting'; import TodoListEditing from '../src/todolistediting'; import ListStyleCommand from '../src/liststylecommand'; +import ListReversedCommand from '../src/listreversedcommand'; +import ListStartCommand from '../src/liststartcommand'; import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor'; describe( 'ListStyleEditing', () => { let editor, model, view; - beforeEach( () => { - return VirtualTestEditor - .create( { - plugins: [ Paragraph, ListStyleEditing, UndoEditing ] - } ) - .then( newEditor => { - editor = newEditor; - model = editor.model; - view = editor.editing.view; - } ); - } ); - - afterEach( () => { - return editor.destroy(); - } ); - it( 'should have pluginName', () => { expect( ListStyleEditing.pluginName ).to.equal( 'ListStyleEditing' ); } ); - it( 'should be loaded', () => { - expect( editor.plugins.get( ListStyleEditing ) ).to.be.instanceOf( ListStyleEditing ); - } ); + describe( 'config', () => { + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ ListStyleEditing ] + } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); - describe( 'schema rules', () => { - it( 'should allow set `listStyle` on the `listItem`', () => { - expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listStyle' ) ).to.be.true; + afterEach( () => { + return editor.destroy(); } ); - } ); - describe( 'command', () => { - it( 'should register listStyle command', () => { - const command = editor.commands.get( 'listStyle' ); + it( 'should have default values', () => { + expect( editor.config.get( 'list' ) ).to.deep.equal( { + properties: { + styles: true, + startIndex: false, + reversed: false + } + } ); + } ); - expect( command ).to.be.instanceOf( ListStyleCommand ); + it( 'should be loaded', () => { + expect( editor.plugins.get( ListStyleEditing ) ).to.be.instanceOf( ListStyleEditing ); } ); } ); - describe( 'conversion in data pipeline', () => { - describe( 'model to data', () => { - it( 'should convert single list (type: bulleted)', () => { - setModelData( model, - 'Foo' + - 'Bar' - ); + describe( 'listStyle', () => { + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, UndoEditing ], + list: { + properties: { styles: true, startIndex: false, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + view = editor.editing.view; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); - expect( editor.getData() ).to.equal( '
  • Foo
  • Bar
' ); + describe( 'schema rules', () => { + it( 'should allow set `listStyle` on the `listItem`', () => { + expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listStyle' ) ).to.be.true; } ); - it( 'should convert single list (type: numbered)', () => { - setModelData( model, - 'Foo' + - 'Bar' - ); + it( 'should not allow set `listReversed` on the `listItem`', () => { + expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listReversed' ) ).to.be.false; + } ); - expect( editor.getData() ).to.equal( '
  1. Foo
  2. Bar
' ); + it( 'should not allow set `listStart` on the `listItem`', () => { + expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listStart' ) ).to.be.false; } ); + } ); - it( 'should convert single list (type: bulleted, style: default)', () => { - setModelData( model, - 'Foo' + - 'Bar' - ); + describe( 'command', () => { + it( 'should register `listStyle` command', () => { + const command = editor.commands.get( 'listStyle' ); - expect( editor.getData() ).to.equal( '
  • Foo
  • Bar
' ); + expect( command ).to.be.instanceOf( ListStyleCommand ); } ); - it( 'should convert single list (type: numbered, style: default)', () => { - setModelData( model, - 'Foo' + - 'Bar' - ); + it( 'should not register `listReversed` command', () => { + const command = editor.commands.get( 'listReversed' ); - expect( editor.getData() ).to.equal( '
  1. Foo
  2. Bar
' ); + expect( command ).to.be.undefined; } ); - it( 'should convert single list (type: bulleted, style: circle)', () => { - setModelData( model, - 'Foo' + - 'Bar' - ); + it( 'should not register `listStart` command', () => { + const command = editor.commands.get( 'listStart' ); - expect( editor.getData() ).to.equal( '
  • Foo
  • Bar
' ); + expect( command ).to.be.undefined; } ); + } ); - it( 'should convert single list (type: numbered, style: upper-alpha)', () => { - setModelData( model, - 'Foo' + - 'Bar' - ); + describe( 'conversion in data pipeline', () => { + describe( 'model to data', () => { + it( 'should convert single list (type: bulleted)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); - expect( editor.getData() ).to.equal( '
  1. Foo
  2. Bar
' ); - } ); + expect( editor.getData() ).to.equal( '
  • Foo
  • Bar
' ); + } ); - it( 'should convert nested bulleted lists (main: circle, nested: disc)', () => { - setModelData( model, - 'Foo 1' + - 'Bar 1' + - 'Bar 2' + - 'Foo 2' + - 'Foo 3' - ); + it( 'should convert single list (type: numbered)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); - expect( editor.getData() ).to.equal( - '
    ' + + expect( editor.getData() ).to.equal( '
    1. Foo
    2. Bar
    ' ); + } ); + + it( 'should convert single list (type: bulleted, style: default)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( editor.getData() ).to.equal( '
    • Foo
    • Bar
    ' ); + } ); + + it( 'should convert single list (type: numbered, style: default)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( editor.getData() ).to.equal( '
    1. Foo
    2. Bar
    ' ); + } ); + + it( 'should convert single list (type: bulleted, style: circle)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( editor.getData() ).to.equal( '
    • Foo
    • Bar
    ' ); + } ); + + it( 'should convert single list (type: numbered, style: upper-alpha)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( editor.getData() ).to.equal( '
    1. Foo
    2. Bar
    ' ); + } ); + + it( 'should convert nested bulleted lists (main: circle, nested: disc)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); + + expect( editor.getData() ).to.equal( + '
      ' + '
    • Foo 1' + '
        ' + '
      • Bar 1
      • ' + @@ -136,20 +188,20 @@ describe( 'ListStyleEditing', () => { '
      • Foo 2
      • ' + '
      • Foo 3
      • ' + '
      ' - ); - } ); + ); + } ); - it( 'should convert nested numbered lists (main: decimal-leading-zero, nested: lower-latin)', () => { - setModelData( model, - 'Foo 1' + - 'Bar 1' + - 'Bar 2' + - 'Foo 2' + - 'Foo 3' - ); + it( 'should convert nested numbered lists (main: decimal-leading-zero, nested: lower-latin)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); - expect( editor.getData() ).to.equal( - '
        ' + + expect( editor.getData() ).to.equal( + '
          ' + '
        1. Foo 1' + '
            ' + '
          1. Bar 1
          2. ' + @@ -159,20 +211,20 @@ describe( 'ListStyleEditing', () => { '
          3. Foo 2
          4. ' + '
          5. Foo 3
          6. ' + '
          ' - ); - } ); + ); + } ); - it( 'should convert nested mixed lists (ul>ol, main: square, nested: lower-roman)', () => { - setModelData( model, - 'Foo 1' + - 'Bar 1' + - 'Bar 2' + - 'Foo 2' + - 'Foo 3' - ); + it( 'should convert nested mixed lists (ul>ol, main: square, nested: lower-roman)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); - expect( editor.getData() ).to.equal( - '
            ' + + expect( editor.getData() ).to.equal( + '
              ' + '
            • Foo 1' + '
                ' + '
              1. Bar 1
              2. ' + @@ -182,19 +234,19 @@ describe( 'ListStyleEditing', () => { '
              3. Foo 2
              4. ' + '
              5. Foo 3
              6. ' + '
            ' - ); - } ); + ); + } ); - it( 'should produce nested lists (different `listIndent` attribute)', () => { - setModelData( model, - 'Foo 1' + - 'Foo 2' + - 'Bar 1' + - 'Bar 2' - ); + it( 'should produce nested lists (different `listIndent` attribute)', () => { + setModelData( model, + 'Foo 1' + + 'Foo 2' + + 'Bar 1' + + 'Bar 2' + ); - expect( editor.getData() ).to.equal( - '
              ' + + expect( editor.getData() ).to.equal( + '
                ' + '
              1. Foo 1
              2. ' + '
              3. Foo 2' + '
                  ' + @@ -203,515 +255,552 @@ describe( 'ListStyleEditing', () => { '
                ' + '
              4. ' + '
              ' - ); - } ); + ); + } ); - it( 'should produce two different lists (different `listType` attribute)', () => { - setModelData( model, - 'Foo 1' + - 'Foo 2' + - 'Bar 1' + - 'Bar 2' - ); + it( 'should produce two different lists (different `listType` attribute)', () => { + setModelData( model, + 'Foo 1' + + 'Foo 2' + + 'Bar 1' + + 'Bar 2' + ); - expect( editor.getData() ).to.equal( - '
                ' + - '
              1. Foo 1
              2. ' + - '
              3. Foo 2
              4. ' + - '
              ' + - '
                ' + - '
              • Bar 1
              • ' + - '
              • Bar 2
              • ' + - '
              ' - ); - } ); + expect( editor.getData() ).to.equal( + '
                ' + + '
              1. Foo 1
              2. ' + + '
              3. Foo 2
              4. ' + + '
              ' + + '
                ' + + '
              • Bar 1
              • ' + + '
              • Bar 2
              • ' + + '
              ' + ); + } ); - it( 'should produce two different lists (different `listStyle` attribute)', () => { - setModelData( model, - 'Foo 1' + - 'Foo 2' + - 'Bar 1' + - 'Bar 2' - ); + it( 'should produce two different lists (different `listStyle` attribute)', () => { + setModelData( model, + 'Foo 1' + + 'Foo 2' + + 'Bar 1' + + 'Bar 2' + ); - expect( editor.getData() ).to.equal( - '
                ' + - '
              • Foo 1
              • ' + - '
              • Foo 2
              • ' + - '
              ' + - '
                ' + - '
              • Bar 1
              • ' + - '
              • Bar 2
              • ' + - '
              ' - ); + expect( editor.getData() ).to.equal( + '
                ' + + '
              • Foo 1
              • ' + + '
              • Foo 2
              • ' + + '
              ' + + '
                ' + + '
              • Bar 1
              • ' + + '
              • Bar 2
              • ' + + '
              ' + ); + } ); } ); - it( 'should not allow to set the `listStyle` attribute in to-do list item', () => { - setModelData( model, 'Foo' ); + describe( 'view to model', () => { + it( 'should convert single list (type: bulleted)', () => { + editor.setData( '
              • Foo
              • Bar
              ' ); - const listItem = model.document.getRoot().getChild( 0 ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Foo' + + 'Bar' + ); + } ); - expect( listItem.hasAttribute( 'listItem' ) ).to.be.false; + it( 'should convert single list (type: numbered)', () => { + editor.setData( '
              1. Foo
              2. Bar
              ' ); - model.change( writer => { - writer.setAttribute( 'listType', 'foo', listItem ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Foo' + + 'Bar' + ); } ); - expect( listItem.hasAttribute( 'listItem' ) ).to.be.false; - } ); - } ); + it( 'should convert single list (type: bulleted, style: circle)', () => { + editor.setData( '
              • Foo
              • Bar
              ' ); - describe( 'view to model', () => { - it( 'should convert single list (type: bulleted)', () => { - editor.setData( '
              • Foo
              • Bar
              ' ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Foo' + + 'Bar' + ); + } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - 'Foo' + - 'Bar' - ); - } ); + it( 'should convert single list (type: numbered, style: upper-alpha)', () => { + editor.setData( '
              1. Foo
              2. Bar
              ' ); - it( 'should convert single list (type: numbered)', () => { - editor.setData( '
              1. Foo
              2. Bar
              ' ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Foo' + + 'Bar' + ); + } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - 'Foo' + - 'Bar' - ); - } ); + it( 'should convert nested and mixed lists', () => { + editor.setData( + '
                ' + + '
              1. OL 1
              2. ' + + '
              3. OL 2' + + '
                  ' + + '
                • UL 1
                • ' + + '
                • UL 2
                • ' + + '
                ' + + '
              4. ' + + '
              5. OL 3
              6. ' + + '
              ' + ); - it( 'should convert single list (type: bulleted, style: circle)', () => { - editor.setData( '
              • Foo
              • Bar
              ' ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'OL 1' + + 'OL 2' + + 'UL 1' + + 'UL 2' + + 'OL 3' + ); + } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - 'Foo' + - 'Bar' - ); - } ); + it( 'should convert when the list is in the middle of the content', () => { + editor.setData( + '

              Paragraph.

              ' + + '
                ' + + '
              1. Foo
              2. ' + + '
              3. Bar
              4. ' + + '
              ' + + '

              Paragraph.

              ' + ); - it( 'should convert single list (type: numbered, style: upper-alpha)', () => { - editor.setData( '
              1. Foo
              2. Bar
              ' ); + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Paragraph.' + + 'Foo' + + 'Bar' + + 'Paragraph.' + ); + } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - 'Foo' + - 'Bar' - ); - } ); + // See: #8262. + describe( 'list conversion with surrounding text nodes', () => { + let editor; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing ], + list: { + properties: { styles: true, startIndex: false, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); - it( 'should convert nested and mixed lists', () => { - editor.setData( - '
                ' + - '
              1. OL 1
              2. ' + - '
              3. OL 2' + - '
                  ' + - '
                • UL 1
                • ' + - '
                • UL 2
                • ' + - '
                ' + - '
              4. ' + - '
              5. OL 3
              6. ' + - '
              ' - ); + afterEach( () => { + return editor.destroy(); + } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - 'OL 1' + - 'OL 2' + - 'UL 1' + - 'UL 2' + - 'OL 3' - ); - } ); + it( 'should convert a list if raw text is before the list', () => { + editor.setData( 'Foo
              • Foo
              ' ); - it( 'should convert when the list is in the middle of the content', () => { - editor.setData( - '

              Paragraph.

              ' + - '
                ' + - '
              1. Foo
              2. ' + - '
              3. Bar
              4. ' + - '
              ' + - '

              Paragraph.

              ' - ); + expect( editor.getData() ).to.equal( '

              Foo

              • Foo
              ' ); + } ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - 'Paragraph.' + - 'Foo' + - 'Bar' + - 'Paragraph.' - ); - } ); + it( 'should convert a list if raw text is after the list', () => { + editor.setData( '
              • Foo
              Foo' ); - // See: #8262. - describe( 'list conversion with surrounding text nodes', () => { - let editor; + expect( editor.getData() ).to.equal( '
              • Foo

              Foo

              ' ); + } ); - beforeEach( () => { - return VirtualTestEditor - .create( { - plugins: [ Paragraph, ListStyleEditing ] - } ) - .then( newEditor => { - editor = newEditor; - } ); - } ); + it( 'should convert a list if it is surrender by two text nodes', () => { + editor.setData( 'Foo
              • Foo
              Foo' ); - afterEach( () => { - return editor.destroy(); + expect( editor.getData() ).to.equal( '

              Foo

              • Foo

              Foo

              ' ); + } ); } ); + } ); + } ); - it( 'should convert a list if raw text is before the list', () => { - editor.setData( 'Foo
              • Foo
              ' ); + // At this moment editing and data pipelines produce exactly the same content. + // Just a few tests will be enough here. `model to data` block contains all cases checked. + describe( 'conversion in editing pipeline', () => { + describe( 'model to view', () => { + it( 'should convert single list (type: bulleted, style: default)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); - expect( editor.getData() ).to.equal( '

              Foo

              • Foo
              ' ); + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
              • Foo
              • Bar
              ' + ); } ); - it( 'should convert a list if raw text is after the list', () => { - editor.setData( '
              • Foo
              Foo' ); + it( 'should convert single list (type: bulleted, style: circle)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); - expect( editor.getData() ).to.equal( '
              • Foo

              Foo

              ' ); + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
              • Foo
              • Bar
              ' + ); } ); - it( 'should convert a list if it is surrender by two text nodes', () => { - editor.setData( 'Foo
              • Foo
              Foo' ); + it( 'should convert nested bulleted lists (main: circle, nested: disc)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); - expect( editor.getData() ).to.equal( '

              Foo

              • Foo

              Foo

              ' ); + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
                ' + + '
              • Foo 1' + + '
                  ' + + '
                • Bar 1
                • ' + + '
                • Bar 2
                • ' + + '
                ' + + '
              • ' + + '
              • Foo 2
              • ' + + '
              • Foo 3
              • ' + + '
              ' + ); } ); - } ); - } ); - } ); - // At this moment editing and data pipelines produce exactly the same content. - // Just a few tests will be enough here. `model to data` block contains all cases checked. - describe( 'conversion in editing pipeline', () => { - describe( 'model to view', () => { - it( 'should convert single list (type: bulleted, style: default)', () => { - setModelData( model, - 'Foo' + - 'Bar' - ); - - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( - '
              • Foo
              • Bar
              ' - ); - } ); - - it( 'should convert single list (type: bulleted, style: circle)', () => { - setModelData( model, - 'Foo' + - 'Bar' - ); + // See: #8081. + it( 'should convert properly nested list styles', () => { + // ■ Level 0 + // ▶ Level 0.1 + // ○ Level 0.1.1 + // ▶ Level 0.2 + // ○ Level 0.2.1 + setModelData( model, + 'Level 0' + + 'Level 0.1' + + 'Level 0.1.1' + + 'Level 0.2' + + 'Level 0.2.1' + ); - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( - '
              • Foo
              • Bar
              ' - ); + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
                ' + + '
              • Level 0' + + '
                  ' + + '
                • Level 0.1' + + '
                    ' + + '
                  • Level 0.1.1
                  • ' + + '
                  ' + + '
                • ' + + '
                • Level 0.2' + + '
                    ' + + '
                  • Level 0.2.1
                  • ' + + '
                  ' + + '
                • ' + + '
                ' + + '
              • ' + + '
              ' + ); + } ); } ); + } ); - it( 'should convert nested bulleted lists (main: circle, nested: disc)', () => { - setModelData( model, - 'Foo 1' + - 'Bar 1' + - 'Bar 2' + - 'Foo 2' + - 'Foo 3' - ); - - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( - '
                ' + - '
              • Foo 1' + - '
                  ' + - '
                • Bar 1
                • ' + - '
                • Bar 2
                • ' + - '
                ' + - '
              • ' + - '
              • Foo 2
              • ' + - '
              • Foo 3
              • ' + - '
              ' - ); - } ); + describe( 'integrations', () => { + describe( 'merging a list into a styled list', () => { + it( 'should inherit the list style attribute when merging the same kind of lists (from top, merge a single item)', () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); - // See: #8081. - it( 'should convert properly nested list styles', () => { - // ■ Level 0 - // ▶ Level 0.1 - // ○ Level 0.1.1 - // ▶ Level 0.2 - // ○ Level 0.2.1 - setModelData( model, - 'Level 0' + - 'Level 0.1' + - 'Level 0.1.1' + - 'Level 0.2' + - 'Level 0.2.1' - ); + editor.execute( 'bulletedList' ); - expect( getViewData( view, { withoutSelection: true } ) ).to.equal( - '
                ' + - '
              • Level 0' + - '
                  ' + - '
                • Level 0.1' + - '
                    ' + - '
                  • Level 0.1.1
                  • ' + - '
                  ' + - '
                • ' + - '
                • Level 0.2' + - '
                    ' + - '
                  • Level 0.2.1
                  • ' + - '
                  ' + - '
                • ' + - '
                ' + - '
              • ' + - '
              ' - ); - } ); - } ); - } ); + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } ); - describe( 'integrations', () => { - describe( 'merging a list into a styled list', () => { - it( 'should inherit the list style attribute when merging the same kind of lists (from top, merge a single item)', () => { - setModelData( model, - 'Foo Bar.[]' + - 'Foo' + - 'Bar' - ); + it( 'should inherit the list style attribute when merging the same kind of lists (from top, merge a few items)', () => { + setModelData( model, + '[Foo Bar 1.' + + 'Foo Bar 2.]' + + 'Foo' + + 'Bar' + ); - editor.execute( 'bulletedList' ); + editor.execute( 'bulletedList' ); - expect( getModelData( model ) ).to.equal( - 'Foo Bar.[]' + - 'Foo' + - 'Bar' - ); - } ); + expect( getModelData( model ) ).to.equal( + '[Foo Bar 1.' + + 'Foo Bar 2.]' + + 'Foo' + + 'Bar' + ); + } ); - it( 'should inherit the list style attribute when merging the same kind of lists (from top, merge a few items)', () => { - setModelData( model, - '[Foo Bar 1.' + - 'Foo Bar 2.]' + - 'Foo' + - 'Bar' - ); + it( 'should not inherit anything if there is no list below the inserted list', () => { + setModelData( model, + 'Foo Bar 1.[]' + + 'Foo Bar 2.' + ); - editor.execute( 'bulletedList' ); + editor.execute( 'bulletedList' ); - expect( getModelData( model ) ).to.equal( - '[Foo Bar 1.' + - 'Foo Bar 2.]' + - 'Foo' + - 'Bar' - ); - } ); + expect( getModelData( model ) ).to.equal( + 'Foo Bar 1.[]' + + 'Foo Bar 2.' + ); + } ); - it( 'should not inherit anything if there is no list below the inserted list', () => { - setModelData( model, - 'Foo Bar 1.[]' + - 'Foo Bar 2.' - ); + it( 'should not inherit anything if replacing the entire content with a list', () => { + setModelData( model, + 'Foo Bar 1.[]' + ); - editor.execute( 'bulletedList' ); + editor.execute( 'bulletedList' ); - expect( getModelData( model ) ).to.equal( - 'Foo Bar 1.[]' + - 'Foo Bar 2.' - ); - } ); + expect( getModelData( model ) ).to.equal( + 'Foo Bar 1.[]' + ); + } ); - it( 'should not inherit anything if replacing the entire content with a list', () => { - setModelData( model, - 'Foo Bar 1.[]' - ); + it( + 'should not inherit the list style attribute when merging different kind of lists (from top, merge a single item)', + () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); - editor.execute( 'bulletedList' ); + editor.execute( 'bulletedList' ); - expect( getModelData( model ) ).to.equal( - 'Foo Bar 1.[]' - ); - } ); + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } ); - it( 'should not inherit the list style attribute when merging different kind of lists (from top, merge a single item)', () => { - setModelData( model, - 'Foo Bar.[]' + - 'Foo' + - 'Bar' + it( + 'should not inherit the list style attribute when merging different kind of lists (from bottom, merge a single item)', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'bulletedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + } ); - editor.execute( 'bulletedList' ); + it( + 'should inherit the list style attribute when merging the same kind of lists (from bottom, merge a single item)', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'bulletedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + } ); - expect( getModelData( model ) ).to.equal( - 'Foo Bar.[]' + - 'Foo' + - 'Bar' + it( + 'should inherit the list style attribute from listIndent=0 element when merging the same kind of lists (from bottom)', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'bulletedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar' + + 'Foo Bar.[]' + ); + } ); } ); - it( - 'should not inherit the list style attribute when merging different kind of lists (from bottom, merge a single item)', - () => { + describe( 'modifying "listType" attribute', () => { + it( 'should inherit the list style attribute when the modified list is the same kind of the list as next sibling', () => { setModelData( model, - 'Foo' + - 'Bar' + - 'Foo Bar.[]' + 'Foo Bar.[]' + + 'Foo' + + 'Bar' ); editor.execute( 'bulletedList' ); expect( getModelData( model ) ).to.equal( - 'Foo' + - 'Bar' + - 'Foo Bar.[]' + 'Foo Bar.[]' + + 'Foo' + + 'Bar' ); - } - ); + } ); - it( 'should inherit the list style attribute when merging the same kind of lists (from bottom, merge a single item)', () => { - setModelData( model, - 'Foo' + - 'Bar' + - 'Foo Bar.[]' - ); + it( + 'should inherit the list style attribute when the modified list is the same kind of the list as previous sibling', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'bulletedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + } ); - editor.execute( 'bulletedList' ); + it( + 'should not inherit the list style attribute when the modified list already has defined it (next sibling check)', + () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'listStyle', { type: 'disc' } ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } ); - expect( getModelData( model ) ).to.equal( - 'Foo' + - 'Bar' + - 'Foo Bar.[]' + it( + 'should not inherit the list style attribute when the modified list already has defined it (previous sibling check)', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'listStyle', { type: 'disc' } ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + } ); } ); - it( - 'should inherit the list style attribute from listIndent=0 element when merging the same kind of lists (from bottom)', - () => { + describe( 'indenting lists', () => { + it( 'should restore the default value for the list style attribute when indenting a single item', () => { setModelData( model, - 'Foo' + - 'Bar' + - 'Foo Bar' + - 'Foo Bar.[]' + '1.' + + '1A.' + + '2B.' + + '2.[]' + + '3.' ); - editor.execute( 'bulletedList' ); + editor.execute( 'indentList' ); expect( getModelData( model ) ).to.equal( - 'Foo' + - 'Bar' + - 'Foo Bar' + - 'Foo Bar.[]' + '1.' + + '1A.' + + '2B.' + + '2.[]' + + '3.' ); - } - ); - } ); - - describe( 'modifying "listType" attribute', () => { - it( 'should inherit the list style attribute when the modified list is the same kind of the list as next sibling', () => { - setModelData( model, - 'Foo Bar.[]' + - 'Foo' + - 'Bar' - ); - - editor.execute( 'bulletedList' ); - - expect( getModelData( model ) ).to.equal( - 'Foo Bar.[]' + - 'Foo' + - 'Bar' - ); - } ); - - it( 'should inherit the list style attribute when the modified list is the same kind of the list as previous sibling', () => { - setModelData( model, - 'Foo' + - 'Bar' + - 'Foo Bar.[]' - ); - - editor.execute( 'bulletedList' ); - - expect( getModelData( model ) ).to.equal( - 'Foo' + - 'Bar' + - 'Foo Bar.[]' - ); - } ); - - it( 'should not inherit the list style attribute when the modified list already has defined it (next sibling check)', () => { - setModelData( model, - 'Foo Bar.[]' + - 'Foo' + - 'Bar' - ); - - editor.execute( 'listStyle', { type: 'disc' } ); - - expect( getModelData( model ) ).to.equal( - 'Foo Bar.[]' + - 'Foo' + - 'Bar' - ); - } ); + } ); - it( - 'should not inherit the list style attribute when the modified list already has defined it (previous sibling check)', - () => { + it( 'should restore the default value for the list style attribute when indenting a few items', () => { setModelData( model, - 'Foo' + - 'Bar' + - 'Foo Bar.[]' + '1.' + + '[2.' + + '3.]' ); - editor.execute( 'listStyle', { type: 'disc' } ); + editor.execute( 'indentList' ); expect( getModelData( model ) ).to.equal( - 'Foo' + - 'Bar' + - 'Foo Bar.[]' + '1.' + + '[2.' + + '3.]' ); - } - ); - } ); - - describe( 'indenting lists', () => { - it( 'should restore the default value for the list style attribute when indenting a single item', () => { - setModelData( model, - '1.' + - '1A.' + - '2B.' + - '2.[]' + - '3.' - ); - - editor.execute( 'indentList' ); - - expect( getModelData( model ) ).to.equal( - '1.' + - '1A.' + - '2B.' + - '2.[]' + - '3.' - ); - } ); + } ); - it( 'should restore the default value for the list style attribute when indenting a few items', () => { - setModelData( model, - '1.' + - '[2.' + - '3.]' + it( + 'should copy the value for the list style attribute when indenting a single item into a nested list (default value)', + () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + + '4.' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + + '4.' + ); + } ); - editor.execute( 'indentList' ); - - expect( getModelData( model ) ).to.equal( - '1.' + - '[2.' + - '3.]' + it( + 'should copy the value for the list style attribute when indenting a single item into a nested list (changed value)', + () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + + '4.' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + + '4.' + ); + } ); - } ); - it( - 'should copy the value for the list style attribute when indenting a single item into a nested list (default value)', - () => { + it( 'should copy the value for the list style attribute when indenting a single item into a nested list', () => { setModelData( model, '1.' + - '2.' + - '3.[]' + + '2.[]' + + '3.' + '4.' ); @@ -719,546 +808,553 @@ describe( 'ListStyleEditing', () => { expect( getModelData( model ) ).to.equal( '1.' + - '2.' + - '3.[]' + + '2.[]' + + '3.' + '4.' ); - } - ); + } ); + + it( + 'should copy the value for the list style attribute when indenting a single item into a nested list ' + + '(many nested lists check)', + () => { + setModelData( model, + '1.' + + '2.' + + '3.' + + '4.[]' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.' + + '4.[]' + ); + } + ); - it( - 'should copy the value for the list style attribute when indenting a single item into a nested list (changed value)', - () => { + it( 'should inherit the list style attribute from nested list if the `listType` is other than indenting element', () => { setModelData( model, '1.' + - '2.' + - '3.[]' + - '4.' + '2.' + + '3.[]' ); editor.execute( 'indentList' ); expect( getModelData( model ) ).to.equal( '1.' + - '2.' + - '3.[]' + - '4.' + '2.' + + '3.[]' ); - } - ); - - it( 'should copy the value for the list style attribute when indenting a single item into a nested list', () => { - setModelData( model, - '1.' + - '2.[]' + - '3.' + - '4.' - ); - - editor.execute( 'indentList' ); - - expect( getModelData( model ) ).to.equal( - '1.' + - '2.[]' + - '3.' + - '4.' - ); - } ); + } ); - it( - 'should copy the value for the list style attribute when indenting a single item into a nested list ' + - '(many nested lists check)', - () => { + // See: #8072. + it( 'should not throw when indenting a list without any other content in the editor', () => { setModelData( model, - '1.' + - '2.' + - '3.' + - '4.[]' + 'Foo' + + '[]' ); editor.execute( 'indentList' ); expect( getModelData( model ) ).to.equal( - '1.' + - '2.' + - '3.' + - '4.[]' + 'Foo' + + '[]' ); - } - ); - - it( 'should inherit the list style attribute from nested list if the `listType` is other than indenting element', () => { - setModelData( model, - '1.' + - '2.' + - '3.[]' - ); - - editor.execute( 'indentList' ); - - expect( getModelData( model ) ).to.equal( - '1.' + - '2.' + - '3.[]' - ); + } ); } ); - // See: #8072. - it( 'should not throw when indenting a list without any other content in the editor', () => { - setModelData( model, - 'Foo' + - '[]' - ); + describe( 'outdenting lists', () => { + it( 'should inherit the list style attribute from parent list (change the first nested item)', () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + ); - editor.execute( 'indentList' ); + editor.execute( 'outdentList' ); - expect( getModelData( model ) ).to.equal( - 'Foo' + - '[]' - ); - } ); - } ); + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '3.' + ); + } ); - describe( 'outdenting lists', () => { - it( 'should inherit the list style attribute from parent list (change the first nested item)', () => { - setModelData( model, - '1.' + - '2.[]' + - '3.' - ); + it( 'should inherit the list style attribute from parent list (change the second nested item)', () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + ); - editor.execute( 'outdentList' ); + editor.execute( 'outdentList' ); - expect( getModelData( model ) ).to.equal( - '1.' + - '2.[]' + - '3.' - ); - } ); + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + ); + } ); - it( 'should inherit the list style attribute from parent list (change the second nested item)', () => { - setModelData( model, - '1.' + - '2.' + - '3.[]' - ); + it( 'should inherit the list style attribute from parent list (modifying nested lists)', () => { + setModelData( model, + '1.' + + '[2.' + + '3.]' + ); - editor.execute( 'outdentList' ); + editor.execute( 'outdentList' ); - expect( getModelData( model ) ).to.equal( - '1.' + - '2.' + - '3.[]' - ); - } ); + expect( getModelData( model ) ).to.equal( + '1.' + + '[2.' + + '3.]' + ); + } ); - it( 'should inherit the list style attribute from parent list (modifying nested lists)', () => { - setModelData( model, - '1.' + - '[2.' + - '3.]' + it( + 'should inherit the list style attribute from parent list (outdenting many items, including the first one in the list)', + () => { + setModelData( model, + '[1.' + + '2.' + + '3.]' + + '4.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '[1.' + + '2.' + + '3.]' + + '4.' + ); + } ); - editor.execute( 'outdentList' ); - - expect( getModelData( model ) ).to.equal( - '1.' + - '[2.' + - '3.]' + it( + 'should inherit the list style attribute from parent list (outdenting the first item that is a parent for next list)', + () => { + setModelData( model, + '1.[]' + + '2.' + + '3.' + + '4.' + + '5.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.[]' + + '2.' + + '3.' + + '4.' + + '5.' + ); + } ); - } ); - it( - 'should inherit the list style attribute from parent list (outdenting many items, including the first one in the list)', - () => { + it( 'should not inherit the list style if outdented the only one item in the list', () => { setModelData( model, - '[1.' + - '2.' + - '3.]' + - '4.' + '1.[]' + + '2.' + + '3.' ); editor.execute( 'outdentList' ); expect( getModelData( model ) ).to.equal( - '[1.' + - '2.' + - '3.]' + - '4.' + '1.[]' + + '2.' + + '3.' ); - } - ); + } ); - it( - 'should inherit the list style attribute from parent list (outdenting the first item that is a parent for next list)', - () => { + it( 'should not inherit the list style if outdented the only one item in the list (a paragraph below the list)', () => { setModelData( model, '1.[]' + - '2.' + + '2.' + '3.' + - '4.' + - '5.' + 'Foo' ); editor.execute( 'outdentList' ); expect( getModelData( model ) ).to.equal( '1.[]' + - '2.' + + '2.' + '3.' + - '4.' + - '5.' + 'Foo' ); - } - ); - - it( 'should not inherit the list style if outdented the only one item in the list', () => { - setModelData( model, - '1.[]' + - '2.' + - '3.' - ); - - editor.execute( 'outdentList' ); - - expect( getModelData( model ) ).to.equal( - '1.[]' + - '2.' + - '3.' - ); - } ); - - it( 'should not inherit the list style if outdented the only one item in the list (a paragraph below the list)', () => { - setModelData( model, - '1.[]' + - '2.' + - '3.' + - 'Foo' - ); - - editor.execute( 'outdentList' ); + } ); - expect( getModelData( model ) ).to.equal( - '1.[]' + - '2.' + - '3.' + - 'Foo' - ); - } ); + it( + 'should not inherit the list style attribute from parent list if the `listType` is other than outdenting element', + () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '3.' + ); + } ); - it( 'should not inherit the list style attribute from parent list if the `listType` is other than outdenting element', () => { - setModelData( model, - '1.' + - '2.[]' + - '3.' - ); + it( 'should not do anything if there is no list after outdenting', () => { + setModelData( model, + '1.[]' + ); - editor.execute( 'outdentList' ); + editor.execute( 'outdentList' ); - expect( getModelData( model ) ).to.equal( - '1.' + - '2.[]' + - '3.' - ); + expect( getModelData( model ) ).to.equal( + '1.[]' + ); + } ); } ); - it( 'should not do anything if there is no list after outdenting', () => { - setModelData( model, - '1.[]' - ); + describe( 'indent/outdent + undo', () => { + it( 'should use the same batch for indenting a list and updating `listType` attribute', () => { + setModelData( model, + '1.' + + '1A.' + + '2B.' + + '2.[]' + + '3.' + ); - editor.execute( 'outdentList' ); + editor.execute( 'indentList' ); + editor.execute( 'undo' ); - expect( getModelData( model ) ).to.equal( - '1.[]' - ); - } ); - } ); + expect( getModelData( model ) ).to.equal( + '1.' + + '1A.' + + '2B.' + + '2.[]' + + '3.' + ); + } ); - describe( 'indent/outdent + undo', () => { - it( 'should use the same batch for indenting a list and updating `listType` attribute', () => { - setModelData( model, - '1.' + - '1A.' + - '2B.' + - '2.[]' + - '3.' - ); + it( 'should use the same batch for outdenting a list and updating `listType` attribute', () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + ); - editor.execute( 'indentList' ); - editor.execute( 'undo' ); + editor.execute( 'outdentList' ); + editor.execute( 'undo' ); - expect( getModelData( model ) ).to.equal( - '1.' + - '1A.' + - '2B.' + - '2.[]' + - '3.' - ); + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '3.' + ); + } ); } ); - it( 'should use the same batch for outdenting a list and updating `listType` attribute', () => { - setModelData( model, - '1.' + - '2.[]' + - '3.' - ); - - editor.execute( 'outdentList' ); - editor.execute( 'undo' ); - - expect( getModelData( model ) ).to.equal( - '1.' + - '2.[]' + - '3.' - ); - } ); - } ); + describe( 'delete + undo', () => { + let editor, model, view; - describe( 'delete + undo', () => { - let editor, model, view; - - beforeEach( () => { - return VirtualTestEditor - .create( { - plugins: [ Paragraph, ListStyleEditing, Typing, UndoEditing ] - } ) - .then( newEditor => { - editor = newEditor; - model = editor.model; - view = editor.editing.view; - } ); - } ); + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, Typing, UndoEditing ], + list: { + properties: { styles: true, startIndex: false, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + view = editor.editing.view; + } ); + } ); - afterEach( () => { - return editor.destroy(); - } ); + afterEach( () => { + return editor.destroy(); + } ); - // See: #7930. - it( 'should restore proper list style attribute after undo merging lists', () => { + // See: #7930. + it( 'should restore proper list style attribute after undo merging lists', () => { // ○ 1. // ○ 2. // ○ 3. // // ■ 1. // ■ 2. - setModelData( model, - '1.' + - '2.' + - '3.' + - '[]' + - '1.' + - '2.' - ); + setModelData( model, + '1.' + + '2.' + + '3.' + + '[]' + + '1.' + + '2.' + ); - expect( getViewData( view, { withoutSelection: true } ), 'initial data' ).to.equal( - '
                ' + - '
              • 1.
              • ' + - '
              • 2.
              • ' + - '
              • 3.
              • ' + - '
              ' + - '

              ' + - '
                ' + - '
              • 1.
              • ' + - '
              • 2.
              • ' + - '
              ' - ); + expect( getViewData( view, { withoutSelection: true } ), 'initial data' ).to.equal( + '
                ' + + '
              • 1.
              • ' + + '
              • 2.
              • ' + + '
              • 3.
              • ' + + '
              ' + + '

              ' + + '
                ' + + '
              • 1.
              • ' + + '
              • 2.
              • ' + + '
              ' + ); - // After removing the paragraph. - // ○ 1. - // ○ 2. - // ○ 3. - // ○ 1. - // ○ 2. - editor.execute( 'delete' ); - - expect( getViewData( view, { withoutSelection: true } ), 'executing delete' ).to.equal( - '
                ' + - '
              • 1.
              • ' + - '
              • 2.
              • ' + - '
              • 3.
              • ' + - '
              • 1.
              • ' + - '
              • 2.
              • ' + - '
              ' - ); + // After removing the paragraph. + // ○ 1. + // ○ 2. + // ○ 3. + // ○ 1. + // ○ 2. + editor.execute( 'delete' ); - // After undo. - // ○ 1. - // ○ 2. - // ○ 3. - // - // ■ 1. - // ■ 2. - editor.execute( 'undo' ); - - expect( getViewData( view, { withoutSelection: true } ), 'initial data' ).to.equal( - '
                ' + - '
              • 1.
              • ' + - '
              • 2.
              • ' + - '
              • 3.
              • ' + - '
              ' + - '

              ' + - '
                ' + - '
              • 1.
              • ' + - '
              • 2.
              • ' + - '
              ' - ); + expect( getViewData( view, { withoutSelection: true } ), 'executing delete' ).to.equal( + '
                ' + + '
              • 1.
              • ' + + '
              • 2.
              • ' + + '
              • 3.
              • ' + + '
              • 1.
              • ' + + '
              • 2.
              • ' + + '
              ' + ); + + // After undo. + // ○ 1. + // ○ 2. + // ○ 3. + // + // ■ 1. + // ■ 2. + editor.execute( 'undo' ); + + expect( getViewData( view, { withoutSelection: true } ), 'initial data' ).to.equal( + '
                ' + + '
              • 1.
              • ' + + '
              • 2.
              • ' + + '
              • 3.
              • ' + + '
              ' + + '

              ' + + '
                ' + + '
              • 1.
              • ' + + '
              • 2.
              • ' + + '
              ' + ); + } ); } ); - } ); - describe( 'todo list', () => { - let editor, model; + describe( 'todo list', () => { + let editor, model; - beforeEach( () => { - return VirtualTestEditor - .create( { + beforeEach( () => { + return VirtualTestEditor + .create( { // TodoListEditing is at the end by design. Check `ListStyleEditing.afterInit()` call. - plugins: [ Paragraph, ListStyleEditing, TodoListEditing ] - } ) - .then( newEditor => { - editor = newEditor; - model = editor.model; - } ); - } ); + plugins: [ Paragraph, ListStyleEditing, TodoListEditing ], + list: { + properties: { styles: true, startIndex: false, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + } ); + } ); - afterEach( () => { - return editor.destroy(); - } ); + afterEach( () => { + return editor.destroy(); + } ); - it( 'should not add the `listStyle` attribute while creating a todo list', () => { - setModelData( model, 'Foo[]' ); + it( 'should not add the `listStyle` attribute while creating a todo list', () => { + setModelData( model, 'Foo[]' ); - editor.execute( 'todoList' ); + editor.execute( 'todoList' ); - expect( getModelData( model ), 'Foo[]' ); - } ); + expect( getModelData( model ), 'Foo[]' ); + } ); - it( 'should not add the `listStyle` attribute while switching the list type', () => { - setModelData( model, 'Foo[]' ); + it( 'should not add the `listStyle` attribute while switching the list type', () => { + setModelData( model, 'Foo[]' ); - editor.execute( 'todoList' ); + editor.execute( 'todoList' ); - expect( getModelData( model ), 'Foo[]' ); - } ); + expect( getModelData( model ), 'Foo[]' ); + } ); - it( 'should remove the `listStyle` attribute while switching the list type that uses the list style feature', () => { - setModelData( model, 'Foo[]' ); + it( 'should remove the `listStyle` attribute while switching the list type that uses the list style feature', () => { + setModelData( model, 'Foo[]' ); - editor.execute( 'todoList' ); + editor.execute( 'todoList' ); - expect( getModelData( model ), 'Foo[]' ); - } ); + expect( getModelData( model ), 'Foo[]' ); + } ); - it( 'should not inherit the list style attribute when inserting a todo list item', () => { - setModelData( model, - 'Foo Bar.[]' + - 'Foo' + - 'Bar' - ); + it( 'should not inherit the list style attribute when inserting a todo list item', () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); - editor.execute( 'todoList' ); + editor.execute( 'todoList' ); - expect( getModelData( model ) ).to.equal( - 'Foo Bar.[]' + - 'Foo' + - 'Bar' - ); - } ); - } ); + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } ); - describe( 'removing content between two lists', () => { - let editor, model; - - beforeEach( () => { - return VirtualTestEditor - .create( { - plugins: [ Paragraph, ListStyleEditing, Typing ] - } ) - .then( newEditor => { - editor = newEditor; - model = editor.model; + it( 'should not allow to set the `listStyle` attribute in to-do list item', () => { + setModelData( model, 'Foo' ); + + const listItem = model.document.getRoot().getChild( 0 ); + + expect( listItem.hasAttribute( 'listStyle' ) ).to.be.false; + + model.change( writer => { + writer.setAttribute( 'listStyle', 'foo', listItem ); } ); - } ); - afterEach( () => { - return editor.destroy(); + expect( listItem.hasAttribute( 'listStyle' ) ).to.be.false; + } ); } ); - it( 'should not do anything while removing a letter inside a listItem', () => { - setModelData( model, - '1.' + - '2.[]' + - '' + - '1.' + - '2.' - ); + describe( 'removing content between two lists', () => { + let editor, model; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, Typing ], + list: { + properties: { styles: true, startIndex: false, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + } ); + } ); - editor.execute( 'delete' ); + afterEach( () => { + return editor.destroy(); + } ); - expect( getModelData( model ) ).to.equal( - '1.' + - '2[]' + - '' + - '1.' + - '2.' - ); - } ); + it( 'should not do anything while removing a letter inside a listItem', () => { + setModelData( model, + '1.' + + '2.[]' + + '' + + '1.' + + '2.' + ); - it( 'should not do anything if there is a non-listItem before the removed content', () => { - setModelData( model, - 'Foo' + - '[]' + - '1.' + - '2.' - ); + editor.execute( 'delete' ); - editor.execute( 'delete' ); + expect( getModelData( model ) ).to.equal( + '1.' + + '2[]' + + '' + + '1.' + + '2.' + ); + } ); - expect( getModelData( model ) ).to.equal( - 'Foo[]' + - '1.' + - '2.' - ); - } ); + it( 'should not do anything if there is a non-listItem before the removed content', () => { + setModelData( model, + 'Foo' + + '[]' + + '1.' + + '2.' + ); - it( 'should not do anything if there is a non-listItem after the removed content', () => { - setModelData( model, - '1.' + - '2.' + - '[]' + - 'Foo' - ); + editor.execute( 'delete' ); - editor.execute( 'delete' ); + expect( getModelData( model ) ).to.equal( + 'Foo[]' + + '1.' + + '2.' + ); + } ); - expect( getModelData( model ) ).to.equal( - '1.' + - '2.[]' + - 'Foo' - ); - } ); + it( 'should not do anything if there is a non-listItem after the removed content', () => { + setModelData( model, + '1.' + + '2.' + + '[]' + + 'Foo' + ); - it( 'should not do anything if there is no element after the removed content', () => { - setModelData( model, - '1.' + - '2.' + - '[]' - ); + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + 'Foo' + ); + } ); + + it( 'should not do anything if there is no element after the removed content', () => { + setModelData( model, + '1.' + + '2.' + + '[]' + ); + + editor.execute( 'delete' ); - editor.execute( 'delete' ); + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + ); + } ); - expect( getModelData( model ) ).to.equal( - '1.' + - '2.[]' + it( + 'should modify the the `listStyle` attribute for the merged (second) list when removing content between those lists', + () => { + setModelData( model, + '1.' + + '2.' + + '[]' + + '1.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '1.' + + '2.' + ); + } ); - } ); - it( - 'should modify the the `listStyle` attribute for the merged (second) list when removing content between those lists', - () => { + it( 'should read the `listStyle` attribute from the most outer list', () => { setModelData( model, '1.' + '2.' + + '2.1.' + + '2.1.1' + '[]' + '1.' + '2.' @@ -1268,411 +1364,4212 @@ describe( 'ListStyleEditing', () => { expect( getModelData( model ) ).to.equal( '1.' + - '2.[]' + + '2.' + + '2.1.' + + '2.1.1[]' + '1.' + '2.' ); - } - ); - - it( 'should read the `listStyle` attribute from the most outer list', () => { - setModelData( model, - '1.' + - '2.' + - '2.1.' + - '2.1.1' + - '[]' + - '1.' + - '2.' + } ); + + it( + 'should not modify the the `listStyle` attribute for the merged (second) list ' + + 'if merging different `listType` attribute', + () => { + setModelData( model, + '1.' + + '2.' + + '[]' + + '1.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '1.' + + '2.' + ); + } ); - editor.execute( 'delete' ); + it( + 'should modify the the `listStyle` attribute for the merged (second) list when removing content from both lists', + () => { + setModelData( model, + '1.' + + '2.' + + '[3.' + + 'Foo' + + '1.]' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '[]' + + '2.' + ); + } + ); - expect( getModelData( model ) ).to.equal( - '1.' + - '2.' + - '2.1.' + - '2.1.1[]' + - '1.' + - '2.' + it( + 'should modify the the `listStyle` attribute for the merged (second) list when typing over content from both lists', + () => { + setModelData( model, + '1.' + + '2.' + + '[3.' + + 'Foo' + + '1.]' + + '2.' + ); + + editor.execute( 'input', { text: 'Foo' } ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + 'Foo[]' + + '2.' + ); + } + ); + + it( + 'should not modify the the `listStyle` if lists were not merged but the content was partially removed', + () => { + setModelData( model, + '111.' + + '222.' + + '[333.' + + 'Foo' + + '1]11.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '111.' + + '222.' + + '[]11.' + + '2.' + ); + } ); - } ); - it( - 'should not modify the the `listStyle` attribute for the merged (second) list if merging different `listType` attribute', - () => { + it( 'should not do anything while typing in a list item', () => { setModelData( model, '1.' + - '2.' + - '[]' + - '1.' + - '2.' + '2.[]' + + '3.' + + '' + + '1.' + + '2.' ); - editor.execute( 'delete' ); + const modelChangeStub = sinon.stub( model, 'change' ).callThrough(); + + simulateTyping( ' Foo' ); + + // Each character calls `editor.model.change()`. + expect( modelChangeStub.callCount ).to.equal( 4 ); expect( getModelData( model ) ).to.equal( '1.' + - '2.[]' + - '1.' + - '2.' + '2. Foo[]' + + '3.' + + '' + + '1.' + + '2.' ); - } - ); + } ); - it( - 'should modify the the `listStyle` attribute for the merged (second) list when removing content from both lists', - () => { + // See: #8073. + it( 'should not crash when removing a content between intended lists', () => { setModelData( model, - '1.' + - '2.' + - '[3.' + - 'Foo' + - '1.]' + - '2.' + 'aaaa' + + 'bb[bb' + + 'cc]cc' + + 'dddd' ); editor.execute( 'delete' ); expect( getModelData( model ) ).to.equal( - '1.' + - '2.' + - '[]' + - '2.' + 'aaaa' + + 'bb[]cc' + + 'dddd' ); - } - ); + } ); - it( - 'should modify the the `listStyle` attribute for the merged (second) list when typing over content from both lists', - () => { + it( 'should read the `listStyle` attribute from the most outer selected list while removing content between lists', () => { setModelData( model, '1.' + '2.' + - '[3.' + + '2.1.' + + '2.1.1[foo' + 'Foo' + - '1.]' + - '2.' + '1.' + + 'bar]2.' ); - editor.execute( 'input', { text: 'Foo' } ); + editor.execute( 'delete' ); expect( getModelData( model ) ).to.equal( '1.' + '2.' + - 'Foo[]' + - '2.' + '2.1.' + + '2.1.1[]2.' ); + } ); + + function simulateTyping( text ) { + // While typing, every character is an atomic change. + text.split( '' ).forEach( character => { + editor.execute( 'input', { + text: character + } ); + } ); } - ); + } ); + + // #8160 + describe( 'pasting a list into another list', () => { + let element; + + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.append( element ); + + return ClassicTestEditor + .create( element, { + plugins: [ Paragraph, Clipboard, ListStyleEditing, UndoEditing ], + list: { + properties: { styles: true, startIndex: false, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + } ); + } ); + + afterEach( () => { + return editor.destroy() + .then( () => { + element.remove(); + } ); + } ); - it( - 'should not modify the the `listStyle` if lists were not merged but the content was partially removed', - () => { + it( 'should inherit attributes from the previous sibling element (collapsed selection)', () => { setModelData( model, - '111.' + - '222.' + - '[333.' + - 'Foo' + - '1]11.' + - '2.' + 'Foo' + + 'Foo Bar' + + '[]' + + 'Bar' ); - editor.execute( 'delete' ); + pasteHtml( editor, + '
                ' + + '
              • Foo 1
              • ' + + '
              • Foo 2
              • ' + + '
              ' + ); expect( getModelData( model ) ).to.equal( - '111.' + - '222.' + - '[]11.' + - '2.' + 'Foo' + + 'Foo Bar' + + 'Foo 1' + + 'Foo 2[]' + + 'Bar' ); - } - ); - - it( 'should not do anything while typing in a list item', () => { - setModelData( model, - '1.' + - '2.[]' + - '3.' + - '' + - '1.' + - '2.' - ); + } ); - const modelChangeStub = sinon.stub( model, 'change' ).callThrough(); + it( 'should inherit attributes from the previous sibling element (non-collapsed selection)', () => { + setModelData( model, + 'Foo' + + 'Foo Bar' + + '[Foo]' + + 'Bar' + ); - simulateTyping( ' Foo' ); + pasteHtml( editor, + '
                ' + + '
              • Foo 1
              • ' + + '
              • Foo 2
              • ' + + '
              ' + ); - // Each character calls `editor.model.change()`. - expect( modelChangeStub.callCount ).to.equal( 4 ); + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo Bar' + + 'Foo 1' + + 'Foo 2[]' + + 'Bar' + ); + } ); - expect( getModelData( model ) ).to.equal( - '1.' + - '2. Foo[]' + - '3.' + - '' + - '1.' + - '2.' - ); - } ); + it( 'should inherit attributes from the previous sibling element (non-collapsed selection over a few elements)', () => { + setModelData( model, + 'Foo' + + 'Foo Bar' + + '[Foo 1.' + + 'Foo 2.' + + 'Foo 3.]' + + 'Bar' + ); - // See: #8073. - it( 'should not crash when removing a content between intended lists', () => { - setModelData( model, - 'aaaa' + - 'bb[bb' + - 'cc]cc' + - 'dddd' - ); + pasteHtml( editor, + '
                ' + + '
              • Foo 1
              • ' + + '
              • Foo 2
              • ' + + '
              ' + ); - editor.execute( 'delete' ); + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo Bar' + + 'Foo 1' + + 'Foo 2[]' + + 'Bar' + ); + } ); - expect( getModelData( model ) ).to.equal( - 'aaaa' + - 'bb[]cc' + - 'dddd' - ); - } ); + it( 'should do nothing when pasting the similar list', () => { + setModelData( model, + 'Foo' + + 'Foo Bar' + + '[]' + + 'Bar' + ); - it( 'should read the `listStyle` attribute from the most outer selected list while removing content between lists', () => { - setModelData( model, - '1.' + - '2.' + - '2.1.' + - '2.1.1[foo' + - 'Foo' + - '1.' + - 'bar]2.' - ); + pasteHtml( editor, + '
                ' + + '
              1. Foo
              2. ' + + '
              ' + ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo Bar' + + 'Foo[]' + + 'Bar' + ); + } ); - editor.execute( 'delete' ); + it( 'should replace the entire list if selected', () => { + setModelData( model, + 'Foo' + + '[Foo Bar]' + + 'Bar' + ); - expect( getModelData( model ) ).to.equal( - '1.' + - '2.' + - '2.1.' + - '2.1.1[]2.' - ); - } ); + pasteHtml( editor, + '
                ' + + '
              • Foo
              • ' + + '
              ' + ); - function simulateTyping( text ) { - // While typing, every character is an atomic change. - text.split( '' ).forEach( character => { - editor.execute( 'input', { - text: character - } ); + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo[]' + + 'Bar' + ); } ); - } - } ); - // #8160 - describe( 'pasting a list into another list', () => { - let element; - - beforeEach( () => { - element = document.createElement( 'div' ); - document.body.append( element ); - - return ClassicTestEditor - .create( element, { - plugins: [ Paragraph, Clipboard, ListStyleEditing, UndoEditing ] - } ) - .then( newEditor => { - editor = newEditor; - model = editor.model; + function pasteHtml( editor, html ) { + editor.editing.view.document.fire( 'paste', { + dataTransfer: createDataTransfer( { 'text/html': html } ), + stopPropagation() {}, + preventDefault() {} } ); - } ); + } - afterEach( () => { - return editor.destroy() - .then( () => { - element.remove(); - } ); + function createDataTransfer( data ) { + return { + getData( type ) { + return data[ type ]; + }, + setData() {} + }; + } } ); - it( 'should inherit attributes from the previous sibling element (collapsed selection)', () => { - setModelData( model, - 'Foo' + - 'Foo Bar' + - '[]' + - 'Bar' - ); + describe( 'the FontColor feature', () => { + let editor, view, container; - pasteHtml( editor, - '
                ' + - '
              • Foo 1
              • ' + - '
              • Foo 2
              • ' + - '
              ' - ); + beforeEach( () => { + container = document.createElement( 'div' ); + document.body.appendChild( container ); - expect( getModelData( model ) ).to.equal( - 'Foo' + - 'Foo Bar' + - 'Foo 1' + - 'Foo 2[]' + - 'Bar' - ); - } ); + return ClassicTestEditor + .create( container, { + plugins: [ Paragraph, ListStyleEditing, FontColor, Typing ] + } ) + .then( newEditor => { + editor = newEditor; + view = editor.editing.view; + } ); + } ); - it( 'should inherit attributes from the previous sibling element (non-collapsed selection)', () => { - setModelData( model, - 'Foo' + - 'Foo Bar' + - '[Foo]' + - 'Bar' - ); + afterEach( () => { + container.remove(); - pasteHtml( editor, - '
                ' + - '
              • Foo 1
              • ' + - '
              • Foo 2
              • ' + - '
              ' - ); + return editor.destroy(); + } ); - expect( getModelData( model ) ).to.equal( - 'Foo' + - 'Foo Bar' + - 'Foo 1' + - 'Foo 2[]' + - 'Bar' - ); + describe( 'spellchecking integration', () => { + it( 'should not throw if a children mutation was fired over colorized text', () => { + editor.setData( + '
                ' + + '
              • helllo
              • ' + + '
              ' + ); + + const viewRoot = view.document.getRoot(); + const viewLi = viewRoot.getChild( 0 ).getChild( 0 ); + + // This should not throw. See #9325. + view.document.fire( 'mutations', + [ + { + type: 'children', + oldChildren: [ + viewLi.getChild( 0 ) + ], + newChildren: view.change( writer => [ + writer.createContainerElement( 'font' ) + ] ), + node: viewLi + } + ] + ); + } ); + } ); } ); + } ); + } ); - it( 'should inherit attributes from the previous sibling element (non-collapsed selection over a few elements)', () => { - setModelData( model, - 'Foo' + - 'Foo Bar' + - '[Foo 1.' + - 'Foo 2.' + - 'Foo 3.]' + - 'Bar' - ); + describe( 'listReversed', () => { + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, UndoEditing ], + list: { + properties: { styles: false, startIndex: false, reversed: true } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + view = editor.editing.view; + } ); + } ); - pasteHtml( editor, - '
                ' + - '
              • Foo 1
              • ' + - '
              • Foo 2
              • ' + - '
              ' - ); + afterEach( () => { + return editor.destroy(); + } ); - expect( getModelData( model ) ).to.equal( - 'Foo' + - 'Foo Bar' + - 'Foo 1' + - 'Foo 2[]' + - 'Bar' - ); + describe( 'schema rules', () => { + it( 'should not allow set `listStyle` on the `listItem`', () => { + expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listStyle' ) ).to.be.false; } ); - it( 'should do nothing when pasting the similar list', () => { - setModelData( model, - 'Foo' + - 'Foo Bar' + - '[]' + - 'Bar' - ); - - pasteHtml( editor, - '
                ' + - '
              1. Foo
              2. ' + - '
              ' - ); - - expect( getModelData( model ) ).to.equal( - 'Foo' + - 'Foo Bar' + - 'Foo[]' + - 'Bar' - ); + it( 'should allow set `listReversed` on the `listItem`', () => { + expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listReversed' ) ).to.be.true; } ); - it( 'should replace the entire list if selected', () => { - setModelData( model, - 'Foo' + - '[Foo Bar]' + - 'Bar' - ); - - pasteHtml( editor, - '
                ' + - '
              • Foo
              • ' + - '
              ' - ); - - expect( getModelData( model ) ).to.equal( - 'Foo' + - 'Foo[]' + - 'Bar' - ); + it( 'should not allow set `listStart` on the `listItem`', () => { + expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listStart' ) ).to.be.false; } ); - - function pasteHtml( editor, html ) { - editor.editing.view.document.fire( 'paste', { - dataTransfer: createDataTransfer( { 'text/html': html } ), - stopPropagation() {}, - preventDefault() {} - } ); - } - - function createDataTransfer( data ) { - return { - getData( type ) { - return data[ type ]; - }, - setData() {} - }; - } } ); - describe( 'the FontColor feature', () => { - let editor, view, container; + describe( 'command', () => { + it( 'should register `listReversed` command', () => { + const command = editor.commands.get( 'listReversed' ); - beforeEach( () => { - container = document.createElement( 'div' ); - document.body.appendChild( container ); + expect( command ).to.be.instanceOf( ListReversedCommand ); + } ); - return ClassicTestEditor - .create( container, { - plugins: [ Paragraph, ListStyleEditing, FontColor, Typing ] - } ) - .then( newEditor => { - editor = newEditor; - view = editor.editing.view; - } ); + it( 'should not register `listStyle` command', () => { + const command = editor.commands.get( 'listStyle' ); + + expect( command ).to.be.undefined; } ); - afterEach( () => { - container.remove(); + it( 'should not register `listStart` command', () => { + const command = editor.commands.get( 'listStart' ); - return editor.destroy(); + expect( command ).to.be.undefined; } ); + } ); - describe( 'spellchecking integration', () => { - it( 'should not throw if a children mutation was fired over colorized text', () => { - editor.setData( - '
                ' + - '
              • helllo
              • ' + - '
              ' + describe( 'conversion in data pipeline', () => { + describe( 'model to data', () => { + it( 'should convert single list (type: numbered, reversed: true)', () => { + setModelData( model, + 'Foo' + + 'Bar' ); - const viewRoot = view.document.getRoot(); - const viewLi = viewRoot.getChild( 0 ).getChild( 0 ); - - // This should not throw. See #9325. - view.document.fire( 'mutations', - [ - { - type: 'children', - oldChildren: [ - viewLi.getChild( 0 ) - ], - newChildren: view.change( writer => [ - writer.createContainerElement( 'font' ) - ] ), - node: viewLi - } - ] - ); + expect( editor.getData() ).to.equal( '
              1. Foo
              2. Bar
              ' ); + } ); + + it( 'should convert single list (type: numbered, reversed: false)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( editor.getData() ).to.equal( '
              1. Foo
              2. Bar
              ' ); + } ); + + it( 'should convert nested numbered lists (main: non-reversed, nested: reversed)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); + + expect( editor.getData() ).to.equal( + '
                ' + + '
              1. Foo 1' + + '
                  ' + + '
                1. Bar 1
                2. ' + + '
                3. Bar 2
                4. ' + + '
                ' + + '
              2. ' + + '
              3. Foo 2
              4. ' + + '
              5. Foo 3
              6. ' + + '
              ' + ); + } ); + + it( 'should convert nested numbered lists (main: reversed, nested: non-reversed)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); + + expect( editor.getData() ).to.equal( + '
                ' + + '
              1. Foo 1' + + '
                  ' + + '
                1. Bar 1
                2. ' + + '
                3. Bar 2
                4. ' + + '
                ' + + '
              2. ' + + '
              3. Foo 2
              4. ' + + '
              5. Foo 3
              6. ' + + '
              ' + ); + } ); + + it( 'should convert nested mixed lists (ul>ol, main: square, nested: reversed)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); + + expect( editor.getData() ).to.equal( + '
                ' + + '
              • Foo 1' + + '
                  ' + + '
                1. Bar 1
                2. ' + + '
                3. Bar 2
                4. ' + + '
                ' + + '
              • ' + + '
              • Foo 2
              • ' + + '
              • Foo 3
              • ' + + '
              ' + ); + } ); + + it( 'should produce nested lists (different `listIndent` attribute)', () => { + setModelData( model, + 'Foo 1' + + 'Foo 2' + + 'Bar 1' + + 'Bar 2' + ); + + expect( editor.getData() ).to.equal( + '
                ' + + '
              1. Foo 1
              2. ' + + '
              3. Foo 2' + + '
                  ' + + '
                1. Bar 1
                2. ' + + '
                3. Bar 2
                4. ' + + '
                ' + + '
              4. ' + + '
              ' + ); + } ); + + it( 'should produce two different lists (different `listReversed` attribute)', () => { + setModelData( model, + 'Foo 1' + + 'Foo 2' + + 'Bar 1' + + 'Bar 2' + ); + + expect( editor.getData() ).to.equal( + '
                ' + + '
              1. Foo 1
              2. ' + + '
              3. Foo 2
              4. ' + + '
              ' + + '
                ' + + '
              1. Bar 1
              2. ' + + '
              3. Bar 2
              4. ' + + '
              ' + ); + } ); + } ); + + describe( 'view to model', () => { + it( 'should convert single list (type: bulleted)', () => { + editor.setData( '
              • Foo
              • Bar
              ' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Foo' + + 'Bar' + ); + } ); + + it( 'should convert single list (type: numbered, reversed: false)', () => { + editor.setData( '
              1. Foo
              2. Bar
              ' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Foo' + + 'Bar' + ); + } ); + + it( 'should convert single list (type: numbered, reversed: true)', () => { + editor.setData( '
              1. Foo
              2. Bar
              ' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Foo' + + 'Bar' + ); + } ); + + it( 'should convert single list (type: numbered, reversed: true) (attribute without value)', () => { + editor.setData( '
              1. Foo
              2. Bar
              ' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Foo' + + 'Bar' + ); + } ); + + it( 'should convert nested and mixed lists', () => { + editor.setData( + '
                ' + + '
              1. OL 1
              2. ' + + '
              3. OL 2' + + '
                  ' + + '
                • UL 1
                • ' + + '
                • UL 2
                • ' + + '
                ' + + '
              4. ' + + '
              5. OL 3
              6. ' + + '
              ' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'OL 1' + + 'OL 2' + + 'UL 1' + + 'UL 2' + + 'OL 3' + ); + } ); + + it( 'should convert when the list is in the middle of the content', () => { + editor.setData( + '

              Paragraph.

              ' + + '
                ' + + '
              1. Foo
              2. ' + + '
              3. Bar
              4. ' + + '
              ' + + '

              Paragraph.

              ' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Paragraph.' + + 'Foo' + + 'Bar' + + 'Paragraph.' + ); + } ); + + // See: #8262. + describe( 'list conversion with surrounding text nodes', () => { + let editor; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing ], + list: { + properties: { styles: false, startIndex: false, reversed: true } + } + } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should convert a list if raw text is before the list', () => { + editor.setData( 'Foo
              1. Foo
              ' ); + + expect( editor.getData() ).to.equal( '

              Foo

              1. Foo
              ' ); + } ); + + it( 'should convert a list if raw text is after the list', () => { + editor.setData( '
              1. Foo
              Foo' ); + + expect( editor.getData() ).to.equal( '
              1. Foo

              Foo

              ' ); + } ); + + it( 'should convert a list if it is surrender by two text nodes', () => { + editor.setData( 'Foo
              1. Foo
              Foo' ); + + expect( editor.getData() ).to.equal( '

              Foo

              1. Foo

              Foo

              ' ); + } ); + } ); + } ); + } ); + + describe( 'conversion in editing pipeline', () => { + describe( 'model to view', () => { + it( 'should convert single list (type: bulleted)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
              • Foo
              • Bar
              ' + ); + } ); + + it( 'should convert single list (type: numbered, reversed: true)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
              1. Foo
              2. Bar
              ' + ); + } ); + + it( 'should convert single list (type: numbered, reversed: false)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
              1. Foo
              2. Bar
              ' + ); + } ); + + it( 'should convert nested numbered lists (main: non-reversed, nested: reversed)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
                ' + + '
              1. Foo 1' + + '
                  ' + + '
                1. Bar 1
                2. ' + + '
                3. Bar 2
                4. ' + + '
                ' + + '
              2. ' + + '
              3. Foo 2
              4. ' + + '
              5. Foo 3
              6. ' + + '
              ' + ); + } ); + + it( 'should convert nested numbered lists (main: reversed, nested: non-reversed)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
                ' + + '
              1. Foo 1' + + '
                  ' + + '
                1. Bar 1
                2. ' + + '
                3. Bar 2
                4. ' + + '
                ' + + '
              2. ' + + '
              3. Foo 2
              4. ' + + '
              5. Foo 3
              6. ' + + '
              ' + ); + } ); + } ); + } ); + + describe( 'integrations', () => { + describe( 'merging a list into a reversed list', () => { + it( + 'should inherit the reversed attribute ' + + 'when merging the same kind of lists (from top, merge a single item, reversed: true)', + () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } + ); + + it( + 'should inherit the reversed attribute ' + + 'when merging the same kind of lists (from top, merge a single item, reversed: false)', + () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } + ); + + it( + 'should inherit the reversed attribute ' + + 'when merging the same kind of lists (from top, merge a single item, bulleted)', + () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'bulletedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } + ); + + it( 'should inherit the reversed attribute when merging the same kind of lists (from top, merge a few items)', () => { + setModelData( model, + '[Foo Bar 1.' + + 'Foo Bar 2.]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + '[Foo Bar 1.' + + 'Foo Bar 2.]' + + 'Foo' + + 'Bar' + ); + } ); + + it( 'should not inherit anything if there is no list below the inserted list (numbered)', () => { + setModelData( model, + 'Foo Bar 1.[]' + + 'Foo Bar 2.' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar 1.[]' + + 'Foo Bar 2.' + ); + } ); + + it( 'should not inherit anything if there is no list below the inserted list (bulleted)', () => { + setModelData( model, + 'Foo Bar 1.[]' + + 'Foo Bar 2.' + ); + + editor.execute( 'bulletedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar 1.[]' + + 'Foo Bar 2.' + ); + } ); + + it( 'should not inherit anything if replacing the entire content with a list', () => { + setModelData( model, + 'Foo Bar 1.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar 1.[]' + ); + } ); + + it( + 'should not inherit the reversed attribute when merging different kind of lists (from top, merge a single item)', + () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } ); + + it( + 'should not inherit the reversed attribute when merging different kind of lists (from bottom, merge a single item)', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + } + ); + + it( + 'should inherit the reversed attribute when merging the same kind of lists (from bottom, merge a single item)', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + } ); + + it( + 'should inherit the reversed attribute from listIndent=0 element when merging the same kind of lists (from bottom)', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar' + + 'Foo Bar.[]' + ); + } + ); + } ); + + describe( 'modifying "listType" attribute', () => { + it( 'should inherit the reversed attribute when the modified list is the same kind of the list as next sibling', () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } ); + + it( + 'should inherit the reversed attribute when the modified list is the same kind of the list as previous sibling', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + } + ); + + it( 'should remove the reversed attribute when changing `listType` to `bulleted`', () => { + setModelData( model, + 'Foo Bar.[]' + ); + + editor.execute( 'bulletedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + ); + } ); + + it( 'should add default reversed attribute when changing `listType` to `numbered`', () => { + setModelData( model, + 'Foo Bar.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + ); + } ); + } ); + + describe( 'indenting lists', () => { + it( 'should restore the default value of the reversed attribute when indenting a single item', () => { + setModelData( model, + '1.' + + '1A.' + + '2B.' + + '2.[]' + + '3.' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '1A.' + + '2B.' + + '2.[]' + + '3.' + ); + } ); + + it( 'should restore the default value of the reversed attribute when indenting a few items', () => { + setModelData( model, + '1.' + + '[2.' + + '3.]' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '[2.' + + '3.]' + ); + } ); + + it( + 'should copy the value of the reversed attribute when indenting a single item into a nested list (default value)', + () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + + '4.' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + + '4.' + ); + } + ); + + it( + 'should copy the value of the reversed attribute when indenting a single item into a nested list (changed value)', + () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + + '4.' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + + '4.' + ); + } + ); + + it( 'should set default value of the reversed attribute when indenting a single item into a nested list', () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + + '4.' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '3.' + + '4.' + ); + } ); + + it( + 'should copy the value of the reversed attribute when indenting a single item into a nested list ' + + '(many nested lists check)', + () => { + setModelData( model, + '1.' + + '2.' + + '3.' + + '4.[]' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.' + + '4.[]' + ); + } + ); + + it( 'should inherit the reversed attribute from nested list if the `listType` is other than indenting element', () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + ); + } ); + } ); + + describe( 'outdenting lists', () => { + it( 'should inherit the reversed attribute from parent list (change the first nested item)', () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '3.' + ); + } ); + + it( 'should inherit the reversed attribute from parent list (change the second nested item)', () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + ); + } ); + + it( 'should inherit the reversed attribute from parent list (modifying nested lists)', () => { + setModelData( model, + '1.' + + '[2.' + + '3.]' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '[2.' + + '3.]' + ); + } ); + + it( + 'should inherit the reversed attribute from parent list (outdenting many items, including the first one in the list)', + () => { + setModelData( model, + '[1.' + + '2.' + + '3.]' + + '4.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '[1.' + + '2.' + + '3.]' + + '4.' + ); + } + ); + + it( + 'should inherit the reversed attribute from parent list (outdenting the first item that is a parent for next list)', + () => { + setModelData( model, + '1.[]' + + '2.' + + '3.' + + '4.' + + '5.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.[]' + + '2.' + + '3.' + + '4.' + + '5.' + ); + } + ); + + it( 'should not inherit the reversed if outdented the only one item in the list', () => { + setModelData( model, + '1.[]' + + '2.' + + '3.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.[]' + + '2.' + + '3.' + ); + } ); + + it( + 'should not inherit the reversed attribute if outdented the only one item in the list (a paragraph below the list)', + () => { + setModelData( model, + '1.[]' + + '2.' + + '3.' + + 'Foo' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.[]' + + '2.' + + '3.' + + 'Foo' + ); + } + ); + + it( 'should not inherit the reversed attribute if outdented bulleted list', () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + ); + } ); + + it( + 'should not inherit the reversed attribute from parent list if the `listType` is other than outdenting element', + () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '3.' + ); + } ); + + it( 'should not do anything if there is no list after outdenting', () => { + setModelData( model, + '1.[]' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.[]' + ); + } ); + } ); + + describe( 'indent/outdent + undo', () => { + it( 'should use the same batch for indenting a list and updating `listType` attribute', () => { + setModelData( model, + '1.' + + '1A.' + + '2B.' + + '2.[]' + + '3.' + ); + + editor.execute( 'indentList' ); + editor.execute( 'undo' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '1A.' + + '2B.' + + '2.[]' + + '3.' + ); + } ); + + it( 'should use the same batch for outdenting a list and updating `listType` attribute', () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + ); + + editor.execute( 'outdentList' ); + editor.execute( 'undo' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '3.' + ); + } ); + } ); + + describe( 'delete + undo', () => { + let editor, model, view; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, Typing, UndoEditing ], + list: { + properties: { styles: false, startIndex: false, reversed: true } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + view = editor.editing.view; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + // See: #7930. + it( 'should restore proper reversed attribute after undo merging lists', () => { + // ○ 1. + // ○ 2. + // ○ 3. + // + // ■ 1. + // ■ 2. + setModelData( model, + '1.' + + '2.' + + '3.' + + '[]' + + '1.' + + '2.' + ); + + expect( getViewData( view, { withoutSelection: true } ), 'initial data' ).to.equal( + '
                ' + + '
              1. 1.
              2. ' + + '
              3. 2.
              4. ' + + '
              5. 3.
              6. ' + + '
              ' + + '

              ' + + '
                ' + + '
              1. 1.
              2. ' + + '
              3. 2.
              4. ' + + '
              ' + ); + + // After removing the paragraph. + // ○ 1. + // ○ 2. + // ○ 3. + // ○ 1. + // ○ 2. + editor.execute( 'delete' ); + + expect( getViewData( view, { withoutSelection: true } ), 'executing delete' ).to.equal( + '
                ' + + '
              1. 1.
              2. ' + + '
              3. 2.
              4. ' + + '
              5. 3.
              6. ' + + '
              7. 1.
              8. ' + + '
              9. 2.
              10. ' + + '
              ' + ); + + // After undo. + // ○ 1. + // ○ 2. + // ○ 3. + // + // ■ 1. + // ■ 2. + editor.execute( 'undo' ); + + expect( getViewData( view, { withoutSelection: true } ), 'initial data' ).to.equal( + '
                ' + + '
              1. 1.
              2. ' + + '
              3. 2.
              4. ' + + '
              5. 3.
              6. ' + + '
              ' + + '

              ' + + '
                ' + + '
              1. 1.
              2. ' + + '
              3. 2.
              4. ' + + '
              ' + ); + } ); + } ); + + describe( 'todo list', () => { + let editor, model; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, TodoListEditing ], + list: { + properties: { styles: false, startIndex: false, reversed: true } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should not add the `listReversed` attribute while creating a todo list', () => { + setModelData( model, 'Foo[]' ); + + editor.execute( 'todoList' ); + + expect( getModelData( model ), 'Foo[]' ); + } ); + + it( 'should not add the `listReversed` attribute while switching the list type', () => { + setModelData( model, 'Foo[]' ); + + editor.execute( 'todoList' ); + + expect( getModelData( model ), 'Foo[]' ); + } ); + + it( 'should remove the `listReversed` attribute while switching the list type that uses the list style feature', () => { + setModelData( model, 'Foo[]' ); + + editor.execute( 'todoList' ); + + expect( getModelData( model ), 'Foo[]' ); + } ); + + it( 'should not inherit the `listReversed` attribute when inserting a todo list item', () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'todoList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } ); + + it( 'should not allow to set the `listReversed` attribute in to-do list item', () => { + setModelData( model, 'Foo' ); + + const listItem = model.document.getRoot().getChild( 0 ); + + expect( listItem.hasAttribute( 'listReversed' ) ).to.be.false; + + model.change( writer => { + writer.setAttribute( 'listReversed', true, listItem ); + } ); + + expect( listItem.hasAttribute( 'listReversed' ) ).to.be.false; + } ); + } ); + + describe( 'removing content between two lists', () => { + let editor, model; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, Typing ], + list: { + properties: { styles: false, startIndex: false, reversed: true } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should not do anything while removing a letter inside a listItem', () => { + setModelData( model, + '1.' + + '2.[]' + + '' + + '1.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2[]' + + '' + + '1.' + + '2.' + ); + } ); + + it( 'should not do anything if there is a non-listItem before the removed content', () => { + setModelData( model, + 'Foo' + + '[]' + + '1.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + 'Foo[]' + + '1.' + + '2.' + ); + } ); + + it( 'should not do anything if there is a non-listItem after the removed content', () => { + setModelData( model, + '1.' + + '2.' + + '[]' + + 'Foo' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + 'Foo' + ); + } ); + + it( 'should not do anything if there is no element after the removed content', () => { + setModelData( model, + '1.' + + '2.' + + '[]' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + ); + } ); + + it( + 'should modify the the `listReversed` attribute for the merged (second) list when removing content between those lists', + () => { + setModelData( model, + '1.' + + '2.' + + '[]' + + '1.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '1.' + + '2.' + ); + } + ); + + it( 'should read the `listReversed` attribute from the most outer list', () => { + setModelData( model, + '1.' + + '2.' + + '2.1.' + + '2.1.1' + + '[]' + + '1.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '2.1.' + + '2.1.1[]' + + '1.' + + '2.' + ); + } ); + + it( + 'should not modify the the `listReversed` attribute for the merged (second) list ' + + 'if merging different `listType` attribute', + () => { + setModelData( model, + '1.' + + '2.' + + '[]' + + '1.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '1.' + + '2.' + ); + } + ); + + it( + 'should modify the the `listReversed` attribute for the merged (second) list when removing content from both lists', + () => { + setModelData( model, + '1.' + + '2.' + + '[3.' + + 'Foo' + + '1.]' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '[]' + + '2.' + ); + } + ); + + it( + 'should modify the the `listReversed` attribute for the merged (second) list when typing over content from both lists', + () => { + setModelData( model, + '1.' + + '2.' + + '[3.' + + 'Foo' + + '1.]' + + '2.' + ); + + editor.execute( 'input', { text: 'Foo' } ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + 'Foo[]' + + '2.' + ); + } + ); + + it( + 'should not modify the the `listReversed` if lists were not merged but the content was partially removed', + () => { + setModelData( model, + '111.' + + '222.' + + '[333.' + + 'Foo' + + '1]11.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '111.' + + '222.' + + '[]11.' + + '2.' + ); + } + ); + + it( 'should not do anything while typing in a list item', () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + + '' + + '1.' + + '2.' + ); + + const modelChangeStub = sinon.stub( model, 'change' ).callThrough(); + + simulateTyping( ' Foo' ); + + // Each character calls `editor.model.change()`. + expect( modelChangeStub.callCount ).to.equal( 4 ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2. Foo[]' + + '3.' + + '' + + '1.' + + '2.' + ); + } ); + + // See: #8073. + it( 'should not crash when removing a content between intended lists', () => { + setModelData( model, + 'aaaa' + + 'bb[bb' + + 'cc]cc' + + 'dddd' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + 'aaaa' + + 'bb[]cc' + + 'dddd' + ); + } ); + + it( + 'should read the `listReversed` attribute from the most outer selected list while removing content between lists', + () => { + setModelData( model, + '1.' + + '2.' + + '2.1.' + + '2.1.1[foo' + + 'Foo' + + '1.' + + 'bar]2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '2.1.' + + '2.1.1[]2.' + ); + } + ); + + function simulateTyping( text ) { + // While typing, every character is an atomic change. + text.split( '' ).forEach( character => { + editor.execute( 'input', { + text: character + } ); + } ); + } + } ); + + // #8160 + describe( 'pasting a list into another list', () => { + let element; + + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.append( element ); + + return ClassicTestEditor + .create( element, { + plugins: [ Paragraph, Clipboard, ListStyleEditing, UndoEditing ], + list: { + properties: { styles: false, startIndex: false, reversed: true } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + } ); + } ); + + afterEach( () => { + return editor.destroy() + .then( () => { + element.remove(); + } ); + } ); + + it( 'should inherit attributes from the previous sibling element (collapsed selection)', () => { + setModelData( model, + 'Foo' + + 'Foo Bar' + + '[]' + + 'Bar' + ); + + pasteHtml( editor, + '
                ' + + '
              1. Foo 1
              2. ' + + '
              3. Foo 2
              4. ' + + '
              ' + ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo Bar' + + 'Foo 1' + + 'Foo 2[]' + + 'Bar' + ); + } ); + + it( 'should inherit attributes from the previous sibling element (non-collapsed selection)', () => { + setModelData( model, + 'Foo' + + 'Foo Bar' + + '[Foo]' + + 'Bar' + ); + + pasteHtml( editor, + '
                ' + + '
              • Foo 1
              • ' + + '
              • Foo 2
              • ' + + '
              ' + ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo Bar' + + 'Foo 1' + + 'Foo 2[]' + + 'Bar' + ); + } ); + + it( 'should inherit attributes from the previous sibling element (non-collapsed selection over a few elements)', () => { + setModelData( model, + 'Foo' + + 'Foo Bar' + + '[Foo 1.' + + 'Foo 2.' + + 'Foo 3.]' + + 'Bar' + ); + + pasteHtml( editor, + '
                ' + + '
              • Foo 1
              • ' + + '
              • Foo 2
              • ' + + '
              ' + ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo Bar' + + 'Foo 1' + + 'Foo 2[]' + + 'Bar' + ); + } ); + + it( 'should do nothing when pasting the similar list', () => { + setModelData( model, + 'Foo' + + 'Foo Bar' + + '[]' + + 'Bar' + ); + + pasteHtml( editor, + '
                ' + + '
              1. Foo
              2. ' + + '
              ' + ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo Bar' + + 'Foo[]' + + 'Bar' + ); + } ); + + it( 'should replace the entire list if selected', () => { + setModelData( model, + 'Foo' + + '[Foo Bar]' + + 'Bar' + ); + + pasteHtml( editor, + '
                ' + + '
              1. Foo
              2. ' + + '
              ' + ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo[]' + + 'Bar' + ); + } ); + + function pasteHtml( editor, html ) { + editor.editing.view.document.fire( 'paste', { + dataTransfer: createDataTransfer( { 'text/html': html } ), + stopPropagation() {}, + preventDefault() {} + } ); + } + + function createDataTransfer( data ) { + return { + getData( type ) { + return data[ type ]; + }, + setData() {} + }; + } + } ); + } ); + } ); + + describe( 'listStart', () => { + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, UndoEditing ], + list: { + properties: { styles: false, startIndex: true, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + view = editor.editing.view; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + describe( 'schema rules', () => { + it( 'should not allow set `listStyle` on the `listItem`', () => { + expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listStyle' ) ).to.be.false; + } ); + + it( 'should not allow set `listReversed` on the `listItem`', () => { + expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listReversed' ) ).to.be.false; + } ); + + it( 'should allow set `listStart` on the `listItem`', () => { + expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listStart' ) ).to.be.true; + } ); + } ); + + describe( 'command', () => { + it( 'should not register `listReversed` command', () => { + const command = editor.commands.get( 'listReversed' ); + + expect( command ).to.be.undefined; + } ); + + it( 'should not register `listStyle` command', () => { + const command = editor.commands.get( 'listStyle' ); + + expect( command ).to.be.undefined; + } ); + + it( 'should register `listStart` command', () => { + const command = editor.commands.get( 'listStart' ); + + expect( command ).to.be.instanceOf( ListStartCommand ); + } ); + } ); + + describe( 'conversion in data pipeline', () => { + describe( 'model to data', () => { + it( 'should convert single list (type: numbered, start: 2)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( editor.getData() ).to.equal( '
              1. Foo
              2. Bar
              ' ); + } ); + + it( 'should convert single list (type: numbered, start: 1)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( editor.getData() ).to.equal( '
              1. Foo
              2. Bar
              ' ); + } ); + + it( 'should convert nested numbered lists (main: 2, nested: 3)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); + + expect( editor.getData() ).to.equal( + '
                ' + + '
              1. Foo 1' + + '
                  ' + + '
                1. Bar 1
                2. ' + + '
                3. Bar 2
                4. ' + + '
                ' + + '
              2. ' + + '
              3. Foo 2
              4. ' + + '
              5. Foo 3
              6. ' + + '
              ' + ); + } ); + + it( 'should convert nested numbered lists (main: 2, nested: 1)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); + + expect( editor.getData() ).to.equal( + '
                ' + + '
              1. Foo 1' + + '
                  ' + + '
                1. Bar 1
                2. ' + + '
                3. Bar 2
                4. ' + + '
                ' + + '
              2. ' + + '
              3. Foo 2
              4. ' + + '
              5. Foo 3
              6. ' + + '
              ' + ); + } ); + + it( 'should convert nested mixed lists (ul>ol, main: square, nested: 2)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); + + expect( editor.getData() ).to.equal( + '
                ' + + '
              • Foo 1' + + '
                  ' + + '
                1. Bar 1
                2. ' + + '
                3. Bar 2
                4. ' + + '
                ' + + '
              • ' + + '
              • Foo 2
              • ' + + '
              • Foo 3
              • ' + + '
              ' + ); + } ); + + it( 'should produce nested lists (different `listIndent` attribute)', () => { + setModelData( model, + 'Foo 1' + + 'Foo 2' + + 'Bar 1' + + 'Bar 2' + ); + + expect( editor.getData() ).to.equal( + '
                ' + + '
              1. Foo 1
              2. ' + + '
              3. Foo 2' + + '
                  ' + + '
                1. Bar 1
                2. ' + + '
                3. Bar 2
                4. ' + + '
                ' + + '
              4. ' + + '
              ' + ); + } ); + + it( 'should produce two different lists (different `listStart` attribute)', () => { + setModelData( model, + 'Foo 1' + + 'Foo 2' + + 'Bar 1' + + 'Bar 2' + ); + + expect( editor.getData() ).to.equal( + '
                ' + + '
              1. Foo 1
              2. ' + + '
              3. Foo 2
              4. ' + + '
              ' + + '
                ' + + '
              1. Bar 1
              2. ' + + '
              3. Bar 2
              4. ' + + '
              ' + ); + } ); + } ); + + describe( 'view to model', () => { + it( 'should convert single list (type: bulleted)', () => { + editor.setData( '
              • Foo
              • Bar
              ' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Foo' + + 'Bar' + ); + } ); + + it( 'should convert single list (type: numbered, start: 1)', () => { + editor.setData( '
              1. Foo
              2. Bar
              ' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Foo' + + 'Bar' + ); + } ); + + it( 'should convert single list (type: numbered, start: 2)', () => { + editor.setData( '
              1. Foo
              2. Bar
              ' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Foo' + + 'Bar' + ); + } ); + + it( 'should convert nested and mixed lists', () => { + editor.setData( + '
                ' + + '
              1. OL 1
              2. ' + + '
              3. OL 2' + + '
                  ' + + '
                • UL 1
                • ' + + '
                • UL 2
                • ' + + '
                ' + + '
              4. ' + + '
              5. OL 3
              6. ' + + '
              ' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'OL 1' + + 'OL 2' + + 'UL 1' + + 'UL 2' + + 'OL 3' + ); + } ); + + it( 'should convert when the list is in the middle of the content', () => { + editor.setData( + '

              Paragraph.

              ' + + '
                ' + + '
              1. Foo
              2. ' + + '
              3. Bar
              4. ' + + '
              ' + + '

              Paragraph.

              ' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'Paragraph.' + + 'Foo' + + 'Bar' + + 'Paragraph.' + ); + } ); + + // See: #8262. + describe( 'list conversion with surrounding text nodes', () => { + let editor; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing ], + list: { + properties: { styles: false, startIndex: true, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should convert a list if raw text is before the list', () => { + editor.setData( 'Foo
              1. Foo
              ' ); + + expect( editor.getData() ).to.equal( '

              Foo

              1. Foo
              ' ); + } ); + + it( 'should convert a list if raw text is after the list', () => { + editor.setData( '
              1. Foo
              Foo' ); + + expect( editor.getData() ).to.equal( '
              1. Foo

              Foo

              ' ); + } ); + + it( 'should convert a list if it is surrender by two text nodes', () => { + editor.setData( 'Foo
              1. Foo
              Foo' ); + + expect( editor.getData() ).to.equal( '

              Foo

              1. Foo

              Foo

              ' ); + } ); + } ); + } ); + } ); + + describe( 'conversion in editing pipeline', () => { + describe( 'model to view', () => { + it( 'should convert single list (type: bulleted)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
              • Foo
              • Bar
              ' + ); + } ); + + it( 'should convert single list (type: numbered, start: 2)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
              1. Foo
              2. Bar
              ' + ); + } ); + + it( 'should convert single list (type: numbered, start: 1)', () => { + setModelData( model, + 'Foo' + + 'Bar' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
              1. Foo
              2. Bar
              ' + ); + } ); + + it( 'should convert nested numbered lists (main: 1, nested: 2)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
                ' + + '
              1. Foo 1' + + '
                  ' + + '
                1. Bar 1
                2. ' + + '
                3. Bar 2
                4. ' + + '
                ' + + '
              2. ' + + '
              3. Foo 2
              4. ' + + '
              5. Foo 3
              6. ' + + '
              ' + ); + } ); + + it( 'should convert nested numbered lists (main: 3, nested: 1)', () => { + setModelData( model, + 'Foo 1' + + 'Bar 1' + + 'Bar 2' + + 'Foo 2' + + 'Foo 3' + ); + + expect( getViewData( view, { withoutSelection: true } ) ).to.equal( + '
                ' + + '
              1. Foo 1' + + '
                  ' + + '
                1. Bar 1
                2. ' + + '
                3. Bar 2
                4. ' + + '
                ' + + '
              2. ' + + '
              3. Foo 2
              4. ' + + '
              5. Foo 3
              6. ' + + '
              ' + ); + } ); + } ); + } ); + + describe( 'integrations', () => { + describe( 'merging a list into a list', () => { + it( + 'should inherit the start attribute ' + + 'when merging the same kind of lists (from top, merge a single item, start: 3)', + () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } + ); + + it( + 'should inherit the start attribute ' + + 'when merging the same kind of lists (from top, merge a single item, start: 1)', + () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } + ); + + it( + 'should inherit the start attribute ' + + 'when merging the same kind of lists (from top, merge a single item, bulleted)', + () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'bulletedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } + ); + + it( 'should inherit the start attribute when merging the same kind of lists (from top, merge a few items)', () => { + setModelData( model, + '[Foo Bar 1.' + + 'Foo Bar 2.]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + '[Foo Bar 1.' + + 'Foo Bar 2.]' + + 'Foo' + + 'Bar' + ); + } ); + + it( 'should not inherit anything if there is no list below the inserted list (numbered)', () => { + setModelData( model, + 'Foo Bar 1.[]' + + 'Foo Bar 2.' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar 1.[]' + + 'Foo Bar 2.' + ); + } ); + + it( 'should not inherit anything if there is no list below the inserted list (bulleted)', () => { + setModelData( model, + 'Foo Bar 1.[]' + + 'Foo Bar 2.' + ); + + editor.execute( 'bulletedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar 1.[]' + + 'Foo Bar 2.' + ); + } ); + + it( 'should not inherit anything if replacing the entire content with a list', () => { + setModelData( model, + 'Foo Bar 1.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar 1.[]' + ); + } ); + + it( + 'should not inherit the start attribute when merging different kind of lists (from top, merge a single item)', + () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } ); + + it( + 'should not inherit the start attribute when merging different kind of lists (from bottom, merge a single item)', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + } + ); + + it( + 'should inherit the start attribute when merging the same kind of lists (from bottom, merge a single item)', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + } ); + + it( + 'should inherit the start attribute from listIndent=0 element when merging the same kind of lists (from bottom)', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar' + + 'Foo Bar.[]' + ); + } + ); + } ); + + describe( 'modifying "listType" attribute', () => { + it( 'should inherit the start attribute when the modified list is the same kind of the list as next sibling', () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } ); + + it( + 'should inherit the start attribute when the modified list is the same kind of the list as previous sibling', + () => { + setModelData( model, + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Bar' + + 'Foo Bar.[]' + ); + } + ); + + it( 'should remove the start attribute when changing `listType` to `bulleted`', () => { + setModelData( model, + 'Foo Bar.[]' + ); + + editor.execute( 'bulletedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + ); + } ); + + it( 'should add default start attribute when changing `listType` to `numbered`', () => { + setModelData( model, + 'Foo Bar.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + ); + } ); + } ); + + describe( 'indenting lists', () => { + it( 'should restore the default value of the start attribute when indenting a single item', () => { + setModelData( model, + '1.' + + '1A.' + + '2B.' + + '2.[]' + + '3.' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '1A.' + + '2B.' + + '2.[]' + + '3.' + ); + } ); + + it( 'should restore the default value of the start attribute when indenting a few items', () => { + setModelData( model, + '1.' + + '[2.' + + '3.]' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '[2.' + + '3.]' + ); + } ); + + it( + 'should copy the value of the start attribute when indenting a single item into a nested list (default value)', + () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + + '4.' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + + '4.' + ); + } + ); + + it( + 'should copy the value of the start attribute when indenting a single item into a nested list (changed value)', + () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + + '4.' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + + '4.' + ); + } + ); + + it( 'should set default value of the start attribute when indenting a single item into a nested list', () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + + '4.' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '3.' + + '4.' + ); + } ); + + it( + 'should copy the value of the start attribute when indenting a single item into a nested list ' + + '(many nested lists check)', + () => { + setModelData( model, + '1.' + + '2.' + + '3.' + + '4.[]' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.' + + '4.[]' + ); + } + ); + + it( 'should inherit the start attribute from nested list if the `listType` is other than indenting element', () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + ); + } ); + } ); + + describe( 'outdenting lists', () => { + it( 'should inherit the start attribute from parent list (change the first nested item)', () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '3.' + ); + } ); + + it( 'should inherit the start attribute from parent list (change the second nested item)', () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + ); + } ); + + it( 'should inherit the start attribute from parent list (modifying nested lists)', () => { + setModelData( model, + '1.' + + '[2.' + + '3.]' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '[2.' + + '3.]' + ); + } ); + + it( + 'should inherit the start attribute from parent list (outdenting many items, including the first one in the list)', + () => { + setModelData( model, + '[1.' + + '2.' + + '3.]' + + '4.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '[1.' + + '2.' + + '3.]' + + '4.' + ); + } + ); + + it( + 'should inherit the start attribute from parent list (outdenting the first item that is a parent for next list)', + () => { + setModelData( model, + '1.[]' + + '2.' + + '3.' + + '4.' + + '5.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.[]' + + '2.' + + '3.' + + '4.' + + '5.' + ); + } + ); + + it( 'should not inherit the start if outdented the only one item in the list', () => { + setModelData( model, + '1.[]' + + '2.' + + '3.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.[]' + + '2.' + + '3.' + ); + } ); + + it( + 'should not inherit the start attribute if outdented the only one item in the list (a paragraph below the list)', + () => { + setModelData( model, + '1.[]' + + '2.' + + '3.' + + 'Foo' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.[]' + + '2.' + + '3.' + + 'Foo' + ); + } + ); + + it( 'should not inherit the start attribute if outdented bulleted list', () => { + setModelData( model, + '1.' + + '2.' + + '3.[]' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '3.[]' + ); + } ); + + it( + 'should not inherit the start attribute from parent list if the `listType` is other than outdenting element', + () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '3.' + ); + } ); + + it( 'should not do anything if there is no list after outdenting', () => { + setModelData( model, + '1.[]' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.[]' + ); + } ); + } ); + + describe( 'indent/outdent + undo', () => { + it( 'should use the same batch for indenting a list and updating `listType` attribute', () => { + setModelData( model, + '1.' + + '1A.' + + '2B.' + + '2.[]' + + '3.' + ); + + editor.execute( 'indentList' ); + editor.execute( 'undo' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '1A.' + + '2B.' + + '2.[]' + + '3.' + ); + } ); + + it( 'should use the same batch for outdenting a list and updating `listType` attribute', () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + ); + + editor.execute( 'outdentList' ); + editor.execute( 'undo' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '3.' + ); + } ); + } ); + + describe( 'delete + undo', () => { + let editor, model, view; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, Typing, UndoEditing ], + list: { + properties: { styles: false, startIndex: true, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + view = editor.editing.view; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + // See: #7930. + it( 'should restore proper start attribute after undo merging lists', () => { + // ○ 1. + // ○ 2. + // ○ 3. + // + // ■ 1. + // ■ 2. + setModelData( model, + '1.' + + '2.' + + '3.' + + '[]' + + '1.' + + '2.' + ); + + expect( getViewData( view, { withoutSelection: true } ), 'initial data' ).to.equal( + '
                ' + + '
              1. 1.
              2. ' + + '
              3. 2.
              4. ' + + '
              5. 3.
              6. ' + + '
              ' + + '

              ' + + '
                ' + + '
              1. 1.
              2. ' + + '
              3. 2.
              4. ' + + '
              ' + ); + + // After removing the paragraph. + // ○ 1. + // ○ 2. + // ○ 3. + // ○ 1. + // ○ 2. + editor.execute( 'delete' ); + + expect( getViewData( view, { withoutSelection: true } ), 'executing delete' ).to.equal( + '
                ' + + '
              1. 1.
              2. ' + + '
              3. 2.
              4. ' + + '
              5. 3.
              6. ' + + '
              7. 1.
              8. ' + + '
              9. 2.
              10. ' + + '
              ' + ); + + // After undo. + // ○ 1. + // ○ 2. + // ○ 3. + // + // ■ 1. + // ■ 2. + editor.execute( 'undo' ); + + expect( getViewData( view, { withoutSelection: true } ), 'initial data' ).to.equal( + '
                ' + + '
              1. 1.
              2. ' + + '
              3. 2.
              4. ' + + '
              5. 3.
              6. ' + + '
              ' + + '

              ' + + '
                ' + + '
              1. 1.
              2. ' + + '
              3. 2.
              4. ' + + '
              ' + ); + } ); + } ); + + describe( 'todo list', () => { + let editor, model; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, TodoListEditing ], + list: { + properties: { styles: false, startIndex: true, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should not add the `listStart` attribute while creating a todo list', () => { + setModelData( model, 'Foo[]' ); + + editor.execute( 'todoList' ); + + expect( getModelData( model ), 'Foo[]' ); + } ); + + it( 'should not add the `listStart` attribute while switching the list type', () => { + setModelData( model, 'Foo[]' ); + + editor.execute( 'todoList' ); + + expect( getModelData( model ), 'Foo[]' ); + } ); + + it( 'should remove the `listStart` attribute while switching the list type that uses the list style feature', () => { + setModelData( model, 'Foo[]' ); + + editor.execute( 'todoList' ); + + expect( getModelData( model ), 'Foo[]' ); + } ); + + it( 'should not inherit the `listStart` attribute when inserting a todo list item', () => { + setModelData( model, + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + + editor.execute( 'todoList' ); + + expect( getModelData( model ) ).to.equal( + 'Foo Bar.[]' + + 'Foo' + + 'Bar' + ); + } ); + + it( 'should not allow to set the `listStart` attribute in to-do list item', () => { + setModelData( model, 'Foo' ); + + const listItem = model.document.getRoot().getChild( 0 ); + + expect( listItem.hasAttribute( 'listStart' ) ).to.be.false; + + model.change( writer => { + writer.setAttribute( 'listStart', 5, listItem ); + } ); + + expect( listItem.hasAttribute( 'listStart' ) ).to.be.false; + } ); + } ); + + describe( 'removing content between two lists', () => { + let editor, model; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, Typing ], + list: { + properties: { styles: false, startIndex: true, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should not do anything while removing a letter inside a listItem', () => { + setModelData( model, + '1.' + + '2.[]' + + '' + + '1.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2[]' + + '' + + '1.' + + '2.' + ); + } ); + + it( 'should not do anything if there is a non-listItem before the removed content', () => { + setModelData( model, + 'Foo' + + '[]' + + '1.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + 'Foo[]' + + '1.' + + '2.' + ); + } ); + + it( 'should not do anything if there is a non-listItem after the removed content', () => { + setModelData( model, + '1.' + + '2.' + + '[]' + + 'Foo' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + 'Foo' + ); + } ); + + it( 'should not do anything if there is no element after the removed content', () => { + setModelData( model, + '1.' + + '2.' + + '[]' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + ); + } ); + + it( + 'should modify the the `listStart` attribute for the merged (second) list when removing content between those lists', + () => { + setModelData( model, + '1.' + + '2.' + + '[]' + + '1.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '1.' + + '2.' + ); + } + ); + + it( 'should read the `listStart` attribute from the most outer list', () => { + setModelData( model, + '1.' + + '2.' + + '2.1.' + + '2.1.1' + + '[]' + + '1.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '2.1.' + + '2.1.1[]' + + '1.' + + '2.' + ); + } ); + + it( + 'should not modify the the `listStart` attribute for the merged (second) list ' + + 'if merging different `listType` attribute', + () => { + setModelData( model, + '1.' + + '2.' + + '[]' + + '1.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.[]' + + '1.' + + '2.' + ); + } + ); + + it( + 'should modify the the `listStart` attribute for the merged (second) list when removing content from both lists', + () => { + setModelData( model, + '1.' + + '2.' + + '[3.' + + 'Foo' + + '1.]' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '[]' + + '2.' + ); + } + ); + + it( + 'should modify the the `listStart` attribute for the merged (second) list when typing over content from both lists', + () => { + setModelData( model, + '1.' + + '2.' + + '[3.' + + 'Foo' + + '1.]' + + '2.' + ); + + editor.execute( 'input', { text: 'Foo' } ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + 'Foo[]' + + '2.' + ); + } + ); + + it( + 'should not modify the the `listStart` attribute if lists were not merged but the content was partially removed', + () => { + setModelData( model, + '111.' + + '222.' + + '[333.' + + 'Foo' + + '1]11.' + + '2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '111.' + + '222.' + + '[]11.' + + '2.' + ); + } + ); + + it( 'should not do anything while typing in a list item', () => { + setModelData( model, + '1.' + + '2.[]' + + '3.' + + '' + + '1.' + + '2.' + ); + + const modelChangeStub = sinon.stub( model, 'change' ).callThrough(); + + simulateTyping( ' Foo' ); + + // Each character calls `editor.model.change()`. + expect( modelChangeStub.callCount ).to.equal( 4 ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2. Foo[]' + + '3.' + + '' + + '1.' + + '2.' + ); + } ); + + // See: #8073. + it( 'should not crash when removing a content between intended lists', () => { + setModelData( model, + 'aaaa' + + 'bb[bb' + + 'cc]cc' + + 'dddd' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + 'aaaa' + + 'bb[]cc' + + 'dddd' + ); + } ); + + it( + 'should read the `listStart` attribute from the most outer selected list while removing content between lists', + () => { + setModelData( model, + '1.' + + '2.' + + '2.1.' + + '2.1.1[foo' + + 'Foo' + + '1.' + + 'bar]2.' + ); + + editor.execute( 'delete' ); + + expect( getModelData( model ) ).to.equal( + '1.' + + '2.' + + '2.1.' + + '2.1.1[]2.' + ); + } + ); + + function simulateTyping( text ) { + // While typing, every character is an atomic change. + text.split( '' ).forEach( character => { + editor.execute( 'input', { + text: character + } ); + } ); + } + } ); + + // #8160 + describe( 'pasting a list into another list', () => { + let element; + + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.append( element ); + + return ClassicTestEditor + .create( element, { + plugins: [ Paragraph, Clipboard, ListStyleEditing, UndoEditing ], + list: { + properties: { styles: false, startIndex: true, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + } ); + } ); + + afterEach( () => { + return editor.destroy() + .then( () => { + element.remove(); + } ); + } ); + + it( 'should inherit attributes from the previous sibling element (collapsed selection)', () => { + setModelData( model, + 'Foo' + + 'Foo Bar' + + '[]' + + 'Bar' + ); + + pasteHtml( editor, + '
                ' + + '
              1. Foo 1
              2. ' + + '
              3. Foo 2
              4. ' + + '
              ' + ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo Bar' + + 'Foo 1' + + 'Foo 2[]' + + 'Bar' + ); + } ); + + it( 'should inherit attributes from the previous sibling element (non-collapsed selection)', () => { + setModelData( model, + 'Foo' + + 'Foo Bar' + + '[Foo]' + + 'Bar' + ); + + pasteHtml( editor, + '
                ' + + '
              • Foo 1
              • ' + + '
              • Foo 2
              • ' + + '
              ' + ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo Bar' + + 'Foo 1' + + 'Foo 2[]' + + 'Bar' + ); + } ); + + it( 'should inherit attributes from the previous sibling element (non-collapsed selection over a few elements)', () => { + setModelData( model, + 'Foo' + + 'Foo Bar' + + '[Foo 1.' + + 'Foo 2.' + + 'Foo 3.]' + + 'Bar' + ); + + pasteHtml( editor, + '
                ' + + '
              • Foo 1
              • ' + + '
              • Foo 2
              • ' + + '
              ' + ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo Bar' + + 'Foo 1' + + 'Foo 2[]' + + 'Bar' + ); + } ); + + it( 'should do nothing when pasting the similar list', () => { + setModelData( model, + 'Foo' + + 'Foo Bar' + + '[]' + + 'Bar' + ); + + pasteHtml( editor, + '
                ' + + '
              1. Foo
              2. ' + + '
              ' + ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo Bar' + + 'Foo[]' + + 'Bar' + ); + } ); + + it( 'should replace the entire list if selected', () => { + setModelData( model, + 'Foo' + + '[Foo Bar]' + + 'Bar' + ); + + pasteHtml( editor, + '
                ' + + '
              1. Foo
              2. ' + + '
              ' + ); + + expect( getModelData( model ) ).to.equal( + 'Foo' + + 'Foo[]' + + 'Bar' + ); + } ); + + function pasteHtml( editor, html ) { + editor.editing.view.document.fire( 'paste', { + dataTransfer: createDataTransfer( { 'text/html': html } ), + stopPropagation() {}, + preventDefault() {} + } ); + } + + function createDataTransfer( data ) { + return { + getData( type ) { + return data[ type ]; + }, + setData() {} + }; + } + } ); + } ); + } ); + + describe( 'listStyle + listStart + listReversed', () => { + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, UndoEditing ], + list: { + properties: { styles: true, startIndex: true, reversed: true } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + view = editor.editing.view; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + describe( 'schema rules', () => { + it( 'should allow set `listStyle` on the `listItem`', () => { + expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listStyle' ) ).to.be.true; + } ); + + it( 'should allow set `listReversed` on the `listItem`', () => { + expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listReversed' ) ).to.be.true; + } ); + + it( 'should allow set `listStart` on the `listItem`', () => { + expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listStart' ) ).to.be.true; + } ); + } ); + + describe( 'command', () => { + it( 'should register `listReversed` command', () => { + const command = editor.commands.get( 'listReversed' ); + + expect( command ).to.be.instanceOf( ListReversedCommand ); + } ); + + it( 'should register `listStyle` command', () => { + const command = editor.commands.get( 'listStyle' ); + + expect( command ).to.be.instanceOf( ListStyleCommand ); + } ); + + it( 'should register `listStart` command', () => { + const command = editor.commands.get( 'listStart' ); + + expect( command ).to.be.instanceOf( ListStartCommand ); + } ); + } ); + + describe( 'conversion in data pipeline', () => { + describe( 'model to data', () => { + it( 'should convert single list (default values)', () => { + setModelData( model, + '' + + 'Foo' + + '' + + '' + + 'Bar' + + '' + ); + + expect( editor.getData() ).to.equal( '
              1. Foo
              2. Bar
              ' ); + } ); + + it( 'should convert single list (non-default values)', () => { + setModelData( model, + '' + + 'Foo' + + '' + + '' + + 'Bar' + + '' + ); + + expect( editor.getData() ).to.equal( + '
              1. Foo
              2. Bar
              ' + ); + } ); + + it( 'should convert nested lists', () => { + setModelData( model, + '' + + '1' + + '' + + '' + + '1.1' + + '' + + '' + + '1.2' + + '' + + '' + + '1.2.1' + + '' + + '' + + '1.2.1.1' + + '' + + '' + + '2' + + '' + + '' + + '3' + + '' + + '' + + '3.1' + + '' + ); + + expect( editor.getData() ).to.equal( + '
                ' + + '
              1. 1' + + '
                  ' + + '
                1. 1.1
                2. ' + + '
                3. 1.2' + + '
                    ' + + '
                  1. 1.2.1' + + '
                      ' + + '
                    1. 1.2.1.1
                    2. ' + + '
                    ' + + '
                  2. ' + + '
                  ' + + '
                4. ' + + '
                ' + + '
              2. ' + + '
              3. 2
              4. ' + + '
              5. 3' + + '
                  ' + + '
                1. 3.1
                2. ' + + '
                ' + + '
              6. ' + + '
              ' + ); + } ); + + it( 'should produce different lists', () => { + setModelData( model, + '' + + 'A1' + + '' + + '' + + 'A2' + + '' + + '' + + 'B1' + + '' + + '' + + 'B2' + + '' + + '' + + 'C1' + + '' + + '' + + 'C2' + + '' + + '' + + 'D1' + + '' + + '' + + 'D2' + + '' + + '' + + 'E1' + + '' + + '' + + 'E2' + + '' + ); + + expect( editor.getData() ).to.equal( + '
                ' + + '
              1. A1
              2. ' + + '
              3. A2
              4. ' + + '
              ' + + '
                ' + + '
              1. B1
              2. ' + + '
              3. B2
              4. ' + + '
              ' + + '
                ' + + '
              1. C1
              2. ' + + '
              3. C2
              4. ' + + '
              ' + + '
                ' + + '
              1. D1
              2. ' + + '
              3. D2
              4. ' + + '
              ' + + '
                ' + + '
              • E1
              • ' + + '
              • E2
              • ' + + '
              ' + ); + } ); + } ); + + describe( 'view to model', () => { + it( 'should convert single list', () => { + editor.setData( + '
              1. Foo
              2. Bar
              ' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + '' + + 'Foo' + + '' + + '' + + 'Bar' + + '' + ); + } ); + + it( 'should convert nested lists', () => { + editor.setData( + '
                ' + + '
              1. 1
              2. ' + + '
              3. 2' + + '
                  ' + + '
                • 2.1
                • ' + + '
                • 2.2
                • ' + + '
                ' + + '
              4. ' + + '
              5. 3' + + '
                  ' + + '
                1. 3.1
                2. ' + + '
                3. 3.2' + + '
                    ' + + '
                  1. 3.2.1
                  2. ' + + '
                  3. 3.2.2
                  4. ' + + '
                  ' + + '
                4. ' + + '
                ' + + '
              6. ' + + '
              7. 4
              8. ' + + '
              ' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + '' + + '1' + + '' + + '' + + '2' + + '' + + '' + + '2.1' + + '' + + '' + + '2.2' + + '' + + '' + + '3' + + '' + + '' + + '3.1' + + '' + + '' + + '3.2' + + '' + + '' + + '3.2.1' + + '' + + '' + + '3.2.2' + + '' + + '' + + '4' + + '' + ); + } ); + } ); + } ); + + describe( 'integrations', () => { + describe( 'merging a list into a reversed list', () => { + it( 'should inherit the attributes when merging the same kind of lists', () => { + setModelData( model, + 'Foo Bar.[]' + + '' + + 'Foo' + + '' + + '' + + 'Bar' + + '' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + '' + + 'Foo Bar.[]' + + '' + + '' + + 'Foo' + + '' + + '' + + 'Bar' + + '' + ); + } ); + + it( 'should not inherit anything if there is no list below the inserted list', () => { + setModelData( model, + 'Foo Bar 1.[]' + + 'Foo Bar 2.' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + '' + + 'Foo Bar 1.[]' + + '' + + 'Foo Bar 2.' + ); + } ); + } ); + + describe( 'modifying "listType" attribute', () => { + it( + 'should inherit the attributes when the modified list is the same kind of the list as previous sibling', + () => { + setModelData( model, + '' + + 'Foo' + + '' + + '' + + 'Bar' + + '' + + 'Foo Bar.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + '' + + 'Foo' + + '' + + '' + + 'Bar' + + '' + + '' + + 'Foo Bar.[]' + + '' + ); + } + ); + + it( 'should add default attributes when changing `listType` to `numbered`', () => { + setModelData( model, + 'Foo Bar.[]' + ); + + editor.execute( 'numberedList' ); + + expect( getModelData( model ) ).to.equal( + '' + + 'Foo Bar.[]' + + '' + ); + } ); + } ); + + describe( 'indenting lists', () => { + it( 'should restore the default value of the start attribute when indenting', () => { + setModelData( model, + '' + + '1.' + + '' + + '' + + '1A.' + + '' + + '' + + '2B.' + + '' + + '' + + '2.[]' + + '' + + '' + + '3.' + + '' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '' + + '1.' + + '' + + '' + + '1A.' + + '' + + '' + + '2B.' + + '' + + '' + + '2.[]' + + '' + + '' + + '3.' + + '' + ); + } ); + + it( + 'should copy the value of the start attribute when indenting a single item into a nested list', + () => { + setModelData( model, + '' + + '1.' + + '' + + '' + + '2.' + + '' + + '' + + '3.[]' + + '' + + '' + + '4.' + + '' + ); + + editor.execute( 'indentList' ); + + expect( getModelData( model ) ).to.equal( + '' + + '1.' + + '' + + '' + + '2.' + + '' + + '' + + '3.[]' + + '' + + '' + + '4.' + + '' + ); + } + ); + } ); + + describe( 'outdenting lists', () => { + it( 'should inherit the attributes from parent list', () => { + setModelData( model, + '' + + '1.' + + '' + + '' + + '2.[]' + + '' + + '' + + '3.' + + '' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '' + + '1.' + + '' + + '' + + '2.[]' + + '' + + '' + + '3.' + + '' + ); + } ); + + it( 'should not inherit the attributes if outdented the only one item in the list', () => { + setModelData( model, + '' + + '1.[]' + + '' + + '' + + '2.' + + '' + + '' + + '3.' + + '' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.[]' + + '' + + '2.' + + '' + + '' + + '3.' + + '' + ); + } ); + + it( 'should not do anything if there is no list after outdenting', () => { + setModelData( model, + '' + + '1.[]' + + '' + ); + + editor.execute( 'outdentList' ); + + expect( getModelData( model ) ).to.equal( + '1.[]' + ); + } ); + } ); + + describe( 'todo list', () => { + let editor, model; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, ListStyleEditing, TodoListEditing ], + list: { + properties: { styles: false, startIndex: true, reversed: false } + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should remove the attributes while switching the list type that uses the list style feature', () => { + setModelData( model, + '' + + 'Foo[]' + + '' + ); + + editor.execute( 'todoList' ); + + expect( getModelData( model ), 'Foo[]' ); } ); } ); } );