` converters.
for ( const child of viewChildren ) {
// If this is a view list element, we will convert it after last `listItem` model element.
if ( child.name == 'ul' || child.name == 'ol' ) {
nextPosition = conversionApi.convertItem( child, nextPosition ).modelCursor;
}
// If it was not a list view element it was a "regular" list item content. Convert it to a `listItem`.
else {
const result = conversionApi.convertItem( child, ModelPosition.createAt( lastListItem, 'end' ) );
const convertedChild = Array.from( result.modelRange )[ 0 ];
// If there is a block element child being converted it will split the current list item, for example:
//
// Foo
//
// will be converted to:
//
// Foo
//
// so we need to update reference to `lastListItem` and `nextPosition`.
if ( convertedChild.type === 'elementStart' && convertedChild.item.is( 'element' ) ) {
nextPosition = result.modelCursor;
lastListItem = nextPosition.getAncestors().pop();
// Depending on the used converter for block elements, usually the position (marked as #)
// points to the second list item after conversion:
//
// `Foo
` -> `Foo#`
//
// However, in some cases like autoparagraphing the position is placed on the end of the block element:
//
// `Foo
` -> `Foo#`
//
// We need to check fo such cases and use proper list item and position based on it.
if ( !lastListItem.is( 'listItem' ) && lastListItem.nextSibling && lastListItem.nextSibling.is( 'listItem' ) ) {
lastListItem = lastListItem.nextSibling;
nextPosition = ModelPosition.createAt( lastListItem, 'end' );
}
// If list item was split by the block element, the text from splitted list item needs to be moved.
//
// `- Foo
123
Bar `
//
// Is converted to:
//
// FooBar123
//
// So the text which was after splitting element (`` in this case) should be move to second list item:
//
// Foo123Bar
} else if ( convertedChild.type === 'text' && listItemModel !== lastListItem ) {
// 1. Remove text part from the list item positioned before splitting element.
writer.remove( ModelRange.createOn( convertedChild.item ) );
// 2. Insert text part into list item positioned after splitting element.
writer.insert( convertedChild.item, nextPosition );
// 3. Update 'nextPosition' so it points after last list item.
nextPosition = ModelPosition.createAfter( lastListItem );
}
}
}
return nextPosition;
}
// Helper function that seeks for a list item sibling of given model item (or position) 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.indent` - used as reference item when first parameter is a position
// Either `options.sameIndent` or `options.biggerIndent` should be set to `true`.
function getSiblingListItem( modelItemOrPosition, options ) {
const sameIndent = !!options.sameIndent;
const smallerIndent = !!options.smallerIndent;
const indent = modelItemOrPosition instanceof ModelElement ? modelItemOrPosition.getAttribute( 'listIndent' ) : options.listIndent;
let item = modelItemOrPosition instanceof ModelElement ? modelItemOrPosition.previousSibling : modelItemOrPosition.nodeBefore;
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( ViewPosition.createAfter( 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 ) {
const injectedList = injectedItem.parent;
const mapper = conversionApi.mapper;
const viewWriter = conversionApi.writer;
// Position where view list will be inserted.
let insertPosition = mapper.toViewPosition( ModelPosition.createBefore( 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, { sameIndent: true, smallerIndent: true } );
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( ViewPosition.createAfter( 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( ModelPosition.createAt( 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( ModelPosition.createBefore( 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 walker = new ViewTreeWalker( {
boundaries: new ViewRange(
ViewPosition.createAt( prevView, 0 ),
insertPosition
),
ignoreElementEnd: true
} );
for ( const value of walker ) {
if ( value.item.is( 'li' ) ) {
const breakPosition = viewWriter.breakContainer( ViewPosition.createBefore( value.item ) );
const viewList = value.item.parent;
const targetPosition = ViewPosition.createAt( injectedItem, 'end' );
mergeViewLists( viewWriter, targetPosition.nodeBefore, targetPosition.nodeAfter );
viewWriter.move( ViewRange.createOn( 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( ViewPosition.createAfter( lastSubChild ) );
viewWriter.move( ViewRange.createOn( lastSubChild.parent ), ViewPosition.createAt( 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 ) {
// Find correct previous model list item element.
// The element has to have either same or smaller indent than given reference indent.
// This will be the model element which will get nested items (if it has smaller indent) or sibling items (if it has same indent).
// Keep in mind that such element might not be found, if removed item was the first item.
const prevModelItem = getSiblingListItem( modelRemoveStartPosition, {
sameIndent: true,
smallerIndent: true,
listIndent: nextIndent
} );
const mapper = conversionApi.mapper;
const viewWriter = conversionApi.writer;
// Indent of found element or `null` if the element has not been found.
const prevIndent = prevModelItem ? prevModelItem.getAttribute( 'listIndent' ) : null;
let insertPosition;
if ( !prevModelItem ) {
// If element has not been found, simply insert lists at the position where the removed item was:
//
// Lorem ipsum.
// 1 -------- <--- this is removed, no previous list item, put nested items in place of removed item.
// 1.1 -------- <--- this is reference indent.
// 1.1.1 --------
// 1.1.2 --------
// 1.2 --------
//
// Becomes:
//
// Lorem ipsum.
// 1.1 --------
// 1.1.1 --------
// 1.1.2 --------
// 1.2 --------
insertPosition = viewRemoveStartPosition;
} else if ( prevIndent == nextIndent ) {
// If element has been found and has same indent as reference indent it means that nested items should
// become siblings of found element:
//
// 1 --------
// 1.1 --------
// 1.2 -------- <--- this is `prevModelItem`.
// 2 -------- <--- this is removed, previous list item has indent same as reference indent.
// 2.1 -------- <--- this is reference indent, this and 2.2 should become siblings of 1.2.
// 2.2 --------
//
// Becomes:
//
// 1 --------
// 1.1 --------
// 1.2 --------
// 2.1 --------
// 2.2 --------
const prevViewList = mapper.toViewElement( prevModelItem ).parent;
insertPosition = ViewPosition.createAfter( prevViewList );
} else {
// If element has been found and has smaller indent as reference indent it means that nested items
// should become nested items of found item:
//
// 1 -------- <--- this is `prevModelItem`.
// 1.1 -------- <--- this is removed, previous list item has indent smaller than reference indent.
// 1.1.1 -------- <--- this is reference indent, this and 1.1.1 should become nested items of 1.
// 1.1.2 --------
// 1.2 --------
//
// Becomes:
//
// 1 --------
// 1.1.1 --------
// 1.1.2 --------
// 1.2 --------
//
// Note: in this case 1.1.1 have indent 2 while 1 have indent 0. In model that should not be possible,
// because following item may have indent bigger only by one. But this is fixed by postfixer.
const modelPosition = ModelPosition.createAt( prevModelItem, 'end' );
insertPosition = mapper.toViewPosition( modelPosition );
}
insertPosition = positionAfterUiElements( insertPosition );
// Handle multiple lists. This happens if list item has nested numbered and bulleted lists. Following lists
// are inserted after the first list (no need to recalculate insertion position for them).
for ( const child of [ ...viewRemovedItem.getChildren() ] ) {
if ( child.is( 'ul' ) || child.is( 'ol' ) ) {
insertPosition = viewWriter.move( ViewRange.createOn( child ), insertPosition ).end;
mergeViewLists( viewWriter, child, child.nextSibling );
mergeViewLists( viewWriter, child.previousSibling, child );
}
}
}
// 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:
// foo^bar
// For position ^, a position before "bar" will be returned.
function positionAfterUiElements( viewPosition ) {
return viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
}