diff --git a/src/converters.js b/src/converters.js
index 463c8fe..4ca2824 100644
--- a/src/converters.js
+++ b/src/converters.js
@@ -365,22 +365,19 @@ export function modelViewMergeAfter( evt, data, conversionApi ) {
export function viewModelConverter( evt, data, conversionApi ) {
if ( conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
const writer = conversionApi.writer;
- const conversionStore = this.conversionApi.store;
// 1. Create `listItem` model element.
const listItem = writer.createElement( 'listItem' );
// 2. Handle `listItem` model element attributes.
- conversionStore.indent = conversionStore.indent || 0;
- writer.setAttribute( 'listIndent', conversionStore.indent, listItem );
+ const indent = getIndent( data.viewItem );
+
+ writer.setAttribute( 'listIndent', indent, listItem );
// Set 'bulleted' as default. If this item is pasted into a context,
const type = data.viewItem.parent && data.viewItem.parent.name == 'ol' ? 'numbered' : 'bulleted';
writer.setAttribute( 'listType', type, listItem );
- // `listItem`s created recursively should have bigger indent.
- conversionStore.indent++;
-
// Try to find allowed parent for list item.
const splitResult = conversionApi.splitToAllowedParent( listItem, data.modelCursor );
@@ -394,8 +391,6 @@ export function viewModelConverter( evt, data, conversionApi ) {
const nextPosition = viewToModelListItemChildrenConverter( listItem, data.viewItem.getChildren(), conversionApi );
- conversionStore.indent--;
-
// Result range starts before the first item and ends after the last.
data.modelRange = writer.createRange( data.modelCursor, nextPosition );
@@ -426,7 +421,9 @@ export function cleanList( evt, data, conversionApi ) {
const children = Array.from( data.viewItem.getChildren() );
for ( const child of children ) {
- if ( !child.is( 'li' ) ) {
+ const isWrongElement = !( child.is( 'li' ) || isList( child ) );
+
+ if ( isWrongElement ) {
child._remove();
}
}
@@ -453,7 +450,7 @@ export function cleanListItem( evt, data, conversionApi ) {
let firstNode = true;
for ( const child of children ) {
- if ( foundList && !child.is( 'ul' ) && !child.is( 'ol' ) ) {
+ if ( foundList && !isList( child ) ) {
child._remove();
}
@@ -464,10 +461,10 @@ export function cleanListItem( evt, data, conversionApi ) {
}
// If this is the last text node before
or , right-trim it.
- if ( !child.nextSibling || ( child.nextSibling.is( 'ul' ) || child.nextSibling.is( 'ol' ) ) ) {
+ if ( !child.nextSibling || isList( child.nextSibling ) ) {
child._data = child.data.replace( /\s+$/, '' );
}
- } else if ( child.is( 'ul' ) || child.is( 'ol' ) ) {
+ } else if ( isList( child ) ) {
// If this is a
or , do not process it, just mark that we already visited list element.
foundList = true;
}
@@ -496,7 +493,7 @@ export function modelToViewPosition( view ) {
if ( modelItem && modelItem.is( 'listItem' ) ) {
const viewItem = data.mapper.toViewElement( modelItem );
- const topmostViewList = viewItem.getAncestors().find( element => element.is( 'ul' ) || element.is( 'ol' ) );
+ const topmostViewList = viewItem.getAncestors().find( isList );
const walker = view.createPositionAt( viewItem, 0 ).getWalker();
for ( const value of walker ) {
@@ -564,7 +561,7 @@ export function viewToModelPosition( model ) {
let modelLength = 1; // Starts from 1 because the original
has to be counted in too.
let viewList = viewPos.nodeBefore;
- while ( viewList && ( viewList.is( 'ul' ) || viewList.is( 'ol' ) ) ) {
+ while ( viewList && isList( viewList ) ) {
modelLength += mapper.getModelLength( viewList );
viewList = viewList.previousSibling;
@@ -626,6 +623,10 @@ export function modelChangePostFixer( model, writer ) {
applied = true;
}
+
+ for ( const innerItem of Array.from( model.createRangeIn( item ) ).filter( e => e.item.is( 'listItem' ) ) ) {
+ _addListToFix( innerItem.previousPosition );
+ }
}
const posAfter = entry.position.getShiftedBy( entry.length );
@@ -984,7 +985,7 @@ function hoistNestedLists( nextIndent, modelRemoveStartPosition, viewRemoveStart
// 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' ) ) {
+ if ( isList( child ) ) {
insertPosition = viewWriter.move( viewWriter.createRangeOn( child ), insertPosition ).end;
mergeViewLists( viewWriter, child, child.nextSibling );
@@ -992,3 +993,71 @@ function hoistNestedLists( nextIndent, modelRemoveStartPosition, viewRemoveStart
}
}
}
+
+// Checks if view element is a list type (ul or ol).
+//
+// @param {module:engine/view/element~Element} viewElement
+// @returns {Boolean}
+function isList( viewElement ) {
+ return viewElement.is( 'ol' ) || viewElement.is( 'ul' );
+}
+
+// Calculates the indent value for a list item. Handles HTML compliant and non-compliant lists.
+//
+// Also, fixes non HTML compliant lists indents:
+//
+// before: fixed list:
+// OL OL
+// |-> LI (parent LIs: 0) |-> LI (indent: 0)
+// |-> OL |-> OL
+// |-> OL |
+// | |-> OL |
+// | |-> OL |
+// | |-> LI (parent LIs: 1) |-> LI (indent: 1)
+// |-> LI (parent LIs: 1) |-> LI (indent: 1)
+//
+// before: fixed list:
+// OL OL
+// |-> OL |
+// |-> OL |
+// |-> OL |
+// |-> LI (parent LIs: 0) |-> LI (indent: 0)
+//
+// before: fixed list:
+// OL OL
+// |-> LI (parent LIs: 0) |-> LI (indent: 0)
+// |-> OL |-> OL
+// |-> LI (parent LIs: 0) |-> LI (indent: 1)
+//
+// @param {module:engine/view/element~Element} listItem
+// @param {Object} conversionStore
+// @returns {Number}
+function getIndent( listItem ) {
+ let indent = 0;
+
+ let parent = listItem.parent;
+
+ while ( parent ) {
+ // Each LI in the tree will result in an increased indent for HTML compliant lists.
+ if ( parent.is( 'li' ) ) {
+ indent++;
+ } else {
+ // If however the list is nested in other list we should check previous sibling of any of the list elements...
+ const previousSibling = parent.previousSibling;
+
+ // ...because the we might need increase its indent:
+ // before: fixed list:
+ // OL OL
+ // |-> LI (parent LIs: 0) |-> LI (indent: 0)
+ // |-> OL |-> OL
+ // |-> LI (parent LIs: 0) |-> LI (indent: 1)
+ if ( previousSibling && previousSibling.is( 'li' ) ) {
+ indent++;
+ }
+ }
+
+ parent = parent.parent;
+ }
+
+ return indent;
+}
diff --git a/tests/listediting.js b/tests/listediting.js
index 34031b9..6c4a694 100644
--- a/tests/listediting.js
+++ b/tests/listediting.js
@@ -21,6 +21,8 @@ import { getData as getViewData, parse as parseView } from '@ckeditor/ckeditor5-
import IndentEditing from '@ckeditor/ckeditor5-indent/src/indentediting';
import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting';
describe( 'ListEditing', () => {
let editor, model, modelDoc, modelRoot, view, viewDoc, viewRoot;
@@ -28,7 +30,7 @@ describe( 'ListEditing', () => {
beforeEach( () => {
return VirtualTestEditor
.create( {
- plugins: [ Clipboard, BoldEditing, ListEditing, UndoEditing, BlockQuoteEditing ]
+ plugins: [ Clipboard, BoldEditing, ListEditing, UndoEditing, BlockQuoteEditing, TableEditing ]
} )
.then( newEditor => {
editor = newEditor;
@@ -1287,14 +1289,381 @@ describe( 'ListEditing', () => {
describe( 'nested lists', () => {
describe( 'setting data', () => {
- function test( testName, string, expectedString = null ) {
- it( testName, () => {
+ function test( string, expectedString = null ) {
+ return () => {
editor.setData( string );
- expect( editor.getData() ).to.equal( expectedString || string );
- } );
+ assertEqualMarkup( editor.getData(), expectedString || string );
+ };
}
- test( 'bullet list simple structure',
+ describe( 'non HTML compliant list fixing', () => {
+ it( 'ul in ul', test(
+ '
' +
+ '
' +
+ '
1.1
' +
+ '
' +
+ '
',
+ '
' +
+ '
1.1
' +
+ '
'
+ ) );
+
+ it( 'ul in ol', test(
+ '' +
+ '
' +
+ '
1.1
' +
+ '
' +
+ '',
+ '
' +
+ '
1.1
' +
+ '
'
+ ) );
+
+ it( 'ul in ul (previous sibling is li)', test(
+ '
' +
+ '
1
' +
+ '
' +
+ '
2.1
' +
+ '
' +
+ '
',
+ '
' +
+ '
1' +
+ '
' +
+ '
2.1
' +
+ '
' +
+ '
' +
+ '
'
+ ) );
+
+ it( 'ul in deeply nested ul - base index > 0 #1', test(
+ '
' +
+ '
1.1
' +
+ '
1.2' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
2.1
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
',
+ '
' +
+ '
1.1
' +
+ '
1.2' +
+ '
' +
+ '
2.1
' +
+ '
' +
+ '
' +
+ '
'
+ ) );
+
+ it( 'ul in deeply nested ul - base index > 0 #2', test(
+ '
' +
+ '
1.1
' +
+ '
1.2' +
+ '
' +
+ '
2.1
' +
+ '
' +
+ '
' +
+ '
' +
+ '
3.1
' +
+ '
' +
+ '
' +
+ '
' +
+ '
2.2
' +
+ '
' +
+ '
' +
+ '
',
+ '
' +
+ '
1.1
' +
+ '
1.2' +
+ '
' +
+ '
2.1' +
+ '
' +
+ '
3.1
' +
+ '
' +
+ '
' +
+ '
2.2
' +
+ '
' +
+ '
' +
+ '
'
+ ) );
+
+ it( 'ul in deeply nested ul inside li', test(
+ '
'
- );
+ ) );
- it( 'model test for nested lists', () => {
- // in the middle will be fixed by postfixer to bulleted list.
- editor.setData(
- '
foo
' +
- '
' +
- '
' +
- '1' +
- '
' +
- '
1.1
' +
- '
' +
- '' +
- '
' +
- '1.2' +
- '' +
- '
1.2.1
' +
- '' +
- '
' +
- '
1.3
' +
- '' +
- '
' +
- '
2
' +
- '
' +
- '
bar
'
- );
+ describe( 'model tests for nested lists', () => {
+ it( 'should properly set listIndent and listType', () => {
+ // in the middle will be fixed by postfixer to bulleted list.
+ editor.setData(
+ '