@ckeditor/ckeditor5-list 35.4.0 → 36.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -1
- package/build/list.js +2 -2
- package/package.json +43 -39
- package/src/documentlist/converters.js +303 -419
- package/src/documentlist/documentlistcommand.js +136 -207
- package/src/documentlist/documentlistediting.js +538 -698
- package/src/documentlist/documentlistindentcommand.js +115 -168
- package/src/documentlist/documentlistmergecommand.js +161 -222
- package/src/documentlist/documentlistsplitcommand.js +59 -103
- package/src/documentlist/documentlistutils.js +31 -45
- package/src/documentlist/utils/listwalker.js +138 -236
- package/src/documentlist/utils/model.js +322 -421
- package/src/documentlist/utils/postfixers.js +98 -126
- package/src/documentlist/utils/view.js +74 -105
- package/src/documentlist.js +13 -19
- package/src/documentlistproperties/converters.js +33 -47
- package/src/documentlistproperties/documentlistpropertiesediting.js +265 -356
- package/src/documentlistproperties/documentlistpropertiesutils.js +32 -57
- package/src/documentlistproperties/documentlistreversedcommand.js +40 -61
- package/src/documentlistproperties/documentliststartcommand.js +42 -61
- package/src/documentlistproperties/documentliststylecommand.js +97 -147
- package/src/documentlistproperties/utils/style.js +27 -47
- package/src/documentlistproperties.js +13 -19
- package/src/index.js +1 -3
- package/src/list/converters.js +772 -929
- package/src/list/indentcommand.js +105 -140
- package/src/list/listcommand.js +262 -315
- package/src/list/listediting.js +141 -200
- package/src/list/listui.js +16 -25
- package/src/list/listutils.js +37 -59
- package/src/list/utils.js +295 -378
- package/src/list.js +13 -44
- package/src/listcommands.js +5 -0
- package/src/listconfig.js +5 -0
- package/src/listproperties/listpropertiesediting.js +656 -803
- package/src/listproperties/listpropertiesui.js +244 -296
- package/src/listproperties/listreversedcommand.js +37 -49
- package/src/listproperties/liststartcommand.js +37 -49
- package/src/listproperties/liststylecommand.js +82 -115
- package/src/listproperties/ui/collapsibleview.js +75 -138
- package/src/listproperties/ui/listpropertiesview.js +289 -415
- package/src/listproperties.js +13 -118
- package/src/liststyle.js +18 -24
- package/src/todolist/checktodolistcommand.js +60 -102
- package/src/todolist/todolistconverters.js +189 -271
- package/src/todolist/todolistediting.js +141 -206
- package/src/todolist/todolistui.js +14 -21
- package/src/todolist.js +13 -19
- package/theme/collapsible.css +1 -1
- package/theme/documentlist.css +1 -1
- package/theme/list.css +40 -0
- package/theme/listproperties.css +1 -1
- package/theme/liststyles.css +1 -37
- package/theme/todolist.css +1 -1
package/src/list/converters.js
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @license Copyright (c) 2003-
|
|
2
|
+
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
3
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
5
|
/**
|
|
7
6
|
* @module list/list/converters
|
|
8
7
|
*/
|
|
9
|
-
|
|
10
8
|
import { TreeWalker } from 'ckeditor5/src/engine';
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
generateLiInUl,
|
|
14
|
-
injectViewList,
|
|
15
|
-
mergeViewLists,
|
|
16
|
-
getSiblingListItem,
|
|
17
|
-
positionAfterUiElements
|
|
18
|
-
} from './utils';
|
|
19
|
-
|
|
9
|
+
import { generateLiInUl, injectViewList, mergeViewLists, getSiblingListItem, positionAfterUiElements } from './utils';
|
|
20
10
|
/**
|
|
21
11
|
* A model-to-view converter for the `listItem` model element insertion.
|
|
22
12
|
*
|
|
@@ -24,75 +14,60 @@ import {
|
|
|
24
14
|
* position, and merges the list with surrounding lists (if available).
|
|
25
15
|
*
|
|
26
16
|
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert
|
|
27
|
-
* @param
|
|
28
|
-
* @returns {Function} Returns a conversion callback.
|
|
17
|
+
* @param model Model instance.
|
|
29
18
|
*/
|
|
30
|
-
export function modelViewInsertion(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const modelItem = data.item;
|
|
46
|
-
const viewItem = generateLiInUl( modelItem, conversionApi );
|
|
47
|
-
|
|
48
|
-
injectViewList( modelItem, viewItem, conversionApi, model );
|
|
49
|
-
};
|
|
19
|
+
export function modelViewInsertion(model) {
|
|
20
|
+
return (evt, data, conversionApi) => {
|
|
21
|
+
const consumable = conversionApi.consumable;
|
|
22
|
+
if (!consumable.test(data.item, 'insert') ||
|
|
23
|
+
!consumable.test(data.item, 'attribute:listType') ||
|
|
24
|
+
!consumable.test(data.item, 'attribute:listIndent')) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
consumable.consume(data.item, 'insert');
|
|
28
|
+
consumable.consume(data.item, 'attribute:listType');
|
|
29
|
+
consumable.consume(data.item, 'attribute:listIndent');
|
|
30
|
+
const modelItem = data.item;
|
|
31
|
+
const viewItem = generateLiInUl(modelItem, conversionApi);
|
|
32
|
+
injectViewList(modelItem, viewItem, conversionApi, model);
|
|
33
|
+
};
|
|
50
34
|
}
|
|
51
|
-
|
|
52
35
|
/**
|
|
53
36
|
* A model-to-view converter for the `listItem` model element removal.
|
|
54
37
|
*
|
|
55
38
|
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove
|
|
56
|
-
* @param
|
|
57
|
-
* @returns
|
|
39
|
+
* @param model Model instance.
|
|
40
|
+
* @returns Returns a conversion callback.
|
|
58
41
|
*/
|
|
59
|
-
export function modelViewRemove(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// 5. Unbind removed view item and all children.
|
|
88
|
-
for ( const child of viewWriter.createRangeIn( removed ).getItems() ) {
|
|
89
|
-
conversionApi.mapper.unbindViewElement( child );
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
evt.stop();
|
|
93
|
-
};
|
|
42
|
+
export function modelViewRemove(model) {
|
|
43
|
+
return (evt, data, conversionApi) => {
|
|
44
|
+
const viewPosition = conversionApi.mapper.toViewPosition(data.position);
|
|
45
|
+
const viewStart = viewPosition.getLastMatchingPosition(value => !value.item.is('element', 'li'));
|
|
46
|
+
const viewItem = viewStart.nodeAfter;
|
|
47
|
+
const viewWriter = conversionApi.writer;
|
|
48
|
+
// 1. Break the container after and before the list item.
|
|
49
|
+
// This will create a view list with one view list item - the one to remove.
|
|
50
|
+
viewWriter.breakContainer(viewWriter.createPositionBefore(viewItem));
|
|
51
|
+
viewWriter.breakContainer(viewWriter.createPositionAfter(viewItem));
|
|
52
|
+
// 2. Remove the list with the item to remove.
|
|
53
|
+
const viewList = viewItem.parent;
|
|
54
|
+
const viewListPrev = viewList.previousSibling;
|
|
55
|
+
const removeRange = viewWriter.createRangeOn(viewList);
|
|
56
|
+
const removed = viewWriter.remove(removeRange);
|
|
57
|
+
// 3. Merge the whole created by breaking and removing the list.
|
|
58
|
+
if (viewListPrev && viewListPrev.nextSibling) {
|
|
59
|
+
mergeViewLists(viewWriter, viewListPrev, viewListPrev.nextSibling);
|
|
60
|
+
}
|
|
61
|
+
// 4. Bring back nested list that was in the removed <li>.
|
|
62
|
+
const modelItem = conversionApi.mapper.toModelElement(viewItem);
|
|
63
|
+
hoistNestedLists(modelItem.getAttribute('listIndent') + 1, data.position, removeRange.start, viewItem, conversionApi, model);
|
|
64
|
+
// 5. Unbind removed view item and all children.
|
|
65
|
+
for (const child of viewWriter.createRangeIn(removed).getItems()) {
|
|
66
|
+
conversionApi.mapper.unbindViewElement(child);
|
|
67
|
+
}
|
|
68
|
+
evt.stop();
|
|
69
|
+
};
|
|
94
70
|
}
|
|
95
|
-
|
|
96
71
|
/**
|
|
97
72
|
* A model-to-view converter for the `type` attribute change on the `listItem` model element.
|
|
98
73
|
*
|
|
@@ -104,95 +79,73 @@ export function modelViewRemove( model ) {
|
|
|
104
79
|
* Check {@link module:list/todolist/todolistconverters~modelViewChangeType} to see an example of it.
|
|
105
80
|
*
|
|
106
81
|
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
|
|
107
|
-
* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
|
108
|
-
* @param {Object} data Additional information about the change.
|
|
109
|
-
* @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
|
|
110
82
|
*/
|
|
111
|
-
export
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const listName = data.attributeNewValue == 'numbered' ? 'ol' : 'ul';
|
|
128
|
-
|
|
129
|
-
viewWriter.rename( listName, viewList );
|
|
130
|
-
}
|
|
131
|
-
|
|
83
|
+
export const modelViewChangeType = (evt, data, conversionApi) => {
|
|
84
|
+
if (!conversionApi.consumable.test(data.item, evt.name)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const viewItem = conversionApi.mapper.toViewElement(data.item);
|
|
88
|
+
const viewWriter = conversionApi.writer;
|
|
89
|
+
// Break the container after and before the list item.
|
|
90
|
+
// This will create a view list with one view list item -- the one that changed type.
|
|
91
|
+
viewWriter.breakContainer(viewWriter.createPositionBefore(viewItem));
|
|
92
|
+
viewWriter.breakContainer(viewWriter.createPositionAfter(viewItem));
|
|
93
|
+
// Change name of the view list that holds the changed view item.
|
|
94
|
+
// We cannot just change name property, because that would not render properly.
|
|
95
|
+
const viewList = viewItem.parent;
|
|
96
|
+
const listName = data.attributeNewValue == 'numbered' ? 'ol' : 'ul';
|
|
97
|
+
viewWriter.rename(listName, viewList);
|
|
98
|
+
};
|
|
132
99
|
/**
|
|
133
100
|
* A model-to-view converter that attempts to merge nodes split by {@link module:list/list/converters~modelViewChangeType}.
|
|
134
101
|
*
|
|
135
102
|
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
|
|
136
|
-
* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
|
137
|
-
* @param {Object} data Additional information about the change.
|
|
138
|
-
* @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
|
|
139
103
|
*/
|
|
140
|
-
export
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
mergeViewLists( viewWriter, viewList.previousSibling, viewList );
|
|
150
|
-
}
|
|
151
|
-
|
|
104
|
+
export const modelViewMergeAfterChangeType = (evt, data, conversionApi) => {
|
|
105
|
+
conversionApi.consumable.consume(data.item, evt.name);
|
|
106
|
+
const viewItem = conversionApi.mapper.toViewElement(data.item);
|
|
107
|
+
const viewList = viewItem.parent;
|
|
108
|
+
const viewWriter = conversionApi.writer;
|
|
109
|
+
// Merge the changed view list with other lists, if possible.
|
|
110
|
+
mergeViewLists(viewWriter, viewList, viewList.nextSibling);
|
|
111
|
+
mergeViewLists(viewWriter, viewList.previousSibling, viewList);
|
|
112
|
+
};
|
|
152
113
|
/**
|
|
153
114
|
* A model-to-view converter for the `listIndent` attribute change on the `listItem` model element.
|
|
154
115
|
*
|
|
155
116
|
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
|
|
156
|
-
* @param
|
|
157
|
-
* @returns
|
|
117
|
+
* @param model Model instance.
|
|
118
|
+
* @returns Returns a conversion callback.
|
|
158
119
|
*/
|
|
159
|
-
export function modelViewChangeIndent(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
injectViewList( data.item, viewItem, conversionApi, model );
|
|
188
|
-
|
|
189
|
-
// 5. Consume insertion of children inside the item. They are already handled by re-building the item in view.
|
|
190
|
-
for ( const child of data.item.getChildren() ) {
|
|
191
|
-
conversionApi.consumable.consume( child, 'insert' );
|
|
192
|
-
}
|
|
193
|
-
};
|
|
120
|
+
export function modelViewChangeIndent(model) {
|
|
121
|
+
return (evt, data, conversionApi) => {
|
|
122
|
+
if (!conversionApi.consumable.consume(data.item, 'attribute:listIndent')) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const viewItem = conversionApi.mapper.toViewElement(data.item);
|
|
126
|
+
const viewWriter = conversionApi.writer;
|
|
127
|
+
// 1. Break the container after and before the list item.
|
|
128
|
+
// This will create a view list with one view list item -- the one that changed type.
|
|
129
|
+
viewWriter.breakContainer(viewWriter.createPositionBefore(viewItem));
|
|
130
|
+
viewWriter.breakContainer(viewWriter.createPositionAfter(viewItem));
|
|
131
|
+
// 2. Extract view list with changed view list item and merge "hole" possibly created by breaking and removing elements.
|
|
132
|
+
const viewList = viewItem.parent;
|
|
133
|
+
const viewListPrev = viewList.previousSibling;
|
|
134
|
+
const removeRange = viewWriter.createRangeOn(viewList);
|
|
135
|
+
viewWriter.remove(removeRange);
|
|
136
|
+
if (viewListPrev && viewListPrev.nextSibling) {
|
|
137
|
+
mergeViewLists(viewWriter, viewListPrev, viewListPrev.nextSibling);
|
|
138
|
+
}
|
|
139
|
+
// 3. Bring back nested list that was in the removed <li>.
|
|
140
|
+
hoistNestedLists(data.attributeOldValue + 1, data.range.start, removeRange.start, viewItem, conversionApi, model);
|
|
141
|
+
// 4. Inject view list like it is newly inserted.
|
|
142
|
+
injectViewList(data.item, viewItem, conversionApi, model);
|
|
143
|
+
// 5. Consume insertion of children inside the item. They are already handled by re-building the item in view.
|
|
144
|
+
for (const child of data.item.getChildren()) {
|
|
145
|
+
conversionApi.consumable.consume(child, 'insert');
|
|
146
|
+
}
|
|
147
|
+
};
|
|
194
148
|
}
|
|
195
|
-
|
|
196
149
|
/**
|
|
197
150
|
* A special model-to-view converter introduced by the {@link module:list/list~List list feature}. This converter is fired for
|
|
198
151
|
* insert change of every model item, and should be fired before the actual converter. The converter checks whether the inserted
|
|
@@ -201,158 +154,142 @@ export function modelViewChangeIndent( model ) {
|
|
|
201
154
|
*
|
|
202
155
|
* The converter prevents such situations:
|
|
203
156
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
157
|
+
* ```xml
|
|
158
|
+
* // Model: // View:
|
|
159
|
+
* <listItem>foo</listItem> <ul>
|
|
160
|
+
* <listItem>bar</listItem> <li>foo</li>
|
|
161
|
+
* <li>bar</li>
|
|
162
|
+
* </ul>
|
|
209
163
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
164
|
+
* // After change: // Correct view guaranteed by this converter:
|
|
165
|
+
* <listItem>foo</listItem> <ul><li>foo</li></ul><p>xxx</p><ul><li>bar</li></ul>
|
|
166
|
+
* <paragraph>xxx</paragraph> // Instead of this wrong view state:
|
|
167
|
+
* <listItem>bar</listItem> <ul><li>foo</li><p>xxx</p><li>bar</li></ul>
|
|
168
|
+
* ```
|
|
214
169
|
*
|
|
215
170
|
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert
|
|
216
|
-
* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
|
217
|
-
* @param {Object} data Additional information about the change.
|
|
218
|
-
* @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
|
|
219
171
|
*/
|
|
220
|
-
export
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if ( mergePos && mergePos.parent == previousList ) {
|
|
312
|
-
viewPosition.offset--;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Merge last inserted list with element after it.
|
|
318
|
-
mergeViewLists( viewWriter, viewPosition.nodeBefore, viewPosition.nodeAfter );
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
172
|
+
export const modelViewSplitOnInsert = (evt, data, conversionApi) => {
|
|
173
|
+
if (!conversionApi.consumable.test(data.item, evt.name)) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (data.item.name != 'listItem') {
|
|
177
|
+
let viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
|
|
178
|
+
const viewWriter = conversionApi.writer;
|
|
179
|
+
const lists = [];
|
|
180
|
+
// Break multiple ULs/OLs if there are.
|
|
181
|
+
//
|
|
182
|
+
// Imagine following list:
|
|
183
|
+
//
|
|
184
|
+
// 1 --------
|
|
185
|
+
// 1.1 --------
|
|
186
|
+
// 1.1.1 --------
|
|
187
|
+
// 1.1.2 --------
|
|
188
|
+
// 1.1.3 --------
|
|
189
|
+
// 1.1.3.1 --------
|
|
190
|
+
// 1.2 --------
|
|
191
|
+
// 1.2.1 --------
|
|
192
|
+
// 2 --------
|
|
193
|
+
//
|
|
194
|
+
// Insert paragraph after item 1.1.1:
|
|
195
|
+
//
|
|
196
|
+
// 1 --------
|
|
197
|
+
// 1.1 --------
|
|
198
|
+
// 1.1.1 --------
|
|
199
|
+
//
|
|
200
|
+
// Lorem ipsum.
|
|
201
|
+
//
|
|
202
|
+
// 1.1.2 --------
|
|
203
|
+
// 1.1.3 --------
|
|
204
|
+
// 1.1.3.1 --------
|
|
205
|
+
// 1.2 --------
|
|
206
|
+
// 1.2.1 --------
|
|
207
|
+
// 2 --------
|
|
208
|
+
//
|
|
209
|
+
// In this case 1.1.2 has to become beginning of a new list.
|
|
210
|
+
// We need to break list before 1.1.2 (obvious), then we need to break list also before 1.2.
|
|
211
|
+
// Then we need to move those broken pieces one after another and merge:
|
|
212
|
+
//
|
|
213
|
+
// 1 --------
|
|
214
|
+
// 1.1 --------
|
|
215
|
+
// 1.1.1 --------
|
|
216
|
+
//
|
|
217
|
+
// Lorem ipsum.
|
|
218
|
+
//
|
|
219
|
+
// 1.1.2 --------
|
|
220
|
+
// 1.1.3 --------
|
|
221
|
+
// 1.1.3.1 --------
|
|
222
|
+
// 1.2 --------
|
|
223
|
+
// 1.2.1 --------
|
|
224
|
+
// 2 --------
|
|
225
|
+
//
|
|
226
|
+
while (viewPosition.parent.name == 'ul' || viewPosition.parent.name == 'ol') {
|
|
227
|
+
viewPosition = viewWriter.breakContainer(viewPosition);
|
|
228
|
+
if (viewPosition.parent.name != 'li') {
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
// Remove lists that are after inserted element.
|
|
232
|
+
// They will be brought back later, below the inserted element.
|
|
233
|
+
const removeStart = viewPosition;
|
|
234
|
+
const removeEnd = viewWriter.createPositionAt(viewPosition.parent, 'end');
|
|
235
|
+
// Don't remove if there is nothing to remove.
|
|
236
|
+
if (!removeStart.isEqual(removeEnd)) {
|
|
237
|
+
const removed = viewWriter.remove(viewWriter.createRange(removeStart, removeEnd));
|
|
238
|
+
lists.push(removed);
|
|
239
|
+
}
|
|
240
|
+
viewPosition = viewWriter.createPositionAfter(viewPosition.parent);
|
|
241
|
+
}
|
|
242
|
+
// Bring back removed lists.
|
|
243
|
+
if (lists.length > 0) {
|
|
244
|
+
for (let i = 0; i < lists.length; i++) {
|
|
245
|
+
const previousList = viewPosition.nodeBefore;
|
|
246
|
+
const insertedRange = viewWriter.insert(viewPosition, lists[i]);
|
|
247
|
+
viewPosition = insertedRange.end;
|
|
248
|
+
// Don't merge first list! We want a split in that place (this is why this converter is introduced).
|
|
249
|
+
if (i > 0) {
|
|
250
|
+
const mergePos = mergeViewLists(viewWriter, previousList, previousList.nextSibling);
|
|
251
|
+
// If `mergePos` is in `previousList` it means that the lists got merged.
|
|
252
|
+
// In this case, we need to fix insert position.
|
|
253
|
+
if (mergePos && mergePos.parent == previousList) {
|
|
254
|
+
viewPosition.offset--;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Merge last inserted list with element after it.
|
|
259
|
+
mergeViewLists(viewWriter, viewPosition.nodeBefore, viewPosition.nodeAfter);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
323
263
|
/**
|
|
324
264
|
* A special model-to-view converter introduced by the {@link module:list/list~List list feature}. This converter takes care of
|
|
325
265
|
* merging view lists after something is removed or moved from near them.
|
|
326
266
|
*
|
|
327
267
|
* Example:
|
|
328
268
|
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
269
|
+
* ```xml
|
|
270
|
+
* // Model: // View:
|
|
271
|
+
* <listItem>foo</listItem> <ul><li>foo</li></ul>
|
|
272
|
+
* <paragraph>xxx</paragraph> <p>xxx</p>
|
|
273
|
+
* <listItem>bar</listItem> <ul><li>bar</li></ul>
|
|
333
274
|
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
275
|
+
* // After change: // Correct view guaranteed by this converter:
|
|
276
|
+
* <listItem>foo</listItem> <ul>
|
|
277
|
+
* <listItem>bar</listItem> <li>foo</li>
|
|
278
|
+
* <li>bar</li>
|
|
279
|
+
* </ul>
|
|
280
|
+
* ```
|
|
339
281
|
*
|
|
340
282
|
* @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove
|
|
341
|
-
* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
|
342
|
-
* @param {Object} data Additional information about the change.
|
|
343
|
-
* @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
|
|
344
283
|
*/
|
|
345
|
-
export
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
284
|
+
export const modelViewMergeAfter = (evt, data, conversionApi) => {
|
|
285
|
+
const viewPosition = conversionApi.mapper.toViewPosition(data.position);
|
|
286
|
+
const viewItemPrev = viewPosition.nodeBefore;
|
|
287
|
+
const viewItemNext = viewPosition.nodeAfter;
|
|
288
|
+
// Merge lists if something (remove, move) was done from inside of list.
|
|
289
|
+
// Merging will be done only if both items are view lists of the same type.
|
|
290
|
+
// The check is done inside the helper function.
|
|
291
|
+
mergeViewLists(conversionApi.writer, viewItemPrev, viewItemNext);
|
|
292
|
+
};
|
|
356
293
|
/**
|
|
357
294
|
* A view-to-model converter that converts the `<li>` view elements into the `listItem` model elements.
|
|
358
295
|
*
|
|
@@ -361,384 +298,310 @@ export function modelViewMergeAfter( evt, data, conversionApi ) {
|
|
|
361
298
|
* * stores and increases the `conversionApi.store.indent` value when `<li>`'s sub-items are converted.
|
|
362
299
|
*
|
|
363
300
|
* @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
|
|
364
|
-
* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
|
365
|
-
* @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
|
|
366
|
-
* @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface to be used by the callback.
|
|
367
301
|
*/
|
|
368
|
-
export
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const nextPosition = viewToModelListItemChildrenConverter( listItem, data.viewItem.getChildren(), conversionApi );
|
|
389
|
-
|
|
390
|
-
// Result range starts before the first item and ends after the last.
|
|
391
|
-
data.modelRange = writer.createRange( data.modelCursor, nextPosition );
|
|
392
|
-
|
|
393
|
-
conversionApi.updateConversionResult( listItem, data );
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
302
|
+
export const viewModelConverter = (evt, data, conversionApi) => {
|
|
303
|
+
if (conversionApi.consumable.consume(data.viewItem, { name: true })) {
|
|
304
|
+
const writer = conversionApi.writer;
|
|
305
|
+
// 1. Create `listItem` model element.
|
|
306
|
+
const listItem = writer.createElement('listItem');
|
|
307
|
+
// 2. Handle `listItem` model element attributes.
|
|
308
|
+
const indent = getIndent(data.viewItem);
|
|
309
|
+
writer.setAttribute('listIndent', indent, listItem);
|
|
310
|
+
// Set 'bulleted' as default. If this item is pasted into a context,
|
|
311
|
+
const type = data.viewItem.parent && data.viewItem.parent.name == 'ol' ? 'numbered' : 'bulleted';
|
|
312
|
+
writer.setAttribute('listType', type, listItem);
|
|
313
|
+
if (!conversionApi.safeInsert(listItem, data.modelCursor)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const nextPosition = viewToModelListItemChildrenConverter(listItem, data.viewItem.getChildren(), conversionApi);
|
|
317
|
+
// Result range starts before the first item and ends after the last.
|
|
318
|
+
data.modelRange = writer.createRange(data.modelCursor, nextPosition);
|
|
319
|
+
conversionApi.updateConversionResult(listItem, data);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
397
322
|
/**
|
|
398
323
|
* A view-to-model converter for the `<ul>` and `<ol>` view elements that cleans the input view of garbage.
|
|
399
324
|
* This is mostly to clean whitespaces from between the `<li>` view elements inside the view list element, however, also
|
|
400
325
|
* incorrect data can be cleared if the view was incorrect.
|
|
401
326
|
*
|
|
402
327
|
* @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
|
|
403
|
-
* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
|
404
|
-
* @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
|
|
405
|
-
* @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface to be used by the callback.
|
|
406
328
|
*/
|
|
407
|
-
export
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
329
|
+
export const cleanList = (evt, data, conversionApi) => {
|
|
330
|
+
if (conversionApi.consumable.test(data.viewItem, { name: true })) {
|
|
331
|
+
// Caching children because when we start removing them iterating fails.
|
|
332
|
+
const children = Array.from(data.viewItem.getChildren());
|
|
333
|
+
for (const child of children) {
|
|
334
|
+
const isWrongElement = !(child.is('element', 'li') || isList(child));
|
|
335
|
+
if (isWrongElement) {
|
|
336
|
+
child._remove();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
422
341
|
/**
|
|
423
342
|
* A view-to-model converter for the `<li>` elements that cleans whitespace formatting from the input view.
|
|
424
343
|
*
|
|
425
344
|
* @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
|
|
426
|
-
* @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
|
|
427
|
-
* @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
|
|
428
|
-
* @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface to be used by the callback.
|
|
429
345
|
*/
|
|
430
|
-
export
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
346
|
+
export const cleanListItem = (evt, data, conversionApi) => {
|
|
347
|
+
if (conversionApi.consumable.test(data.viewItem, { name: true })) {
|
|
348
|
+
if (data.viewItem.childCount === 0) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const children = [...data.viewItem.getChildren()];
|
|
352
|
+
let foundList = false;
|
|
353
|
+
for (const child of children) {
|
|
354
|
+
if (foundList && !isList(child)) {
|
|
355
|
+
child._remove();
|
|
356
|
+
}
|
|
357
|
+
if (isList(child)) {
|
|
358
|
+
// If this is a <ul> or <ol>, do not process it, just mark that we already visited list element.
|
|
359
|
+
foundList = true;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
};
|
|
453
364
|
/**
|
|
454
365
|
* Returns a callback for model position to view position mapping for {@link module:engine/conversion/mapper~Mapper}. The callback fixes
|
|
455
366
|
* positions between the `listItem` elements that would be incorrectly mapped because of how list items are represented in the model
|
|
456
367
|
* and in the view.
|
|
457
|
-
*
|
|
458
|
-
* @see module:engine/conversion/mapper~Mapper#event:modelToViewPosition
|
|
459
|
-
* @param {module:engine/view/view~View} view A view instance.
|
|
460
|
-
* @returns {Function}
|
|
461
368
|
*/
|
|
462
|
-
export function modelToViewPosition(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
};
|
|
369
|
+
export function modelToViewPosition(view) {
|
|
370
|
+
return (evt, data) => {
|
|
371
|
+
if (data.isPhantom) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const modelItem = data.modelPosition.nodeBefore;
|
|
375
|
+
if (modelItem && modelItem.is('element', 'listItem')) {
|
|
376
|
+
const viewItem = data.mapper.toViewElement(modelItem);
|
|
377
|
+
const topmostViewList = viewItem.getAncestors().find(isList);
|
|
378
|
+
const walker = view.createPositionAt(viewItem, 0).getWalker();
|
|
379
|
+
for (const value of walker) {
|
|
380
|
+
if (value.type == 'elementStart' && value.item.is('element', 'li')) {
|
|
381
|
+
data.viewPosition = value.previousPosition;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
else if (value.type == 'elementEnd' && value.item == topmostViewList) {
|
|
385
|
+
data.viewPosition = value.nextPosition;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
488
391
|
}
|
|
489
|
-
|
|
490
392
|
/**
|
|
491
393
|
* The callback for view position to model position mapping for {@link module:engine/conversion/mapper~Mapper}. The callback fixes
|
|
492
394
|
* positions between the `<li>` elements that would be incorrectly mapped because of how list items are represented in the model
|
|
493
395
|
* and in the view.
|
|
494
396
|
*
|
|
495
397
|
* @see module:engine/conversion/mapper~Mapper#event:viewToModelPosition
|
|
496
|
-
* @param
|
|
497
|
-
* @returns
|
|
398
|
+
* @param model Model instance.
|
|
399
|
+
* @returns Returns a conversion callback.
|
|
498
400
|
*/
|
|
499
|
-
export function viewToModelPosition(
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
viewList = viewList.previousSibling;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
data.modelPosition = model.createPositionBefore( modelNode ).getShiftedBy( modelLength );
|
|
547
|
-
|
|
548
|
-
evt.stop();
|
|
549
|
-
}
|
|
550
|
-
};
|
|
401
|
+
export function viewToModelPosition(model) {
|
|
402
|
+
return (evt, data) => {
|
|
403
|
+
const viewPos = data.viewPosition;
|
|
404
|
+
const viewParent = viewPos.parent;
|
|
405
|
+
const mapper = data.mapper;
|
|
406
|
+
if (viewParent.name == 'ul' || viewParent.name == 'ol') {
|
|
407
|
+
// Position is directly in <ul> or <ol>.
|
|
408
|
+
if (!viewPos.isAtEnd) {
|
|
409
|
+
// If position is not at the end, it must be before <li>.
|
|
410
|
+
// Get that <li>, map it to `listItem` and set model position before that `listItem`.
|
|
411
|
+
const modelNode = mapper.toModelElement(viewPos.nodeAfter);
|
|
412
|
+
data.modelPosition = model.createPositionBefore(modelNode);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
// Position is at the end of <ul> or <ol>, so there is no <li> after it to be mapped.
|
|
416
|
+
// There is <li> before the position, but we cannot just map it to `listItem` and set model position after it,
|
|
417
|
+
// because that <li> may contain nested items.
|
|
418
|
+
// We will check "model length" of that <li>, in other words - how many `listItem`s are in that <li>.
|
|
419
|
+
const modelNode = mapper.toModelElement(viewPos.nodeBefore);
|
|
420
|
+
const modelLength = mapper.getModelLength(viewPos.nodeBefore);
|
|
421
|
+
// Then we get model position before mapped `listItem` and shift it accordingly.
|
|
422
|
+
data.modelPosition = model.createPositionBefore(modelNode).getShiftedBy(modelLength);
|
|
423
|
+
}
|
|
424
|
+
evt.stop();
|
|
425
|
+
}
|
|
426
|
+
else if (viewParent.name == 'li' &&
|
|
427
|
+
viewPos.nodeBefore &&
|
|
428
|
+
(viewPos.nodeBefore.name == 'ul' || viewPos.nodeBefore.name == 'ol')) {
|
|
429
|
+
// In most cases when view position is in <li> it is in text and this is a correct position.
|
|
430
|
+
// However, if position is after <ul> or <ol> we have to fix it -- because in model <ul>/<ol> are not in the `listItem`.
|
|
431
|
+
const modelNode = mapper.toModelElement(viewParent);
|
|
432
|
+
// Check all <ul>s and <ol>s that are in the <li> but before mapped position.
|
|
433
|
+
// Get model length of those elements and then add it to the offset of `listItem` mapped to the original <li>.
|
|
434
|
+
let modelLength = 1; // Starts from 1 because the original <li> has to be counted in too.
|
|
435
|
+
let viewList = viewPos.nodeBefore;
|
|
436
|
+
while (viewList && isList(viewList)) {
|
|
437
|
+
modelLength += mapper.getModelLength(viewList);
|
|
438
|
+
viewList = viewList.previousSibling;
|
|
439
|
+
}
|
|
440
|
+
data.modelPosition = model.createPositionBefore(modelNode).getShiftedBy(modelLength);
|
|
441
|
+
evt.stop();
|
|
442
|
+
}
|
|
443
|
+
};
|
|
551
444
|
}
|
|
552
|
-
|
|
553
445
|
/**
|
|
554
446
|
* Post-fixer that reacts to changes on document and fixes incorrect model states.
|
|
555
447
|
*
|
|
556
448
|
* In the example below, there is a correct list structure.
|
|
557
449
|
* Then the middle element is removed so the list structure will become incorrect:
|
|
558
450
|
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
451
|
+
* ```xml
|
|
452
|
+
* <listItem listType="bulleted" listIndent=0>Item 1</listItem>
|
|
453
|
+
* <listItem listType="bulleted" listIndent=1>Item 2</listItem> <--- this is removed.
|
|
454
|
+
* <listItem listType="bulleted" listIndent=2>Item 3</listItem>
|
|
455
|
+
* ```
|
|
562
456
|
*
|
|
563
457
|
* The list structure after the middle element is removed:
|
|
564
458
|
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
459
|
+
* ```xml
|
|
460
|
+
* <listItem listType="bulleted" listIndent=0>Item 1</listItem>
|
|
461
|
+
* <listItem listType="bulleted" listIndent=2>Item 3</listItem>
|
|
462
|
+
* ```
|
|
567
463
|
*
|
|
568
464
|
* Should become:
|
|
569
465
|
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
466
|
+
* ```xml
|
|
467
|
+
* <listItem listType="bulleted" listIndent=0>Item 1</listItem>
|
|
468
|
+
* <listItem listType="bulleted" listIndent=1>Item 3</listItem> <--- note that indent got post-fixed.
|
|
469
|
+
* ```
|
|
572
470
|
*
|
|
573
|
-
* @param
|
|
574
|
-
* @param
|
|
575
|
-
* @returns
|
|
471
|
+
* @param model The data model.
|
|
472
|
+
* @param writer The writer to do changes with.
|
|
473
|
+
* @returns `true` if any change has been applied, `false` otherwise.
|
|
576
474
|
*/
|
|
577
|
-
export function modelChangePostFixer(
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
item = item.nextSibling;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function _fixListTypes( item ) {
|
|
712
|
-
let typesStack = [];
|
|
713
|
-
let prev = null;
|
|
714
|
-
|
|
715
|
-
while ( item && item.is( 'element', 'listItem' ) ) {
|
|
716
|
-
const itemIndent = item.getAttribute( 'listIndent' );
|
|
717
|
-
|
|
718
|
-
if ( prev && prev.getAttribute( 'listIndent' ) > itemIndent ) {
|
|
719
|
-
typesStack = typesStack.slice( 0, itemIndent + 1 );
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
if ( itemIndent != 0 ) {
|
|
723
|
-
if ( typesStack[ itemIndent ] ) {
|
|
724
|
-
const type = typesStack[ itemIndent ];
|
|
725
|
-
|
|
726
|
-
if ( item.getAttribute( 'listType' ) != type ) {
|
|
727
|
-
writer.setAttribute( 'listType', type, item );
|
|
728
|
-
|
|
729
|
-
applied = true;
|
|
730
|
-
}
|
|
731
|
-
} else {
|
|
732
|
-
typesStack[ itemIndent ] = item.getAttribute( 'listType' );
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
prev = item;
|
|
737
|
-
item = item.nextSibling;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
475
|
+
export function modelChangePostFixer(model, writer) {
|
|
476
|
+
const changes = model.document.differ.getChanges();
|
|
477
|
+
const itemToListHead = new Map();
|
|
478
|
+
let applied = false;
|
|
479
|
+
for (const entry of changes) {
|
|
480
|
+
if (entry.type == 'insert' && entry.name == 'listItem') {
|
|
481
|
+
_addListToFix(entry.position);
|
|
482
|
+
}
|
|
483
|
+
else if (entry.type == 'insert' && entry.name != 'listItem') {
|
|
484
|
+
if (entry.name != '$text') {
|
|
485
|
+
// In case of renamed element.
|
|
486
|
+
const item = entry.position.nodeAfter;
|
|
487
|
+
if (item.hasAttribute('listIndent')) {
|
|
488
|
+
writer.removeAttribute('listIndent', item);
|
|
489
|
+
applied = true;
|
|
490
|
+
}
|
|
491
|
+
if (item.hasAttribute('listType')) {
|
|
492
|
+
writer.removeAttribute('listType', item);
|
|
493
|
+
applied = true;
|
|
494
|
+
}
|
|
495
|
+
if (item.hasAttribute('listStyle')) {
|
|
496
|
+
writer.removeAttribute('listStyle', item);
|
|
497
|
+
applied = true;
|
|
498
|
+
}
|
|
499
|
+
if (item.hasAttribute('listReversed')) {
|
|
500
|
+
writer.removeAttribute('listReversed', item);
|
|
501
|
+
applied = true;
|
|
502
|
+
}
|
|
503
|
+
if (item.hasAttribute('listStart')) {
|
|
504
|
+
writer.removeAttribute('listStart', item);
|
|
505
|
+
applied = true;
|
|
506
|
+
}
|
|
507
|
+
for (const innerItem of Array.from(model.createRangeIn(item)).filter(e => e.item.is('element', 'listItem'))) {
|
|
508
|
+
_addListToFix(innerItem.previousPosition);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const posAfter = entry.position.getShiftedBy(entry.length);
|
|
512
|
+
_addListToFix(posAfter);
|
|
513
|
+
}
|
|
514
|
+
else if (entry.type == 'remove' && entry.name == 'listItem') {
|
|
515
|
+
_addListToFix(entry.position);
|
|
516
|
+
}
|
|
517
|
+
else if (entry.type == 'attribute' && entry.attributeKey == 'listIndent') {
|
|
518
|
+
_addListToFix(entry.range.start);
|
|
519
|
+
}
|
|
520
|
+
else if (entry.type == 'attribute' && entry.attributeKey == 'listType') {
|
|
521
|
+
_addListToFix(entry.range.start);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
for (const listHead of itemToListHead.values()) {
|
|
525
|
+
_fixListIndents(listHead);
|
|
526
|
+
_fixListTypes(listHead);
|
|
527
|
+
}
|
|
528
|
+
return applied;
|
|
529
|
+
function _addListToFix(position) {
|
|
530
|
+
const previousNode = position.nodeBefore;
|
|
531
|
+
if (!previousNode || !previousNode.is('element', 'listItem')) {
|
|
532
|
+
const item = position.nodeAfter;
|
|
533
|
+
if (item && item.is('element', 'listItem')) {
|
|
534
|
+
itemToListHead.set(item, item);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
let listHead = previousNode;
|
|
539
|
+
if (itemToListHead.has(listHead)) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
for (
|
|
543
|
+
// Cache previousSibling and reuse for performance reasons. See #6581.
|
|
544
|
+
let previousSibling = listHead.previousSibling; previousSibling && previousSibling.is('element', 'listItem'); previousSibling = listHead.previousSibling) {
|
|
545
|
+
listHead = previousSibling;
|
|
546
|
+
if (itemToListHead.has(listHead)) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
itemToListHead.set(previousNode, listHead);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function _fixListIndents(item) {
|
|
554
|
+
let maxIndent = 0;
|
|
555
|
+
let fixBy = null;
|
|
556
|
+
while (item && item.is('element', 'listItem')) {
|
|
557
|
+
const itemIndent = item.getAttribute('listIndent');
|
|
558
|
+
if (itemIndent > maxIndent) {
|
|
559
|
+
let newIndent;
|
|
560
|
+
if (fixBy === null) {
|
|
561
|
+
fixBy = itemIndent - maxIndent;
|
|
562
|
+
newIndent = maxIndent;
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
if (fixBy > itemIndent) {
|
|
566
|
+
fixBy = itemIndent;
|
|
567
|
+
}
|
|
568
|
+
newIndent = itemIndent - fixBy;
|
|
569
|
+
}
|
|
570
|
+
writer.setAttribute('listIndent', newIndent, item);
|
|
571
|
+
applied = true;
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
fixBy = null;
|
|
575
|
+
maxIndent = item.getAttribute('listIndent') + 1;
|
|
576
|
+
}
|
|
577
|
+
item = item.nextSibling;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function _fixListTypes(item) {
|
|
581
|
+
let typesStack = [];
|
|
582
|
+
let prev = null;
|
|
583
|
+
while (item && item.is('element', 'listItem')) {
|
|
584
|
+
const itemIndent = item.getAttribute('listIndent');
|
|
585
|
+
if (prev && prev.getAttribute('listIndent') > itemIndent) {
|
|
586
|
+
typesStack = typesStack.slice(0, itemIndent + 1);
|
|
587
|
+
}
|
|
588
|
+
if (itemIndent != 0) {
|
|
589
|
+
if (typesStack[itemIndent]) {
|
|
590
|
+
const type = typesStack[itemIndent];
|
|
591
|
+
if (item.getAttribute('listType') != type) {
|
|
592
|
+
writer.setAttribute('listType', type, item);
|
|
593
|
+
applied = true;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
typesStack[itemIndent] = item.getAttribute('listType');
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
prev = item;
|
|
601
|
+
item = item.nextSibling;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
740
604
|
}
|
|
741
|
-
|
|
742
605
|
/**
|
|
743
606
|
* A fixer for pasted content that includes list items.
|
|
744
607
|
*
|
|
@@ -746,317 +609,297 @@ export function modelChangePostFixer( model, writer ) {
|
|
|
746
609
|
*
|
|
747
610
|
* Example:
|
|
748
611
|
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
612
|
+
* ```xml
|
|
613
|
+
* <listItem listType="bulleted" listIndent=0>A</listItem>
|
|
614
|
+
* <listItem listType="bulleted" listIndent=1>B^</listItem>
|
|
615
|
+
* // At ^ paste: <listItem listType="bulleted" listIndent=4>X</listItem>
|
|
616
|
+
* // <listItem listType="bulleted" listIndent=5>Y</listItem>
|
|
617
|
+
* <listItem listType="bulleted" listIndent=2>C</listItem>
|
|
618
|
+
* ```
|
|
754
619
|
*
|
|
755
620
|
* Should become:
|
|
756
621
|
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
* @param {Array} args Arguments of {@link module:engine/model/model~Model#insertContent}.
|
|
622
|
+
* ```xml
|
|
623
|
+
* <listItem listType="bulleted" listIndent=0>A</listItem>
|
|
624
|
+
* <listItem listType="bulleted" listIndent=1>BX</listItem>
|
|
625
|
+
* <listItem listType="bulleted" listIndent=2>Y/listItem>
|
|
626
|
+
* <listItem listType="bulleted" listIndent=2>C</listItem>
|
|
627
|
+
* ```
|
|
764
628
|
*/
|
|
765
|
-
export function
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
nextPosition = writer.createPositionAfter( listItemModel );
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
return nextPosition;
|
|
629
|
+
export const modelIndentPasteFixer = function (evt, [content, selectable, placeOrOffset]) {
|
|
630
|
+
const model = this;
|
|
631
|
+
// Check whether inserted content starts from a `listItem`. If it does not, it means that there are some other
|
|
632
|
+
// elements before it and there is no need to fix indents, because even if we insert that content into a list,
|
|
633
|
+
// that list will be broken.
|
|
634
|
+
// Note: we also need to handle singular elements because inserting item with indent 0 into 0,1,[],2
|
|
635
|
+
// would create incorrect model.
|
|
636
|
+
let item = content.is('documentFragment') ? content.getChild(0) : content;
|
|
637
|
+
let selection;
|
|
638
|
+
if (!selectable) {
|
|
639
|
+
selection = model.document.selection;
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
selection = model.createSelection(selectable, placeOrOffset);
|
|
643
|
+
}
|
|
644
|
+
if (item && item.is('element', 'listItem')) {
|
|
645
|
+
// Get a reference list item. Inserted list items will be fixed according to that item.
|
|
646
|
+
const pos = selection.getFirstPosition();
|
|
647
|
+
let refItem = null;
|
|
648
|
+
if (pos.parent.is('element', 'listItem')) {
|
|
649
|
+
refItem = pos.parent;
|
|
650
|
+
}
|
|
651
|
+
else if (pos.nodeBefore && pos.nodeBefore.is('element', 'listItem')) {
|
|
652
|
+
refItem = pos.nodeBefore;
|
|
653
|
+
}
|
|
654
|
+
// If there is `refItem` it means that we do insert list items into an existing list.
|
|
655
|
+
if (refItem) {
|
|
656
|
+
// First list item in `data` has indent equal to 0 (it is a first list item). It should have indent equal
|
|
657
|
+
// to the indent of reference item. We have to fix the first item and all of it's children and following siblings.
|
|
658
|
+
// Indent of all those items has to be adjusted to reference item.
|
|
659
|
+
const indentChange = refItem.getAttribute('listIndent');
|
|
660
|
+
// Fix only if there is anything to fix.
|
|
661
|
+
if (indentChange > 0) {
|
|
662
|
+
// Adjust indent of all "first" list items in inserted data.
|
|
663
|
+
while (item && item.is('element', 'listItem')) {
|
|
664
|
+
item._setAttribute('listIndent', item.getAttribute('listIndent') + indentChange);
|
|
665
|
+
item = item.nextSibling;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
/**
|
|
672
|
+
* Helper function that converts children of a given `<li>` view element into corresponding model elements.
|
|
673
|
+
* The function maintains proper order of elements if model `listItem` is split during the conversion
|
|
674
|
+
* due to block children conversion.
|
|
675
|
+
*
|
|
676
|
+
* @param listItemModel List item model element to which converted children will be inserted.
|
|
677
|
+
* @param viewChildren View elements which will be converted.
|
|
678
|
+
* @param conversionApi Conversion interface to be used by the callback.
|
|
679
|
+
* @returns Position on which next elements should be inserted after children conversion.
|
|
680
|
+
*/
|
|
681
|
+
function viewToModelListItemChildrenConverter(listItemModel, viewChildren, conversionApi) {
|
|
682
|
+
const { writer, schema } = conversionApi;
|
|
683
|
+
// A position after the last inserted `listItem`.
|
|
684
|
+
let nextPosition = writer.createPositionAfter(listItemModel);
|
|
685
|
+
// Check all children of the converted `<li>`. At this point we assume there are no "whitespace" view text nodes
|
|
686
|
+
// in view list, between view list items. This should be handled by `<ul>` and `<ol>` converters.
|
|
687
|
+
for (const child of viewChildren) {
|
|
688
|
+
if (child.name == 'ul' || child.name == 'ol') {
|
|
689
|
+
// If the children is a list, we will insert its conversion result after currently handled `listItem`.
|
|
690
|
+
// Then, next insertion position will be set after all the new list items (and maybe other elements if
|
|
691
|
+
// something split list item).
|
|
692
|
+
//
|
|
693
|
+
// If this is a list, we expect that some `listItem`s and possibly other blocks will be inserted, however `.modelCursor`
|
|
694
|
+
// should be set after last `listItem` (or block). This is why it feels safe to use it as `nextPosition`
|
|
695
|
+
nextPosition = conversionApi.convertItem(child, nextPosition).modelCursor;
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
// If this is not a list, try inserting content at the end of the currently handled `listItem`.
|
|
699
|
+
const result = conversionApi.convertItem(child, writer.createPositionAt(listItemModel, 'end'));
|
|
700
|
+
// It may end up that the current `listItem` becomes split (if that content cannot be inside `listItem`). For example:
|
|
701
|
+
//
|
|
702
|
+
// <li><p>Foo</p></li>
|
|
703
|
+
//
|
|
704
|
+
// will be converted to:
|
|
705
|
+
//
|
|
706
|
+
// <listItem></listItem><paragraph>Foo</paragraph><listItem></listItem>
|
|
707
|
+
//
|
|
708
|
+
const convertedChild = result.modelRange.start.nodeAfter;
|
|
709
|
+
const wasSplit = convertedChild && convertedChild.is('element') && !schema.checkChild(listItemModel, convertedChild.name);
|
|
710
|
+
if (wasSplit) {
|
|
711
|
+
// As `lastListItem` got split, we need to update it to the second part of the split `listItem` element.
|
|
712
|
+
//
|
|
713
|
+
// `modelCursor` should be set to a position where the conversion should continue. There are multiple possible scenarios
|
|
714
|
+
// that may happen. Usually, `modelCursor` (marked as `#` below) would point to the second list item after conversion:
|
|
715
|
+
//
|
|
716
|
+
// `<li><p>Foo</p></li>` -> `<listItem></listItem><paragraph>Foo</paragraph><listItem>#</listItem>`
|
|
717
|
+
//
|
|
718
|
+
// However, in some cases, like auto-paragraphing, the position is placed at the end of the block element:
|
|
719
|
+
//
|
|
720
|
+
// `<li><div>Foo</div></li>` -> `<listItem></listItem><paragraph>Foo#</paragraph><listItem></listItem>`
|
|
721
|
+
//
|
|
722
|
+
// or after an element if another element broken auto-paragraphed element:
|
|
723
|
+
//
|
|
724
|
+
// `<li><div><h2>Foo</h2></div></li>` -> `<listItem></listItem><heading1>Foo</heading1>#<listItem></listItem>`
|
|
725
|
+
//
|
|
726
|
+
// We need to check for such cases and use proper list item and position based on it.
|
|
727
|
+
//
|
|
728
|
+
if (result.modelCursor.parent.is('element', 'listItem')) {
|
|
729
|
+
// (1).
|
|
730
|
+
listItemModel = result.modelCursor.parent;
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
// (2), (3).
|
|
734
|
+
listItemModel = findNextListItem(result.modelCursor);
|
|
735
|
+
}
|
|
736
|
+
nextPosition = writer.createPositionAfter(listItemModel);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return nextPosition;
|
|
884
741
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
return value.value.item;
|
|
742
|
+
/**
|
|
743
|
+
* Helper function that seeks for a next list item starting from given `startPosition`.
|
|
744
|
+
*/
|
|
745
|
+
function findNextListItem(startPosition) {
|
|
746
|
+
const treeWalker = new TreeWalker({ startPosition });
|
|
747
|
+
let value;
|
|
748
|
+
do {
|
|
749
|
+
value = treeWalker.next();
|
|
750
|
+
} while (!value.value.item.is('element', 'listItem'));
|
|
751
|
+
return value.value.item;
|
|
897
752
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
mergeViewLists( viewWriter, child, child.nextSibling );
|
|
991
|
-
mergeViewLists( viewWriter, child.previousSibling, child );
|
|
992
|
-
}
|
|
993
|
-
}
|
|
753
|
+
/**
|
|
754
|
+
* Helper function that takes all children of given `viewRemovedItem` and moves them in a correct place, according
|
|
755
|
+
* to other given parameters.
|
|
756
|
+
*/
|
|
757
|
+
function hoistNestedLists(nextIndent, modelRemoveStartPosition, viewRemoveStartPosition, viewRemovedItem, conversionApi, model) {
|
|
758
|
+
// Find correct previous model list item element.
|
|
759
|
+
// The element has to have either same or smaller indent than given reference indent.
|
|
760
|
+
// This will be the model element which will get nested items (if it has smaller indent) or sibling items (if it has same indent).
|
|
761
|
+
// Keep in mind that such element might not be found, if removed item was the first item.
|
|
762
|
+
const prevModelItem = getSiblingListItem(modelRemoveStartPosition.nodeBefore, {
|
|
763
|
+
sameIndent: true,
|
|
764
|
+
smallerIndent: true,
|
|
765
|
+
listIndent: nextIndent
|
|
766
|
+
});
|
|
767
|
+
const mapper = conversionApi.mapper;
|
|
768
|
+
const viewWriter = conversionApi.writer;
|
|
769
|
+
// Indent of found element or `null` if the element has not been found.
|
|
770
|
+
const prevIndent = prevModelItem ? prevModelItem.getAttribute('listIndent') : null;
|
|
771
|
+
let insertPosition;
|
|
772
|
+
if (!prevModelItem) {
|
|
773
|
+
// If element has not been found, simply insert lists at the position where the removed item was:
|
|
774
|
+
//
|
|
775
|
+
// Lorem ipsum.
|
|
776
|
+
// 1 -------- <--- this is removed, no previous list item, put nested items in place of removed item.
|
|
777
|
+
// 1.1 -------- <--- this is reference indent.
|
|
778
|
+
// 1.1.1 --------
|
|
779
|
+
// 1.1.2 --------
|
|
780
|
+
// 1.2 --------
|
|
781
|
+
//
|
|
782
|
+
// Becomes:
|
|
783
|
+
//
|
|
784
|
+
// Lorem ipsum.
|
|
785
|
+
// 1.1 --------
|
|
786
|
+
// 1.1.1 --------
|
|
787
|
+
// 1.1.2 --------
|
|
788
|
+
// 1.2 --------
|
|
789
|
+
insertPosition = viewRemoveStartPosition;
|
|
790
|
+
}
|
|
791
|
+
else if (prevIndent == nextIndent) {
|
|
792
|
+
// If element has been found and has same indent as reference indent it means that nested items should
|
|
793
|
+
// become siblings of found element:
|
|
794
|
+
//
|
|
795
|
+
// 1 --------
|
|
796
|
+
// 1.1 --------
|
|
797
|
+
// 1.2 -------- <--- this is `prevModelItem`.
|
|
798
|
+
// 2 -------- <--- this is removed, previous list item has indent same as reference indent.
|
|
799
|
+
// 2.1 -------- <--- this is reference indent, this and 2.2 should become siblings of 1.2.
|
|
800
|
+
// 2.2 --------
|
|
801
|
+
//
|
|
802
|
+
// Becomes:
|
|
803
|
+
//
|
|
804
|
+
// 1 --------
|
|
805
|
+
// 1.1 --------
|
|
806
|
+
// 1.2 --------
|
|
807
|
+
// 2.1 --------
|
|
808
|
+
// 2.2 --------
|
|
809
|
+
const prevViewList = mapper.toViewElement(prevModelItem).parent;
|
|
810
|
+
insertPosition = viewWriter.createPositionAfter(prevViewList);
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
// If element has been found and has smaller indent as reference indent it means that nested items
|
|
814
|
+
// should become nested items of found item:
|
|
815
|
+
//
|
|
816
|
+
// 1 -------- <--- this is `prevModelItem`.
|
|
817
|
+
// 1.1 -------- <--- this is removed, previous list item has indent smaller than reference indent.
|
|
818
|
+
// 1.1.1 -------- <--- this is reference indent, this and 1.1.1 should become nested items of 1.
|
|
819
|
+
// 1.1.2 --------
|
|
820
|
+
// 1.2 --------
|
|
821
|
+
//
|
|
822
|
+
// Becomes:
|
|
823
|
+
//
|
|
824
|
+
// 1 --------
|
|
825
|
+
// 1.1.1 --------
|
|
826
|
+
// 1.1.2 --------
|
|
827
|
+
// 1.2 --------
|
|
828
|
+
//
|
|
829
|
+
// Note: in this case 1.1.1 have indent 2 while 1 have indent 0. In model that should not be possible,
|
|
830
|
+
// because following item may have indent bigger only by one. But this is fixed by postfixer.
|
|
831
|
+
const modelPosition = model.createPositionAt(prevModelItem, 'end');
|
|
832
|
+
insertPosition = mapper.toViewPosition(modelPosition);
|
|
833
|
+
}
|
|
834
|
+
insertPosition = positionAfterUiElements(insertPosition);
|
|
835
|
+
// Handle multiple lists. This happens if list item has nested numbered and bulleted lists. Following lists
|
|
836
|
+
// are inserted after the first list (no need to recalculate insertion position for them).
|
|
837
|
+
for (const child of [...viewRemovedItem.getChildren()]) {
|
|
838
|
+
if (isList(child)) {
|
|
839
|
+
insertPosition = viewWriter.move(viewWriter.createRangeOn(child), insertPosition).end;
|
|
840
|
+
mergeViewLists(viewWriter, child, child.nextSibling);
|
|
841
|
+
mergeViewLists(viewWriter, child.previousSibling, child);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
994
844
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
function isList( viewElement ) {
|
|
1001
|
-
return viewElement.is( 'element', 'ol' ) || viewElement.is( 'element', 'ul' );
|
|
845
|
+
/**
|
|
846
|
+
* Checks if view element is a list type (ul or ol).
|
|
847
|
+
*/
|
|
848
|
+
function isList(viewElement) {
|
|
849
|
+
return viewElement.is('element', 'ol') || viewElement.is('element', 'ul');
|
|
1002
850
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
parent = parent.parent;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
return indent;
|
|
851
|
+
/**
|
|
852
|
+
* Calculates the indent value for a list item. Handles HTML compliant and non-compliant lists.
|
|
853
|
+
*
|
|
854
|
+
* Also, fixes non HTML compliant lists indents:
|
|
855
|
+
*
|
|
856
|
+
* ```
|
|
857
|
+
* before: fixed list:
|
|
858
|
+
* OL OL
|
|
859
|
+
* |-> LI (parent LIs: 0) |-> LI (indent: 0)
|
|
860
|
+
* |-> OL |-> OL
|
|
861
|
+
* |-> OL |
|
|
862
|
+
* | |-> OL |
|
|
863
|
+
* | |-> OL |
|
|
864
|
+
* | |-> LI (parent LIs: 1) |-> LI (indent: 1)
|
|
865
|
+
* |-> LI (parent LIs: 1) |-> LI (indent: 1)
|
|
866
|
+
*
|
|
867
|
+
* before: fixed list:
|
|
868
|
+
* OL OL
|
|
869
|
+
* |-> OL |
|
|
870
|
+
* |-> OL |
|
|
871
|
+
* |-> OL |
|
|
872
|
+
* |-> LI (parent LIs: 0) |-> LI (indent: 0)
|
|
873
|
+
*
|
|
874
|
+
* before: fixed list:
|
|
875
|
+
* OL OL
|
|
876
|
+
* |-> LI (parent LIs: 0) |-> LI (indent: 0)
|
|
877
|
+
* |-> OL |-> OL
|
|
878
|
+
* |-> LI (parent LIs: 0) |-> LI (indent: 1)
|
|
879
|
+
* ```
|
|
880
|
+
*/
|
|
881
|
+
function getIndent(listItem) {
|
|
882
|
+
let indent = 0;
|
|
883
|
+
let parent = listItem.parent;
|
|
884
|
+
while (parent) {
|
|
885
|
+
// Each LI in the tree will result in an increased indent for HTML compliant lists.
|
|
886
|
+
if (parent.is('element', 'li')) {
|
|
887
|
+
indent++;
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
// If however the list is nested in other list we should check previous sibling of any of the list elements...
|
|
891
|
+
const previousSibling = parent.previousSibling;
|
|
892
|
+
// ...because the we might need increase its indent:
|
|
893
|
+
// before: fixed list:
|
|
894
|
+
// OL OL
|
|
895
|
+
// |-> LI (parent LIs: 0) |-> LI (indent: 0)
|
|
896
|
+
// |-> OL |-> OL
|
|
897
|
+
// |-> LI (parent LIs: 0) |-> LI (indent: 1)
|
|
898
|
+
if (previousSibling && previousSibling.is('element', 'li')) {
|
|
899
|
+
indent++;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
parent = parent.parent;
|
|
903
|
+
}
|
|
904
|
+
return indent;
|
|
1062
905
|
}
|