Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Introduced todo lists. #133

Merged
merged 44 commits into from
Aug 13, 2019
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b1513a4
Introduced the editing part of TODO list feature.
oskarwrobel Aug 5, 2019
70ff55b
Move todo conversion out of list converters.
oskarwrobel Aug 5, 2019
4f562b4
Added data pipeline conversion for todo lists.
oskarwrobel Aug 5, 2019
ca05039
Moved helpers from todolistutils to todolistconverters.
oskarwrobel Aug 6, 2019
7d0c1d1
Unify order of parameters in helper functions.
oskarwrobel Aug 6, 2019
05dcbf5
Jump over checkbox element on left arrow key press.
oskarwrobel Aug 6, 2019
09a7508
Minor code reorganisation.
oskarwrobel Aug 7, 2019
f4669dd
Renamed model attribute from listChecked to todoListChecked.
oskarwrobel Aug 7, 2019
c787683
Added data pipeline v -> m conversion.
oskarwrobel Aug 7, 2019
0d95558
Removed redundant converters.
oskarwrobel Aug 7, 2019
9353588
Removed TodoList from List dependencies.
oskarwrobel Aug 7, 2019
0d448f1
Added tests for TodoList and TodoListUI plugins.
oskarwrobel Aug 7, 2019
d8c6e23
Added TodoListEditing tests and some minor improvements.
oskarwrobel Aug 7, 2019
5687d00
Added more tests.
oskarwrobel Aug 8, 2019
4b3e6fe
Introduced command for toggling checked state on todo list.
oskarwrobel Aug 8, 2019
1dd136b
Minor improvement.
oskarwrobel Aug 8, 2019
7fd081a
Registered TodoListCheckCommand in TodoListEditing.
oskarwrobel Aug 8, 2019
0662dcb
Small refactoring of todo list converters.
oskarwrobel Aug 8, 2019
8d45c1f
Added very basic manual test for todo lists.
oskarwrobel Aug 8, 2019
49c9dbd
Added toggling check state of selected todo list items on keystroke.
oskarwrobel Aug 8, 2019
9ae9e5a
Improved schema validation of todoListChecked attribute.
oskarwrobel Aug 11, 2019
1fcf007
Added v -> m conversion for data pipeline.
oskarwrobel Aug 11, 2019
0ac9f6b
Merge branch 'master' into t/1434
oskarwrobel Aug 11, 2019
526b0f4
Added test for Ctrl+space keystroke handler.
oskarwrobel Aug 11, 2019
8038f00
Added missing API docs.
oskarwrobel Aug 11, 2019
378bf5b
Removed redundant comma.
oskarwrobel Aug 11, 2019
edbb988
Remove todoListChecked attribute in a postfixer instead of converter.
oskarwrobel Aug 11, 2019
5800bee
Change `todoListChecked` state using command when clicking on checkbo…
oskarwrobel Aug 12, 2019
31275b6
Added missing destroy in tests.
oskarwrobel Aug 12, 2019
492a56a
Visual improvements.
oskarwrobel Aug 12, 2019
7020e67
Do not use renderer for getting changed UIElements.
oskarwrobel Aug 12, 2019
a54f67b
Improved CC.
oskarwrobel Aug 13, 2019
69cd4e1
Selection improvements.
oskarwrobel Aug 13, 2019
c48cb3f
Do not convert `todoListChecked` attribute on non-todo list item.
oskarwrobel Aug 13, 2019
e125d83
Added more plugins to todo list manual test.
oskarwrobel Aug 13, 2019
4a792cd
Improved manual test.
oskarwrobel Aug 13, 2019
890cd8a
Docs.
oskarwrobel Aug 13, 2019
ba1beea
Merge branch 'master' into t/1434
oskarwrobel Aug 13, 2019
597c479
Improved tests.
oskarwrobel Aug 13, 2019
42035d4
Changed view post fixers to work properly with dynamic roots.
oskarwrobel Aug 13, 2019
2229454
Added missing dependencies.
oskarwrobel Aug 13, 2019
7b00109
Fixed failing test.
oskarwrobel Aug 13, 2019
6e96eae
Manual test description improvement.
oskarwrobel Aug 13, 2019
5944925
Fixed invalid docs.
oskarwrobel Aug 13, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
"@ckeditor/ckeditor5-editor-classic": "^12.1.3",
"@ckeditor/ckeditor5-enter": "^11.0.4",
"@ckeditor/ckeditor5-heading": "^11.0.4",
"@ckeditor/ckeditor5-highlight": "^11.0.4",
"@ckeditor/ckeditor5-indent": "^10.0.1",
"@ckeditor/ckeditor5-link": "^11.1.1",
"@ckeditor/ckeditor5-table": "^13.0.2",
"@ckeditor/ckeditor5-typing": "^12.1.1",
"@ckeditor/ckeditor5-undo": "^11.0.4",
"eslint": "^5.5.0",
Expand Down
195 changes: 33 additions & 162 deletions src/converters.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
* @module list/converters
*/

import { createViewListItemElement } from './utils';
import {
generateLiInUl,
injectViewList,
mergeViewLists,
getSiblingListItem,
positionAfterUiElements
} from './utils';
import TreeWalker from '@ckeditor/ckeditor5-engine/src/model/treewalker';

/**
Expand Down Expand Up @@ -89,7 +95,11 @@ export function modelViewRemove( model ) {
* A model-to-view converter for the `type` attribute change on the `listItem` model element.
*
* This change means that the `<li>` element 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.
*
* Splitting this conversion into 2 steps makes it possible to add an additional conversion in the middle.
* Check {@link module:list/todolistconverters~modelViewChangeType} to see an example of it.
*
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
Expand All @@ -104,22 +114,37 @@ export function modelViewChangeType( evt, data, conversionApi ) {
const viewItem = conversionApi.mapper.toViewElement( data.item );
const viewWriter = conversionApi.writer;

// 1. Break the container after and before the list item.
// 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.
// 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 viewList = viewItem.parent;
const listName = data.attributeNewValue == 'numbered' ? 'ol' : 'ul';
viewList = viewWriter.rename( listName, viewList );

// 3. Merge the changed view list with other lists, if possible.
viewWriter.rename( listName, viewList );
}

/**
* 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 );

// 4. Consumable insertion of children inside the item. They are already handled by re-building the item in view.
// 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' );
}
Expand Down Expand Up @@ -784,23 +809,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 ) {
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 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.
Expand Down Expand Up @@ -888,134 +896,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 UL or OL elements.
function mergeViewLists( viewWriter, firstList, secondList ) {
if ( firstList && secondList && ( firstList.name == 'ul' || firstList.name == 'ol' ) && firstList.name == secondList.name ) {
return viewWriter.mergeContainers( viewWriter.createPositionAfter( firstList ) );
}

return null;
}

// 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 ) {
Expand Down Expand Up @@ -1112,12 +992,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' ) );
}
4 changes: 2 additions & 2 deletions src/listcommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 7 additions & 6 deletions src/listediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
cleanListItem,
modelViewInsertion,
modelViewChangeType,
modelViewMergeAfterChangeType,
modelViewMergeAfter,
modelViewRemove,
modelViewSplitOnInsert,
Expand Down Expand Up @@ -77,15 +78,12 @@ 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, { priority: 'high' } );
editing.downcastDispatcher.on( 'attribute:listType:listItem', modelViewMergeAfterChangeType, { priority: 'low' } );
editing.downcastDispatcher.on( 'attribute:listIndent:listItem', modelViewChangeIndent( editor.model ) );
data.downcastDispatcher.on( 'attribute:listIndent:listItem', modelViewChangeIndent( editor.model ) );

editing.downcastDispatcher.on( 'remove:listItem', modelViewRemove( editor.model ) );
editing.downcastDispatcher.on( 'remove', modelViewMergeAfter, { priority: 'low' } );
data.downcastDispatcher.on( 'remove:listItem', modelViewRemove( editor.model ) );
data.downcastDispatcher.on( 'remove', modelViewMergeAfter, { priority: 'low' } );

data.upcastDispatcher.on( 'element:ul', cleanList, { priority: 'high' } );
data.upcastDispatcher.on( 'element:ol', cleanList, { priority: 'high' } );
Expand All @@ -103,7 +101,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.
Expand Down Expand Up @@ -172,6 +170,9 @@ export default class ListEditing extends Plugin {
editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( 'outdentList' ) );
}

/**
* @inheritDoc
*/
afterInit() {
const commands = this.editor.commands;

Expand Down
41 changes: 5 additions & 36 deletions src/listui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,42 +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,
isToggleable: 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 );
}
}
Loading