|
|
@@ -11,10 +11,12 @@ import ViewListItemElement from './viewlistitemelement';
|
|
|
|
|
|
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
|
|
|
import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
|
|
|
+import modelWriter from '@ckeditor/ckeditor5-engine/src/model/writer';
|
|
|
|
|
|
import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
|
|
|
import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
|
|
|
import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
|
|
|
+import ViewTreeWalker from '@ckeditor/ckeditor5-engine/src/view/treewalker';
|
|
|
import viewWriter from '@ckeditor/ckeditor5-engine/src/view/writer';
|
|
|
|
|
|
/**
|
|
|
@@ -96,16 +98,32 @@ export function modelViewRemove( evt, data, consumable, conversionApi ) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const viewItem = conversionApi.mapper.toViewElement( data.item );
|
|
|
+ const viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
|
|
|
+ const viewItem = viewPosition.nodeAfter.is( 'li' ) ? viewPosition.nodeAfter : viewPosition.nodeAfter.getChild( 0 );
|
|
|
|
|
|
// 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( ViewPosition.createBefore( viewItem ) );
|
|
|
viewWriter.breakContainer( ViewPosition.createAfter( viewItem ) );
|
|
|
|
|
|
- // 2. Remove the UL that contains just the removed LI.
|
|
|
+ // 2. Remove the UL that contains just the removed <li>.
|
|
|
const viewList = viewItem.parent;
|
|
|
- viewWriter.remove( ViewRange.createOn( viewList ) );
|
|
|
+ const viewListPrev = viewList.previousSibling;
|
|
|
+ const removeRange = ViewRange.createOn( viewList );
|
|
|
+ viewWriter.remove( removeRange );
|
|
|
+
|
|
|
+ if ( viewListPrev && viewListPrev.nextSibling ) {
|
|
|
+ mergeViewLists( viewListPrev, viewListPrev.nextSibling );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. Bring back nested list that was in the removed <li>.
|
|
|
+ hoistNestedLists( data.item.getAttribute( 'indent' ) + 1, data.sourcePosition, removeRange.start, viewItem, conversionApi.mapper );
|
|
|
+
|
|
|
+ // Unbind this element only if it was moved to graveyard.
|
|
|
+ // See #847.
|
|
|
+ if ( data.item.root.rootName == '$graveyard' ) {
|
|
|
+ conversionApi.mapper.unbindModelElement( data.item );
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -118,7 +136,6 @@ export function modelViewRemove( evt, data, consumable, conversionApi ) {
|
|
|
* @param {Object} conversionApi Conversion interface.
|
|
|
*/
|
|
|
export function modelViewChangeIndent( evt, data, consumable, conversionApi ) {
|
|
|
- /* istanbul ignore if */ // Part of code connected with indenting that is not yet complete.
|
|
|
if ( !consumable.consume( data.item, 'changeAttribute:indent' ) ) {
|
|
|
return;
|
|
|
}
|
|
|
@@ -133,14 +150,25 @@ export function modelViewChangeIndent( evt, data, consumable, conversionApi ) {
|
|
|
// 2. Extract view list with changed view list item and merge "hole" possibly created by breaking and removing elements.
|
|
|
const viewList = viewItem.parent;
|
|
|
const viewListPrev = viewList.previousSibling;
|
|
|
+ const removeRange = ViewRange.createOn( viewList );
|
|
|
+ viewWriter.remove( removeRange );
|
|
|
|
|
|
- viewWriter.remove( ViewRange.createOn( viewList ) );
|
|
|
+ // TODO: get rid of `removePosition` when conversion is done on `changesDone`.
|
|
|
+ let removePosition;
|
|
|
|
|
|
- // If there is no `viewListPrev` it means that the first item was indented which is an error.
|
|
|
- mergeViewLists( viewListPrev, viewListPrev.nextSibling );
|
|
|
+ if ( viewListPrev && viewListPrev.nextSibling ) {
|
|
|
+ removePosition = mergeViewLists( viewListPrev, viewListPrev.nextSibling );
|
|
|
+ }
|
|
|
|
|
|
- // 3. Inject view list like it is newly inserted.
|
|
|
- injectViewList( data.item, viewItem, conversionApi.mapper );
|
|
|
+ if ( !removePosition ) {
|
|
|
+ removePosition = removeRange.start;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. Bring back nested list that was in the removed <li>.
|
|
|
+ hoistNestedLists( data.attributeOldValue + 1, data.range.start, removeRange.start, viewItem, conversionApi.mapper );
|
|
|
+
|
|
|
+ // 4. Inject view list like it is newly inserted.
|
|
|
+ injectViewList( data.item, viewItem, conversionApi.mapper, removePosition );
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -172,17 +200,96 @@ export function modelViewSplitOnInsert( evt, data, consumable, conversionApi ) {
|
|
|
if ( data.item.name != 'listItem' ) {
|
|
|
let viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
|
|
|
|
|
|
+ const lists = [];
|
|
|
+
|
|
|
// Break multiple ULs/OLs if there are.
|
|
|
+ //
|
|
|
+ // Imagine following list:
|
|
|
+ //
|
|
|
+ // 1 --------
|
|
|
+ // 1.1 --------
|
|
|
+ // 1.1.1 --------
|
|
|
+ // 1.1.2 --------
|
|
|
+ // 1.1.3 --------
|
|
|
+ // 1.1.3.1 --------
|
|
|
+ // 1.2 --------
|
|
|
+ // 1.2.1 --------
|
|
|
+ // 2 --------
|
|
|
+ //
|
|
|
+ // Insert paragraph after item 1.1.1:
|
|
|
+ //
|
|
|
+ // 1 --------
|
|
|
+ // 1.1 --------
|
|
|
+ // 1.1.1 --------
|
|
|
+ //
|
|
|
+ // Lorem ipsum.
|
|
|
+ //
|
|
|
+ // 1.1.2 --------
|
|
|
+ // 1.1.3 --------
|
|
|
+ // 1.1.3.1 --------
|
|
|
+ // 1.2 --------
|
|
|
+ // 1.2.1 --------
|
|
|
+ // 2 --------
|
|
|
+ //
|
|
|
+ // In this case 1.1.2 has to become beginning of a new list.
|
|
|
+ // We need to break list before 1.1.2 (obvious), then we need to break list also before 1.2.
|
|
|
+ // Then we need to move those broken pieces one after another and merge:
|
|
|
+ //
|
|
|
+ // 1 --------
|
|
|
+ // 1.1 --------
|
|
|
+ // 1.1.1 --------
|
|
|
+ //
|
|
|
+ // Lorem ipsum.
|
|
|
+ //
|
|
|
+ // 1.1.2 --------
|
|
|
+ // 1.1.3 --------
|
|
|
+ // 1.1.3.1 --------
|
|
|
+ // 1.2 --------
|
|
|
+ // 1.2.1 --------
|
|
|
+ // 2 --------
|
|
|
+ //
|
|
|
while ( viewPosition.parent.name == 'ul' || viewPosition.parent.name == 'ol' ) {
|
|
|
viewPosition = viewWriter.breakContainer( viewPosition );
|
|
|
|
|
|
- /* istanbul ignore else */ // Part of code connected with indenting that is not yet complete.
|
|
|
- if ( viewPosition.parent.parent === null ) {
|
|
|
+ if ( viewPosition.parent.name != 'li' ) {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
- /* istanbul ignore next */ // Part of code connected with indenting that is not yet complete.
|
|
|
- viewPosition = ViewPosition.createBefore( viewPosition.parent );
|
|
|
+ // Remove lists that are after inserted element.
|
|
|
+ // They will be brought back later, below the inserted element.
|
|
|
+ const removeStart = viewPosition;
|
|
|
+ const removeEnd = ViewPosition.createAt( viewPosition.parent, 'end' );
|
|
|
+
|
|
|
+ // Don't remove if there is nothing to remove.
|
|
|
+ if ( !removeStart.isEqual( removeEnd ) ) {
|
|
|
+ const removed = viewWriter.remove( new ViewRange( removeStart, removeEnd ) );
|
|
|
+ lists.push( removed );
|
|
|
+ }
|
|
|
+
|
|
|
+ viewPosition = ViewPosition.createAfter( viewPosition.parent );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Bring back removed lists.
|
|
|
+ if ( lists.length > 0 ) {
|
|
|
+ for ( let i = 0; i < lists.length; i++ ) {
|
|
|
+ const previousList = viewPosition.nodeBefore;
|
|
|
+ const insertedRange = viewWriter.insert( viewPosition, lists[ i ] );
|
|
|
+ viewPosition = insertedRange.end;
|
|
|
+
|
|
|
+ // Don't merge first list! We want a split in that place (this is why this converter is introduced).
|
|
|
+ if ( i > 0 ) {
|
|
|
+ let mergePos = mergeViewLists( previousList, previousList.nextSibling );
|
|
|
+
|
|
|
+ // If `mergePos` is in `previousList` it means that the lists got merged.
|
|
|
+ // In this case, we need to fix insert position.
|
|
|
+ if ( mergePos && mergePos.parent == previousList ) {
|
|
|
+ viewPosition.offset--;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Merge last inserted list with element after it.
|
|
|
+ mergeViewLists( viewPosition.nodeBefore, viewPosition.nodeAfter );
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -212,14 +319,16 @@ export function modelViewSplitOnInsert( evt, data, consumable, conversionApi ) {
|
|
|
* @param {Object} conversionApi Conversion interface.
|
|
|
*/
|
|
|
export function modelViewMergeAfter( evt, data, consumable, conversionApi ) {
|
|
|
- const viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
|
|
|
- const viewItemPrev = viewPosition.nodeBefore;
|
|
|
- const viewItemNext = viewPosition.nodeAfter;
|
|
|
-
|
|
|
- // Merge lists if something (remove, move) was done from inside of list.
|
|
|
- // Merging will be done only if both items are view lists of the same type.
|
|
|
- // The check is done inside the helper function.
|
|
|
- mergeViewLists( viewItemPrev, viewItemNext );
|
|
|
+ if ( !data.item.is( 'listItem' ) ) {
|
|
|
+ const viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
|
|
|
+ const viewItemPrev = viewPosition.nodeBefore;
|
|
|
+ const viewItemNext = viewPosition.nodeAfter;
|
|
|
+
|
|
|
+ // Merge lists if something (remove, move) was done from inside of list.
|
|
|
+ // Merging will be done only if both items are view lists of the same type.
|
|
|
+ // The check is done inside the helper function.
|
|
|
+ mergeViewLists( viewItemPrev, viewItemNext );
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -250,7 +359,7 @@ export function viewModelConverter( evt, data, consumable, conversionApi ) {
|
|
|
// 3. Handle `<li>` children.
|
|
|
data.context.push( listItem );
|
|
|
|
|
|
- // `listItem`s created recursievly should have bigger indent.
|
|
|
+ // `listItem`s created recursively should have bigger indent.
|
|
|
data.indent++;
|
|
|
|
|
|
// `listItem`s will be kept in flat structure.
|
|
|
@@ -270,15 +379,14 @@ export function viewModelConverter( evt, data, consumable, conversionApi ) {
|
|
|
}
|
|
|
// If it was not a list it was a "regular" list item content. Just append it to `listItem`.
|
|
|
else {
|
|
|
- listItem.appendChildren( converted );
|
|
|
+ modelWriter.insert( ModelPosition.createAt( listItem, 'end' ), converted );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
data.indent--;
|
|
|
data.context.pop();
|
|
|
|
|
|
- /* istanbul ignore next */ // Part of code connected with indenting that is not yet complete.
|
|
|
- data.output = data.output ? data.output.concat( items ) : items;
|
|
|
+ data.output = items;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -298,7 +406,7 @@ export function cleanList( evt, data, consumable ) {
|
|
|
const children = Array.from( data.input.getChildren() );
|
|
|
|
|
|
for ( let child of children ) {
|
|
|
- if ( !child.name || child.name != 'li' ) {
|
|
|
+ if ( !child.is( 'li' ) ) {
|
|
|
child.remove();
|
|
|
}
|
|
|
}
|
|
|
@@ -306,6 +414,50 @@ export function cleanList( evt, data, consumable ) {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * View to model converter for `<li>, that cleans white space formatting from the input view.
|
|
|
+ *
|
|
|
+ * @see module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
|
|
|
+ * @param {module:utils/eventinfo~EventInfo} evt Object containing information about the fired event.
|
|
|
+ * @param {Object} data Object containing conversion input and a placeholder for conversion output and possibly other values.
|
|
|
+ * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
|
|
|
+ */
|
|
|
+export function cleanListItem( evt, data, consumable ) {
|
|
|
+ if ( consumable.test( data.input, { name: true } ) ) {
|
|
|
+ if ( data.input.childCount === 0 ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const children = [ ...data.input.getChildren() ];
|
|
|
+
|
|
|
+ let foundList = false;
|
|
|
+ let firstNode = true;
|
|
|
+
|
|
|
+ for ( let child of children ) {
|
|
|
+ if ( foundList && !child.is( 'ul' ) && !child.is( 'ol' ) ) {
|
|
|
+ child.remove();
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( child.is( 'text' ) ) {
|
|
|
+ // If this is the first node and it's a text node, left-trim it.
|
|
|
+ if ( firstNode ) {
|
|
|
+ child.data = child.data.replace( /^\s+/, '' );
|
|
|
+ }
|
|
|
+
|
|
|
+ // If this is the last text node before <ul> or <ol>, right-trim it.
|
|
|
+ if ( !child.nextSibling || ( child.nextSibling.is( 'ul' ) || child.nextSibling.is( 'ol' ) ) ) {
|
|
|
+ child.data = child.data.replace( /\s+$/, '' );
|
|
|
+ }
|
|
|
+ } else if ( child.is( 'ul' ) || child.is( 'ol' ) ) {
|
|
|
+ // If this is a <ul> or <ol>, do not process it, just mark that we already visited list element.
|
|
|
+ foundList = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ firstNode = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
* Callback for model position to view position mapping for {@link module:engine/conversion/mapper~Mapper}. The callback fixes positions
|
|
|
* between `listItem` elements, that would be incorrectly mapped because of how list items are represented in model
|
|
|
* and view.
|
|
|
@@ -315,10 +467,26 @@ export function cleanList( evt, data, consumable ) {
|
|
|
* @param {Object} data Object containing additional data and placeholder for mapping result.
|
|
|
*/
|
|
|
export function modelToViewPosition( evt, data ) {
|
|
|
- if ( data.viewPosition.parent.name == 'li' && data.modelPosition.parent.name != 'listItem' ) {
|
|
|
- data.viewPosition = ViewPosition.createBefore( data.viewPosition.parent );
|
|
|
+ const modelItem = data.modelPosition.nodeBefore;
|
|
|
|
|
|
- evt.stop();
|
|
|
+ if ( modelItem && modelItem.is( 'listItem' ) ) {
|
|
|
+ const viewItem = data.mapper.toViewElement( modelItem );
|
|
|
+ const topmostViewList = viewItem.getAncestors().find( ( element ) => element.is( 'ul' ) || element.is( 'ol' ) );
|
|
|
+ const walker = new ViewTreeWalker( {
|
|
|
+ startPosition: ViewPosition.createAt( viewItem, 0 )
|
|
|
+ } );
|
|
|
+
|
|
|
+ for ( let value of walker ) {
|
|
|
+ if ( value.type == 'elementStart' && value.item.is( 'li' ) ) {
|
|
|
+ data.viewPosition = value.previousPosition;
|
|
|
+
|
|
|
+ break;
|
|
|
+ } else if ( value.type == 'elementEnd' && value.item == topmostViewList ) {
|
|
|
+ data.viewPosition = value.nextPosition;
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -332,63 +500,202 @@ export function modelToViewPosition( evt, data ) {
|
|
|
* @param {Object} data Object containing additional data and placeholder for mapping result.
|
|
|
*/
|
|
|
export function viewToModelPosition( evt, data ) {
|
|
|
- const viewPosition = data.viewPosition;
|
|
|
+ const viewPos = data.viewPosition;
|
|
|
+ const viewParent = viewPos.parent;
|
|
|
const mapper = data.mapper;
|
|
|
- const nodeAfter = viewPosition.nodeAfter;
|
|
|
- const nodeBefore = viewPosition.nodeBefore;
|
|
|
-
|
|
|
- let modelNode;
|
|
|
-
|
|
|
- if ( nodeAfter ) {
|
|
|
- if ( nodeAfter.name == 'ul' || nodeAfter.name == 'ol' ) {
|
|
|
- // If the position is before view list, model position should be placed before `listItem`
|
|
|
- // that is bound to the first `<li>` of that view list.
|
|
|
- // Default algorithm would work like this but only for top-level list.
|
|
|
- modelNode = mapper.toModelElement( nodeAfter.getChild( 0 ) );
|
|
|
- } else if ( nodeAfter.name == 'li' ) {
|
|
|
- // If the position is before view list item, just place model position before bound `listItem` element.
|
|
|
- modelNode = mapper.toModelElement( nodeAfter );
|
|
|
- }
|
|
|
|
|
|
- if ( modelNode ) {
|
|
|
+ if ( viewParent.name == 'ul' || viewParent.name == 'ol' ) {
|
|
|
+ // Position is directly in <ul> or <ol>.
|
|
|
+ if ( !viewPos.isAtEnd ) {
|
|
|
+ // If position is not at the end, it must be before <li>.
|
|
|
+ // Get that <li>, map it to `listItem` and set model position before that `listItem`.
|
|
|
+ const modelNode = mapper.toModelElement( viewPos.nodeAfter );
|
|
|
+
|
|
|
data.modelPosition = ModelPosition.createBefore( modelNode );
|
|
|
+ } else {
|
|
|
+ // Position is at the end of <ul> or <ol>, so there is no <li> after it to be mapped.
|
|
|
+ // There is <li> before the position, but we cannot just map it to `listItem` and set model position after it,
|
|
|
+ // because that <li> may contain nested items.
|
|
|
+ // We will check "model length" of that <li>, in other words - how many `listItem`s are in that <li>.
|
|
|
+ const modelNode = mapper.toModelElement( viewPos.nodeBefore );
|
|
|
+ const modelLength = mapper.getModelLength( viewPos.nodeBefore );
|
|
|
+
|
|
|
+ // Then we get model position before mapped `listItem` and shift it accordingly.
|
|
|
+ data.modelPosition = ModelPosition.createBefore( modelNode ).getShiftedBy( modelLength );
|
|
|
}
|
|
|
- } else if ( nodeBefore ) {
|
|
|
- let viewNode;
|
|
|
-
|
|
|
- // Find `<li>` after which we want to place position.
|
|
|
- // We want to find a `<li>` that will be mapped to model `listItem` element. That `listItem` will
|
|
|
- // be used as a reference point to evaluate model position.
|
|
|
- /* istanbul ignore if */ // Part of code connected with indenting that is not yet complete.
|
|
|
- if ( nodeBefore.name == 'ul' || nodeBefore.name == 'ol' ) {
|
|
|
- // If the position is before view list, take the last `<li>` of that view list.
|
|
|
- viewNode = nodeBefore.getChild( nodeBefore.childCount - 1 );
|
|
|
- } else if ( nodeBefore.name == 'li' ) {
|
|
|
- // If the position is before view list item, take that `<li>`.
|
|
|
- viewNode = nodeBefore;
|
|
|
+
|
|
|
+ evt.stop();
|
|
|
+ } else if ( viewParent.name == 'li' && viewPos.nodeBefore && ( viewPos.nodeBefore.name == 'ul' || viewPos.nodeBefore.name == 'ol' ) ) {
|
|
|
+ // In most cases when view position is in <li> it is in text and this is a correct position.
|
|
|
+ // However, if position is after <ul> or <ol> we have to fix it -- because in model <ul>/<ol> are not in the `listItem`.
|
|
|
+ const modelNode = mapper.toModelElement( viewParent );
|
|
|
+
|
|
|
+ // Check all <ul>s and <ol>s that are in the <li> but before mapped position.
|
|
|
+ // Get model length of those elements and then add it to the offset of `listItem` mapped to the original <li>.
|
|
|
+ let modelLength = 1; // Starts from 1 because the original <li> has to be counted in too.
|
|
|
+ let viewList = viewPos.nodeBefore;
|
|
|
+
|
|
|
+ while ( viewList && ( viewList.is( 'ul' ) || viewList.is( 'ol' ) ) ) {
|
|
|
+ modelLength += mapper.getModelLength( viewList );
|
|
|
+
|
|
|
+ viewList = viewList.previousSibling;
|
|
|
}
|
|
|
|
|
|
- // Evaluate correct model position.
|
|
|
- // At this stage we have a `<li>`. This `<li>` may have nested `<li>`s inside. We will use `mapper`
|
|
|
- // to obtain this `<li>`'s model length. Placing model position after that `<li>` will be done
|
|
|
- // by placing it before the bound `listItem` and moving by offset equal to `<li>`s length.
|
|
|
- if ( viewNode ) {
|
|
|
- modelNode = mapper.toModelElement( viewNode );
|
|
|
- const offset = mapper.getModelLength( viewNode );
|
|
|
+ data.modelPosition = ModelPosition.createBefore( modelNode ).getShiftedBy( modelLength );
|
|
|
+
|
|
|
+ evt.stop();
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- data.modelPosition = ModelPosition.createBefore( modelNode ).getShiftedBy( offset );
|
|
|
+/**
|
|
|
+ * Post fixer that reacts to changes on document and fixed incorrect model states.
|
|
|
+ *
|
|
|
+ * Example:
|
|
|
+ *
|
|
|
+ * <listItem type="bulleted" indent=0>Item 1</listItem>
|
|
|
+ * <listItem type="bulleted" indent=1>Item 2</listItem> <--- this is removed.
|
|
|
+ * <listItem type="bulleted" indent=2>Item 3</listItem>
|
|
|
+ *
|
|
|
+ * Should become:
|
|
|
+ *
|
|
|
+ * <listItem type="bulleted" indent=0>Item 1</listItem>
|
|
|
+ * <listItem type="bulleted" indent=1>Item 3</listItem> <--- note that indent got postfixed.
|
|
|
+ *
|
|
|
+ * @param {module:engine/model/document~Document} document Document to observe.
|
|
|
+ * @returns {Function} Callback to be attached to {@link module:engine/model/document~Document#event:change document change event}.
|
|
|
+ */
|
|
|
+export function modelChangePostFixer( document ) {
|
|
|
+ return ( evt, type, changes, batch ) => {
|
|
|
+ if ( type == 'remove' ) {
|
|
|
+ // Fix list items after the cut-out range.
|
|
|
+ // This fix is needed if items in model after cut-out range have now wrong indents compared to their previous siblings.
|
|
|
+ _fixItemsIndent( changes.sourcePosition, document, batch );
|
|
|
+ // This fix is needed if two different nested lists got merged, change types of list items "below".
|
|
|
+ _fixItemsType( changes.sourcePosition, false, document, batch );
|
|
|
+ } else if ( type == 'move' ) {
|
|
|
+ // Fix list items after the cut-out range.
|
|
|
+ // This fix is needed if items in model after cut-out range have now wrong indents compared to their previous siblings.
|
|
|
+ _fixItemsIndent( changes.sourcePosition, document, batch );
|
|
|
+ // This fix is needed if two different nested lists got merged, change types of list items "below".
|
|
|
+ _fixItemsType( changes.sourcePosition, false, document, batch );
|
|
|
+
|
|
|
+ // Fix items in moved range.
|
|
|
+ // This fix is needed if inserted items are too deeply intended.
|
|
|
+ _fixItemsIndent( changes.range.start, document, batch );
|
|
|
+ // This fix is needed if one or more first inserted items have different type.
|
|
|
+ _fixItemsType( changes.range.start, false, document, batch );
|
|
|
+
|
|
|
+ // Fix list items after inserted range.
|
|
|
+ // This fix is needed if items in model after inserted range have wrong indents.
|
|
|
+ _fixItemsIndent( changes.range.end, document, batch );
|
|
|
+ // This fix is needed if one or more last inserted items have different type.
|
|
|
+ _fixItemsType( changes.range.end, true, document, batch );
|
|
|
+ } else if ( type == 'rename' && changes.oldName == 'listItem' && changes.newName != 'listItem' ) {
|
|
|
+ const element = changes.element;
|
|
|
+
|
|
|
+ // Element name is changed from list to something else. Remove useless attributes.
|
|
|
+ document.enqueueChanges( () => {
|
|
|
+ batch.removeAttribute( element, 'indent' ).removeAttribute( element, 'type' );
|
|
|
+ } );
|
|
|
+
|
|
|
+ const changePos = ModelPosition.createAfter( changes.element );
|
|
|
+
|
|
|
+ // Fix list items after the renamed element.
|
|
|
+ // This fix is needed if there are items after renamed element, those items should start from indent = 0.
|
|
|
+ _fixItemsIndent( changePos, document, batch );
|
|
|
+ } else if ( type == 'insert' ) {
|
|
|
+ // Fix list items in inserted range.
|
|
|
+ // This fix is needed if inserted items are too deeply intended.
|
|
|
+ _fixItemsIndent( changes.range.start, document, batch );
|
|
|
+ // This fix is needed if one or more first inserted items have different type.
|
|
|
+ _fixItemsType( changes.range.start, false, document, batch );
|
|
|
+
|
|
|
+ // Fix list items after inserted range.
|
|
|
+ // This fix is needed if items in model after inserted range have wrong indents.
|
|
|
+ _fixItemsIndent( changes.range.end, document, batch );
|
|
|
+ // This fix is needed if one or more last inserted items have different type.
|
|
|
+ _fixItemsType( changes.range.end, true, document, batch );
|
|
|
+ }
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// Helper function for post fixer callback. Performs fixing of model `listElement` items indent attribute. Checks the model at the
|
|
|
+// `changePosition`. Looks at the node before position where change occurred and uses that node as a reference for following list items.
|
|
|
+function _fixItemsIndent( changePosition, document, batch ) {
|
|
|
+ const prevItem = changePosition.nodeBefore;
|
|
|
+ let nextItem = changePosition.nodeAfter;
|
|
|
+
|
|
|
+ if ( nextItem && nextItem.name == 'listItem' ) {
|
|
|
+ // This is the maximum indent that following model list item may have.
|
|
|
+ const maxIndent = prevItem && prevItem.is( 'listItem' ) ? prevItem.getAttribute( 'indent' ) + 1 : 0;
|
|
|
+
|
|
|
+ // Check how much the next item needs to be outdented.
|
|
|
+ let outdentBy = nextItem.getAttribute( 'indent' ) - maxIndent;
|
|
|
+ const items = [];
|
|
|
+
|
|
|
+ while ( nextItem && nextItem.name == 'listItem' && nextItem.getAttribute( 'indent' ) > maxIndent ) {
|
|
|
+ if ( outdentBy > nextItem.getAttribute( 'indent' ) ) {
|
|
|
+ outdentBy = nextItem.getAttribute( 'indent' );
|
|
|
+ }
|
|
|
+
|
|
|
+ const newIndent = nextItem.getAttribute( 'indent' ) - outdentBy;
|
|
|
+
|
|
|
+ items.push( { item: nextItem, indent: newIndent } );
|
|
|
+
|
|
|
+ nextItem = nextItem.nextSibling;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( items.length > 0 ) {
|
|
|
+ document.enqueueChanges( () => {
|
|
|
+ // Since we are outdenting list items, it is safer to start from the last one (it will maintain correct model state).
|
|
|
+ for ( let item of items.reverse() ) {
|
|
|
+ batch.setAttribute( item.item, 'indent', item.indent );
|
|
|
+ }
|
|
|
+ } );
|
|
|
}
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- // If we found a model position, stop the event.
|
|
|
- if ( data.modelPosition !== null ) {
|
|
|
- evt.stop();
|
|
|
+// Helper function for post fixer callback. Performs fixing of model nested `listElement` items type attribute.
|
|
|
+// Checks the model at the `changePosition`. Looks at nodes after/before that position and changes those items type
|
|
|
+// to the same as node before/after `changePosition`.
|
|
|
+function _fixItemsType( changePosition, fixPrevious, document, batch ) {
|
|
|
+ let item = changePosition[ fixPrevious ? 'nodeBefore' : 'nodeAfter' ];
|
|
|
+
|
|
|
+ if ( !item ) {
|
|
|
+ // May happen if last item got removed.
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const refItem = getSiblingListItem( item, { checkAllSiblings: true, getNext: fixPrevious, sameIndent: true } );
|
|
|
+
|
|
|
+ if ( !refItem ) {
|
|
|
+ // May happen if first list item is inserted.
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const refIndent = refItem.getAttribute( 'indent' );
|
|
|
+ const refType = refItem.getAttribute( 'type' );
|
|
|
+
|
|
|
+ if ( refIndent === 0 ) {
|
|
|
+ // Happens if changes are done on top level lists.
|
|
|
+ return;
|
|
|
}
|
|
|
+
|
|
|
+ document.enqueueChanges( () => {
|
|
|
+ while ( item && item.is( 'listItem' ) && item.getAttribute( 'indent' ) >= refIndent ) {
|
|
|
+ if ( item.getAttribute( 'type' ) != refType && item.getAttribute( 'indent' ) == refIndent ) {
|
|
|
+ batch.setAttribute( item, 'type', refType );
|
|
|
+ }
|
|
|
+
|
|
|
+ item = item[ fixPrevious ? 'previousSibling' : 'nextSibling' ];
|
|
|
+ }
|
|
|
+ } );
|
|
|
}
|
|
|
|
|
|
-// Helper function that creates a `<ul><li></li></ul>` 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).
|
|
|
+// 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, mapper ) {
|
|
|
const listType = modelItem.getAttribute( 'type' ) == 'numbered' ? 'ol' : 'ul';
|
|
|
const viewItem = new ViewListItemElement();
|
|
|
@@ -401,29 +708,47 @@ function generateLiInUl( modelItem, mapper ) {
|
|
|
return viewItem;
|
|
|
}
|
|
|
|
|
|
-// Helper function that seeks for a sibling of given `modelItem` that is a `listItem` element and meets given criteria.
|
|
|
+// 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.getNext` - whether next or previous siblings should be checked (default = previous)
|
|
|
// `options.checkAllSiblings` - whether all siblings or just the first one should be checked (default = only one),
|
|
|
// `options.sameIndent` - whether sought sibling should have same indent (default = no),
|
|
|
// `options.biggerIndent` - whether sought sibling should have bigger indent (default = no).
|
|
|
+// `options.smallerIndent` - whether sought sibling should have smaller indent (default = no).
|
|
|
+// `options.isMapped` - whether sought sibling must be mapped to view (default = no).
|
|
|
+// `options.mapper` - used to map model elements when `isMapped` option is set to true.
|
|
|
+// `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( modelItem, options ) {
|
|
|
+function getSiblingListItem( modelItemOrPosition, options ) {
|
|
|
const direction = options.getNext ? 'nextSibling' : 'previousSibling';
|
|
|
+ const posDirection = options.getNext ? 'nodeAfter' : 'nodeBefore';
|
|
|
const checkAllSiblings = !!options.checkAllSiblings;
|
|
|
const sameIndent = !!options.sameIndent;
|
|
|
const biggerIndent = !!options.biggerIndent;
|
|
|
+ const smallerIndent = !!options.smallerIndent;
|
|
|
+ const isMapped = !!options.isMapped;
|
|
|
|
|
|
- const indent = modelItem.getAttribute( 'indent' );
|
|
|
-
|
|
|
- let item = modelItem[ direction ];
|
|
|
+ const indent = modelItemOrPosition instanceof ModelElement ? modelItemOrPosition.getAttribute( 'indent' ) : options.indent;
|
|
|
+ let item = modelItemOrPosition instanceof ModelElement ? modelItemOrPosition[ direction ] : modelItemOrPosition[ posDirection ];
|
|
|
|
|
|
while ( item && item.name == 'listItem' ) {
|
|
|
let itemIndent = item.getAttribute( 'indent' );
|
|
|
|
|
|
- if ( sameIndent && indent == itemIndent || biggerIndent && indent < itemIndent ) {
|
|
|
- return item;
|
|
|
- } else if ( !checkAllSiblings || indent > itemIndent ) {
|
|
|
+ if (
|
|
|
+ ( sameIndent && indent == itemIndent ) ||
|
|
|
+ ( biggerIndent && indent < itemIndent ) ||
|
|
|
+ ( smallerIndent && indent > itemIndent )
|
|
|
+ ) {
|
|
|
+ if ( !isMapped || options.mapper.toViewElement( item ) ) {
|
|
|
+ return item;
|
|
|
+ } else {
|
|
|
+ item = item[ direction ];
|
|
|
+
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( !checkAllSiblings ) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
@@ -437,65 +762,203 @@ function getSiblingListItem( modelItem, options ) {
|
|
|
// The merge happen only if both parameters are UL or OL elements.
|
|
|
function mergeViewLists( firstList, secondList ) {
|
|
|
if ( firstList && secondList && ( firstList.name == 'ul' || firstList.name == 'ol' ) && firstList.name == secondList.name ) {
|
|
|
- viewWriter.mergeContainers( ViewPosition.createAfter( firstList ) );
|
|
|
+ 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, mapper ) {
|
|
|
+function injectViewList( modelItem, injectedItem, mapper, removePosition ) {
|
|
|
const injectedList = injectedItem.parent;
|
|
|
|
|
|
- // 1. Break after previous `listItem` if it has same or bigger indent.
|
|
|
- const prevModelItem = getSiblingListItem( modelItem, { sameIndent: true, biggerIndent: true } );
|
|
|
+ // Position where view list will be inserted.
|
|
|
+ let insertPosition;
|
|
|
|
|
|
- if ( prevModelItem ) {
|
|
|
- let viewItem = mapper.toViewElement( prevModelItem );
|
|
|
- let viewPosition = ViewPosition.createAfter( viewItem );
|
|
|
- viewWriter.breakContainer( viewPosition );
|
|
|
- }
|
|
|
+ // 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" if injected model item.
|
|
|
+ // If there is no such list item, it means that injected list item is the first item in "its list".
|
|
|
+ let prevItem = getSiblingListItem( modelItem, { sameIndent: true, smallerIndent: true, checkAllSiblings: true } );
|
|
|
|
|
|
- // 2. Break after closest previous `listItem` sibling with same indent.
|
|
|
- const sameIndentModelItem = getSiblingListItem( modelItem, { sameIndent: true, checkAllSiblings: true } );
|
|
|
- // Position between broken lists will be a place where new list is inserted.
|
|
|
- // If there is nothing to break (`sameIndentModelItem` is falsy) it means that converted list item
|
|
|
- // is (will be) the first list item.
|
|
|
- let insertionPosition;
|
|
|
-
|
|
|
- if ( sameIndentModelItem ) {
|
|
|
- let viewItem = mapper.toViewElement( sameIndentModelItem );
|
|
|
- let viewPosition = ViewPosition.createAfter( viewItem );
|
|
|
- insertionPosition = viewWriter.breakContainer( viewPosition );
|
|
|
+ if ( prevItem && prevItem.getAttribute( 'indent' ) == modelItem.getAttribute( 'indent' ) ) {
|
|
|
+ // 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.
|
|
|
+ let viewItem = mapper.toViewElement( prevItem );
|
|
|
+ insertPosition = viewWriter.breakContainer( ViewPosition.createAfter( viewItem ) );
|
|
|
} else {
|
|
|
- // If there is a list item before converted list item, it means that that list item has lower indent.
|
|
|
- // In such case the created view list should be appended as a child of that item.
|
|
|
- const prevSibling = modelItem.previousSibling;
|
|
|
-
|
|
|
- if ( prevSibling && prevSibling.name == 'listItem' ) {
|
|
|
- insertionPosition = ViewPosition.createAt( mapper.toViewElement( prevSibling ), 'end' );
|
|
|
+ // There is no list item with same indent. Check previous model item.
|
|
|
+ prevItem = modelItem.previousSibling;
|
|
|
+
|
|
|
+ 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' ) );
|
|
|
+ // ^ ACTUALLY NOT BECAUSE FIXING DOES NOT WORK PROPERLY.
|
|
|
+ // TODO: fix this part of code when conversion from model to view is done on `changesDone` event or post/prefixing is better.
|
|
|
+ if ( prevItem.getAttribute( 'indent' ) < modelItem.getAttribute( 'indent' ) ) {
|
|
|
+ // Lower indent, correct model, previous item is a parent and this model item is its nested item.
|
|
|
+ insertPosition = mapper.toViewPosition( ModelPosition.createAt( prevItem, 'end' ) );
|
|
|
+ } else {
|
|
|
+ // Higher indent, incorrect model that is probably being fixed. Inject the view list where it was.
|
|
|
+ // TODO: get rid of `removePosition` when conversion is done on `changesDone`.
|
|
|
+ if ( removePosition.parent.is( 'ul' ) || removePosition.parent.is( 'ol' ) ) {
|
|
|
+ insertPosition = viewWriter.breakContainer( removePosition );
|
|
|
+ } else {
|
|
|
+ insertPosition = removePosition;
|
|
|
+ }
|
|
|
+ }
|
|
|
} else {
|
|
|
- // This is the very first list item, use position mapping to get correct insertion position.
|
|
|
- insertionPosition = mapper.toViewPosition( ModelPosition.createBefore( modelItem ) );
|
|
|
+ // 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 ) );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 3. Append new UL/OL in position after breaking in step 2.
|
|
|
- viewWriter.insert( insertionPosition, injectedList );
|
|
|
+ // Insert the view item.
|
|
|
+ viewWriter.insert( insertPosition, injectedList );
|
|
|
+
|
|
|
+ // 2. Handle possible children of injected model item.
|
|
|
+ // We have to check if next list item in model has bigger indent. If it has, it means that it and possibly
|
|
|
+ // some following list items should be nested in the injected view item.
|
|
|
+ // Look only after model elements that are already mapped to view. Some following model items might not be mapped
|
|
|
+ // if multiple items in model were inserted/moved at once.
|
|
|
+ const nextItem = getSiblingListItem(
|
|
|
+ modelItem,
|
|
|
+ { biggerIndent: true, getNext: true, isMapped: true, mapper: mapper }
|
|
|
+ );
|
|
|
+
|
|
|
+ if ( nextItem ) {
|
|
|
+ let viewItem = mapper.toViewElement( nextItem );
|
|
|
+
|
|
|
+ // Break the list between found view item and its preceding `<li>`s.
|
|
|
+ viewWriter.breakContainer( ViewPosition.createBefore( viewItem ) );
|
|
|
|
|
|
- // 4. If next sibling is list item with bigger indent, append it's UL/OL to new LI.
|
|
|
- const nextModelItem = getSiblingListItem( modelItem, { getNext: true, biggerIndent: true } );
|
|
|
- const nextViewItem = mapper.toViewElement( nextModelItem );
|
|
|
+ // The broken ("lower") part will be moved as nested children of the inserted view item.
|
|
|
+ const sourceStart = ViewPosition.createBefore( viewItem.parent );
|
|
|
|
|
|
- /* istanbul ignore if */ // Part of code connected with indenting that is not yet complete.
|
|
|
- if ( nextViewItem ) {
|
|
|
- let sourceRange = ViewRange.createOn( nextViewItem.parent );
|
|
|
- let targetPosition = ViewPosition.createAt( injectedItem, 'end' );
|
|
|
+ const lastModelItem = _getModelLastItem( nextItem );
|
|
|
+ const lastViewItem = mapper.toViewElement( lastModelItem );
|
|
|
+ const sourceEnd = viewWriter.breakContainer( ViewPosition.createAfter( lastViewItem ) );
|
|
|
+ const sourceRange = new ViewRange( sourceStart, sourceEnd );
|
|
|
+
|
|
|
+ const targetPosition = ViewPosition.createAt( injectedItem, 'end' );
|
|
|
viewWriter.move( sourceRange, targetPosition );
|
|
|
}
|
|
|
|
|
|
- // 5. Merge new UL/OL with above and below items (ULs/OLs or LIs).
|
|
|
+ // Merge inserted view list with its possible neighbour lists.
|
|
|
mergeViewLists( injectedList, injectedList.nextSibling );
|
|
|
mergeViewLists( 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, mapper ) {
|
|
|
+ // 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,
|
|
|
+ checkAllSiblings: true,
|
|
|
+ indent: nextIndent
|
|
|
+ } );
|
|
|
+
|
|
|
+ // Indent of found element or `null` if the element has not been found.
|
|
|
+ const prevIndent = prevModelItem ? prevModelItem.getAttribute( 'indent' ) : 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 );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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 ( let child of [ ...viewRemovedItem.getChildren() ] ) {
|
|
|
+ if ( child.is( 'ul' ) || child.is( 'ol' ) ) {
|
|
|
+ insertPosition = viewWriter.move( ViewRange.createOn( child ), insertPosition ).end;
|
|
|
+
|
|
|
+ mergeViewLists( child, child.nextSibling );
|
|
|
+ mergeViewLists( child.previousSibling, child );
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Helper function to obtain the last model list item that is a forward sibling of given model list item that has
|
|
|
+// same or bigger indent. In other words, it looks for the last model item that is a nested item of the same item
|
|
|
+// that given item.
|
|
|
+function _getModelLastItem( item ) {
|
|
|
+ const indent = item.getAttribute( 'indent' );
|
|
|
+ let result = item;
|
|
|
+
|
|
|
+ while ( item.nextSibling && item.nextSibling.is( 'listItem' ) && item.nextSibling.getAttribute( 'indent' ) >= indent ) {
|
|
|
+ item = item.nextSibling;
|
|
|
+
|
|
|
+ if ( item.getAttribute( 'indent' ) == indent ) {
|
|
|
+ result = item;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|