Procházet zdrojové kódy

Added tests for ListStylesCommand, added integrations with other plugins.

Kamil Piechaczek před 5 roky
rodič
revize
8f5ff694fe

+ 1 - 0
packages/ckeditor5-list/package.json

@@ -20,6 +20,7 @@
     "@ckeditor/ckeditor5-basic-styles": "^21.0.0",
     "@ckeditor/ckeditor5-block-quote": "^21.0.0",
     "@ckeditor/ckeditor5-clipboard": "^21.0.0",
+    "@ckeditor/ckeditor5-essential": "^21.0.0",
     "@ckeditor/ckeditor5-editor-classic": "^21.0.0",
     "@ckeditor/ckeditor5-enter": "^21.0.0",
     "@ckeditor/ckeditor5-font": "^21.0.0",

+ 11 - 2
packages/ckeditor5-list/src/indentcommand.js

@@ -47,13 +47,14 @@ export default class IndentCommand extends Command {
 	 * Indents or outdents (depending on the {@link #constructor}'s `indentDirection` parameter) selected list items.
 	 *
 	 * @fires execute
+	 * @fires executeCleanup
 	 */
 	execute() {
 		const model = this.editor.model;
 		const doc = model.document;
 		let itemsToChange = Array.from( doc.selection.getSelectedBlocks() );
 
-		return model.change( writer => {
+		model.change( writer => {
 			const lastItem = itemsToChange[ itemsToChange.length - 1 ];
 
 			// Indenting a list item should also indent all the items that are already sub-items of indented item.
@@ -91,7 +92,15 @@ export default class IndentCommand extends Command {
 				}
 			}
 
-			return itemsToChange;
+			/**
+			 * Event fired by the {@link #execute} method.
+			 *
+			 * It allows to execute an action after executing the {@link ~IndentCommand#execute} method, e.g. adjusting
+			 * attributes of changed list items.
+			 *
+			 * @event executeCleanup
+			 */
+			this.fire( 'executeCleanup', itemsToChange );
 		} );
 	}
 

+ 3 - 1
packages/ckeditor5-list/src/listcommand.js

@@ -65,7 +65,7 @@ export default class ListCommand extends Command {
 		const turnOff = this.value === true;
 		// If we are turning off items, we are going to rename them to paragraphs.
 
-		model.change( writer => {
+		return model.change( writer => {
 			// If part of a list got turned off, we need to handle (outdent) all of sub-items of the last turned-off item.
 			// To be sure that model is all the time in a good state, we first fix items below turned-off item.
 			if ( turnOff ) {
@@ -211,6 +211,8 @@ export default class ListCommand extends Command {
 					writer.setAttribute( 'listType', this.type, element );
 				}
 			}
+
+			return blocks;
 		} );
 	}
 

+ 118 - 3
packages/ckeditor5-list/src/liststylescommand.js

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { getSiblingNodes } from './utils';
+import TreeWalker from '@ckeditor/ckeditor5-engine/src/model/treewalker';
 
 /**
  * The list style command. It is used by the {@link module:list/liststyles~ListStyles list styles feature}.
@@ -17,6 +17,25 @@ import { getSiblingNodes } from './utils';
  */
 export default class ListStylesCommand extends Command {
 	/**
+	 * Creates an instance of the command.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor The editor instance.
+	 * @param {String} defaultType The List type that will be used by default if the value was not specified during
+	 * the command execution.
+	 */
+	constructor( editor, defaultType ) {
+		super( editor );
+
+		/**
+		 * The type of the list created by the command.
+		 *
+		 * @private
+		 * @member {'numbered'|'bulleted'|'todo'}
+		 */
+		this._defaultType = defaultType;
+	}
+
+	/**
 	 * @inheritDoc
 	 */
 	refresh() {
@@ -57,10 +76,16 @@ export default class ListStylesCommand extends Command {
 	 * Checks the command's {@link #value}.
 	 *
 	 * @private
-	 * @returns {Boolean} The current value.
+	 * @returns {String|null} The current value.
 	 */
 	_getValue() {
+		const listItem = this.editor.model.document.selection.getFirstPosition().parent;
 
+		if ( listItem && listItem.is( 'element', 'listItem' ) ) {
+			return listItem.getAttribute( 'listStyle' );
+		}
+
+		return null;
 	}
 
 	/**
@@ -70,10 +95,20 @@ export default class ListStylesCommand extends Command {
 	 * @returns {Boolean} Whether the command should be enabled.
 	 */
 	_checkEnabled() {
-		return true;
+		const editor = this.editor;
+
+		const numberedList = editor.commands.get( 'numberedList' );
+		const bulletedList = editor.commands.get( 'bulletedList' );
+
+		return numberedList.isEnabled || bulletedList.isEnabled;
 	}
 }
 
+// Returns a position that is hooked in the `listItem` element.
+// It can be the selection starting position or end.
+//
+// @param {module:engine/model/selection~Selection} selection
+// @returns {module:engine/model/position~Position|null}
 function findPositionInsideList( selection ) {
 	const startPosition = selection.getFirstPosition();
 
@@ -89,3 +124,83 @@ function findPositionInsideList( selection ) {
 
 	return null;
 }
+
+// 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.
+//
+// @param {module:engine/model/position~Position} position Starting position.
+// @param {'forward'|'backward'} direction Walking direction.
+// @returns {Array.<module:engine/model/element~Element>
+function getSiblingNodes( position, direction ) {
+	const items = [];
+	const listItem = position.parent;
+	const walkerOptions = {
+		ignoreElementEnd: true,
+		startPosition: position,
+		shallow: true,
+		direction
+	};
+	const limitIndent = listItem.getAttribute( 'listIndent' );
+	const nodes = [ ...new TreeWalker( walkerOptions ) ]
+		.filter( value => value.item.is( 'element' ) )
+		.map( value => value.item );
+
+	for ( const element of nodes ) {
+		// If found something else than `listItem`, we're out of the list scope.
+		if ( !element.is( 'element', 'listItem' ) ) {
+			break;
+		}
+
+		// If current parsed item has lower indent that element that the element that was a starting point,
+		// it means we left a nested list. Abort searching items.
+		//
+		// ■ List item 1.       [listIndent=0]
+		//     ○ List item 2.[] [listIndent=1], limitIndent = 1,
+		//     ○ List item 3.   [listIndent=1]
+		// ■ List item 4.       [listIndent=0]
+		//
+		// Abort searching when leave nested list.
+		if ( element.getAttribute( 'listIndent' ) < limitIndent ) {
+			break;
+		}
+
+		// ■ List item 1.[]     [listIndent=0] limitIndent = 0,
+		//     ○ List item 2.   [listIndent=1]
+		//     ○ List item 3.   [listIndent=1]
+		// ■ List item 4.       [listIndent=0]
+		//
+		// Ignore nested lists.
+		if ( element.getAttribute( 'listIndent' ) !== limitIndent ) {
+			continue;
+		}
+
+		// ■ List item 1.[]  [listType=bulleted]
+		// 1. List item 2.   [listType=numbered]
+		// 2.List item 3.    [listType=numbered]
+		//
+		// Abort searching when found a different kind of a list.
+		if ( element.getAttribute( 'listType' ) !== listItem.getAttribute( 'listType' ) ) {
+			break;
+		}
+
+		// ■ List item 1.[]  [listType=bulleted]
+		// ■ List item 2.    [listType=bulleted]
+		// ○ List item 3.    [listType=bulleted]
+		// ○ List item 4.    [listType=bulleted]
+		//
+		// Abort searching when found a different list style.
+		if ( element.getAttribute( 'listStyle' ) !== listItem.getAttribute( 'listStyle' ) ) {
+			break;
+		}
+
+		if ( direction === 'backward' ) {
+			items.unshift( element );
+		} else {
+			items.push( element );
+		}
+	}
+
+	return items;
+}

+ 163 - 69
packages/ckeditor5-list/src/liststylesediting.js

@@ -10,6 +10,7 @@
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ListEditing from './listediting';
 import ListStylesCommand from './liststylescommand';
+import { getSiblingListItem } from './utils';
 
 const DEFAULT_LIST_TYPE = 'default';
 
@@ -36,6 +37,9 @@ export default class ListStylesEditing extends Plugin {
 		return 'ListStylesEditing';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	init() {
 		const editor = this.editor;
 		const model = editor.model;
@@ -45,28 +49,32 @@ export default class ListStylesEditing extends Plugin {
 			allowAttributes: [ 'listStyle' ]
 		} );
 
-		editor.commands.add( 'listStyles', new ListStylesCommand( editor ) );
+		editor.commands.add( 'listStyles', new ListStylesCommand( editor, DEFAULT_LIST_TYPE ) );
 
 		// Fix list attributes when modifying their nesting levels (the `listIndent` attribute).
-		this.listenTo( editor.commands.get( 'indentList' ), 'execute', fixListAfterIndentListCommand( editor ) );
-		this.listenTo( editor.commands.get( 'outdentList' ), 'execute', fixListAfterOutdentListCommand( editor ) );
+		this.listenTo( editor.commands.get( 'indentList' ), 'executeCleanup', fixListAfterIndentListCommand( editor ) );
+		this.listenTo( editor.commands.get( 'outdentList' ), 'executeCleanup', fixListAfterOutdentListCommand( editor ) );
 
-		// Register a post-fix that ensures that the `listStyle` attribute is specified in each `listItem` element.
+		// Register a post-fixer that ensures that the `listStyle` attribute is specified in each `listItem` element.
 		model.document.registerPostFixer( fixListStyleAttributeOnListItemElements( editor ) );
 
-		// Disallow the `listStyle` attribute on to-do lists.
-		model.schema.addAttributeCheck( ( context, attributeName ) => {
-			const item = context.last;
-
-			if ( attributeName == 'listStyle' && item.name == 'listItem' && item.getAttribute( 'listType' ) == 'todo' ) {
-				return false;
-			}
-		} );
-
 		// Set up conversion.
 		editor.conversion.for( 'upcast' ).add( upcastListItem() );
 		editor.conversion.for( 'downcast' ).add( downcastListStyleAttribute() );
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	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.
+		// We need to registry the hook here since the `TodoList` plugin can be added after the `ListStylesEditing`.
+		if ( editor.commands.get( 'todoList' ) ) {
+			editor.model.document.registerPostFixer( removeListStyleAttributeFromTodoList( editor ) );
+		}
+	}
 }
 
 // Returns a converter that consumes the `style` attribute and search for `list-style-type` definition.
@@ -94,6 +102,7 @@ function upcastListItem() {
 function downcastListStyleAttribute() {
 	return dispatcher => {
 		dispatcher.on( 'attribute:listStyle:listItem', ( evt, data, conversionApi ) => {
+			// TODO: This converter is a little buggy. I know. The things could be done better.
 			const viewWriter = conversionApi.writer;
 			const currentItem = data.item;
 			const previousItem = currentItem.previousSibling;
@@ -168,7 +177,7 @@ function downcastListStyleAttribute() {
 	}
 }
 
-// When indenting list, nested list should clear its value for the `listStyle` attribute.
+// When indenting list, nested list should clear its value for the `listStyle` attribute or inherit from nested lists.
 //
 // ■ List item 1.
 // ■ List item 2.[]
@@ -183,48 +192,38 @@ function downcastListStyleAttribute() {
 // @param {module:core/editor/editor~Editor} editor
 // @returns {Function}
 function fixListAfterIndentListCommand( editor ) {
-	return () => { // evt
-		return editor;
-		// This function must be re-written since it does not work correctly.
-		// const changedItems = evt.return;
-		// const previousSibling = changedItems[ 0 ].previousSibling;
-		//
-		// const indent = changedItems[ 0 ].getAttribute( 'listIndent' );
-		// let listItem = changedItems[ 0 ];
-		//
-		// console.log( 'looking for indent = ', indent );
-		// console.log( 'before do while' );
-		//
-		// // ■ List item 1.
-		// //     ⬤ List item 2.
-		// // ■ List item 3.[]
-		// // ■ List item 4.
-		// //
-		// // After indenting the list, `List item 3` should inherit the `listStyle` attribute from `List item 2`.
-		// //
-		// // ■ List item 1.
-		// //     ⬤ List item 2.
-		// //     ⬤ List item 3.[]
-		// // ■ List item 4.
-		//
-		// while ( listItem.getAttribute( 'listIndent' ) === indent ) {
-		// 	listItem = listItem.previousSibling;
-		//
-		// 	console.log( listItem.getChild( 0 )._data, 'indent = ', listItem.getAttribute( 'listIndent' ) );
-		// }
-		//
-		// console.log( 'after do while' );
-		// console.log( listItem, listItem.getChild( 0 )._data );
-		//
-		// const itemsToUpdate = changedItems.filter( item => {
-		// 	return item.getAttribute( 'listIndent' ) === previousSibling.getAttribute( 'listIndent' ) + 1;
-		// } );
+	return ( evt, changedItems ) => {
+		let valueToSet;
+
+		const root = changedItems[ 0 ];
+		const rootIndent = root.getAttribute( 'listIndent' );
+
+		const itemsToUpdate = changedItems.filter( item => item.getAttribute( 'listIndent' ) === rootIndent );
+
+		// A case where a few list items are intended must be checked separately
+		// since `getSiblingListItem()` returns the first changed element.
+		// ■ List item 1.
+		//     ○ [List item 2.
+		//     ○ List item 3.]
+		// ■ List item 4.
 		//
-		// editor.model.change( writer => {
-		// 	for ( const item of itemsToUpdate ) {
-		// 		writer.setAttribute( 'listStyle', DEFAULT_LIST_TYPE, item );
-		// 	}
-		// } );
+		// 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, {
+				sameIndent: true, direction: 'backward', listIndent: rootIndent
+			} );
+
+			valueToSet = previousSibling.getAttribute( 'listStyle' );
+		}
+
+		editor.model.change( writer => {
+			for ( const item of itemsToUpdate ) {
+				writer.setAttribute( 'listStyle', valueToSet, item );
+			}
+		} );
 	};
 }
 
@@ -245,15 +244,15 @@ function fixListAfterIndentListCommand( editor ) {
 // @param {module:core/editor/editor~Editor} editor
 // @returns {Function}
 function fixListAfterOutdentListCommand( editor ) {
-	return evt => {
-		const changedItems = evt.return.reverse()
-			.filter( item => item.is( 'element', 'listItem' ) );
+	return ( evt, changedItems ) => {
+		changedItems = changedItems.reverse().filter( item => item.is( 'element', 'listItem' ) );
 
 		if ( !changedItems.length ) {
 			return;
 		}
 
 		const indent = changedItems[ 0 ].getAttribute( 'listIndent' );
+		const listType = changedItems[ 0 ].getAttribute( 'listType' );
 		let listItem = changedItems[ 0 ].previousSibling;
 
 		// ■ List item 1.
@@ -281,22 +280,24 @@ function fixListAfterOutdentListCommand( editor ) {
 		//     ○ List item 3.
 		// ■ List item 4.
 		if ( !listItem ) {
-			listItem = changedItems[ 0 ].nextSibling;
-
-			while ( changedItems.includes( listItem ) && listItem.getAttribute( 'listIndent' ) === indent ) {
-				listItem = listItem.nextSibling;
-			}
+			listItem = changedItems[ changedItems.length - 1 ].nextSibling;
 		}
 
 		// And such a list should not modify anything.
+		// However, `listItem` can indicate a node below the list. Be sure that we have the `listItem` element.
 		// ■ List item 1.[]
 		//     ○ List item 2.
 		//     ○ List item 3.
-		// "The later if check."
+		// <paragraph>The later if check.</paragraph>
 		if ( !listItem || !listItem.is( 'element', 'listItem' ) ) {
 			return;
 		}
 
+		// Do not modify the list if found `listItem` represents other type of list than outdented list items.
+		if ( listItem.getAttribute( 'listType' ) !== listType ) {
+			return;
+		}
+
 		editor.model.change( writer => {
 			const itemsToUpdate = changedItems.filter( item => item.getAttribute( 'listIndent' ) === indent );
 
@@ -345,7 +346,7 @@ function fixListAfterOutdentListCommand( editor ) {
 function fixListStyleAttributeOnListItemElements( editor ) {
 	return writer => {
 		let wasFixed = false;
-		const insertedListItems = [];
+		let insertedListItems = [];
 
 		for ( const change of editor.model.document.differ.getChanges() ) {
 			if ( change.type == 'insert' && change.name == 'listItem' ) {
@@ -353,19 +354,44 @@ function fixListStyleAttributeOnListItemElements( editor ) {
 			}
 		}
 
+		// Don't touch todo lists.
+		insertedListItems = insertedListItems.filter( item => item.getAttribute( 'listType' ) !== 'todo' );
+
 		if ( !insertedListItems.length ) {
 			return wasFixed;
 		}
 
-		const existingListItem = insertedListItems[ insertedListItems.length - 1 ].nextSibling;
+		// Check whether the last inserted element is next to the `listItem` element.
+		//
+		// ■ Paragraph[]  // <-- The inserted item.
+		// ■ List item 1.
+		let existingListItem = insertedListItems[ insertedListItems.length - 1 ].nextSibling;
 
-		if ( !existingListItem ) {
-			return wasFixed;
+		// If doesn't, maybe the `listItem` was inserted at the end of the list.
+		//
+		// ■ List item 1.
+		// ■ Paragraph[]  // <-- The inserted item.
+		if ( !existingListItem || !existingListItem.is( 'element', 'listItem' ) ) {
+			existingListItem = insertedListItems[ insertedListItems.length - 1 ].previousSibling;
+
+			if ( existingListItem ) {
+				const indent = insertedListItems[ 0 ].getAttribute( 'listIndent' );
+
+				// But we need to find a `listItem` with the `listIndent=0` attribute.
+				// If doesn't, maybe the `listItem` was inserted at the end of the list.
+				//
+				// ■ List item 1.
+				//     ○ List item 2.
+				// ■ Paragraph[]  // <-- The inserted item.
+				while ( existingListItem.is( 'element', 'listItem' ) && existingListItem.getAttribute( 'listIndent' ) !== indent ) {
+					existingListItem = existingListItem.previousSibling;
+				}
+			}
 		}
 
 		for ( const item of insertedListItems ) {
 			if ( !item.hasAttribute( 'listStyle' ) ) {
-				if ( existingListItem.getAttribute( 'listType' ) === item.getAttribute( 'listType' ) ) {
+				if ( shouldInheritListType( existingListItem ) ) {
 					writer.setAttribute( 'listStyle', existingListItem.getAttribute( 'listStyle' ), item );
 				} else {
 					writer.setAttribute( 'listStyle', DEFAULT_LIST_TYPE, item );
@@ -377,4 +403,72 @@ function fixListStyleAttributeOnListItemElements( editor ) {
 
 		return wasFixed;
 	};
+
+	// Checks whether the `listStyle` attribute 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
+	// @returns {Boolean}
+	function shouldInheritListType( baseItem ) {
+		if ( !baseItem ) {
+			return false;
+		}
+
+		const baseListStyle = baseItem.getAttribute( 'listStyle' );
+
+		if ( !baseListStyle ) {
+			return false;
+		}
+
+		if ( baseListStyle === DEFAULT_LIST_TYPE ) {
+			return false;
+		}
+
+		return true;
+	}
+}
+
+// Removes the `listStyle` attribute from "todo" list items.
+//
+// @private
+// @param {module:core/editor/editor~Editor} editor
+// @returns {Function}
+function removeListStyleAttributeFromTodoList( editor ) {
+	return writer => {
+		let todoListItems = [];
+
+		for ( const change of editor.model.document.differ.getChanges() ) {
+			const item = getItemFromChange( change );
+
+			if ( item && item.is( 'element', 'listItem' ) && item.getAttribute( 'listType' ) === 'todo' ) {
+				todoListItems.push( item );
+			}
+		}
+
+		todoListItems = todoListItems.filter( item => item.hasAttribute( 'listStyle' ) );
+
+		if ( !todoListItems.length ) {
+			return false;
+		}
+
+		for ( const item of todoListItems ) {
+			writer.removeAttribute( 'listStyle', item );
+		}
+
+		return true;
+	};
+
+	function getItemFromChange( change ) {
+		if ( change.type === 'attribute' ) {
+			return change.range.start.nodeAfter;
+		}
+
+		if ( change.type === 'insert' ) {
+			return change.position.nodeAfter;
+		}
+
+		return null;
+	}
 }

+ 6 - 50
packages/ckeditor5-list/src/utils.js

@@ -9,7 +9,6 @@
 
 import { getFillerOffset } from '@ckeditor/ckeditor5-engine/src/view/containerelement';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import TreeWalker from '@ckeditor/ckeditor5-engine/src/model/treewalker';
 
 /**
  * Creates a list item {@link module:engine/view/containerelement~ContainerElement}.
@@ -208,6 +207,7 @@ export function positionAfterUiElements( viewPosition ) {
  * @param {Boolean} [options.sameIndent=false] Whether the sought sibling should have the same indentation.
  * @param {Boolean} [options.smallerIndent=false] Whether the sought sibling should have a smaller indentation.
  * @param {Number} [options.listIndent] The reference indentation.
+ * @param {'forward'|'backward'} [options.direction='backward'] Walking direction.
  * @returns {module:engine/model/item~Item|null}
  */
 export function getSiblingListItem( modelItem, options ) {
@@ -224,7 +224,11 @@ export function getSiblingListItem( modelItem, options ) {
 			return item;
 		}
 
-		item = item.previousSibling;
+		if ( options.direction === 'forward' ) {
+			item = item.nextSibling;
+		} else {
+			item = item.previousSibling;
+		}
 	}
 
 	return null;
@@ -280,54 +284,6 @@ export function findNestedList( viewElement ) {
 	return null;
 }
 
-export function getSiblingNodes( fromOrBoundaries, direction ) {
-	const items = [];
-	const walkerOptions = {
-		ignoreElementEnd: true,
-		shallow: true,
-		direction
-	};
-	let parentElement;
-
-	if ( fromOrBoundaries.is( 'range' ) ) {
-		walkerOptions.boundaries = fromOrBoundaries;
-		parentElement = fromOrBoundaries.start.parent;
-	} else {
-		walkerOptions.startPosition = fromOrBoundaries;
-		parentElement = fromOrBoundaries.parent;
-	}
-
-	const nodes = [ ...new TreeWalker( walkerOptions ) ]
-		.filter( value => value.item.is( 'element' ) )
-		.map( value => value.item );
-
-	for ( const element of nodes ) {
-		if ( !element.is( 'element', 'listItem' ) ) {
-			break;
-		}
-
-		if ( element.getAttribute( 'listIndent' ) !== parentElement.getAttribute( 'listIndent' ) ) {
-			break;
-		}
-
-		if ( element.getAttribute( 'listType' ) !== parentElement.getAttribute( 'listType' ) ) {
-			break;
-		}
-
-		if ( element.getAttribute( 'listStyle' ) !== parentElement.getAttribute( 'listStyle' ) ) {
-			break;
-		}
-
-		if ( direction === 'backward' ) {
-			items.unshift( element );
-		} else {
-			items.push( element );
-		}
-	}
-
-	return items;
-}
-
 // Implementation of getFillerOffset for view list item element.
 //
 // @returns {Number|null} Block filler offset or `null` if block filler is not needed.

+ 20 - 0
packages/ckeditor5-list/tests/indentcommand.js

@@ -193,6 +193,26 @@ describe( 'IndentCommand', () => {
 					'<listItem listIndent="0" listType="bulleted">g</listItem>'
 				);
 			} );
+
+			it( 'should fire "executeCleanup" event after finish all operations with all changed items', done => {
+				model.change( writer => {
+					writer.setSelection( root.getChild( 1 ), 0 );
+				} );
+
+				command.on( 'executeCleanup', ( evt, data ) => {
+					expect( data ).to.deep.equal( [
+						root.getChild( 1 ),
+						root.getChild( 2 ),
+						root.getChild( 3 ),
+						root.getChild( 4 ),
+						root.getChild( 5 )
+					] );
+
+					done();
+				} );
+
+				command.execute();
+			} );
 		} );
 	} );
 

+ 342 - 0
packages/ckeditor5-list/tests/liststylescommand.js

@@ -0,0 +1,342 @@
+/**
+ * @license Copyright (c) 2003-2020, 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 ListStylesEditing from '../src/liststylesediting';
+
+describe( 'ListStylesCommand', () => {
+	let editor, model, bulletedListCommand, numberedListCommand, listStylesCommand;
+
+	beforeEach( () => {
+		return VirtualTestEditor
+			.create( {
+				plugins: [ Paragraph, ListStylesEditing ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+
+				bulletedListCommand = editor.commands.get( 'bulletedList' );
+				numberedListCommand = editor.commands.get( 'numberedList' );
+				listStylesCommand = editor.commands.get( 'listStyles' );
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	describe( '#isEnabled', () => {
+		it( 'should be true if bulletedList or numberedList is enabled', () => {
+			bulletedListCommand.isEnabled = true;
+			numberedListCommand.isEnabled = false;
+			listStylesCommand.refresh();
+
+			expect( listStylesCommand.isEnabled ).to.equal( true );
+
+			bulletedListCommand.isEnabled = false;
+			numberedListCommand.isEnabled = true;
+			listStylesCommand.refresh();
+
+			expect( listStylesCommand.isEnabled ).to.equal( true );
+		} );
+
+		it( 'should be false if bulletedList and numberedList are enabled', () => {
+			bulletedListCommand.isEnabled = false;
+			numberedListCommand.isEnabled = false;
+
+			listStylesCommand.refresh();
+
+			expect( listStylesCommand.isEnabled ).to.equal( false );
+		} );
+	} );
+
+	describe( '#value', () => {
+		it( 'should return null if selected a paragraph', () => {
+			setData( model, '<paragraph>Foo[]</paragraph>' );
+
+			expect( listStylesCommand.value ).to.equal( null );
+		} );
+
+		it( 'should return null if selection starts in a paragraph and ends in a list item', () => {
+			setData( model,
+				'<paragraph>Fo[o</paragraph>' +
+				'<listItem listIndent="0" listType="bulleted" listStyle="default">Foo]</listItem>'
+			);
+
+			expect( listStylesCommand.value ).to.equal( null );
+		} );
+
+		it( 'should return the value of `listStyle` attribute if selection is inside a listItem (collapsed selection)', () => {
+			setData( model, '<listItem listIndent="0" listType="bulleted" listStyle="default">Foo[]</listItem>' );
+
+			expect( listStylesCommand.value ).to.equal( 'default' );
+		} );
+
+		it( 'should return the value of `listStyle` attribute if selection is inside a listItem (non-collapsed selection)', () => {
+			setData( model, '<listItem listIndent="0" listType="bulleted" listStyle="default">[Foo]</listItem>' );
+
+			expect( listStylesCommand.value ).to.equal( 'default' );
+		} );
+
+		it( 'should return the value of `listStyle` attribute if selected more elements in the same list', () => {
+			setData( model,
+				'<listItem listIndent="0" listType="bulleted" listStyle="square">[1.</listItem>' +
+				'<listItem listIndent="0" listType="bulleted" listStyle="square">2.]</listItem>' +
+				'<listItem listIndent="0" listType="bulleted" listStyle="square">3.</listItem>'
+			);
+
+			expect( listStylesCommand.value ).to.equal( 'square' );
+		} );
+
+		it( 'should return the value of `listStyle` attribute for the selection inside a nested list', () => {
+			setData( model,
+				'<listItem listIndent="0" listType="bulleted" listStyle="square">1.</listItem>' +
+				'<listItem listIndent="0" listType="bulleted" listStyle="disc">1.1.[]</listItem>' +
+				'<listItem listIndent="0" listType="bulleted" listStyle="square">2.</listItem>'
+			);
+
+			expect( listStylesCommand.value ).to.equal( 'disc' );
+		} );
+
+		it( 'should return the value of `listStyle` attribute from a list where the selection starts (selection over nested list)', () => {
+			setData( model,
+				'<listItem listIndent="0" listType="bulleted" listStyle="square">1.</listItem>' +
+				'<listItem listIndent="0" listType="bulleted" listStyle="disc">1.1.[</listItem>' +
+				'<listItem listIndent="0" listType="bulleted" listStyle="square">2.]</listItem>'
+			);
+
+			expect( listStylesCommand.value ).to.equal( 'disc' );
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should set the `listStyle` attribute for collapsed selection', () => {
+			setData( model,
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.[]</listItem>'
+			);
+
+			listStylesCommand.execute( { type: 'circle' } );
+
+			expect( getData( model ) ).to.equal(
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>'
+			);
+		} );
+
+		it( 'should set the `listStyle` attribute for non-collapsed selection', () => {
+			setData( model,
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">[1.]</listItem>'
+			);
+
+			listStylesCommand.execute( { type: 'circle' } );
+
+			expect( getData( model ) ).to.equal(
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">[1.]</listItem>'
+			);
+		} );
+
+		it( 'should set the `listStyle` attribute for all the same list items (collapsed selection)', () => {
+			setData( model,
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.[]</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">3.</listItem>'
+			);
+
+			listStylesCommand.execute( { type: 'circle' } );
+
+			expect( getData( model ) ).to.equal(
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+			);
+		} );
+
+		it( 'should set the `listStyle` attribute for all the same list items and ignores nested lists', () => {
+			setData( model,
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.[]</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="1" listStyle="default" listType="bulleted">2.1.</listItem>' +
+				'<listItem listIndent="1" listStyle="default" listType="bulleted">2.2.</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">3.</listItem>' +
+				'<listItem listIndent="1" listStyle="default" listType="bulleted">3.1.</listItem>'
+			);
+
+			listStylesCommand.execute( { type: 'circle' } );
+
+			expect( getData( model ) ).to.equal(
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="1" listStyle="default" listType="bulleted">2.1.</listItem>' +
+				'<listItem listIndent="1" listStyle="default" listType="bulleted">2.2.</listItem>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>' +
+				'<listItem listIndent="1" listStyle="default" listType="bulleted">3.1.</listItem>'
+			);
+		} );
+
+		it( 'should set the `listStyle` attribute for all the same list items and ignores nested lists (selection in nested list)', () => {
+			setData( model,
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="1" listStyle="default" listType="bulleted">2.1.[]</listItem>' +
+				'<listItem listIndent="1" listStyle="default" listType="bulleted">2.2.</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">3.</listItem>' +
+				'<listItem listIndent="1" listStyle="default" listType="bulleted">3.1.</listItem>'
+			);
+
+			listStylesCommand.execute( { type: 'disc' } );
+
+			expect( getData( model ) ).to.equal(
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="1" listStyle="disc" listType="bulleted">2.1.[]</listItem>' +
+				'<listItem listIndent="1" listStyle="disc" listType="bulleted">2.2.</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">3.</listItem>' +
+				'<listItem listIndent="1" listStyle="default" listType="bulleted">3.1.</listItem>'
+			);
+		} );
+
+		it( 'should stop searching for the list items when spotted non-listItem element', () => {
+			setData( model,
+				'<paragraph>Foo.</paragraph>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.[]</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">3.</listItem>'
+			);
+
+			listStylesCommand.execute( { type: 'circle' } );
+
+			expect( getData( model ) ).to.equal(
+				'<paragraph>Foo.</paragraph>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+			);
+		} );
+
+		it( 'should stop searching for the list items when spotted listItem with different listType attribute', () => {
+			setData( model,
+				'<paragraph>Foo.</paragraph>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.[]</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="numbered">1.</listItem>'
+			);
+
+			listStylesCommand.execute( { type: 'circle' } );
+
+			expect( getData( model ) ).to.equal(
+				'<paragraph>Foo.</paragraph>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="numbered">1.</listItem>'
+			);
+		} );
+
+		it( 'should stop searching for the list items when spotted listItem with different listStyle attribute', () => {
+			setData( model,
+				'<paragraph>Foo.</paragraph>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.[]</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="0" listStyle="disc" listType="bulleted">1.</listItem>'
+			);
+
+			listStylesCommand.execute( { type: 'circle' } );
+
+			expect( getData( model ) ).to.equal(
+				'<paragraph>Foo.</paragraph>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="0" listStyle="disc" listType="bulleted">1.</listItem>'
+			);
+		} );
+
+		it( 'should start searching for the list items from starting position (collapsed selection)', () => {
+			setData( model,
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">[3.</listItem>' +
+				'<paragraph>Foo.]</paragraph>'
+			);
+
+			listStylesCommand.execute( { type: 'circle' } );
+
+			expect( getData( model ) ).to.equal(
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">[3.</listItem>' +
+				'<paragraph>Foo.]</paragraph>'
+			);
+		} );
+
+		it( 'should start searching for the list items from ending position (collapsed selection)', () => {
+			setData( model,
+				'<paragraph>[Foo.</paragraph>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.]</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">3.</listItem>'
+			);
+
+			listStylesCommand.execute( { type: 'circle' } );
+
+			expect( getData( model ) ).to.equal(
+				'<paragraph>[Foo.</paragraph>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.]</listItem>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>' +
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+			);
+		} );
+
+		it( 'should use default type if not specified (no options passed)', () => {
+			setData( model,
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>'
+			);
+
+			listStylesCommand.execute();
+
+			expect( getData( model ) ).to.equal(
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.[]</listItem>'
+			);
+		} );
+
+		it( 'should use default type if not specified (passed an empty object)', () => {
+			setData( model,
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>'
+			);
+
+			listStylesCommand.execute( {} );
+
+			expect( getData( model ) ).to.equal(
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.[]</listItem>'
+			);
+		} );
+
+		it( 'should use default type if not specified (passed null as value)', () => {
+			setData( model,
+				'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>'
+			);
+
+			listStylesCommand.execute( { type: null } );
+
+			expect( getData( model ) ).to.equal(
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.[]</listItem>'
+			);
+		} );
+
+		it( 'should not update anything if no listItem found in the selection', () => {
+			setData( model,
+				'<paragraph>[Foo.]</paragraph>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.</listItem>'
+			);
+
+			listStylesCommand.execute( { type: 'circle' } );
+
+			expect( getData( model ) ).to.equal(
+				'<paragraph>[Foo.]</paragraph>' +
+				'<listItem listIndent="0" listStyle="default" listType="bulleted">1.</listItem>'
+			);
+		} );
+	} );
+} );

+ 572 - 60
packages/ckeditor5-list/tests/liststylesediting.js

@@ -5,11 +5,12 @@
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 import ListStylesEditing from '../src/liststylesediting';
 import TodoListEditing from '../src/todolistediting';
+import ListStylesCommand from '../src/liststylescommand';
 
 describe( 'ListStylesEditing', () => {
 	let editor, model, view;
@@ -17,7 +18,7 @@ describe( 'ListStylesEditing', () => {
 	beforeEach( () => {
 		return VirtualTestEditor
 			.create( {
-				plugins: [ Paragraph, ListStylesEditing, TodoListEditing ]
+				plugins: [ Paragraph, ListStylesEditing, UndoEditing ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -42,11 +43,13 @@ describe( 'ListStylesEditing', () => {
 		it( 'should allow set `listStyle` on the `listItem`', () => {
 			expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'listStyle' ) ).to.be.true;
 		} );
+	} );
 
-		it( 'should not accept `listStyle` on the `listItem` element with `listType:todo` attribute', () => {
-			const todoListItem = new ModelElement( 'listItem', { listType: 'todo' } );
+	describe( 'command', () => {
+		it( 'should register listStyle command', () => {
+			const command = editor.commands.get( 'listStyles' );
 
-			expect( model.schema.checkAttribute( [ '$root', todoListItem ], 'listStyle' ) ).to.be.false;
+			expect( command ).to.be.instanceOf( ListStylesCommand );
 		} );
 	} );
 
@@ -117,14 +120,14 @@ describe( 'ListStylesEditing', () => {
 
 				expect( editor.getData() ).to.equal(
 					'<ul style="list-style-type:circle;">' +
-						'<li>Foo 1' +
-							'<ul style="list-style-type:disc;">' +
-								'<li>Bar 1</li>' +
-								'<li>Bar 2</li>' +
-							'</ul>' +
-						'</li>' +
-						'<li>Foo 2</li>' +
-						'<li>Foo 3</li>' +
+					'<li>Foo 1' +
+					'<ul style="list-style-type:disc;">' +
+					'<li>Bar 1</li>' +
+					'<li>Bar 2</li>' +
+					'</ul>' +
+					'</li>' +
+					'<li>Foo 2</li>' +
+					'<li>Foo 3</li>' +
 					'</ul>'
 				);
 			} );
@@ -140,14 +143,14 @@ describe( 'ListStylesEditing', () => {
 
 				expect( editor.getData() ).to.equal(
 					'<ol style="list-style-type:decimal-leading-zero;">' +
-						'<li>Foo 1' +
-							'<ol style="list-style-type:lower-latin;">' +
-								'<li>Bar 1</li>' +
-								'<li>Bar 2</li>' +
-							'</ol>' +
-						'</li>' +
-						'<li>Foo 2</li>' +
-						'<li>Foo 3</li>' +
+					'<li>Foo 1' +
+					'<ol style="list-style-type:lower-latin;">' +
+					'<li>Bar 1</li>' +
+					'<li>Bar 2</li>' +
+					'</ol>' +
+					'</li>' +
+					'<li>Foo 2</li>' +
+					'<li>Foo 3</li>' +
 					'</ol>'
 				);
 			} );
@@ -163,14 +166,14 @@ describe( 'ListStylesEditing', () => {
 
 				expect( editor.getData() ).to.equal(
 					'<ul style="list-style-type:square;">' +
-						'<li>Foo 1' +
-							'<ol style="list-style-type:lower-roman;">' +
-								'<li>Bar 1</li>' +
-								'<li>Bar 2</li>' +
-							'</ol>' +
-						'</li>' +
-						'<li>Foo 2</li>' +
-						'<li>Foo 3</li>' +
+					'<li>Foo 1' +
+					'<ol style="list-style-type:lower-roman;">' +
+					'<li>Bar 1</li>' +
+					'<li>Bar 2</li>' +
+					'</ol>' +
+					'</li>' +
+					'<li>Foo 2</li>' +
+					'<li>Foo 3</li>' +
 					'</ul>'
 				);
 			} );
@@ -185,13 +188,13 @@ describe( 'ListStylesEditing', () => {
 
 				expect( editor.getData() ).to.equal(
 					'<ol style="list-style-type:decimal;">' +
-						'<li>Foo 1</li>' +
-						'<li>Foo 2' +
-							'<ol style="list-style-type:decimal;">' +
-								'<li>Bar 1</li>' +
-								'<li>Bar 2</li>' +
-							'</ol>' +
-						'</li>' +
+					'<li>Foo 1</li>' +
+					'<li>Foo 2' +
+					'<ol style="list-style-type:decimal;">' +
+					'<li>Bar 1</li>' +
+					'<li>Bar 2</li>' +
+					'</ol>' +
+					'</li>' +
 					'</ol>'
 				);
 			} );
@@ -206,12 +209,12 @@ describe( 'ListStylesEditing', () => {
 
 				expect( editor.getData() ).to.equal(
 					'<ol style="list-style-type:decimal;">' +
-						'<li>Foo 1</li>' +
-						'<li>Foo 2</li>' +
+					'<li>Foo 1</li>' +
+					'<li>Foo 2</li>' +
 					'</ol>' +
 					'<ul style="list-style-type:disc;">' +
-						'<li>Bar 1</li>' +
-						'<li>Bar 2</li>' +
+					'<li>Bar 1</li>' +
+					'<li>Bar 2</li>' +
 					'</ul>'
 				);
 			} );
@@ -226,12 +229,12 @@ describe( 'ListStylesEditing', () => {
 
 				expect( editor.getData() ).to.equal(
 					'<ul style="list-style-type:disc;">' +
-						'<li>Foo 1</li>' +
-						'<li>Foo 2</li>' +
+					'<li>Foo 1</li>' +
+					'<li>Foo 2</li>' +
 					'</ul>' +
 					'<ul style="list-style-type:circle;">' +
-						'<li>Bar 1</li>' +
-						'<li>Bar 2</li>' +
+					'<li>Bar 1</li>' +
+					'<li>Bar 2</li>' +
 					'</ul>'
 				);
 			} );
@@ -291,14 +294,14 @@ describe( 'ListStylesEditing', () => {
 			it( 'should convert nested and mixed lists', () => {
 				editor.setData(
 					'<ol style="list-style-type:upper-alpha;">' +
-						'<li>OL 1</li>' +
-						'<li>OL 2' +
-							'<ul style="list-style-type:circle;">' +
-								'<li>UL 1</li>' +
-								'<li>UL 2</li>' +
-							'</ul>' +
-						'</li>' +
-						'<li>OL 3</li>' +
+					'<li>OL 1</li>' +
+					'<li>OL 2' +
+					'<ul style="list-style-type:circle;">' +
+					'<li>UL 1</li>' +
+					'<li>UL 2</li>' +
+					'</ul>' +
+					'</li>' +
+					'<li>OL 3</li>' +
 					'</ol>'
 				);
 
@@ -365,17 +368,526 @@ describe( 'ListStylesEditing', () => {
 
 				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
 					'<ul style="list-style-type:circle">' +
-						'<li>Foo 1' +
-							'<ul style="list-style-type:disc">' +
-								'<li>Bar 1</li>' +
-								'<li>Bar 2</li>' +
-							'</ul>' +
-						'</li>' +
-						'<li>Foo 2</li>' +
-						'<li>Foo 3</li>' +
+					'<li>Foo 1' +
+					'<ul style="list-style-type:disc">' +
+					'<li>Bar 1</li>' +
+					'<li>Bar 2</li>' +
+					'</ul>' +
+					'</li>' +
+					'<li>Foo 2</li>' +
+					'<li>Foo 3</li>' +
 					'</ul>'
 				);
 			} );
 		} );
 	} );
+
+	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,
+					'<paragraph>Foo Bar.[]</paragraph>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Bar</listItem>'
+				);
+
+				editor.execute( 'bulletedList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo Bar.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Bar</listItem>'
+				);
+			} );
+
+			it( 'should inherit the list style attribute when merging the same kind of lists (from top, merge a few items)', () => {
+				setModelData( model,
+					'<paragraph>[Foo Bar 1.</paragraph>' +
+					'<paragraph>Foo Bar 2.]</paragraph>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Bar</listItem>'
+				);
+
+				editor.execute( 'bulletedList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">[Foo Bar 1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo Bar 2.]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Bar</listItem>'
+				);
+			} );
+
+			it( 'should not inherit anything if there is no list below the inserted list', () => {
+				setModelData( model,
+					'<paragraph>Foo Bar 1.[]</paragraph>' +
+					'<paragraph>Foo Bar 2.</paragraph>'
+				);
+
+				editor.execute( 'bulletedList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="default" listType="bulleted">Foo Bar 1.[]</listItem>' +
+					'<paragraph>Foo Bar 2.</paragraph>'
+				);
+			} );
+
+			it( 'should not inherit anything if replacing the entire content with a list', () => {
+				setModelData( model,
+					'<paragraph>Foo Bar 1.[]</paragraph>'
+				);
+
+				editor.execute( 'bulletedList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="default" listType="bulleted">Foo Bar 1.[]</listItem>'
+				);
+			} );
+
+			it( 'should not inherit the list style attribute when merging different kind of lists (from top, merge a single item)', () => {
+				setModelData( model,
+					'<paragraph>Foo Bar.[]</paragraph>' +
+					'<listItem listIndent="0" listStyle="default"  listType="numbered">Foo</listItem>' +
+					'<listItem listIndent="0" listStyle="default"  listType="numbered">Bar</listItem>'
+				);
+
+				editor.execute( 'bulletedList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="default" listType="bulleted">Foo Bar.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="default" listType="numbered">Foo</listItem>' +
+					'<listItem listIndent="0" listStyle="default" listType="numbered">Bar</listItem>'
+				);
+			} );
+
+			it( 'should inherit the list style attribute when merging the same kind of lists (from bottom, merge a single item)', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Bar</listItem>' +
+					'<paragraph>Foo Bar.[]</paragraph>'
+				);
+
+				editor.execute( 'bulletedList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Bar</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo Bar.[]</listItem>'
+				);
+			} );
+
+			it(
+				'should inherit the list style attribute from listIndent=0 element when merging the same kind of lists (from bottom)',
+				() => {
+					setModelData( model,
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo</listItem>' +
+						'<listItem listIndent="1" listStyle="square" listType="bulleted">Bar</listItem>' +
+						'<listItem listIndent="2" listStyle="disc" listType="bulleted">Foo Bar</listItem>' +
+						'<paragraph>Foo Bar.[]</paragraph>'
+					);
+
+					editor.execute( 'bulletedList' );
+
+					expect( getModelData( model ) ).to.equal(
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo</listItem>' +
+						'<listItem listIndent="1" listStyle="square" listType="bulleted">Bar</listItem>' +
+						'<listItem listIndent="2" listStyle="disc" listType="bulleted">Foo Bar</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo Bar.[]</listItem>'
+					);
+				}
+			);
+		} );
+
+		describe( 'indenting lists', () => {
+			it( 'should restore the default value for the list style attribute when indenting a single item', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">1A.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2B.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+				);
+
+				editor.execute( 'indentList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">1A.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2B.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">2.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+				);
+			} );
+
+			it( 'should restore the default value for the list style attribute when indenting a few items', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">[2.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.]</listItem>'
+				);
+
+				editor.execute( 'indentList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">[2.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">3.]</listItem>'
+				);
+			} );
+
+			it(
+				'should copy the value for the list style attribute when indenting a single item into a nested list (default value)',
+				() => {
+					setModelData( model,
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="1" listStyle="default" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.[]</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">4.</listItem>'
+					);
+
+					editor.execute( 'indentList' );
+
+					expect( getModelData( model ) ).to.equal(
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="1" listStyle="default" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="1" listStyle="default" listType="bulleted">3.[]</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">4.</listItem>'
+					);
+				}
+			);
+
+			it(
+				'should copy the value for the list style attribute when indenting a single item into a nested list (changed value)',
+				() => {
+					setModelData( model,
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="1" listStyle="disc" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.[]</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">4.</listItem>'
+					);
+
+					editor.execute( 'indentList' );
+
+					expect( getModelData( model ) ).to.equal(
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="1" listStyle="disc" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="1" listStyle="disc" listType="bulleted">3.[]</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">4.</listItem>'
+					);
+				}
+			);
+
+			it( 'should copy the value for the list style attribute when indenting a single item into a nested list', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.[]</listItem>' +
+					'<listItem listIndent="1" listStyle="square" listType="bulleted">3.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">4.</listItem>'
+				);
+
+				editor.execute( 'indentList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">2.[]</listItem>' +
+					'<listItem listIndent="2" listStyle="square" listType="bulleted">3.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">4.</listItem>'
+				);
+			} );
+
+			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,
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="1" listStyle="disc" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="2" listStyle="square" listType="bulleted">3.</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">4.[]</listItem>'
+					);
+
+					editor.execute( 'indentList' );
+
+					expect( getModelData( model ) ).to.equal(
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+						'<listItem listIndent="1" listStyle="disc" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="2" listStyle="square" listType="bulleted">3.</listItem>' +
+						'<listItem listIndent="1" listStyle="disc" listType="bulleted">4.[]</listItem>'
+					);
+				}
+			);
+
+			it( 'should inherit the list style attribute from nested list if the `listType` is other than indenting element', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="decimal" listType="numbered">2.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.[]</listItem>'
+				);
+
+				editor.execute( 'indentList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="decimal" listType="numbered">2.</listItem>' +
+					'<listItem listIndent="1" listStyle="decimal" listType="numbered">3.[]</listItem>'
+				);
+			} );
+		} );
+
+		describe( 'outdenting lists', () => {
+			it( 'should inherit the list style attribute from parent list (change the first nested item)', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">2.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+				);
+
+				editor.execute( 'outdentList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+				);
+			} );
+
+			it( 'should inherit the list style attribute from parent list (change the second nested item)', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">2.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">3.[]</listItem>'
+				);
+
+				editor.execute( 'outdentList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">2.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.[]</listItem>'
+				);
+			} );
+
+			it( 'should inherit the list style attribute from parent list (modifying nested lists)', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">[2.</listItem>' +
+					'<listItem listIndent="2" listStyle="square" listType="bulleted">3.]</listItem>'
+				);
+
+				editor.execute( 'outdentList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">[2.</listItem>' +
+					'<listItem listIndent="1" listStyle="square" listType="bulleted">3.]</listItem>'
+				);
+			} );
+
+			it(
+				'should inherit the list style attribute from parent list (outdenting many items, including the first one in the list)',
+				() => {
+					setModelData( model,
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">[1.</listItem>' +
+						'<listItem listIndent="1" listStyle="default" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="2" listStyle="square" listType="bulleted">3.]</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">4.</listItem>'
+					);
+
+					editor.execute( 'outdentList' );
+
+					expect( getModelData( model ) ).to.equal(
+						'<paragraph>[1.</paragraph>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="1" listStyle="square" listType="bulleted">3.]</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">4.</listItem>'
+					);
+				}
+			);
+
+			it(
+				'should inherit the list style attribute from parent list (outdenting the first item that is a parent for next list)',
+				() => {
+					setModelData( model,
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>' +
+						'<listItem listIndent="1" listStyle="default" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="2" listStyle="square" listType="bulleted">3.</listItem>' +
+						'<listItem listIndent="3" listStyle="disc" listType="bulleted">4.</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">5.</listItem>'
+					);
+
+					editor.execute( 'outdentList' );
+
+					expect( getModelData( model ) ).to.equal(
+						'<paragraph>1.[]</paragraph>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.</listItem>' +
+						'<listItem listIndent="1" listStyle="square" listType="bulleted">3.</listItem>' +
+						'<listItem listIndent="2" listStyle="disc" listType="bulleted">4.</listItem>' +
+						'<listItem listIndent="0" listStyle="circle" listType="bulleted">5.</listItem>'
+					);
+				}
+			);
+
+			it( 'should not inherit the list style if outdented the only one item in the list', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>' +
+					'<listItem listIndent="1" listStyle="disc" listType="bulleted">2.</listItem>' +
+					'<listItem listIndent="2" listStyle="square" listType="bulleted">3.</listItem>'
+				);
+
+				editor.execute( 'outdentList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>1.[]</paragraph>' +
+					'<listItem listIndent="0" listStyle="disc" listType="bulleted">2.</listItem>' +
+					'<listItem listIndent="1" listStyle="square" listType="bulleted">3.</listItem>'
+				);
+			} );
+
+			it( 'should not inherit the list style if outdented the only one item in the list (a paragraph below the list)', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>' +
+					'<listItem listIndent="1" listStyle="disc" listType="bulleted">2.</listItem>' +
+					'<listItem listIndent="2" listStyle="square" listType="bulleted">3.</listItem>' +
+					'<paragraph>Foo</paragraph>'
+				);
+
+				editor.execute( 'outdentList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>1.[]</paragraph>' +
+					'<listItem listIndent="0" listStyle="disc" listType="bulleted">2.</listItem>' +
+					'<listItem listIndent="1" listStyle="square" listType="bulleted">3.</listItem>' +
+					'<paragraph>Foo</paragraph>'
+				);
+			} );
+
+			it( 'should not inherit the list style attribute from parent list if the `listType` is other than outdenting element', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="decimal" listType="numbered">2.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+				);
+
+				editor.execute( 'outdentList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="0" listStyle="decimal" listType="numbered">2.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+				);
+			} );
+
+			it( 'should not do anything if there is no list after outdenting', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.[]</listItem>'
+				);
+
+				editor.execute( 'outdentList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>1.[]</paragraph>'
+				);
+			} );
+		} );
+
+		describe( 'indent/outdent + undo', () => {
+			it( 'should use the same batch for indenting a list and updating `listType` attribute', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">1A.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2B.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+				);
+
+				editor.execute( 'indentList' );
+				editor.execute( 'undo' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">1A.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2B.</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">2.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+				);
+			} );
+
+			it( 'should use the same batch for outdenting a list and updating `listType` attribute', () => {
+				setModelData( model,
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">2.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+				);
+
+				editor.execute( 'outdentList' );
+				editor.execute( 'undo' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">1.</listItem>' +
+					'<listItem listIndent="1" listStyle="default" listType="bulleted">2.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">3.</listItem>'
+				);
+			} );
+		} );
+
+		describe( 'todo list', () => {
+			let editor, model;
+
+			beforeEach( () => {
+				return VirtualTestEditor
+					.create( {
+						// TodoListEditing is at the end by design. Check `ListStylesEditing.afterInit()` call.
+						plugins: [ Paragraph, ListStylesEditing, TodoListEditing ]
+					} )
+					.then( newEditor => {
+						editor = newEditor;
+						model = editor.model;
+					} );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			it( 'should not add the `listStyle` attribute while creating a todo list', () => {
+				setModelData( model, '<paragraph>Foo[]</paragraph>' );
+
+				editor.execute( 'todoList' );
+
+				expect( getModelData( model ), '<listItem listIndent="0" listType="todo">Foo[]</listItem>' );
+			} );
+
+			it( 'should not add the `listStyle` attribute while switching the list type', () => {
+				setModelData( model, '<listItem listIndent="0" listType="bulleted">Foo[]</listItem>' );
+
+				editor.execute( 'todoList' );
+
+				expect( getModelData( model ), '<listItem listIndent="0" listType="todo">Foo[]</listItem>' );
+			} );
+
+			it( 'should remove the `listStyle` attribute while switching the list type that uses the list style feature', () => {
+				setModelData( model, '<listItem listIndent="0" listType="bulleted" listStyle="circle">Foo[]</listItem>' );
+
+				editor.execute( 'todoList' );
+
+				expect( getModelData( model ), '<listItem listIndent="0" listType="todo">Foo[]</listItem>' );
+			} );
+
+			it( 'should  not inherit the list style attribute when inserting a todo list item', () => {
+				setModelData( model,
+					'<paragraph>Foo Bar.[]</paragraph>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Bar</listItem>'
+				);
+
+				editor.execute( 'todoList' );
+
+				expect( getModelData( model ) ).to.equal(
+					'<listItem listIndent="0" listType="todo">Foo Bar.[]</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Foo</listItem>' +
+					'<listItem listIndent="0" listStyle="circle" listType="bulleted">Bar</listItem>'
+				);
+			} );
+		} );
+	} );
 } );

+ 25 - 178
packages/ckeditor5-list/tests/manual/list-styles.html

@@ -1,180 +1,27 @@
-<div id="editor">
-    <h2>Default styles</h2>
-    <figure class="table">
-        <table style="width: 600px;">
-            <tbody>
-            <tr>
-                <td colspan="2" style="text-align: center">
-                    <span class="text-big" style="font-weight: bold">Default styles</span>
-                </td>
-            </tr>
-            <tr>
-                <td style="text-align: center"><span class="text-big" style="font-weight: bold">Bulleted</span></td>
-                <td style="text-align: center"><span class="text-big" style="font-weight: bold">Numbered</span></td>
-            </tr>
-            <tr>
-                <td style="vertical-align:top; width: 50%">
-                    <ul>
-                        <li>UL List item 1</li>
-                        <li>UL List item 2</li>
-                        <li>UL List item 3</li>
-                    </ul>
-                </td>
-                <td style="vertical-align:top; width: 50%">
-                    <ol>
-                        <li>OL List item 1</li>
-                        <li>OL List item 2</li>
-                        <li>OL List item 3</li>
-                    </ol>
-                </td>
-            </tr>
-            </tbody>
-        </table>
-    </figure>
-
-    <h2>Bulleted lists</h2>
-    <figure class="table">
-        <table style="width: 600px;">
-            <tbody>
-            <tr>
-                <td colspan="4" style="text-align: center">
-                    <span class="text-big" style="font-weight: bold">Type of list</span>
-                </td>
-            </tr>
-            <tr>
-                <td style="text-align: center"><span class="text-big" style="font-weight: bold">Circle</span></td>
-                <td style="text-align: center"><span class="text-big" style="font-weight: bold">Disc</span></td>
-                <td style="text-align: center"><span class="text-big" style="font-weight: bold">Square</span></td>
-                <td style="text-align: center"><span class="text-big" style="font-weight: bold">'👉'</span></td>
-            </tr>
-            <tr>
-                <td style="vertical-align:top; width: 25%">
-                    <ul style="list-style-type: circle;">
-                        <li>UL List item 1</li>
-                        <li>UL List item 2</li>
-                        <li>UL List item 3</li>
-                    </ul>
-                </td>
-                <td style="vertical-align:top; width: 25%">
-                    <ul style="list-style-type: disc;">
-                        <li>UL List item 1</li>
-                        <li>UL List item 2</li>
-                        <li>UL List item 3</li>
-                    </ul>
-                </td>
-                <td style="vertical-align:top; width: 25%">
-                    <ul style="list-style-type: square;">
-                        <li>UL List item 1</li>
-                        <li>UL List item 2</li>
-                        <li>UL List item 3</li>
-                    </ul>
-                </td>
-                <td style="vertical-align:top; width: 25%">
-                    <ul style="list-style-type: '👉';">
-                        <li>UL List item 1</li>
-                        <li>UL List item 2</li>
-                        <li>UL List item 3</li>
-                    </ul>
-                </td>
-            </tr>
-            </tbody>
-        </table>
-    </figure>
-
-    <h2>Numbered lists</h2>
-    <figure class="table">
-        <table style="width: 800px;">
-            <tbody>
-            <tr>
-                <td colspan="4" style="text-align: center">
-                    <span class="text-big" style="font-weight: bold">Type of list</span>
-                </td>
-            </tr>
-            <tr>
-                <td style="text-align: center"><span class="text-big" style="font-weight: bold">Decimal</span></td>
-                <td style="text-align: center"><span class="text-big" style="font-weight: bold">Decimal-leading-zero</span></td>
-                <td style="text-align: center"><span class="text-big" style="font-weight: bold">Lower-latin</span></td>
-                <td style="text-align: center"><span class="text-big" style="font-weight: bold">Lower-roman</span></td>
-            </tr>
-            <tr>
-                <td style="vertical-align:top; width: 25%">
-                    <ol style="list-style-type: decimal;">
-                        <li>OL List item 1</li>
-                        <li>OL List item 2</li>
-                        <li>OL List item 3</li>
-                    </ol>
-                </td>
-                <td style="vertical-align:top; width: 25%">
-                    <ol style="list-style-type: decimal-leading-zero;">
-                        <li>OL List item 1</li>
-                        <li>OL List item 2</li>
-                        <li>OL List item 3</li>
-                    </ol>
-                </td>
-                <td style="vertical-align:top; width: 25%">
-                    <ol style="list-style-type: lower-latin;">
-                        <li>OL List item 1</li>
-                        <li>OL List item 2</li>
-                        <li>OL List item 3</li>
-                    </ol>
-                </td>
-                <td style="vertical-align:top; width: 25%">
-                    <ol style="list-style-type: lower-roman;">
-                        <li>OL List item 1</li>
-                        <li>OL List item 2</li>
-                        <li>OL List item 3</li>
-                    </ol>
-                </td>
-            </tr>
-            </tbody>
-        </table>
-    </figure>
+<h3>List Styles UI</h3>
+<div class="list-styles-ui">
+    <button type="button" data-list="disc">disc</button>
+    <button type="button" data-list="circle">circle</button>
+    <button type="button" data-list="square">square</button>
+    <button type="button" data-list="decimal">decimal</button>
+    <button type="button" data-list="decimal-leading-zero">decimal-leading-zero</button>
+    <button type="button" data-list="lower-latin">lower-latin</button>
+    <button type="button" data-list="lower-roman">lower-roman</button>
+    <button type="button" data-list="default">default</button>
+</div>
+<br>
 
-    <h2>Nested/mixed lists (numbered and bulleted)</h2>
-    <figure class="table">
-        <table style="width: 1200px;">
-            <tr>
-                <td style="width: 50%">
-                    <ol style="list-style-type: lower-roman;">
-                        <li>OL 1.1 <code>OL[style: lower-roman]</code></li>
-                        <li>OL 1.2 <code>OL[style: lower-roman]</code>
-                            <ul style="list-style-type: square;">
-                                <li>UL 1.2.1 <code>OL[style: lower-roman] &gt; UL[style: square]</code></li>
-                                <li>UL 1.2.2 <code>OL[style: lower-roman] &gt; UL[style: square]</code></li>
-                                <li>UL 1.2.3 <code>OL[style: lower-roman] &gt; UL[style: square]</code></li>
-                            </ul>
-                        </li>
-                        <li>OL 1.3 <code>OL[style: lower-roman]</code></li>
-                        <li>OL 1.4 <code>OL[style: lower-roman]</code></li>
-                        <ol style="list-style-type: lower-alpha;">
-                            <li>OL 1.4.1 <code>OL[style: lower-roman] &gt; OL[style: lower-alpha]</code></li>
-                            <li>OL 1.4.2 <code>OL[style: lower-roman] &gt; OL[style: lower-alpha]</code></li>
-                            <li>OL 1.4.3 <code>OL[style: lower-roman] &gt; OL[style: lower-alpha]</code></li>
-                        </ol>
-                        </li>
-                        <li>OL 1.5 <code>OL[style: lower-roman]</code></li>
-                        <li>OL 1.6 <code>OL[style: lower-roman]</code></li>
-                    </ol>
-                </td>
-                <td style="width: 50%">
-                    <ol style="list-style-type: upper-alpha;">
-                        <li>OL 2.1 <code>OL[style: upper-alpha]</code>
-                            <ol style="list-style-type: lower-greek;">
-                                <li>OL 2.2.1 <code>OL[style: lower-alpha] &gt; OL[style: lower-greek]</code>
-                                    <ul style="list-style-type: circle;">
-                                        <li>UL 2.2.1.1 <code>... &gt; OL[style: lower-greek] &gt; UL[style: circle]</code></li>
-                                        <li>UL 2.2.1.2 <code>... &gt; OL[style: lower-greek] &gt; UL[style: circle]</code></li>
-                                        <li>UL 2.2.1.3 <code>... &gt; OL[style: lower-greek] &gt; UL[style: circle]</code></li>
-                                    </ul>
-                                </li>
-                                <li>OL 2.2.2 <code>OL[style: lower-alpha] &gt; OL[style: lower-greek]</code></li>
-                                <li>OL 2.2.3 <code>OL[style: lower-alpha] &gt; OL[style: lower-greek]</code></li>
-                            </ol>
-                        </li>
-                        <li>OL 2.2 <code>OL[style: upper-alpha]</code></li>
-                    </ol>
-                </td>
-            </tr>
-        </table>
-    </figure>
+<div id="editor">
+    <ul style="list-style-type: square">
+        <li>UL List item 1</li>
+        <li>UL List item 2</li>
+        <li>UL List item 3</li>
+    </ul>
+    <p>A</p>
+    <ul style="list-style-type: disc">
+        <li>UL List item 1</li>
+        <li>UL List item 2</li>
+        <li>UL List item 3</li>
+    </ul>
 </div>
+

+ 18 - 2
packages/ckeditor5-list/tests/manual/list-styles.js

@@ -15,6 +15,9 @@ import TablePropertiesEditing from '@ckeditor/ckeditor5-table/src/tablepropertie
 import TableCellPropertiesEditing from '@ckeditor/ckeditor5-table/src/tablecellproperties/tablecellpropertiesediting';
 import List from '../../src/list';
 import ListStyles from '../../src/liststyles';
+import Indent from '@ckeditor/ckeditor5-indent/src/indent';
+import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock';
+import TodoList from '../../src/todolist';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
@@ -23,22 +26,35 @@ ClassicEditor
 			Code,
 			Heading,
 			List,
+			TodoList,
 			Paragraph,
 			ListStyles,
 			Table,
 			TablePropertiesEditing,
-			TableCellPropertiesEditing
+			TableCellPropertiesEditing,
+			Indent,
+			IndentBlock
 		],
 		toolbar: [
 			'heading',
 			'|',
-			'bulletedList', 'numberedList',
+			'bulletedList', 'numberedList', 'todoList',
+			'|',
+			'outdent',
+			'indent',
 			'|',
 			'undo', 'redo'
 		]
 	} )
 	.then( editor => {
 		window.editor = editor;
+
+		// TODO: Remove.
+		for ( const button of [ ...document.querySelectorAll( '.list-styles-ui button' ) ] ) {
+			button.addEventListener( 'click', () => {
+				editor.execute( 'listStyles', { type: button.getAttribute( 'data-list' ) } );
+			} );
+		}
 	} )
 	.catch( err => {
 		console.error( err.stack );