8
0
Pārlūkot izejas kodu

Move todo conversion out of list converters.

Oskar Wróbel 6 gadi atpakaļ
vecāks
revīzija
f1c4d41060

+ 50 - 201
packages/ckeditor5-list/src/converters.js

@@ -7,8 +7,13 @@
  * @module list/converters
  */
 
-import { createViewListItemElement } from './utils';
-import { addTodoElementsToListItem, removeTodoElementsFromListItem } from './todolistutils';
+import {
+	generateLiInUl,
+	injectViewList,
+	mergeViewLists,
+	getSiblingListItem,
+	positionAfterUiElements
+} from './utils';
 import TreeWalker from '@ckeditor/ckeditor5-engine/src/model/treewalker';
 
 /**
@@ -37,7 +42,7 @@ export function modelViewInsertion( model ) {
 		consumable.consume( data.item, 'attribute:listIndent' );
 
 		const modelItem = data.item;
-		const viewItem = generateLiInUl( modelItem, conversionApi, model );
+		const viewItem = generateLiInUl( modelItem, conversionApi );
 
 		injectViewList( modelItem, viewItem, conversionApi, model );
 	};
@@ -90,47 +95,56 @@ export function modelViewRemove( model ) {
  * A model-to-view converter for `type` attribute change on `listItem` model element.
  *
  * This change means that `<li>` elements parent changes from `<ul>` to `<ol>` (or vice versa). This is accomplished
- * by breaking view elements, changing their name and merging them.
+ * by breaking view elements and changing their name. Next {@link module:list/converters~modelViewMergeAfterChangeType}
+ * converter will try to merge split nodes.
  *
  * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
- * @param {module:engine/model/model~Model} model Model instance.
+ * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
+ * @param {Object} data Additional information about the change.
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
  */
-export function modelViewChangeType( model ) {
-	return ( evt, data, conversionApi ) => {
-		if ( !conversionApi.consumable.consume( data.item, 'attribute:listType' ) ) {
-			return;
-		}
+export function modelViewChangeType( evt, data, conversionApi ) {
+	if ( !conversionApi.consumable.consume( data.item, 'attribute:listType' ) ) {
+		return;
+	}
 
-		const viewItem = conversionApi.mapper.toViewElement( data.item );
-		const viewWriter = conversionApi.writer;
+	const viewItem = conversionApi.mapper.toViewElement( data.item );
+	const viewWriter = conversionApi.writer;
 
-		// Break the container after and before the list item.
-		// This will create a view list with one view list item -- the one that changed type.
-		viewWriter.breakContainer( viewWriter.createPositionBefore( viewItem ) );
-		viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );
+	// Break the container after and before the list item.
+	// This will create a view list with one view list item -- the one that changed type.
+	viewWriter.breakContainer( viewWriter.createPositionBefore( viewItem ) );
+	viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );
 
-		// Change name of the view list that holds the changed view item.
-		// We cannot just change name property, because that would not render properly.
-		let viewList = viewItem.parent;
-		const listName = data.attributeNewValue == 'numbered' ? 'ol' : 'ul';
-		viewList = viewWriter.rename( listName, viewList );
-
-		// Add or remove checkbox for toto list.
-		if ( data.attributeNewValue == 'todo' ) {
-			addTodoElementsToListItem( viewWriter, viewItem, data.item, model );
-		} else if ( data.attributeOldValue == 'todo' ) {
-			removeTodoElementsFromListItem( viewWriter, viewItem, data.item, model );
-		}
+	// Change name of the view list that holds the changed view item.
+	// We cannot just change name property, because that would not render properly.
+	const viewList = viewItem.parent;
+	const listName = data.attributeNewValue == 'numbered' ? 'ol' : 'ul';
 
-		// Merge the changed view list with other lists, if possible.
-		mergeViewLists( viewWriter, viewList, viewList.nextSibling );
-		mergeViewLists( viewWriter, viewList.previousSibling, viewList );
+	viewWriter.rename( listName, viewList );
+}
 
-		// Consumable insertion of children inside the item. They are already handled by re-building the item in view.
-		for ( const child of data.item.getChildren() ) {
-			conversionApi.consumable.consume( child, 'insert' );
-		}
-	};
+/**
+ * A model-to-view converter that try to merge nodes split by {@link module:list/converters~modelViewChangeType}.
+ *
+ * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
+ * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
+ * @param {Object} data Additional information about the change.
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
+ */
+export function modelViewMergeAfterChangeType( evt, data, conversionApi ) {
+	const viewItem = conversionApi.mapper.toViewElement( data.item );
+	const viewList = viewItem.parent;
+	const viewWriter = conversionApi.writer;
+
+	// Merge the changed view list with other lists, if possible.
+	mergeViewLists( viewWriter, viewList, viewList.nextSibling );
+	mergeViewLists( viewWriter, viewList.previousSibling, viewList );
+
+	// Consumable insertion of children inside the item. They are already handled by re-building the item in view.
+	for ( const child of data.item.getChildren() ) {
+		conversionApi.consumable.consume( child, 'insert' );
+	}
 }
 
 /**
@@ -792,28 +806,6 @@ export function modelIndentPasteFixer( evt, [ content, selectable ] ) {
 	}
 }
 
-// Helper function that creates a `<ul><li></li></ul>` or (`<ol>`) structure out of given `modelItem` model `listItem` element.
-// Then, it binds created view list item (<li>) with model `listItem` element.
-// The function then returns created view list item (<li>).
-function generateLiInUl( modelItem, conversionApi, model ) {
-	const mapper = conversionApi.mapper;
-	const viewWriter = conversionApi.writer;
-	const listType = modelItem.getAttribute( 'listType' ) == 'numbered' ? 'ol' : 'ul';
-	const viewItem = createViewListItemElement( viewWriter );
-
-	const viewList = viewWriter.createContainerElement( listType, null );
-
-	viewWriter.insert( viewWriter.createPositionAt( viewList, 0 ), viewItem );
-
-	if ( modelItem.getAttribute( 'listType' ) == 'todo' ) {
-		addTodoElementsToListItem( viewWriter, viewItem, modelItem, model );
-	}
-
-	mapper.bindElements( modelItem, viewItem );
-
-	return viewItem;
-}
-
 // Helper function that converts children of a given `<li>` view element into corresponding model elements.
 // The function maintains proper order of elements if model `listItem` is split during the conversion
 // due to block children conversion.
@@ -901,140 +893,6 @@ function findNextListItem( startPosition ) {
 	return value.value.item;
 }
 
-// Helper function that seeks for a previous list item sibling of given model item which meets given criteria.
-// `options` object may contain one or more of given values (by default they are `false`):
-// `options.sameIndent` - whether sought sibling should have same indent (default = no),
-// `options.smallerIndent` - whether sought sibling should have smaller indent (default = no).
-// `options.listIndent` - the reference indent.
-// Either `options.sameIndent` or `options.smallerIndent` should be set to `true`.
-function getSiblingListItem( modelItem, options ) {
-	const sameIndent = !!options.sameIndent;
-	const smallerIndent = !!options.smallerIndent;
-	const indent = options.listIndent;
-
-	let item = modelItem;
-
-	while ( item && item.name == 'listItem' ) {
-		const itemIndent = item.getAttribute( 'listIndent' );
-
-		if ( ( sameIndent && indent == itemIndent ) || ( smallerIndent && indent > itemIndent ) ) {
-			return item;
-		}
-
-		item = item.previousSibling;
-	}
-
-	return null;
-}
-
-// Helper function that takes two parameters, that are expected to be view list elements, and merges them.
-// The merge happen only if both parameters are list elements of the same types (the same element name and the same class attributes).
-function mergeViewLists( viewWriter, firstList, secondList ) {
-	// Check if two lists are going to be merged.
-	if ( !firstList || !secondList || ( firstList.name != 'ul' && firstList.name != 'ol' ) ) {
-		return null;
-	}
-
-	// Both parameters are list elements, so compare types now.
-	if ( firstList.name != secondList.name || firstList.getAttribute( 'class' ) !== secondList.getAttribute( 'class' ) ) {
-		return null;
-	}
-
-	return viewWriter.mergeContainers( viewWriter.createPositionAfter( firstList ) );
-}
-
-// Helper function that takes model list item element `modelItem`, corresponding view list item element `injectedItem`
-// that is not added to the view and is inside a view list element (`ul` or `ol`) and is that's list only child.
-// The list is inserted at correct position (element breaking may be needed) and then merged with it's siblings.
-// See comments below to better understand the algorithm.
-function injectViewList( modelItem, injectedItem, conversionApi, model ) {
-	const injectedList = injectedItem.parent;
-	const mapper = conversionApi.mapper;
-	const viewWriter = conversionApi.writer;
-
-	// Position where view list will be inserted.
-	let insertPosition = mapper.toViewPosition( model.createPositionBefore( modelItem ) );
-
-	// 1. Find previous list item that has same or smaller indent. Basically we are looking for a first model item
-	// that is "parent" or "sibling" of injected model item.
-	// If there is no such list item, it means that injected list item is the first item in "its list".
-	const refItem = getSiblingListItem( modelItem.previousSibling, {
-		sameIndent: true,
-		smallerIndent: true,
-		listIndent: modelItem.getAttribute( 'listIndent' )
-	} );
-	const prevItem = modelItem.previousSibling;
-
-	if ( refItem && refItem.getAttribute( 'listIndent' ) == modelItem.getAttribute( 'listIndent' ) ) {
-		// There is a list item with same indent - we found same-level sibling.
-		// Break the list after it. Inserted view item will be inserted in the broken space.
-		const viewItem = mapper.toViewElement( refItem );
-		insertPosition = viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );
-	} else {
-		// There is no list item with same indent. Check previous model item.
-		if ( prevItem && prevItem.name == 'listItem' ) {
-			// If it is a list item, it has to have lower indent.
-			// It means that inserted item should be added to it as its nested item.
-			insertPosition = mapper.toViewPosition( model.createPositionAt( prevItem, 'end' ) );
-		} else {
-			// Previous item is not a list item (or does not exist at all).
-			// Just map the position and insert the view item at mapped position.
-			insertPosition = mapper.toViewPosition( model.createPositionBefore( modelItem ) );
-		}
-	}
-
-	insertPosition = positionAfterUiElements( insertPosition );
-
-	// Insert the view item.
-	viewWriter.insert( insertPosition, injectedList );
-
-	// 2. Handle possible children of injected model item.
-	if ( prevItem && prevItem.name == 'listItem' ) {
-		const prevView = mapper.toViewElement( prevItem );
-
-		const walkerBoundaries = viewWriter.createRange( viewWriter.createPositionAt( prevView, 0 ), insertPosition );
-		const walker = walkerBoundaries.getWalker( { ignoreElementEnd: true } );
-
-		for ( const value of walker ) {
-			if ( value.item.is( 'li' ) ) {
-				const breakPosition = viewWriter.breakContainer( viewWriter.createPositionBefore( value.item ) );
-				const viewList = value.item.parent;
-
-				const targetPosition = viewWriter.createPositionAt( injectedItem, 'end' );
-				mergeViewLists( viewWriter, targetPosition.nodeBefore, targetPosition.nodeAfter );
-				viewWriter.move( viewWriter.createRangeOn( viewList ), targetPosition );
-
-				walker.position = breakPosition;
-			}
-		}
-	} else {
-		const nextViewList = injectedList.nextSibling;
-
-		if ( nextViewList && ( nextViewList.is( 'ul' ) || nextViewList.is( 'ol' ) ) ) {
-			let lastSubChild = null;
-
-			for ( const child of nextViewList.getChildren() ) {
-				const modelChild = mapper.toModelElement( child );
-
-				if ( modelChild && modelChild.getAttribute( 'listIndent' ) > modelItem.getAttribute( 'listIndent' ) ) {
-					lastSubChild = child;
-				} else {
-					break;
-				}
-			}
-
-			if ( lastSubChild ) {
-				viewWriter.breakContainer( viewWriter.createPositionAfter( lastSubChild ) );
-				viewWriter.move( viewWriter.createRangeOn( lastSubChild.parent ), viewWriter.createPositionAt( injectedItem, 'end' ) );
-			}
-		}
-	}
-
-	// Merge inserted view list with its possible neighbour lists.
-	mergeViewLists( viewWriter, injectedList, injectedList.nextSibling );
-	mergeViewLists( viewWriter, injectedList.previousSibling, injectedList );
-}
-
 // Helper function that takes all children of given `viewRemovedItem` and moves them in a correct place, according
 // to other given parameters.
 function hoistNestedLists( nextIndent, modelRemoveStartPosition, viewRemoveStartPosition, viewRemovedItem, conversionApi, model ) {
@@ -1131,12 +989,3 @@ function hoistNestedLists( nextIndent, modelRemoveStartPosition, viewRemoveStart
 		}
 	}
 }
-
-// Helper function that for given `view.Position`, returns a `view.Position` that is after all `view.UIElement`s that
-// are after given position.
-// For example:
-// <container:p>foo^<ui:span></ui:span><ui:span></ui:span>bar</contain:p>
-// For position ^, a position before "bar" will be returned.
-function positionAfterUiElements( viewPosition ) {
-	return viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
-}

+ 5 - 3
packages/ckeditor5-list/src/listediting.js

@@ -18,6 +18,7 @@ import {
 	cleanListItem,
 	modelViewInsertion,
 	modelViewChangeType,
+	modelViewMergeAfterChangeType,
 	modelViewMergeAfter,
 	modelViewRemove,
 	modelViewSplitOnInsert,
@@ -77,8 +78,9 @@ export default class ListEditing extends Plugin {
 		data.downcastDispatcher.on( 'insert', modelViewSplitOnInsert, { priority: 'high' } );
 		data.downcastDispatcher.on( 'insert:listItem', modelViewInsertion( editor.model ) );
 
-		editing.downcastDispatcher.on( 'attribute:listType:listItem', modelViewChangeType( editor.model ) );
-		data.downcastDispatcher.on( 'attribute:listType:listItem', modelViewChangeType( editor.model ) );
+		editing.downcastDispatcher.on( 'attribute:listType:listItem', modelViewChangeType, { priority: 'high' } );
+		editing.downcastDispatcher.on( 'attribute:listType:listItem', modelViewMergeAfterChangeType, { priority: 'low' } );
+		data.downcastDispatcher.on( 'attribute:listType:listItem', modelViewChangeType );
 		editing.downcastDispatcher.on( 'attribute:listIndent:listItem', modelViewChangeIndent( editor.model ) );
 		data.downcastDispatcher.on( 'attribute:listIndent:listItem', modelViewChangeIndent( editor.model ) );
 
@@ -103,7 +105,7 @@ export default class ListEditing extends Plugin {
 		editor.commands.add( 'indentList', new IndentCommand( editor, 'forward' ) );
 		editor.commands.add( 'outdentList', new IndentCommand( editor, 'backward' ) );
 
-		const viewDocument = this.editor.editing.view.document;
+		const viewDocument = editing.view.document;
 
 		// Overwrite default Enter key behavior.
 		// If Enter key is pressed with selection collapsed in empty list item, outdent it instead of breaking it.

+ 70 - 1
packages/ckeditor5-list/src/todolistconverters.js

@@ -7,8 +7,53 @@
  * @module list/todolistconverters
  */
 
-import { createCheckMarkElement } from './todolistutils';
+import { addTodoElementsToListItem, createCheckMarkElement, removeTodoElementsFromListItem } from './todolistutils';
+import { generateLiInUl, injectViewList } from './utils';
 
+/**
+ * A model-to-view converter for `listItem` model element insertion.
+ *
+ * It creates a `<ul><li></li><ul>` (or `<ol>`) view structure out of a `listItem` model element, inserts it at the correct
+ * position, and merges the list with surrounding lists (if available).
+ *
+ * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert
+ * @param {module:engine/model/model~Model} model Model instance.
+ * @returns {Function} Returns a conversion callback.
+ */
+export function modelViewInsertion( model ) {
+	return ( evt, data, conversionApi ) => {
+		const consumable = conversionApi.consumable;
+
+		if ( !consumable.test( data.item, 'insert' ) ||
+			!consumable.test( data.item, 'attribute:listType' ) ||
+			!consumable.test( data.item, 'attribute:listIndent' )
+		) {
+			return;
+		}
+
+		if ( data.item.getAttribute( 'listType' ) != 'todo' ) {
+			return;
+		}
+
+		consumable.consume( data.item, 'insert' );
+		consumable.consume( data.item, 'attribute:listType' );
+		consumable.consume( data.item, 'attribute:listIndent' );
+
+		const viewWriter = conversionApi.writer;
+		const modelItem = data.item;
+		const viewItem = generateLiInUl( modelItem, conversionApi );
+
+		addTodoElementsToListItem( viewWriter, viewItem, modelItem, model );
+		injectViewList( modelItem, viewItem, conversionApi, model );
+	};
+}
+
+/**
+ * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
+ * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
+ * @param {Object} data Additional information about the change.
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
+ */
 export function modelViewTextInsertion( evt, data, conversionApi ) {
 	const parent = data.range.start.parent;
 
@@ -25,6 +70,30 @@ export function modelViewTextInsertion( evt, data, conversionApi ) {
 	viewWriter.insert( viewPosition.offset ? viewPosition : viewPosition.getShiftedBy( 1 ), viewText );
 }
 
+/**
+ * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
+ * @param {module:engine/model/model~Model} model Model instance.
+ * @returns {Function} Returns a conversion callback.
+ */
+export function modelViewChangeType( model ) {
+	return ( evt, data, conversionApi ) => {
+		const viewItem = conversionApi.mapper.toViewElement( data.item );
+		const viewWriter = conversionApi.writer;
+
+		// Add or remove checkbox for toto list.
+		if ( data.attributeNewValue == 'todo' ) {
+			addTodoElementsToListItem( viewWriter, viewItem, data.item, model );
+		} else if ( data.attributeOldValue == 'todo' ) {
+			removeTodoElementsFromListItem( viewWriter, viewItem, data.item, model );
+		}
+	};
+}
+
+/**
+ * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
+ * @param {module:engine/model/model~Model} model Model instance.
+ * @returns {Function} Returns a conversion callback.
+ */
 export function modelViewChangeChecked( model ) {
 	return ( evt, data, conversionApi ) => {
 		if ( !conversionApi.consumable.consume( data.item, 'attribute:listChecked' ) ) {

+ 7 - 2
packages/ckeditor5-list/src/todolistediting.js

@@ -13,8 +13,10 @@ import ListEditing from './listediting';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 import {
+	modelViewInsertion,
 	modelViewTextInsertion,
-	modelViewChangeChecked
+	modelViewChangeChecked,
+	modelViewChangeType
 } from './todolistconverters';
 
 /**
@@ -33,7 +35,7 @@ export default class TodoListEditing extends Plugin {
 	 */
 	init() {
 		const editor = this.editor;
-		const editing = editor.editing;
+		const { editing } = editor;
 		const viewDocument = editing.view.document;
 
 		editor.model.schema.extend( 'listItem', {
@@ -41,7 +43,10 @@ export default class TodoListEditing extends Plugin {
 		} );
 
 		// Converters.
+		editing.downcastDispatcher.on( 'insert:listItem', modelViewInsertion( editor.model ), { priority: 'high' } );
 		editing.downcastDispatcher.on( 'insert:$text', modelViewTextInsertion, { priority: 'high' } );
+
+		editing.downcastDispatcher.on( 'attribute:listType:listItem', modelViewChangeType( editor.model ) );
 		editing.downcastDispatcher.on( 'attribute:listChecked:listItem', modelViewChangeChecked( editor.model ) );
 
 		// Register command for todo list.

+ 193 - 1
packages/ckeditor5-list/src/utils.js

@@ -18,12 +18,205 @@ import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
  */
 export function createViewListItemElement( writer ) {
 	const viewItem = writer.createContainerElement( 'li' );
+
 	viewItem.getFillerOffset = getListItemFillerOffset;
 
 	return viewItem;
 }
 
 /**
+ * Helper function that creates a `<ul><li></li></ul>` or (`<ol>`) structure out of given `modelItem` model `listItem` element.
+ * Then, it binds created view list item (<li>) with model `listItem` element.
+ * The function then returns created view list item (<li>).
+ *
+ * @param {module:engine/model/item~Item} modelItem Model list item.
+ * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface.
+ * @returns {module:engine/view/containerelement~ContainerElement} View list element.
+ */
+export function generateLiInUl( modelItem, conversionApi ) {
+	const mapper = conversionApi.mapper;
+	const viewWriter = conversionApi.writer;
+	const listType = modelItem.getAttribute( 'listType' ) == 'numbered' ? 'ol' : 'ul';
+	const viewItem = createViewListItemElement( viewWriter );
+
+	const viewList = viewWriter.createContainerElement( listType, null );
+
+	viewWriter.insert( viewWriter.createPositionAt( viewList, 0 ), viewItem );
+
+	mapper.bindElements( modelItem, viewItem );
+
+	return viewItem;
+}
+
+/**
+ * Helper function that takes model list item element `modelItem`, corresponding view list item element `injectedItem`
+ * that is not added to the view and is inside a view list element (`ul` or `ol`) and is that's list only child.
+ * The list is inserted at correct position (element breaking may be needed) and then merged with it's siblings.
+ * See comments below to better understand the algorithm.
+ *
+ * @param {module:engine/view/item~Item} modelItem Model list item.
+ * @param {module:engine/view/containerelement~ContainerElement} injectedItem
+ * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface.
+ * @param {module:engine/model/model~Model} model The model instance.
+ */
+export function injectViewList( modelItem, injectedItem, conversionApi, model ) {
+	const injectedList = injectedItem.parent;
+	const mapper = conversionApi.mapper;
+	const viewWriter = conversionApi.writer;
+
+	// Position where view list will be inserted.
+	let insertPosition = mapper.toViewPosition( model.createPositionBefore( modelItem ) );
+
+	// 1. Find previous list item that has same or smaller indent. Basically we are looking for a first model item
+	// that is "parent" or "sibling" of injected model item.
+	// If there is no such list item, it means that injected list item is the first item in "its list".
+	const refItem = getSiblingListItem( modelItem.previousSibling, {
+		sameIndent: true,
+		smallerIndent: true,
+		listIndent: modelItem.getAttribute( 'listIndent' )
+	} );
+	const prevItem = modelItem.previousSibling;
+
+	if ( refItem && refItem.getAttribute( 'listIndent' ) == modelItem.getAttribute( 'listIndent' ) ) {
+		// There is a list item with same indent - we found same-level sibling.
+		// Break the list after it. Inserted view item will be inserted in the broken space.
+		const viewItem = mapper.toViewElement( refItem );
+		insertPosition = viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );
+	} else {
+		// There is no list item with same indent. Check previous model item.
+		if ( prevItem && prevItem.name == 'listItem' ) {
+			// If it is a list item, it has to have lower indent.
+			// It means that inserted item should be added to it as its nested item.
+			insertPosition = mapper.toViewPosition( model.createPositionAt( prevItem, 'end' ) );
+		} else {
+			// Previous item is not a list item (or does not exist at all).
+			// Just map the position and insert the view item at mapped position.
+			insertPosition = mapper.toViewPosition( model.createPositionBefore( modelItem ) );
+		}
+	}
+
+	insertPosition = positionAfterUiElements( insertPosition );
+
+	// Insert the view item.
+	viewWriter.insert( insertPosition, injectedList );
+
+	// 2. Handle possible children of injected model item.
+	if ( prevItem && prevItem.name == 'listItem' ) {
+		const prevView = mapper.toViewElement( prevItem );
+
+		const walkerBoundaries = viewWriter.createRange( viewWriter.createPositionAt( prevView, 0 ), insertPosition );
+		const walker = walkerBoundaries.getWalker( { ignoreElementEnd: true } );
+
+		for ( const value of walker ) {
+			if ( value.item.is( 'li' ) ) {
+				const breakPosition = viewWriter.breakContainer( viewWriter.createPositionBefore( value.item ) );
+				const viewList = value.item.parent;
+
+				const targetPosition = viewWriter.createPositionAt( injectedItem, 'end' );
+				mergeViewLists( viewWriter, targetPosition.nodeBefore, targetPosition.nodeAfter );
+				viewWriter.move( viewWriter.createRangeOn( viewList ), targetPosition );
+
+				walker.position = breakPosition;
+			}
+		}
+	} else {
+		const nextViewList = injectedList.nextSibling;
+
+		if ( nextViewList && ( nextViewList.is( 'ul' ) || nextViewList.is( 'ol' ) ) ) {
+			let lastSubChild = null;
+
+			for ( const child of nextViewList.getChildren() ) {
+				const modelChild = mapper.toModelElement( child );
+
+				if ( modelChild && modelChild.getAttribute( 'listIndent' ) > modelItem.getAttribute( 'listIndent' ) ) {
+					lastSubChild = child;
+				} else {
+					break;
+				}
+			}
+
+			if ( lastSubChild ) {
+				viewWriter.breakContainer( viewWriter.createPositionAfter( lastSubChild ) );
+				viewWriter.move( viewWriter.createRangeOn( lastSubChild.parent ), viewWriter.createPositionAt( injectedItem, 'end' ) );
+			}
+		}
+	}
+
+	// Merge inserted view list with its possible neighbour lists.
+	mergeViewLists( viewWriter, injectedList, injectedList.nextSibling );
+	mergeViewLists( viewWriter, injectedList.previousSibling, injectedList );
+}
+
+/**
+ * Helper function that takes two parameters, that are expected to be view list elements, and merges them.
+ * The merge happen only if both parameters are list elements of the same types (the same element name and the same class attributes).
+ *
+ * @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter The writer instance.
+ * @param {module:engine/view/item~Item} firstList First element o compare.
+ * @param {module:engine/view/item~Item} secondList Second parameter to compare.
+ * @returns {module:engine/view/position~Position|null} Position after merge or `null` when there was no merge.
+ */
+export function mergeViewLists( viewWriter, firstList, secondList ) {
+	// Check if two lists are going to be merged.
+	if ( !firstList || !secondList || ( firstList.name != 'ul' && firstList.name != 'ol' ) ) {
+		return null;
+	}
+
+	// Both parameters are list elements, so compare types now.
+	if ( firstList.name != secondList.name || firstList.getAttribute( 'class' ) !== secondList.getAttribute( 'class' ) ) {
+		return null;
+	}
+
+	return viewWriter.mergeContainers( viewWriter.createPositionAfter( firstList ) );
+}
+
+/**
+ * Helper function that for given `view.Position`, returns a `view.Position` that is after all `view.UIElement`s that
+ * are after given position.
+ *
+ * For example:
+ * <container:p>foo^<ui:span></ui:span><ui:span></ui:span>bar</contain:p>
+ * For position ^, a position before "bar" will be returned.
+ *
+ * @param {module:engine/view/position~Position} viewPosition
+ * @returns {module:engine/view/position~Position}
+ */
+export function positionAfterUiElements( viewPosition ) {
+	return viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
+}
+
+/**
+ * Helper function that seeks for a previous list item sibling of given model item which meets given criteria
+ * passed by an options object.
+ *
+ * @param {module:engine/model/item~Item} modelItem
+ * @param {Object} options Search criteria.
+ * @param {Boolean} [options.sameIndent=false] Whether sought sibling should have same indent.
+ * @param {Boolean} [options.smallerIndent=false] Whether sought sibling should have smaller indent.
+ * @param {Number} [options.listIndent] The reference indent.
+ * @returns {module:engine/model/item~Item|null}
+ */
+export function getSiblingListItem( modelItem, options ) {
+	const sameIndent = !!options.sameIndent;
+	const smallerIndent = !!options.smallerIndent;
+	const indent = options.listIndent;
+
+	let item = modelItem;
+
+	while ( item && item.name == 'listItem' ) {
+		const itemIndent = item.getAttribute( 'listIndent' );
+
+		if ( ( sameIndent && indent == itemIndent ) || ( smallerIndent && indent > itemIndent ) ) {
+			return item;
+		}
+
+		item = item.previousSibling;
+	}
+
+	return null;
+}
+
+/**
  * Helper method for creating an UI button and linking it with an appropriate command.
  *
  * @private
@@ -35,7 +228,6 @@ export function createViewListItemElement( writer ) {
 export function createUIComponent( editor, commandName, label, icon ) {
 	editor.ui.componentFactory.add( commandName, locale => {
 		const command = editor.commands.get( commandName );
-
 		const buttonView = new ButtonView( locale );
 
 		buttonView.set( {