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

Commit 58ae829

Browse files
authored
Merge pull request #94 from gjhenrique:nested-list-reborn
Feature: Added support for basic list indentation when pasting from Microsoft Word. Closes ckeditor/ckeditor5#2518. Thanks to [gjhenrique](https://github.com/gjhenrique) for the contribution!
2 parents e7949c9 + af4cbcc commit 58ae829

19 files changed

+3941
-11
lines changed

src/filters/list.js

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,43 @@ export function transformListItemLikeElementsIntoLists( documentFragment, styles
3434
}
3535

3636
let currentList = null;
37+
let currentIndentation = 1;
3738

3839
itemLikeElements.forEach( ( itemLikeElement, i ) => {
39-
if ( !currentList || isNewListNeeded( itemLikeElements[ i - 1 ], itemLikeElement ) ) {
40+
const isDifferentList = isNewListNeeded( itemLikeElements[ i - 1 ], itemLikeElement );
41+
const previousItemLikeElement = isDifferentList ? null : itemLikeElements[ i - 1 ];
42+
const indentationDifference = getIndentationDifference( previousItemLikeElement, itemLikeElement );
43+
44+
if ( isDifferentList ) {
45+
currentList = null;
46+
currentIndentation = 1;
47+
}
48+
49+
if ( !currentList || indentationDifference !== 0 ) {
4050
const listStyle = detectListStyle( itemLikeElement, stylesString );
4151

42-
currentList = insertNewEmptyList( listStyle, itemLikeElement.element, writer );
52+
if ( !currentList ) {
53+
currentList = insertNewEmptyList( listStyle, itemLikeElement.element, writer );
54+
} else if ( itemLikeElement.indent > currentIndentation ) {
55+
const lastListItem = currentList.getChild( currentList.childCount - 1 );
56+
const lastListItemChild = lastListItem.getChild( lastListItem.childCount - 1 );
57+
58+
currentList = insertNewEmptyList( listStyle, lastListItemChild, writer );
59+
60+
currentIndentation += 1;
61+
} else if ( itemLikeElement.indent < currentIndentation ) {
62+
const differentIndentation = currentIndentation - itemLikeElement.indent;
63+
64+
currentList = findParentListAtLevel( currentList, differentIndentation );
65+
66+
currentIndentation = parseInt( itemLikeElement.indent );
67+
}
68+
69+
if ( itemLikeElement.indent <= currentIndentation ) {
70+
if ( !currentList.is( listStyle.type ) ) {
71+
currentList = writer.rename( listStyle.type, currentList );
72+
}
73+
}
4374
}
4475

4576
const listItem = transformElementIntoListItem( itemLikeElement.element, writer );
@@ -155,14 +186,16 @@ function detectListStyle( listLikeItem, stylesString ) {
155186
//
156187
// @param {Object} listStyle List style object which determines the type of newly created list.
157188
// Usually a result of `detectListStyle()` function.
158-
// @param {module:engine/view/element~Element} element Element before which list is inserted.
189+
// @param {module:engine/view/element~Element} element Element after which list is inserted.
159190
// @param {module:engine/view/upcastwriter~UpcastWriter} writer
160191
// @returns {module:engine/view/element~Element} Newly created list element.
192+
161193
function insertNewEmptyList( listStyle, element, writer ) {
194+
const parent = element.parent;
162195
const list = writer.createElement( listStyle.type );
163-
const position = element.parent.getChildIndex( element );
196+
const position = parent.getChildIndex( element ) + 1;
164197

165-
writer.insertChild( position, list, element.parent );
198+
writer.insertChild( position, list, parent );
166199

167200
return list;
168201
}
@@ -243,6 +276,10 @@ function removeBulletElement( element, writer ) {
243276
// @param {Object} currentItem
244277
// @returns {Boolean}
245278
function isNewListNeeded( previousItem, currentItem ) {
279+
if ( !previousItem ) {
280+
return true;
281+
}
282+
246283
if ( previousItem.id !== currentItem.id ) {
247284
return true;
248285
}
@@ -260,3 +297,38 @@ function isNewListNeeded( previousItem, currentItem ) {
260297
function isList( element ) {
261298
return element.is( 'ol' ) || element.is( 'ul' );
262299
}
300+
301+
// Calculates the indentation difference between two given list items (based on indent attribute
302+
// extracted from `mso-list` style, see #getListItemData).
303+
//
304+
// @param {Object} previousItem
305+
// @param {Object} currentItem
306+
// @returns {Number}
307+
function getIndentationDifference( previousItem, currentItem ) {
308+
return previousItem ? currentItem.indent - previousItem.indent : currentItem.indent - 1;
309+
}
310+
311+
// Finds parent list element (ul/ol) of a given list element with indentation level lower by a given value.
312+
//
313+
// @param {module:engine/view/element~Element} listElement List element from which to start looking for a parent list.
314+
// @param {Number} indentationDifference Indentation difference between lists.
315+
// @returns {module:engine/view/element~Element} Found list element with indentation level lower by a given value.
316+
function findParentListAtLevel( listElement, indentationDifference ) {
317+
const ancestors = listElement.getAncestors( { parentFirst: true } );
318+
319+
let parentList = null;
320+
let levelChange = 0;
321+
322+
for ( const ancestor of ancestors ) {
323+
if ( ancestor.name === 'ul' || ancestor.name === 'ol' ) {
324+
levelChange++;
325+
}
326+
327+
if ( levelChange === indentationDifference ) {
328+
parentList = ancestor;
329+
break;
330+
}
331+
}
332+
333+
return parentList;
334+
}

tests/_data/list/index.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import heading1 from './heading1/input.word2016.html';
1313
import heading3Styled from './heading3-styled/input.word2016.html';
1414
import heading7 from './heading7/input.word2016.html';
1515
import resumeTemplate from './resume-template/input.word2016.html';
16+
import nested from './nested/input.word2016.html';
17+
import nestedMixed from './nested-mixed/input.word2016.html';
18+
import nestedMultiple from './nested-multiple/input.word2016.html';
1619

1720
import simpleNormalized from './simple/normalized.word2016.html';
1821
import styledNormalized from './styled/normalized.word2016.html';
@@ -23,6 +26,9 @@ import heading1Normalized from './heading1/normalized.word2016.html';
2326
import heading3StyledNormalized from './heading3-styled/normalized.word2016.html';
2427
import heading7Normalized from './heading7/normalized.word2016.html';
2528
import resumeTemplateNormalized from './resume-template/normalized.word2016.html';
29+
import nestedNormalized from './nested/normalized.word2016.html';
30+
import nestedMixedNormalized from './nested-mixed/normalized.word2016.html';
31+
import nestedMultipleNormalized from './nested-multiple/normalized.word2016.html';
2632

2733
import simpleModel from './simple/model.word2016.html';
2834
import styledModel from './styled/model.word2016.html';
@@ -33,6 +39,9 @@ import heading1Model from './heading1/model.word2016.html';
3339
import heading3StyledModel from './heading3-styled/model.word2016.html';
3440
import heading7Model from './heading7/model.word2016.html';
3541
import resumeTemplateModel from './resume-template/model.word2016.html';
42+
import nestedModel from './nested/model.word2016.html';
43+
import nestedMixedModel from './nested-mixed/model.word2016.html';
44+
import nestedMultipleModel from './nested-multiple/model.word2016.html';
3645

3746
export const fixtures = {
3847
input: {
@@ -44,7 +53,10 @@ export const fixtures = {
4453
heading1,
4554
heading3Styled,
4655
heading7,
47-
resumeTemplate
56+
resumeTemplate,
57+
nested,
58+
nestedMixed,
59+
nestedMultiple
4860
},
4961
normalized: {
5062
simple: simpleNormalized,
@@ -55,7 +67,10 @@ export const fixtures = {
5567
heading1: heading1Normalized,
5668
heading3Styled: heading3StyledNormalized,
5769
heading7: heading7Normalized,
58-
resumeTemplate: resumeTemplateNormalized
70+
resumeTemplate: resumeTemplateNormalized,
71+
nested: nestedNormalized,
72+
nestedMixed: nestedMixedNormalized,
73+
nestedMultiple: nestedMultipleNormalized
5974
},
6075
model: {
6176
simple: simpleModel,
@@ -66,7 +81,10 @@ export const fixtures = {
6681
heading1: heading1Model,
6782
heading3Styled: heading3StyledModel,
6883
heading7: heading7Model,
69-
resumeTemplate: resumeTemplateModel
84+
resumeTemplate: resumeTemplateModel,
85+
nested: nestedModel,
86+
nestedMixed: nestedMixedModel,
87+
nestedMultiple: nestedMultipleModel
7088
}
7189
};
7290

@@ -80,6 +98,9 @@ import heading1Safari from './heading1/input.safari.word2016.html';
8098
import heading3StyledSafari from './heading3-styled/input.safari.word2016.html';
8199
import heading7Safari from './heading7/input.safari.word2016.html';
82100
import resumeTemplateSafari from './resume-template/input.safari.word2016.html';
101+
import nestedSafari from './nested/input.safari.word2016.html';
102+
import nestedMixedSafari from './nested-mixed/input.safari.word2016.html';
103+
import nestedMultipleSafari from './nested-multiple/input.safari.word2016.html';
83104

84105
import simpleNormalizedSafari from './simple/normalized.safari.word2016.html';
85106
import styledNormalizedSafari from './styled/normalized.safari.word2016.html';
@@ -90,6 +111,7 @@ import heading1NormalizedSafari from './heading1/normalized.safari.word2016.html
90111
import heading3StyledNormalizedSafari from './heading3-styled/normalized.safari.word2016.html';
91112
import heading7NormalizedSafari from './heading7/normalized.safari.word2016.html';
92113
import resumeTemplateNormalizedSafari from './resume-template/normalized.safari.word2016.html';
114+
import nestedMultipleNormalizedSafari from './nested-multiple/normalized.safari.word2016.html';
93115

94116
import styledSafariModel from './styled/model.safari.word2016.html';
95117
import resumeTemplateSafariModel from './resume-template/model.safari.word2016.html';
@@ -105,7 +127,10 @@ export const browserFixtures = {
105127
heading1: heading1Safari,
106128
heading3Styled: heading3StyledSafari,
107129
heading7: heading7Safari,
108-
resumeTemplate: resumeTemplateSafari
130+
resumeTemplate: resumeTemplateSafari,
131+
nested: nestedSafari,
132+
nestedMixed: nestedMixedSafari,
133+
nestedMultiple: nestedMultipleSafari
109134
},
110135
normalized: {
111136
simple: simpleNormalizedSafari,
@@ -116,7 +141,10 @@ export const browserFixtures = {
116141
heading1: heading1NormalizedSafari,
117142
heading3Styled: heading3StyledNormalizedSafari,
118143
heading7: heading7NormalizedSafari,
119-
resumeTemplate: resumeTemplateNormalizedSafari
144+
resumeTemplate: resumeTemplateNormalizedSafari,
145+
nested: nestedNormalized,
146+
nestedMixed: nestedMixedNormalized,
147+
nestedMultiple: nestedMultipleNormalizedSafari
120148
},
121149
model: {
122150
simple: simpleModel,
@@ -127,7 +155,10 @@ export const browserFixtures = {
127155
heading1: heading1Model,
128156
heading3Styled: heading3StyledModel,
129157
heading7: heading7Model,
130-
resumeTemplate: resumeTemplateSafariModel
158+
resumeTemplate: resumeTemplateSafariModel,
159+
nested: nestedModel,
160+
nestedMixed: nestedMixedModel,
161+
nestedMultiple: nestedMultipleModel
131162
}
132163
}
133164
};

0 commit comments

Comments
 (0)