Browse Source

Introduced the editing part of TODO list feature.

Oskar Wróbel 6 years ago
parent
commit
0a392e3e0d

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

@@ -8,6 +8,7 @@
  */
 
 import { createViewListItemElement } from './utils';
+import { addTodoElementsToListItem, removeTodoElementsFromListItem } from './todolistutils';
 import TreeWalker from '@ckeditor/ckeditor5-engine/src/model/treewalker';
 
 /**
@@ -36,7 +37,7 @@ export function modelViewInsertion( model ) {
 		consumable.consume( data.item, 'attribute:listIndent' );
 
 		const modelItem = data.item;
-		const viewItem = generateLiInUl( modelItem, conversionApi );
+		const viewItem = generateLiInUl( modelItem, conversionApi, model );
 
 		injectViewList( modelItem, viewItem, conversionApi, model );
 	};
@@ -92,37 +93,44 @@ export function modelViewRemove( model ) {
  * by breaking view elements, changing their name and merging them.
  *
  * @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.
+ * @param {module:engine/model/model~Model} model Model instance.
  */
-export function modelViewChangeType( evt, data, conversionApi ) {
-	if ( !conversionApi.consumable.consume( data.item, 'attribute:listType' ) ) {
-		return;
-	}
+export function modelViewChangeType( model ) {
+	return ( 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;
 
-	// 1. 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 ) );
 
-	// 2. 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 );
+		// 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 );
+		}
 
-	// 3. Merge the changed view list with other lists, if possible.
-	mergeViewLists( viewWriter, viewList, viewList.nextSibling );
-	mergeViewLists( viewWriter, viewList.previousSibling, viewList );
+		// Merge the changed view list with other lists, if possible.
+		mergeViewLists( viewWriter, viewList, viewList.nextSibling );
+		mergeViewLists( viewWriter, viewList.previousSibling, viewList );
 
-	// 4. 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' );
-	}
+		// 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' );
+		}
+	};
 }
 
 /**
@@ -787,15 +795,20 @@ 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 ) {
+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;
@@ -915,13 +928,19 @@ function getSiblingListItem( modelItem, options ) {
 }
 
 // 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 UL or OL elements.
+// 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 ) {
-	if ( firstList && secondList && ( firstList.name == 'ul' || firstList.name == 'ol' ) && firstList.name == secondList.name ) {
-		return viewWriter.mergeContainers( viewWriter.createPositionAfter( firstList ) );
+	// Check if two lists are going to be merged.
+	if ( !firstList || !secondList || ( firstList.name != 'ul' && firstList.name != 'ol' ) ) {
+		return null;
 	}
 
-	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`

+ 2 - 1
packages/ckeditor5-list/src/list.js

@@ -9,6 +9,7 @@
 
 import ListEditing from './listediting';
 import ListUI from './listui';
+import TodoList from './todolist';
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
@@ -25,7 +26,7 @@ export default class List extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ ListEditing, ListUI ];
+		return [ ListEditing, ListUI, TodoList ];
 	}
 
 	/**

+ 2 - 2
packages/ckeditor5-list/src/listcommand.js

@@ -29,9 +29,9 @@ export default class ListCommand extends Command {
 		 * The type of the list created by the command.
 		 *
 		 * @readonly
-		 * @member {'numbered'|'bulleted'}
+		 * @member {'numbered'|'bulleted'|'todo'}
 		 */
-		this.type = type == 'bulleted' ? 'bulleted' : 'numbered';
+		this.type = type;
 
 		/**
 		 * A flag indicating whether the command is active, which means that the selection starts in a list of the same type.

+ 4 - 4
packages/ckeditor5-list/src/listediting.js

@@ -26,7 +26,7 @@ import {
 	modelIndentPasteFixer,
 	viewModelConverter,
 	modelToViewPosition,
-	viewToModelPosition
+	viewToModelPosition,
 } from './converters';
 
 /**
@@ -56,7 +56,7 @@ export default class ListEditing extends Plugin {
 		// If there are blocks allowed inside list item, algorithms using `getSelectedBlocks()` will have to be modified.
 		editor.model.schema.register( 'listItem', {
 			inheritAllFrom: '$block',
-			allowAttributes: [ 'listType', 'listIndent' ]
+			allowAttributes: [ 'listType', 'listIndent', 'listChecked' ]
 		} );
 
 		// Converters.
@@ -77,8 +77,8 @@ 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 );
-		data.downcastDispatcher.on( 'attribute:listType:listItem', modelViewChangeType );
+		editing.downcastDispatcher.on( 'attribute:listType:listItem', modelViewChangeType( editor.model ) );
+		data.downcastDispatcher.on( 'attribute:listType:listItem', modelViewChangeType( editor.model ) );
 		editing.downcastDispatcher.on( 'attribute:listIndent:listItem', modelViewChangeIndent( editor.model ) );
 		data.downcastDispatcher.on( 'attribute:listIndent:listItem', modelViewChangeIndent( editor.model ) );
 

+ 5 - 35
packages/ckeditor5-list/src/listui.js

@@ -7,11 +7,12 @@
  * @module list/listui
  */
 
+import { createUIComponent } from './utils';
+
 import numberedListIcon from '../theme/icons/numberedlist.svg';
 import bulletedListIcon from '../theme/icons/bulletedlist.svg';
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 
 /**
  * The list UI feature. It introduces the `'numberedList'` and `'bulletedList'` buttons that
@@ -24,41 +25,10 @@ export default class ListUI extends Plugin {
 	 * @inheritDoc
 	 */
 	init() {
-		// Create two buttons and link them with numberedList and bulletedList commands.
 		const t = this.editor.t;
-		this._addButton( 'numberedList', t( 'Numbered List' ), numberedListIcon );
-		this._addButton( 'bulletedList', t( 'Bulleted List' ), bulletedListIcon );
-	}
-
-	/**
-	 * Helper method for initializing a button and linking it with an appropriate command.
-	 *
-	 * @private
-	 * @param {String} commandName The name of the command.
-	 * @param {Object} label The button label.
-	 * @param {String} icon The source of the icon.
-	 */
-	_addButton( commandName, label, icon ) {
-		const editor = this.editor;
 
-		editor.ui.componentFactory.add( commandName, locale => {
-			const command = editor.commands.get( commandName );
-
-			const buttonView = new ButtonView( locale );
-
-			buttonView.set( {
-				label,
-				icon,
-				tooltip: true
-			} );
-
-			// Bind button model to command.
-			buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
-
-			// Execute command.
-			this.listenTo( buttonView, 'execute', () => editor.execute( commandName ) );
-
-			return buttonView;
-		} );
+		// Create two buttons and link them with numberedList and bulletedList commands.
+		createUIComponent( this.editor, 'numberedList', t( 'Numbered List' ), numberedListIcon );
+		createUIComponent( this.editor, 'bulletedList', t( 'Bulleted List' ), bulletedListIcon );
 	}
 }

+ 34 - 0
packages/ckeditor5-list/src/todolist.js

@@ -0,0 +1,34 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module list/todolist
+ */
+
+import TodoListEditing from './todolistediting';
+import TodoListUI from './todolistui';
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+/**
+ * The todo list feature.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class List extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ TodoListEditing, TodoListUI ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TodoList';
+	}
+}

+ 55 - 0
packages/ckeditor5-list/src/todolistconverters.js

@@ -0,0 +1,55 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module list/todolistconverters
+ */
+
+import { createCheckMarkElement } from './todolistutils';
+
+export function modelViewTextInsertion( evt, data, conversionApi ) {
+	const parent = data.range.start.parent;
+
+	if ( parent.name != 'listItem' || parent.getAttribute( 'listType' ) != 'todo' ) {
+		return;
+	}
+
+	conversionApi.consumable.consume( data.item, 'insert' );
+
+	const viewWriter = conversionApi.writer;
+	const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
+	const viewText = viewWriter.createText( data.item.data );
+
+	viewWriter.insert( viewPosition.offset ? viewPosition : viewPosition.getShiftedBy( 1 ), viewText );
+}
+
+export function modelViewChangeChecked( model ) {
+	return ( evt, data, conversionApi ) => {
+		if ( !conversionApi.consumable.consume( data.item, 'attribute:listChecked' ) ) {
+			return;
+		}
+
+		const { mapper, writer: viewWriter } = conversionApi;
+		const isChecked = !!data.item.getAttribute( 'listChecked' );
+		const viewItem = mapper.toViewElement( data.item );
+		const uiElement = findCheckmarkElement( viewWriter, viewItem );
+
+		viewWriter.insert(
+			viewWriter.createPositionAfter( uiElement ),
+			createCheckMarkElement( isChecked, viewWriter, isChecked => {
+				model.change( writer => writer.setAttribute( 'listChecked', isChecked, data.item ) );
+			} )
+		);
+		viewWriter.remove( uiElement );
+	};
+}
+
+function findCheckmarkElement( viewWriter, parent ) {
+	for ( const item of viewWriter.createRangeIn( parent ).getItems() ) {
+		if ( item.is( 'uiElement' ) && item.hasClass( 'todo-list__checkmark' ) ) {
+			return item;
+		}
+	}
+}

+ 171 - 0
packages/ckeditor5-list/src/todolistediting.js

@@ -0,0 +1,171 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module list/todolistediting
+ */
+
+import ListCommand from './listcommand';
+import ListEditing from './listediting';
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import {
+	modelViewTextInsertion,
+	modelViewChangeChecked
+} from './todolistconverters';
+
+/**
+ * @extends module:core/plugin~Plugin
+ */
+export default class TodoListEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ListEditing ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const editing = editor.editing;
+		const viewDocument = editing.view.document;
+
+		editor.model.schema.extend( 'listItem', {
+			allowAttributes: [ 'listChecked' ]
+		} );
+
+		// Converters.
+		editing.downcastDispatcher.on( 'insert:$text', modelViewTextInsertion, { priority: 'high' } );
+		editing.downcastDispatcher.on( 'attribute:listChecked:listItem', modelViewChangeChecked( editor.model ) );
+
+		// Register command for todo list.
+		editor.commands.add( 'todoList', new ListCommand( editor, 'todo' ) );
+
+		// Move selection after a checkbox element.
+		viewDocument.registerPostFixer( writer => moveUIElementsAfterCheckmark( writer, this._getChangedCheckmarkElements() ) );
+
+		// Move all uiElements after a checkbox element.
+		viewDocument.registerPostFixer( writer => moveSelectionAfterCheckmark( writer, viewDocument.selection ) );
+
+		// Remove `listChecked` attribute when a host element is no longer a todo list item.
+		const listItemsToFix = new Set();
+
+		this.listenTo( editor.model, 'applyOperation', ( evt, args ) => {
+			const operation = args[ 0 ];
+
+			if ( operation.type != 'changeAttribute' && operation.key != 'listType' && operation.oldValue == 'todoList' ) {
+				for ( const item of operation.range.getItems() ) {
+					if ( item.name == 'listItem' && item.hasAttribute( 'listChecked' ) ) {
+						listItemsToFix.add( item );
+					}
+				}
+			}
+
+			if ( operation.type == 'rename' && operation.oldName == 'listItem' ) {
+				const item = operation.position.nodeAfter;
+
+				if ( item.hasAttribute( 'listChecked' ) ) {
+					listItemsToFix.add( item );
+				}
+			}
+		} );
+
+		editor.model.document.registerPostFixer( writer => {
+			let hasChanged = false;
+
+			for ( const listItem of listItemsToFix ) {
+				writer.removeAttribute( 'listChecked', listItem );
+				hasChanged = true;
+			}
+
+			listItemsToFix.clear();
+
+			return hasChanged;
+		} );
+	}
+
+	/**
+	 * Gets list of all checkmark elements that are going to be rendered.
+	 *
+	 * @private
+	 * @returns {Array.<module:engine/view/uielement~UIElement>}
+	 */
+	_getChangedCheckmarkElements() {
+		const editingView = this.editor.editing.view;
+		const elements = [];
+
+		for ( const element of Array.from( editingView._renderer.markedChildren ) ) {
+			for ( const item of editingView.createRangeIn( element ).getItems() ) {
+				if ( item.is( 'uiElement' ) && item.hasClass( 'todo-list__checkmark' ) && !elements.includes( item ) ) {
+					elements.push( item );
+				}
+			}
+		}
+
+		return elements;
+	}
+}
+
+// @private
+// @param {module:engine/view/downcastwriter~DowncastWriter} writer
+// @param {Array.<module:engine/view/uielement~UIElement>} uiElements
+// @returns {Boolean}
+function moveUIElementsAfterCheckmark( writer, uiElements ) {
+	let hasChanged = false;
+
+	for ( const uiElement of uiElements ) {
+		const listItem = getListItemAncestor( uiElement );
+		const positionAtListItem = writer.createPositionAt( listItem, 0 );
+		const positionBeforeUiElement = writer.createPositionBefore( uiElement );
+
+		if ( positionAtListItem.isEqual( positionBeforeUiElement ) ) {
+			continue;
+		}
+
+		const range = writer.createRange( positionAtListItem, positionBeforeUiElement );
+
+		for ( const item of Array.from( range.getItems() ) ) {
+			if ( item.is( 'uiElement' ) ) {
+				writer.move( writer.createRangeOn( item ), writer.createPositionAfter( uiElement ) );
+				hasChanged = true;
+			}
+		}
+	}
+
+	return hasChanged;
+}
+
+// @private
+// @param {module:engine/view/downcastwriter~DowncastWriter} writer
+// @param {module:engine/view/documentselection~DocumentSelection} selection
+function moveSelectionAfterCheckmark( writer, selection ) {
+	if ( !selection.isCollapsed ) {
+		return false;
+	}
+
+	const position = selection.getFirstPosition();
+
+	if ( position.parent.name === 'li' && position.offset == 0 && position.nodeAfter && position.nodeAfter.is( 'uiElement' ) ) {
+		writer.setSelection( position.parent, 1 );
+
+		return true;
+	}
+
+	return false;
+}
+
+// @private
+// @param {module:engine/view/uielement~UIElement} element
+function getListItemAncestor( element ) {
+	for ( const parent of element.getAncestors() ) {
+		if ( parent.name == 'li' ) {
+			return parent;
+		}
+	}
+}

+ 29 - 0
packages/ckeditor5-list/src/todolistui.js

@@ -0,0 +1,29 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module list/todolistui
+ */
+
+import { createUIComponent } from './utils';
+
+import todoListIcon from '../theme/icons/todolist.svg';
+import '../theme/list.css';
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+/**
+ * @extends module:core/plugin~Plugin
+ */
+export default class TodoListUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const t = this.editor.t;
+
+		createUIComponent( this.editor, 'todoList', t( 'Todo List' ), todoListIcon );
+	}
+}

+ 56 - 0
packages/ckeditor5-list/src/todolistutils.js

@@ -0,0 +1,56 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module list/todolistutils
+ */
+
+/* global document */
+
+export function addTodoElementsToListItem( viewWriter, viewItem, modelItem, model ) {
+	const isChecked = !!modelItem.getAttribute( 'listChecked' );
+
+	viewWriter.addClass( 'todo-list', viewItem.parent );
+
+	viewWriter.insert(
+		viewWriter.createPositionAt( viewItem, 0 ),
+		createCheckMarkElement( isChecked, viewWriter, isChecked => {
+			model.change( writer => writer.setAttribute( 'listChecked', isChecked, modelItem ) );
+		} )
+	);
+}
+
+export function removeTodoElementsFromListItem( viewWriter, viewItem, modelItem, model ) {
+	viewWriter.removeClass( 'todo-list', viewItem.parent );
+	viewWriter.remove( viewItem.getChild( 0 ) );
+	model.change( writer => writer.removeAttribute( 'listChecked', modelItem ) );
+}
+
+export function createCheckMarkElement( isChecked, viewWriter, onChange ) {
+	const uiElement = viewWriter.createUIElement(
+		'label',
+		{
+			class: 'todo-list__checkmark',
+			contenteditable: false
+		},
+		function( domDocument ) {
+			const domElement = this.toDomElement( domDocument );
+			const checkbox = document.createElement( 'input' );
+
+			checkbox.type = 'checkbox';
+			checkbox.checked = isChecked;
+			checkbox.addEventListener( 'change', evt => onChange( evt.target.checked ) );
+			domElement.appendChild( checkbox );
+
+			return domElement;
+		}
+	);
+
+	if ( isChecked ) {
+		viewWriter.addClass( 'todo-list__checkmark_checked', uiElement );
+	}
+
+	return uiElement;
+}

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

@@ -8,6 +8,7 @@
  */
 
 import { getFillerOffset } from '@ckeditor/ckeditor5-engine/src/view/containerelement';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 
 /**
  * Creates list item {@link module:engine/view/containerelement~ContainerElement}.
@@ -22,6 +23,37 @@ export function createViewListItemElement( writer ) {
 	return viewItem;
 }
 
+/**
+ * Helper method for creating an UI button and linking it with an appropriate command.
+ *
+ * @private
+ * @param {module:core/editor/editor~Editor} editor The editor instance to which UI component will be added.
+ * @param {String} commandName The name of the command.
+ * @param {Object} label The button label.
+ * @param {String} icon The source of the icon.
+ */
+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( {
+			label,
+			icon,
+			tooltip: true
+		} );
+
+		// Bind button model to command.
+		buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );
+
+		// Execute command.
+		buttonView.on( 'execute', () => editor.execute( commandName ) );
+
+		return buttonView;
+	} );
+}
+
 // Implementation of getFillerOffset for view list item element.
 //
 // @returns {Number|null} Block filler offset or `null` if block filler is not needed.

File diff suppressed because it is too large
+ 1 - 0
packages/ckeditor5-list/theme/icons/todolist.svg


+ 70 - 0
packages/ckeditor5-list/theme/list.css

@@ -0,0 +1,70 @@
+:root {
+	--ck-todo-list-checkmark-size: 14px;
+	--ck-color-todo-list-checkmark-background: hsl(120, 100%, 42%);
+	--ck-color-todo-list-checkmark-border: hsl( 120, 100%, 27%);
+}
+
+.ck .todo-list {
+	list-style: none;
+	position: relative;
+}
+
+.ck .todo-list > li {
+	margin-bottom: 5px;
+}
+
+.ck .todo-list > li > .todo-list {
+	margin-top: 5px;
+}
+
+/* Let's hide native checkbox and make a fancy one. */
+.ck .todo-list__checkmark input[type='checkbox'] {
+	display: block;
+	width: 100%;
+	height: 100%;
+	opacity: 0;
+}
+
+.ck .todo-list__checkmark {
+	cursor: pointer;
+	width: var(--ck-todo-list-checkmark-size);
+	height: var(--ck-todo-list-checkmark-size);
+	position: relative;
+	display: inline-block;
+	left: -20px;
+	margin-right: -10px;
+}
+
+.ck .todo-list__checkmark::before {
+	content: '';
+	position: absolute;
+	display: block;
+	width: 100%;
+	height: 100%;
+	border: 1px solid var(--ck-color-base-text);
+	border-radius: var(--ck-border-radius);
+	transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border;
+}
+
+.ck .todo-list__checkmark::after {
+	pointer-events: none;
+	content: '';
+	position: absolute;
+	left: 5px;
+	top: 3px;
+	display: block;
+	width: 3px;
+	height: 6px;
+	border: solid #fff;
+	border-width: 0 2px 2px 0;
+	transform: rotate(45deg);
+}
+
+.ck .todo-list__checkmark:hover::before {
+	box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1)
+}
+
+.ck .todo-list__checkmark_checked::before {
+	background: var(--ck-color-todo-list-checkmark-background);
+	border-color: var(--ck-color-todo-list-checkmark-border);
+}