瀏覽代碼

Added TodoListEditing tests and some minor improvements.

Oskar Wróbel 6 年之前
父節點
當前提交
0c19e6dbd3

+ 10 - 37
packages/ckeditor5-list/src/todolistconverters.js

@@ -9,7 +9,7 @@
 
 /* global document */
 
-import { generateLiInUl, injectViewList } from './utils';
+import { generateLiInUl, injectViewList, findInRange } from './utils';
 
 /**
  * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert
@@ -34,6 +34,7 @@ export function modelViewInsertion( model ) {
 		consumable.consume( data.item, 'insert' );
 		consumable.consume( data.item, 'attribute:listType' );
 		consumable.consume( data.item, 'attribute:listIndent' );
+		consumable.consume( data.item, 'attribute:todoListChecked' );
 
 		const viewWriter = conversionApi.writer;
 		const modelItem = data.item;
@@ -57,7 +58,9 @@ export function modelViewTextInsertion( evt, data, conversionApi ) {
 		return;
 	}
 
-	conversionApi.consumable.consume( data.item, 'insert' );
+	if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
+		return;
+	}
 
 	const viewWriter = conversionApi.writer;
 	const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
@@ -127,7 +130,9 @@ export function dataModelViewTextInsertion( evt, data, conversionApi ) {
 		return;
 	}
 
-	conversionApi.consumable.consume( data.item, 'insert' );
+	if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
+		return;
+	}
 
 	const viewWriter = conversionApi.writer;
 	const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
@@ -173,7 +178,8 @@ export function modelViewChangeChecked( model ) {
 		const { mapper, writer: viewWriter } = conversionApi;
 		const isChecked = !!data.item.getAttribute( 'todoListChecked' );
 		const viewItem = mapper.toViewElement( data.item );
-		const uiElement = findCheckmarkElement( viewItem, viewWriter );
+		const itemRange = viewWriter.createRangeIn( viewItem );
+		const uiElement = findInRange( itemRange, item => item.is( 'uiElement' ) ? item : false );
 
 		viewWriter.insert(
 			viewWriter.createPositionAfter( uiElement ),
@@ -185,31 +191,6 @@ export function modelViewChangeChecked( model ) {
 	};
 }
 
-/**
- * @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
- * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
- * @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
- * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface to be used by the callback.
- */
-export function dataViewModelCheckmarkInsertion( evt, data, conversionApi ) {
-	if ( data.viewItem.getAttribute( 'type' ) != 'checkbox' || data.modelCursor.parent.name != 'listItem' ) {
-		return;
-	}
-
-	if ( !conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
-		return;
-	}
-
-	const { writer } = conversionApi;
-	const modelItem = data.modelCursor.parent;
-
-	writer.setAttribute( 'listType', 'todo', modelItem );
-
-	if ( data.viewItem.getAttribute( 'checked' ) == 'checked' ) {
-		writer.setAttribute( 'todoListChecked', true, modelItem );
-	}
-}
-
 function addTodoElementsToListItem( modelItem, viewItem, viewWriter, model ) {
 	const isChecked = !!modelItem.getAttribute( 'todoListChecked' );
 
@@ -255,11 +236,3 @@ function createCheckMarkElement( isChecked, viewWriter, onChange ) {
 
 	return uiElement;
 }
-
-function findCheckmarkElement( element, viewWriter ) {
-	for ( const item of viewWriter.createRangeIn( element ).getItems() ) {
-		if ( item.is( 'uiElement' ) && item.hasClass( 'todo-list__checkmark' ) ) {
-			return item;
-		}
-	}
-}

+ 18 - 6
packages/ckeditor5-list/src/todolistediting.js

@@ -18,10 +18,11 @@ import {
 	dataModelViewInsertion,
 	dataModelViewTextInsertion,
 	modelViewChangeChecked,
-	modelViewChangeType,
-	dataViewModelCheckmarkInsertion
+	modelViewChangeType
 } from './todolistconverters';
 
+import { findInRange } from './utils';
+
 /**
  * @extends module:core/plugin~Plugin
  */
@@ -54,8 +55,6 @@ export default class TodoListEditing extends Plugin {
 		editing.downcastDispatcher.on( 'attribute:listType:listItem', modelViewChangeType( model ) );
 		editing.downcastDispatcher.on( 'attribute:todoListChecked:listItem', modelViewChangeChecked( model ) );
 
-		data.upcastDispatcher.on( 'element:input', dataViewModelCheckmarkInsertion, { priority: 'high' } );
-
 		// Register command for todo list.
 		editor.commands.add( 'todoList', new ListCommand( editor, 'todo' ) );
 
@@ -153,8 +152,21 @@ function moveSelectionAfterCheckmark( writer, selection ) {
 
 	const position = selection.getFirstPosition();
 
-	if ( position.parent.name === 'li' && position.offset == 0 && position.nodeAfter && position.nodeAfter.is( 'uiElement' ) ) {
-		writer.setSelection( position.parent, 1 );
+	if ( position.parent.name != 'li' || !position.parent.parent.hasClass( 'todo-list' ) ) {
+		return false;
+	}
+
+	const parentEndPosition = writer.createPositionAt( position.parent, 'end' );
+	const uiElement = findInRange( writer.createRange( position, parentEndPosition ), item => {
+		return ( item.is( 'uiElement' ) && item.hasClass( 'todo-list__checkmark' ) ) ? item : false;
+	} );
+
+	if ( uiElement && !position.isAfter( writer.createPositionBefore( uiElement ) ) ) {
+		const range = writer.createRange( writer.createPositionAfter( uiElement ), parentEndPosition );
+		const text = findInRange( range, item => item.is( 'textProxy' ) ? item.textNode : false );
+		const nextPosition = text ? writer.createPositionAt( text, 0 ) : parentEndPosition;
+
+		writer.setSelection( nextPosition );
 
 		return true;
 	}

+ 10 - 0
packages/ckeditor5-list/src/utils.js

@@ -216,6 +216,16 @@ export function getSiblingListItem( modelItem, options ) {
 	return null;
 }
 
+export function findInRange( range, comparator ) {
+	for ( const item of range.getItems() ) {
+		const result = comparator( item );
+
+		if ( result ) {
+			return result;
+		}
+	}
+}
+
 /**
  * Helper method for creating an UI button and linking it with an appropriate command.
  *

+ 575 - 0
packages/ckeditor5-list/tests/todolistediting.js

@@ -0,0 +1,575 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import TodoListEditing from '../src/todolistediting';
+import ListEditing from '../src/listediting';
+import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import ListCommand from '../src/listcommand';
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+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';
+
+describe( 'TodoListEditing', () => {
+	let editor, model, modelDoc, modelRoot, view, viewDoc, viewRoot;
+
+	beforeEach( () => {
+		return VirtualTestEditor
+			.create( {
+				plugins: [ TodoListEditing, Typing, BoldEditing ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+
+				model = editor.model;
+				modelDoc = model.document;
+				modelRoot = modelDoc.getRoot();
+
+				view = editor.editing.view;
+				viewDoc = view.document;
+				viewRoot = viewDoc.getRoot();
+
+				model.schema.register( 'foo', {
+					allowWhere: '$block',
+					allowAttributes: [ 'listIndent', 'listType' ],
+					isBlock: true,
+					isObject: true
+				} );
+			} );
+	} );
+
+	it( 'should load ListEditing', () => {
+		expect( TodoListEditing.requires ).to.have.members( [ ListEditing ] );
+	} );
+
+	it( 'should set proper schema rules', () => {
+		expect( model.schema.checkAttribute( [ '$root', 'listItem' ], 'todoListChecked' ) ).to.be.true;
+	} );
+
+	describe( 'command', () => {
+		it( 'should register todoList list command', () => {
+			const command = editor.commands.get( 'todoList' );
+
+			expect( command ).to.be.instanceOf( ListCommand );
+			expect( command ).to.have.property( 'type', 'todo' );
+		} );
+
+		it( 'should create todo list item and change to paragraph in normal usage flow', () => {
+			expect( getViewData( view ) ).to.equal( '<p>[]</p>' );
+			expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
+
+			editor.execute( 'todoList' );
+
+			expect( getModelData( model ) ).to.equal( '<listItem listIndent="0" listType="todo">[]</listItem>' );
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>[]</li>' +
+				'</ul>'
+			);
+
+			editor.execute( 'input', { text: 'a' } );
+
+			expect( getModelData( model ) ).to.equal( '<listItem listIndent="0" listType="todo">a[]</listItem>' );
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>a{}</li>' +
+				'</ul>'
+			);
+
+			editor.execute( 'input', { text: 'b' } );
+
+			expect( getModelData( model ) ).to.equal( '<listItem listIndent="0" listType="todo">ab[]</listItem>' );
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>ab{}</li>' +
+				'</ul>'
+			);
+
+			editor.execute( 'todoList' );
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>ab[]</paragraph>' );
+			expect( getViewData( view ) ).to.equal( '<p>ab{}</p>' );
+		} );
+	} );
+
+	describe( 'editing pipeline', () => {
+		it( 'should convert todo list item', () => {
+			setModelData( model,
+				'<listItem listType="todo" listIndent="0">1</listItem>' +
+				'<listItem listType="todo" listIndent="0" todoListChecked="true">2</listItem>'
+			);
+
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>{}1</li>' +
+					'<li><label class="todo-list__checkmark todo-list__checkmark_checked" contenteditable="false"></label>2</li>' +
+				'</ul>'
+			);
+		} );
+
+		it( 'should convert nested todo list items', () => {
+			setModelData( model,
+				'<listItem listType="todo" listIndent="0">1.0</listItem>' +
+				'<listItem listType="todo" listIndent="1">2.1</listItem>' +
+				'<listItem listType="todo" listIndent="1">3.1</listItem>' +
+				'<listItem listType="todo" listIndent="2">4.2</listItem>' +
+				'<listItem listType="todo" listIndent="2">5.2</listItem>' +
+				'<listItem listType="todo" listIndent="1">6.1</listItem>'
+			);
+
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li>' +
+						'<label class="todo-list__checkmark" contenteditable="false"></label>{}1.0' +
+						'<ul class="todo-list">' +
+							'<li><label class="todo-list__checkmark" contenteditable="false"></label>2.1</li>' +
+							'<li>' +
+								'<label class="todo-list__checkmark" contenteditable="false"></label>3.1' +
+								'<ul class="todo-list">' +
+									'<li><label class="todo-list__checkmark" contenteditable="false"></label>4.2</li>' +
+									'<li><label class="todo-list__checkmark" contenteditable="false"></label>5.2</li>' +
+								'</ul>' +
+							'</li>' +
+							'<li><label class="todo-list__checkmark" contenteditable="false"></label>6.1</li>' +
+						'</ul>' +
+					'</li>' +
+				'</ul>'
+			);
+		} );
+
+		it( 'should convert todo list items mixed with bulleted list items', () => {
+			setModelData( model,
+				'<listItem listType="todo" listIndent="0">1.0</listItem>' +
+				'<listItem listType="bulleted" listIndent="0">2.0</listItem>' +
+				'<listItem listType="todo" listIndent="1">3.1</listItem>' +
+				'<listItem listType="bulleted" listIndent="2">4.2</listItem>' +
+				'<listItem listType="todo" listIndent="1">5.1</listItem>'
+			);
+
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>{}1.0</li>' +
+				'</ul>' +
+				'<ul>' +
+					'<li>2.0' +
+						'<ul class="todo-list">' +
+							'<li>' +
+								'<label class="todo-list__checkmark" contenteditable="false"></label>3.1' +
+								'<ul>' +
+									'<li>4.2</li>' +
+								'</ul>' +
+							'</li>' +
+							'<li><label class="todo-list__checkmark" contenteditable="false"></label>5.1</li>' +
+						'</ul>' +
+					'</li>' +
+				'</ul>'
+			);
+		} );
+
+		it( 'should convert todo list items mixed with numbered list items', () => {
+			setModelData( model,
+				'<listItem listType="todo" listIndent="0">1.0</listItem>' +
+				'<listItem listType="numbered" listIndent="0">2.0</listItem>' +
+				'<listItem listType="todo" listIndent="1">3.1</listItem>' +
+				'<listItem listType="numbered" listIndent="2">4.2</listItem>' +
+				'<listItem listType="todo" listIndent="1">5.1</listItem>'
+			);
+
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>{}1.0</li>' +
+				'</ul>' +
+				'<ol>' +
+					'<li>2.0' +
+						'<ul class="todo-list">' +
+							'<li>' +
+								'<label class="todo-list__checkmark" contenteditable="false"></label>3.1' +
+								'<ol>' +
+									'<li>4.2</li>' +
+								'</ol>' +
+							'</li>' +
+							'<li><label class="todo-list__checkmark" contenteditable="false"></label>5.1</li>' +
+						'</ul>' +
+					'</li>' +
+				'</ol>'
+			);
+		} );
+
+		it( 'should properly convert list type change #1', () => {
+			setModelData( model,
+				'<listItem listType="numbered" listIndent="0">1.0</listItem>' +
+				'<listItem listType="numbered" listIndent="0">[]2.0</listItem>' +
+				'<listItem listType="numbered" listIndent="0">3.0</listItem>'
+			);
+
+			editor.execute( 'todoList' );
+
+			expect( getViewData( view ) ).to.equal(
+				'<ol>' +
+					'<li>1.0</li>' +
+				'</ol>' +
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>{}2.0</li>' +
+				'</ul>' +
+				'<ol>' +
+					'<li>3.0</li>' +
+				'</ol>'
+			);
+		} );
+
+		it( 'should properly convert list type change #2', () => {
+			setModelData( model,
+				'<listItem listType="todo" listIndent="0">1.0</listItem>' +
+				'<listItem listType="todo" listIndent="0">[]2.0</listItem>' +
+				'<listItem listType="todo" listIndent="0">3.0</listItem>'
+			);
+
+			editor.execute( 'numberedList' );
+
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>1.0</li>' +
+				'</ul>' +
+				'<ol>' +
+					'<li>{}2.0</li>' +
+				'</ol>' +
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>3.0</li>' +
+				'</ul>'
+			);
+		} );
+
+		it( 'should properly convert list type change #3', () => {
+			setModelData( model,
+				'<listItem listType="todo" listIndent="0">1.0</listItem>' +
+				'<listItem listType="numbered" listIndent="0">[]2.0</listItem>' +
+				'<listItem listType="todo" listIndent="0">3.0</listItem>'
+			);
+
+			editor.execute( 'bulletedList' );
+
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>1.0</li>' +
+				'</ul>' +
+				'<ul>' +
+					'<li>{}2.0</li>' +
+				'</ul>' +
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>3.0</li>' +
+				'</ul>'
+			);
+		} );
+
+		it( 'should convert todoListChecked attribute change', () => {
+			setModelData( model, '<listItem listType="todo" listIndent="0">1.0</listItem>' );
+
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>{}1.0</li>' +
+				'</ul>'
+			);
+
+			model.change( writer => {
+				writer.setAttribute( 'todoListChecked', true, modelRoot.getChild( 0 ) );
+			} );
+
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark todo-list__checkmark_checked" contenteditable="false"></label>{}1.0</li>' +
+				'</ul>'
+			);
+
+			model.change( writer => {
+				writer.setAttribute( 'todoListChecked', false, modelRoot.getChild( 0 ) );
+			} );
+
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>{}1.0</li>' +
+				'</ul>'
+			);
+		} );
+
+		it( 'should be overwritable', () => {
+			editor.editing.downcastDispatcher.on( 'insert:listItem', ( evt, data, api ) => {
+				const { consumable, writer, mapper } = api;
+
+				consumable.consume( data.item, 'insert' );
+				consumable.consume( data.item, 'attribute:listType' );
+				consumable.consume( data.item, 'attribute:listIndent' );
+
+				const insertPosition = mapper.toViewPosition( model.createPositionBefore( data.item ) );
+				const element = writer.createContainerElement( 'test' );
+
+				mapper.bindElements( data.item, element );
+				writer.insert( insertPosition, element );
+			}, { priority: 'highest' } );
+
+			editor.editing.downcastDispatcher.on( 'insert:$text', ( evt, data, api ) => {
+				const { consumable, writer, mapper } = api;
+
+				consumable.consume( data.item, 'insert' );
+
+				const insertPosition = mapper.toViewPosition( model.createPositionBefore( data.item ) );
+				const element = writer.createText( data.item.data );
+
+				writer.insert( insertPosition, element );
+				mapper.bindElements( data.item, element );
+			}, { priority: 'highest' } );
+
+			editor.editing.downcastDispatcher.on( 'attribute:todoListChecked:listItem', ( evt, data, api ) => {
+				const { consumable, writer, mapper } = api;
+
+				consumable.consume( data.item, 'attribute:todoListChecked' );
+
+				const viewElement = mapper.toViewElement( data.item );
+
+				writer.addClass( 'checked', viewElement );
+			}, { priority: 'highest' } );
+
+			setModelData( model, '<listItem listType="todo" listIndent="0">Foo</listItem>' );
+			expect( getViewData( view ) ).to.equal( '<test>{}Foo</test>' );
+
+			model.change( writer => writer.setAttribute( 'todoListChecked', true, modelRoot.getChild( 0 ) ) );
+			expect( getViewData( view ) ).to.equal( '<test class="checked">{}Foo</test>' );
+		} );
+	} );
+
+	describe( 'data pipeline', () => {
+		it( 'should convert todo list item', () => {
+			setModelData( model,
+				'<listItem listType="todo" listIndent="0">1</listItem>' +
+				'<listItem listType="todo" listIndent="0" todoListChecked="true">2</listItem>'
+			);
+
+			expect( editor.getData() ).to.equal(
+				'<ul class="todo-list">' +
+					'<li>' +
+						'<label>' +
+							'<input class="todo-list__checkmark" type="checkbox" disabled="disabled">' +
+							'<span class="todo-list__label">1</span>' +
+						'</label>' +
+					'</li>' +
+					'<li>' +
+						'<label>' +
+							'<input class="todo-list__checkmark" type="checkbox" disabled="disabled" checked="checked">' +
+							'<span class="todo-list__label">2</span>' +
+						'</label>' +
+					'</li>' +
+				'</ul>'
+			);
+		} );
+
+		it( 'should convert nested todo list item', () => {
+			setModelData( model,
+				'<listItem listType="todo" listIndent="0">1.0</listItem>' +
+				'<listItem listType="todo" listIndent="1">2.1</listItem>'
+			);
+
+			expect( editor.getData() ).to.equal(
+				'<ul class="todo-list">' +
+					'<li>' +
+						'<label>' +
+							'<input class="todo-list__checkmark" type="checkbox" disabled="disabled">' +
+							'<span class="todo-list__label">1.0</span>' +
+						'</label>' +
+						'<ul class="todo-list">' +
+							'<li>' +
+								'<label>' +
+									'<input class="todo-list__checkmark" type="checkbox" disabled="disabled">' +
+									'<span class="todo-list__label">2.1</span>' +
+								'</label>' +
+							'</li>' +
+						'</ul>' +
+					'</li>' +
+				'</ul>'
+			);
+		} );
+
+		it( 'should convert todo list item mixed with bulleted list items', () => {
+			setModelData( model,
+				'<listItem listType="todo" listIndent="0">1.0</listItem>' +
+				'<listItem listType="bulleted" listIndent="0">2.0</listItem>' +
+				'<listItem listType="todo" listIndent="1">3.1</listItem>' +
+				'<listItem listType="bulleted" listIndent="2">4.2</listItem>' +
+				'<listItem listType="todo" listIndent="1">5.1</listItem>'
+			);
+
+			expect( editor.getData() ).to.equal(
+				'<ul class="todo-list">' +
+					'<li>' +
+						'<label>' +
+							'<input class="todo-list__checkmark" type="checkbox" disabled="disabled">' +
+							'<span class="todo-list__label">1.0</span>' +
+						'</label>' +
+					'</li>' +
+				'</ul>' +
+				'<ul>' +
+					'<li>2.0' +
+						'<ul class="todo-list">' +
+							'<li>' +
+								'<label>' +
+									'<input class="todo-list__checkmark" type="checkbox" disabled="disabled">' +
+									'<span class="todo-list__label">3.1</span>' +
+								'</label>' +
+								'<ul>' +
+									'<li>4.2</li>' +
+								'</ul>' +
+							'</li>' +
+							'<li>' +
+								'<label>' +
+									'<input class="todo-list__checkmark" type="checkbox" disabled="disabled">' +
+									'<span class="todo-list__label">5.1</span>' +
+								'</label>' +
+							'</li>' +
+						'</ul>' +
+					'</li>' +
+				'</ul>'
+			);
+		} );
+
+		it( 'should convert todo list item mixed with numbered list items', () => {
+			setModelData( model,
+				'<listItem listType="todo" listIndent="0">1.0</listItem>' +
+				'<listItem listType="numbered" listIndent="0">2.0</listItem>' +
+				'<listItem listType="todo" listIndent="1">3.1</listItem>' +
+				'<listItem listType="numbered" listIndent="2">4.2</listItem>' +
+				'<listItem listType="todo" listIndent="1">5.1</listItem>'
+			);
+
+			expect( editor.getData() ).to.equal(
+				'<ul class="todo-list">' +
+					'<li>' +
+						'<label>' +
+							'<input class="todo-list__checkmark" type="checkbox" disabled="disabled">' +
+							'<span class="todo-list__label">1.0</span>' +
+						'</label>' +
+					'</li>' +
+				'</ul>' +
+				'<ol>' +
+					'<li>2.0' +
+						'<ul class="todo-list">' +
+							'<li>' +
+								'<label>' +
+									'<input class="todo-list__checkmark" type="checkbox" disabled="disabled">' +
+									'<span class="todo-list__label">3.1</span>' +
+								'</label>' +
+								'<ol>' +
+									'<li>4.2</li>' +
+								'</ol>' +
+							'</li>' +
+							'<li>' +
+								'<label>' +
+									'<input class="todo-list__checkmark" type="checkbox" disabled="disabled">' +
+									'<span class="todo-list__label">5.1</span>' +
+								'</label>' +
+							'</li>' +
+						'</ul>' +
+					'</li>' +
+				'</ol>'
+			);
+		} );
+
+		it( 'should be overwritable', () => {
+			editor.data.downcastDispatcher.on( 'insert:listItem', ( evt, data, api ) => {
+				const { consumable, writer, mapper } = api;
+
+				consumable.consume( data.item, 'insert' );
+				consumable.consume( data.item, 'attribute:listType' );
+				consumable.consume( data.item, 'attribute:listIndent' );
+
+				const element = writer.createContainerElement( 'test' );
+				const insertPosition = mapper.toViewPosition( model.createPositionBefore( data.item ) );
+
+				writer.insert( insertPosition, element );
+				mapper.bindElements( data.item, element );
+			}, { priority: 'highest' } );
+
+			editor.data.downcastDispatcher.on( 'insert:$text', ( evt, data, api ) => {
+				const { consumable, writer, mapper } = api;
+
+				consumable.consume( data.item, 'insert' );
+
+				const insertPosition = mapper.toViewPosition( model.createPositionBefore( data.item ) );
+				const element = writer.createText( data.item.data );
+
+				writer.insert( insertPosition, element );
+				mapper.bindElements( data.item, element );
+			}, { priority: 'highest' } );
+
+			setModelData( model, '<listItem listType="todo" listIndent="0">Foo</listItem>' );
+
+			expect( editor.getData() ).to.equal( '<test>Foo</test>' );
+		} );
+	} );
+
+	describe( 'selection view post-fixer', () => {
+		it( 'should move selection after checkmark element to the first text node', () => {
+			setModelData( model, '<listItem listType="todo" listIndent="0">Foo</listItem>' );
+
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>{}Foo</li>' +
+				'</ul>'
+			);
+		} );
+
+		it( 'should move selection after checkmark element when list item does not contain any text node', () => {
+			setModelData( model, '<listItem listType="todo" listIndent="0">[]</listItem>' );
+
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li><label class="todo-list__checkmark" contenteditable="false"></label>[]</li>' +
+				'</ul>'
+			);
+		} );
+	} );
+
+	describe( 'uiElements view post-fixer', () => {
+		it( 'should move all UIElements from before a checkmark after the checkmark element', () => {
+			setModelData( model, '<listItem listType="todo" listIndent="0">foo</listItem>' );
+
+			editor.conversion.for( 'downcast' ).markerToElement( {
+				model: 'foo',
+				view: ( data, writer ) => writer.createUIElement( 'something' )
+			} );
+
+			editor.conversion.for( 'downcast' ).markerToHighlight( {
+				model: 'bar',
+				view: { classes: 'bar' }
+			} );
+
+			model.change( writer => {
+				writer.addMarker( 'foo', {
+					range: writer.createRangeIn( writer.createPositionAt( modelRoot.getChild( 0 ), 0 ) ),
+					usingOperation: false
+				} );
+
+				writer.addMarker( 'bar', {
+					range: writer.createRangeIn( modelRoot.getChild( 0 ) ),
+					usingOperation: false
+				} );
+
+				// VirtualTestEeditor does not render V to DOM, so we need to mock element market to be rendered
+				// because view post-fixer uses it.
+				view._renderer.markedChildren = new Set( [ viewRoot.getChild( 0 ).getChild( 0 ) ] );
+			} );
+
+			expect( getViewData( view ) ).to.equal(
+				'<ul class="todo-list">' +
+					'<li>' +
+						'<span class="bar">' +
+							'<label class="todo-list__checkmark" contenteditable="false"></label>' +
+							'<something></something>' +
+							'{}foo' +
+						'</span>' +
+					'</li>' +
+				'</ul>'
+			);
+		} );
+	} );
+} );