@ckeditor/ckeditor5-list 35.3.2 → 36.0.0

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.
Files changed (55) hide show
  1. package/LICENSE.md +1 -1
  2. package/build/list.js +2 -2
  3. package/package.json +43 -39
  4. package/src/documentlist/converters.js +303 -419
  5. package/src/documentlist/documentlistcommand.js +136 -207
  6. package/src/documentlist/documentlistediting.js +538 -697
  7. package/src/documentlist/documentlistindentcommand.js +115 -168
  8. package/src/documentlist/documentlistmergecommand.js +161 -222
  9. package/src/documentlist/documentlistsplitcommand.js +59 -103
  10. package/src/documentlist/documentlistutils.js +41 -0
  11. package/src/documentlist/utils/listwalker.js +138 -236
  12. package/src/documentlist/utils/model.js +322 -421
  13. package/src/documentlist/utils/postfixers.js +98 -118
  14. package/src/documentlist/utils/view.js +74 -105
  15. package/src/documentlist.js +13 -19
  16. package/src/documentlistproperties/converters.js +33 -47
  17. package/src/documentlistproperties/documentlistpropertiesediting.js +266 -354
  18. package/src/documentlistproperties/documentlistpropertiesutils.js +44 -0
  19. package/src/documentlistproperties/documentlistreversedcommand.js +40 -61
  20. package/src/documentlistproperties/documentliststartcommand.js +42 -61
  21. package/src/documentlistproperties/documentliststylecommand.js +97 -147
  22. package/src/documentlistproperties/utils/style.js +27 -47
  23. package/src/documentlistproperties.js +13 -19
  24. package/src/index.js +4 -3
  25. package/src/list/converters.js +772 -929
  26. package/src/list/indentcommand.js +105 -140
  27. package/src/list/listcommand.js +262 -315
  28. package/src/list/listediting.js +142 -200
  29. package/src/list/listui.js +16 -25
  30. package/src/list/listutils.js +46 -0
  31. package/src/list/utils.js +295 -378
  32. package/src/list.js +13 -44
  33. package/src/listcommands.js +5 -0
  34. package/src/listconfig.js +5 -0
  35. package/src/listproperties/listpropertiesediting.js +656 -801
  36. package/src/listproperties/listpropertiesui.js +244 -296
  37. package/src/listproperties/listreversedcommand.js +37 -49
  38. package/src/listproperties/liststartcommand.js +37 -49
  39. package/src/listproperties/liststylecommand.js +82 -115
  40. package/src/listproperties/ui/collapsibleview.js +75 -138
  41. package/src/listproperties/ui/listpropertiesview.js +289 -414
  42. package/src/listproperties.js +13 -118
  43. package/src/liststyle.js +18 -24
  44. package/src/todolist/checktodolistcommand.js +60 -102
  45. package/src/todolist/todolistconverters.js +189 -271
  46. package/src/todolist/todolistediting.js +141 -206
  47. package/src/todolist/todolistui.js +14 -21
  48. package/src/todolist.js +13 -19
  49. package/theme/collapsible.css +1 -1
  50. package/theme/documentlist.css +1 -1
  51. package/theme/list.css +40 -0
  52. package/theme/listproperties.css +1 -1
  53. package/theme/liststyles.css +1 -37
  54. package/theme/todolist.css +1 -1
  55. package/build/list.js.map +0 -1
@@ -1,470 +1,354 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
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/documentlist/converters
8
7
  */
9
-
10
- import {
11
- getAllListItemBlocks,
12
- getListItemBlocks,
13
- isListItemBlock,
14
- ListItemUid
15
- } from './utils/model';
16
- import {
17
- createListElement,
18
- createListItemElement,
19
- getIndent,
20
- isListView,
21
- isListItemView
22
- } from './utils/view';
8
+ import { UpcastWriter } from 'ckeditor5/src/engine';
9
+ import { getAllListItemBlocks, getListItemBlocks, isListItemBlock, ListItemUid } from './utils/model';
10
+ import { createListElement, createListItemElement, getIndent, isListView, isListItemView } from './utils/view';
23
11
  import ListWalker, { iterateSiblingListBlocks } from './utils/listwalker';
24
12
  import { findAndAddListHeadToMap } from './utils/postfixers';
25
-
26
- import { UpcastWriter } from 'ckeditor5/src/engine';
27
-
28
13
  /**
29
14
  * Returns the upcast converter for list items. It's supposed to work after the block converters (content inside list items) is converted.
30
15
  *
31
- * @protected
32
- * @returns {Function}
16
+ * @internal
33
17
  */
34
18
  export function listItemUpcastConverter() {
35
- return ( evt, data, conversionApi ) => {
36
- const { writer, schema } = conversionApi;
37
-
38
- if ( !data.modelRange ) {
39
- return;
40
- }
41
-
42
- const items = Array.from( data.modelRange.getItems( { shallow: true } ) )
43
- .filter( item => schema.checkAttribute( item, 'listItemId' ) );
44
-
45
- if ( !items.length ) {
46
- return;
47
- }
48
-
49
- const attributes = {
50
- listItemId: ListItemUid.next(),
51
- listIndent: getIndent( data.viewItem ),
52
- listType: data.viewItem.parent && data.viewItem.parent.name == 'ol' ? 'numbered' : 'bulleted'
53
- };
54
-
55
- for ( const item of items ) {
56
- // Set list attributes only on same level items, those nested deeper are already handled by the recursive conversion.
57
- if ( !isListItemBlock( item ) ) {
58
- writer.setAttributes( attributes, item );
59
- }
60
- }
61
-
62
- if ( items.length > 1 ) {
63
- // Make sure that list item that contain only nested list will preserve paragraph for itself:
64
- // <ul>
65
- // <li>
66
- // <p></p> <-- this one must be kept
67
- // <ul>
68
- // <li></li>
69
- // </ul>
70
- // </li>
71
- // </ul>
72
- if ( items[ 1 ].getAttribute( 'listItemId' ) != attributes.listItemId ) {
73
- conversionApi.keepEmptyElement( items[ 0 ] );
74
- }
75
- }
76
- };
19
+ return (evt, data, conversionApi) => {
20
+ const { writer, schema } = conversionApi;
21
+ if (!data.modelRange) {
22
+ return;
23
+ }
24
+ const items = Array.from(data.modelRange.getItems({ shallow: true }))
25
+ .filter((item) => schema.checkAttribute(item, 'listItemId'));
26
+ if (!items.length) {
27
+ return;
28
+ }
29
+ const attributes = {
30
+ listItemId: ListItemUid.next(),
31
+ listIndent: getIndent(data.viewItem),
32
+ listType: data.viewItem.parent && data.viewItem.parent.is('element', 'ol') ? 'numbered' : 'bulleted'
33
+ };
34
+ for (const item of items) {
35
+ // Set list attributes only on same level items, those nested deeper are already handled by the recursive conversion.
36
+ if (!isListItemBlock(item)) {
37
+ writer.setAttributes(attributes, item);
38
+ }
39
+ }
40
+ if (items.length > 1) {
41
+ // Make sure that list item that contain only nested list will preserve paragraph for itself:
42
+ // <ul>
43
+ // <li>
44
+ // <p></p> <-- this one must be kept
45
+ // <ul>
46
+ // <li></li>
47
+ // </ul>
48
+ // </li>
49
+ // </ul>
50
+ if (items[1].getAttribute('listItemId') != attributes.listItemId) {
51
+ conversionApi.keepEmptyElement(items[0]);
52
+ }
53
+ }
54
+ };
77
55
  }
78
-
79
56
  /**
80
57
  * Returns the upcast converter for the `<ul>` and `<ol>` view elements that cleans the input view of garbage.
81
58
  * This is mostly to clean whitespaces from between the `<li>` view elements inside the view list element, however, also
82
59
  * incorrect data can be cleared if the view was incorrect.
83
60
  *
84
- * @protected
85
- * @returns {Function}
61
+ * @internal
86
62
  */
87
63
  export function listUpcastCleanList() {
88
- return ( evt, data, conversionApi ) => {
89
- if ( !conversionApi.consumable.test( data.viewItem, { name: true } ) ) {
90
- return;
91
- }
92
-
93
- const viewWriter = new UpcastWriter( data.viewItem.document );
94
-
95
- for ( const child of Array.from( data.viewItem.getChildren() ) ) {
96
- if ( !isListItemView( child ) && !isListView( child ) ) {
97
- viewWriter.remove( child );
98
- }
99
- }
100
- };
64
+ return (evt, data, conversionApi) => {
65
+ if (!conversionApi.consumable.test(data.viewItem, { name: true })) {
66
+ return;
67
+ }
68
+ const viewWriter = new UpcastWriter(data.viewItem.document);
69
+ for (const child of Array.from(data.viewItem.getChildren())) {
70
+ if (!isListItemView(child) && !isListView(child)) {
71
+ viewWriter.remove(child);
72
+ }
73
+ }
74
+ };
101
75
  }
102
-
103
76
  /**
104
77
  * Returns a model document change:data event listener that triggers conversion of related items if needed.
105
78
  *
106
- * @protected
107
- * @param {module:engine/model/model~Model} model The editor model.
108
- * @param {module:engine/controller/editingcontroller~EditingController} editing The editing controller.
109
- * @param {Array.<String>} attributeNames The list of all model list attributes (including registered strategies).
110
- * @param {module:list/documentlist/documentlistediting~DocumentListEditing} documentListEditing The document list editing plugin.
111
- * @return {Function}
79
+ * @internal
80
+ * @param model The editor model.
81
+ * @param editing The editing controller.
82
+ * @param attributeNames The list of all model list attributes (including registered strategies).
83
+ * @param documentListEditing The document list editing plugin.
112
84
  */
113
- export function reconvertItemsOnDataChange( model, editing, attributeNames, documentListEditing ) {
114
- return () => {
115
- const changes = model.document.differ.getChanges();
116
- const itemsToRefresh = [];
117
- const itemToListHead = new Map();
118
- const changedItems = new Set();
119
-
120
- for ( const entry of changes ) {
121
- if ( entry.type == 'insert' && entry.name != '$text' ) {
122
- findAndAddListHeadToMap( entry.position, itemToListHead );
123
-
124
- // Insert of a non-list item.
125
- if ( !entry.attributes.has( 'listItemId' ) ) {
126
- findAndAddListHeadToMap( entry.position.getShiftedBy( entry.length ), itemToListHead );
127
- } else {
128
- changedItems.add( entry.position.nodeAfter );
129
- }
130
- }
131
- // Removed list item.
132
- else if ( entry.type == 'remove' && entry.attributes.has( 'listItemId' ) ) {
133
- findAndAddListHeadToMap( entry.position, itemToListHead );
134
- }
135
- // Changed list attribute.
136
- else if ( entry.type == 'attribute' ) {
137
- const item = entry.range.start.nodeAfter;
138
-
139
- if ( attributeNames.includes( entry.attributeKey ) ) {
140
- findAndAddListHeadToMap( entry.range.start, itemToListHead );
141
-
142
- if ( entry.attributeNewValue === null ) {
143
- findAndAddListHeadToMap( entry.range.start.getShiftedBy( 1 ), itemToListHead );
144
-
145
- // Check if paragraph should be converted from bogus to plain paragraph.
146
- if ( doesItemParagraphRequiresRefresh( item ) ) {
147
- itemsToRefresh.push( item );
148
- }
149
- } else {
150
- changedItems.add( item );
151
- }
152
- } else if ( isListItemBlock( item ) ) {
153
- // Some other attribute was changed on the list item,
154
- // check if paragraph does not need to be converted to bogus or back.
155
- if ( doesItemParagraphRequiresRefresh( item ) ) {
156
- itemsToRefresh.push( item );
157
- }
158
- }
159
- }
160
- }
161
-
162
- for ( const listHead of itemToListHead.values() ) {
163
- itemsToRefresh.push( ...collectListItemsToRefresh( listHead, changedItems ) );
164
- }
165
-
166
- for ( const item of new Set( itemsToRefresh ) ) {
167
- editing.reconvertItem( item );
168
- }
169
- };
170
-
171
- function collectListItemsToRefresh( listHead, changedItems ) {
172
- const itemsToRefresh = [];
173
- const visited = new Set();
174
- const stack = [];
175
-
176
- for ( const { node, previous } of iterateSiblingListBlocks( listHead, 'forward' ) ) {
177
- if ( visited.has( node ) ) {
178
- continue;
179
- }
180
-
181
- const itemIndent = node.getAttribute( 'listIndent' );
182
-
183
- // Current node is at the lower indent so trim the stack.
184
- if ( previous && itemIndent < previous.getAttribute( 'listIndent' ) ) {
185
- stack.length = itemIndent + 1;
186
- }
187
-
188
- // Update the stack for the current indent level.
189
- stack[ itemIndent ] = Object.fromEntries(
190
- Array.from( node.getAttributes() )
191
- .filter( ( [ key ] ) => attributeNames.includes( key ) )
192
- );
193
-
194
- // Find all blocks of the current node.
195
- const blocks = getListItemBlocks( node, { direction: 'forward' } );
196
-
197
- for ( const block of blocks ) {
198
- visited.add( block );
199
-
200
- // Check if bogus vs plain paragraph needs refresh.
201
- if ( doesItemParagraphRequiresRefresh( block, blocks ) ) {
202
- itemsToRefresh.push( block );
203
- }
204
- // Check if wrapping with UL, OL, LIs needs refresh.
205
- else if ( doesItemWrappingRequiresRefresh( block, stack, changedItems ) ) {
206
- itemsToRefresh.push( block );
207
- }
208
- }
209
- }
210
-
211
- return itemsToRefresh;
212
- }
213
-
214
- function doesItemParagraphRequiresRefresh( item, blocks ) {
215
- if ( !item.is( 'element', 'paragraph' ) ) {
216
- return false;
217
- }
218
-
219
- const viewElement = editing.mapper.toViewElement( item );
220
-
221
- if ( !viewElement ) {
222
- return false;
223
- }
224
-
225
- const useBogus = shouldUseBogusParagraph( item, attributeNames, blocks );
226
-
227
- if ( useBogus && viewElement.is( 'element', 'p' ) ) {
228
- return true;
229
- } else if ( !useBogus && viewElement.is( 'element', 'span' ) ) {
230
- return true;
231
- }
232
-
233
- return false;
234
- }
235
-
236
- function doesItemWrappingRequiresRefresh( item, stack, changedItems ) {
237
- // Items directly affected by some "change" don't need a refresh, they will be converted by their own changes.
238
- if ( changedItems.has( item ) ) {
239
- return false;
240
- }
241
-
242
- const viewElement = editing.mapper.toViewElement( item );
243
- let indent = stack.length - 1;
244
-
245
- // Traverse down the stack to the root to verify if all ULs, OLs, and LIs are as expected.
246
- for (
247
- let element = viewElement.parent;
248
- !element.is( 'editableElement' );
249
- element = element.parent
250
- ) {
251
- const isListItemElement = isListItemView( element );
252
- const isListElement = isListView( element );
253
-
254
- if ( !isListElement && !isListItemElement ) {
255
- continue;
256
- }
257
-
258
- /**
259
- * Event fired on changes detected on the model list element to verify if the view representation of a list element
260
- * is representing those attributes.
261
- *
262
- * It allows triggering a re-wrapping of a list item.
263
- *
264
- * **Note**: For convenience this event is namespaced and could be captured as `checkAttributes:list` or `checkAttributes:item`.
265
- *
266
- * @protected
267
- * @event module:list/documentlist/documentlistediting~DocumentListEditing#event:checkAttributes
268
- * @param {module:engine/view/element~Element} viewElement
269
- * @param {Object} modelAttributes
270
- */
271
- const eventName = `checkAttributes:${ isListItemElement ? 'item' : 'list' }`;
272
- const needsRefresh = documentListEditing.fire( eventName, {
273
- viewElement: element,
274
- modelAttributes: stack[ indent ]
275
- } );
276
-
277
- if ( needsRefresh ) {
278
- break;
279
- }
280
-
281
- if ( isListElement ) {
282
- indent--;
283
-
284
- // Don't need to iterate further if we already know that the item is wrapped appropriately.
285
- if ( indent < 0 ) {
286
- return false;
287
- }
288
- }
289
- }
290
-
291
- return true;
292
- }
85
+ export function reconvertItemsOnDataChange(model, editing, attributeNames, documentListEditing) {
86
+ return () => {
87
+ const changes = model.document.differ.getChanges();
88
+ const itemsToRefresh = [];
89
+ const itemToListHead = new Map();
90
+ const changedItems = new Set();
91
+ for (const entry of changes) {
92
+ if (entry.type == 'insert' && entry.name != '$text') {
93
+ findAndAddListHeadToMap(entry.position, itemToListHead);
94
+ // Insert of a non-list item.
95
+ if (!entry.attributes.has('listItemId')) {
96
+ findAndAddListHeadToMap(entry.position.getShiftedBy(entry.length), itemToListHead);
97
+ }
98
+ else {
99
+ changedItems.add(entry.position.nodeAfter);
100
+ }
101
+ }
102
+ // Removed list item.
103
+ else if (entry.type == 'remove' && entry.attributes.has('listItemId')) {
104
+ findAndAddListHeadToMap(entry.position, itemToListHead);
105
+ }
106
+ // Changed list attribute.
107
+ else if (entry.type == 'attribute') {
108
+ const item = entry.range.start.nodeAfter;
109
+ if (attributeNames.includes(entry.attributeKey)) {
110
+ findAndAddListHeadToMap(entry.range.start, itemToListHead);
111
+ if (entry.attributeNewValue === null) {
112
+ findAndAddListHeadToMap(entry.range.start.getShiftedBy(1), itemToListHead);
113
+ // Check if paragraph should be converted from bogus to plain paragraph.
114
+ if (doesItemParagraphRequiresRefresh(item)) {
115
+ itemsToRefresh.push(item);
116
+ }
117
+ }
118
+ else {
119
+ changedItems.add(item);
120
+ }
121
+ }
122
+ else if (isListItemBlock(item)) {
123
+ // Some other attribute was changed on the list item,
124
+ // check if paragraph does not need to be converted to bogus or back.
125
+ if (doesItemParagraphRequiresRefresh(item)) {
126
+ itemsToRefresh.push(item);
127
+ }
128
+ }
129
+ }
130
+ }
131
+ for (const listHead of itemToListHead.values()) {
132
+ itemsToRefresh.push(...collectListItemsToRefresh(listHead, changedItems));
133
+ }
134
+ for (const item of new Set(itemsToRefresh)) {
135
+ editing.reconvertItem(item);
136
+ }
137
+ };
138
+ function collectListItemsToRefresh(listHead, changedItems) {
139
+ const itemsToRefresh = [];
140
+ const visited = new Set();
141
+ const stack = [];
142
+ for (const { node, previous } of iterateSiblingListBlocks(listHead, 'forward')) {
143
+ if (visited.has(node)) {
144
+ continue;
145
+ }
146
+ const itemIndent = node.getAttribute('listIndent');
147
+ // Current node is at the lower indent so trim the stack.
148
+ if (previous && itemIndent < previous.getAttribute('listIndent')) {
149
+ stack.length = itemIndent + 1;
150
+ }
151
+ // Update the stack for the current indent level.
152
+ stack[itemIndent] = Object.fromEntries(Array.from(node.getAttributes())
153
+ .filter(([key]) => attributeNames.includes(key)));
154
+ // Find all blocks of the current node.
155
+ const blocks = getListItemBlocks(node, { direction: 'forward' });
156
+ for (const block of blocks) {
157
+ visited.add(block);
158
+ // Check if bogus vs plain paragraph needs refresh.
159
+ if (doesItemParagraphRequiresRefresh(block, blocks)) {
160
+ itemsToRefresh.push(block);
161
+ }
162
+ // Check if wrapping with UL, OL, LIs needs refresh.
163
+ else if (doesItemWrappingRequiresRefresh(block, stack, changedItems)) {
164
+ itemsToRefresh.push(block);
165
+ }
166
+ }
167
+ }
168
+ return itemsToRefresh;
169
+ }
170
+ function doesItemParagraphRequiresRefresh(item, blocks) {
171
+ if (!item.is('element', 'paragraph')) {
172
+ return false;
173
+ }
174
+ const viewElement = editing.mapper.toViewElement(item);
175
+ if (!viewElement) {
176
+ return false;
177
+ }
178
+ const useBogus = shouldUseBogusParagraph(item, attributeNames, blocks);
179
+ if (useBogus && viewElement.is('element', 'p')) {
180
+ return true;
181
+ }
182
+ else if (!useBogus && viewElement.is('element', 'span')) {
183
+ return true;
184
+ }
185
+ return false;
186
+ }
187
+ function doesItemWrappingRequiresRefresh(item, stack, changedItems) {
188
+ // Items directly affected by some "change" don't need a refresh, they will be converted by their own changes.
189
+ if (changedItems.has(item)) {
190
+ return false;
191
+ }
192
+ const viewElement = editing.mapper.toViewElement(item);
193
+ let indent = stack.length - 1;
194
+ // Traverse down the stack to the root to verify if all ULs, OLs, and LIs are as expected.
195
+ for (let element = viewElement.parent; !element.is('editableElement'); element = element.parent) {
196
+ const isListItemElement = isListItemView(element);
197
+ const isListElement = isListView(element);
198
+ if (!isListElement && !isListItemElement) {
199
+ continue;
200
+ }
201
+ const eventName = `checkAttributes:${isListItemElement ? 'item' : 'list'}`;
202
+ const needsRefresh = documentListEditing.fire(eventName, {
203
+ viewElement: element,
204
+ modelAttributes: stack[indent]
205
+ });
206
+ if (needsRefresh) {
207
+ break;
208
+ }
209
+ if (isListElement) {
210
+ indent--;
211
+ // Don't need to iterate further if we already know that the item is wrapped appropriately.
212
+ if (indent < 0) {
213
+ return false;
214
+ }
215
+ }
216
+ }
217
+ return true;
218
+ }
293
219
  }
294
-
295
220
  /**
296
221
  * Returns the list item downcast converter.
297
222
  *
298
- * @protected
299
- * @param {Array.<String>} attributeNames A list of attribute names that should be converted if are set.
300
- * @param {Array.<module:list/documentlistproperties/documentlistpropertiesediting~AttributeStrategy>} strategies The strategies.
301
- * @param {module:engine/model/model~Model} model The model.
302
- * @returns {Function}
223
+ * @internal
224
+ * @param attributeNames A list of attribute names that should be converted if are set.
225
+ * @param strategies The strategies.
226
+ * @param model The model.
303
227
  */
304
- export function listItemDowncastConverter( attributeNames, strategies, model ) {
305
- const consumer = createAttributesConsumer( attributeNames );
306
-
307
- return ( evt, data, conversionApi ) => {
308
- const { writer, mapper, consumable } = conversionApi;
309
-
310
- const listItem = data.item;
311
-
312
- if ( !attributeNames.includes( data.attributeKey ) ) {
313
- return;
314
- }
315
-
316
- // Test if attributes on the converted items are not consumed.
317
- if ( !consumer( listItem, consumable ) ) {
318
- return;
319
- }
320
-
321
- // Use positions mapping instead of mapper.toViewElement( listItem ) to find outermost view element.
322
- // This is for cases when mapping is using inner view element like in the code blocks (pre > code).
323
- const viewElement = findMappedViewElement( listItem, mapper, model );
324
-
325
- // Unwrap element from current list wrappers.
326
- unwrapListItemBlock( viewElement, writer );
327
-
328
- // Then wrap them with the new list wrappers.
329
- wrapListItemBlock( listItem, writer.createRangeOn( viewElement ), strategies, writer );
330
- };
228
+ export function listItemDowncastConverter(attributeNames, strategies, model) {
229
+ const consumer = createAttributesConsumer(attributeNames);
230
+ return (evt, data, conversionApi) => {
231
+ const { writer, mapper, consumable } = conversionApi;
232
+ const listItem = data.item;
233
+ if (!attributeNames.includes(data.attributeKey)) {
234
+ return;
235
+ }
236
+ // Test if attributes on the converted items are not consumed.
237
+ if (!consumer(listItem, consumable)) {
238
+ return;
239
+ }
240
+ // Use positions mapping instead of mapper.toViewElement( listItem ) to find outermost view element.
241
+ // This is for cases when mapping is using inner view element like in the code blocks (pre > code).
242
+ const viewElement = findMappedViewElement(listItem, mapper, model);
243
+ // Unwrap element from current list wrappers.
244
+ unwrapListItemBlock(viewElement, writer);
245
+ // Then wrap them with the new list wrappers.
246
+ wrapListItemBlock(listItem, writer.createRangeOn(viewElement), strategies, writer);
247
+ };
331
248
  }
332
-
333
249
  /**
334
250
  * Returns the bogus paragraph view element creator. A bogus paragraph is used if a list item contains only a single block or nested list.
335
251
  *
336
- * @protected
337
- * @param {Array.<String>} attributeNames The list of all model list attributes (including registered strategies).
338
- * @param {Object} [options]
339
- * @param {Boolean} [options.dataPipeline=false]
340
- * @returns {Function}
252
+ * @internal
253
+ * @param attributeNames The list of all model list attributes (including registered strategies).
341
254
  */
342
- export function bogusParagraphCreator( attributeNames, { dataPipeline } = {} ) {
343
- return ( modelElement, { writer } ) => {
344
- // Convert only if a bogus paragraph should be used.
345
- if ( !shouldUseBogusParagraph( modelElement, attributeNames ) ) {
346
- return;
347
- }
348
-
349
- const viewElement = writer.createContainerElement( 'span', { class: 'ck-list-bogus-paragraph' } );
350
-
351
- if ( dataPipeline ) {
352
- writer.setCustomProperty( 'dataPipeline:transparentRendering', true, viewElement );
353
- }
354
-
355
- return viewElement;
356
- };
255
+ export function bogusParagraphCreator(attributeNames, { dataPipeline } = {}) {
256
+ return (modelElement, { writer }) => {
257
+ // Convert only if a bogus paragraph should be used.
258
+ if (!shouldUseBogusParagraph(modelElement, attributeNames)) {
259
+ return null;
260
+ }
261
+ if (!dataPipeline) {
262
+ return writer.createContainerElement('span', { class: 'ck-list-bogus-paragraph' });
263
+ }
264
+ // Using `<p>` in case there are some markers on it and transparentRendering will render it anyway.
265
+ const viewElement = writer.createContainerElement('p');
266
+ writer.setCustomProperty('dataPipeline:transparentRendering', true, viewElement);
267
+ return viewElement;
268
+ };
357
269
  }
358
-
359
270
  /**
360
271
  * Helper for mapping mode to view elements. It's using positions mapping instead of mapper.toViewElement( element )
361
272
  * to find outermost view element. This is for cases when mapping is using inner view element like in the code blocks (pre > code).
362
273
  *
363
- * @protected
364
- * @param {module:engine/model/element~Element} element The model element.
365
- * @param {module:engine/conversion/mapper~Mapper} mapper The mapper instance.
366
- * @param {module:engine/model/model~Model} model The model.
367
- * @returns {module:engine/view/element~Element|null}
274
+ * @internal
275
+ * @param element The model element.
276
+ * @param mapper The mapper instance.
277
+ * @param model The model.
368
278
  */
369
- export function findMappedViewElement( element, mapper, model ) {
370
- const modelRange = model.createRangeOn( element );
371
- const viewRange = mapper.toViewRange( modelRange ).getTrimmed();
372
-
373
- return viewRange.getContainedElement();
279
+ export function findMappedViewElement(element, mapper, model) {
280
+ const modelRange = model.createRangeOn(element);
281
+ const viewRange = mapper.toViewRange(modelRange).getTrimmed();
282
+ return viewRange.getContainedElement();
374
283
  }
375
-
376
284
  // Unwraps all ol, ul, and li attribute elements that are wrapping the provided view element.
377
- function unwrapListItemBlock( viewElement, viewWriter ) {
378
- let attributeElement = viewElement.parent;
379
-
380
- while ( attributeElement.is( 'attributeElement' ) && [ 'ul', 'ol', 'li' ].includes( attributeElement.name ) ) {
381
- const parentElement = attributeElement.parent;
382
-
383
- viewWriter.unwrap( viewWriter.createRangeOn( viewElement ), attributeElement );
384
-
385
- attributeElement = parentElement;
386
- }
285
+ function unwrapListItemBlock(viewElement, viewWriter) {
286
+ let attributeElement = viewElement.parent;
287
+ while (attributeElement.is('attributeElement') && ['ul', 'ol', 'li'].includes(attributeElement.name)) {
288
+ const parentElement = attributeElement.parent;
289
+ viewWriter.unwrap(viewWriter.createRangeOn(viewElement), attributeElement);
290
+ attributeElement = parentElement;
291
+ }
387
292
  }
388
-
389
293
  // Wraps the given list item with appropriate attribute elements for ul, ol, and li.
390
- function wrapListItemBlock( listItem, viewRange, strategies, writer ) {
391
- if ( !listItem.hasAttribute( 'listIndent' ) ) {
392
- return;
393
- }
394
-
395
- const listItemIndent = listItem.getAttribute( 'listIndent' );
396
- let currentListItem = listItem;
397
-
398
- for ( let indent = listItemIndent; indent >= 0; indent-- ) {
399
- const listItemViewElement = createListItemElement( writer, indent, currentListItem.getAttribute( 'listItemId' ) );
400
- const listViewElement = createListElement( writer, indent, currentListItem.getAttribute( 'listType' ) );
401
-
402
- for ( const strategy of strategies ) {
403
- if ( currentListItem.hasAttribute( strategy.attributeName ) ) {
404
- strategy.setAttributeOnDowncast(
405
- writer,
406
- currentListItem.getAttribute( strategy.attributeName ),
407
- strategy.scope == 'list' ? listViewElement : listItemViewElement
408
- );
409
- }
410
- }
411
-
412
- viewRange = writer.wrap( viewRange, listItemViewElement );
413
- viewRange = writer.wrap( viewRange, listViewElement );
414
-
415
- if ( indent == 0 ) {
416
- break;
417
- }
418
-
419
- currentListItem = ListWalker.first( currentListItem, { lowerIndent: true } );
420
-
421
- // There is no list item with lower indent, this means this is a document fragment containing
422
- // only a part of nested list (like copy to clipboard) so we don't need to try to wrap it further.
423
- if ( !currentListItem ) {
424
- break;
425
- }
426
- }
294
+ function wrapListItemBlock(listItem, viewRange, strategies, writer) {
295
+ if (!listItem.hasAttribute('listIndent')) {
296
+ return;
297
+ }
298
+ const listItemIndent = listItem.getAttribute('listIndent');
299
+ let currentListItem = listItem;
300
+ for (let indent = listItemIndent; indent >= 0; indent--) {
301
+ const listItemViewElement = createListItemElement(writer, indent, currentListItem.getAttribute('listItemId'));
302
+ const listViewElement = createListElement(writer, indent, currentListItem.getAttribute('listType'));
303
+ for (const strategy of strategies) {
304
+ if (currentListItem.hasAttribute(strategy.attributeName)) {
305
+ strategy.setAttributeOnDowncast(writer, currentListItem.getAttribute(strategy.attributeName), strategy.scope == 'list' ? listViewElement : listItemViewElement);
306
+ }
307
+ }
308
+ viewRange = writer.wrap(viewRange, listItemViewElement);
309
+ viewRange = writer.wrap(viewRange, listViewElement);
310
+ if (indent == 0) {
311
+ break;
312
+ }
313
+ currentListItem = ListWalker.first(currentListItem, { lowerIndent: true });
314
+ // There is no list item with lower indent, this means this is a document fragment containing
315
+ // only a part of nested list (like copy to clipboard) so we don't need to try to wrap it further.
316
+ if (!currentListItem) {
317
+ break;
318
+ }
319
+ }
427
320
  }
428
-
429
321
  // Returns the function that is responsible for consuming attributes that are set on the model node.
430
- function createAttributesConsumer( attributeNames ) {
431
- return ( node, consumable ) => {
432
- const events = [];
433
-
434
- // Collect all set attributes that are triggering conversion.
435
- for ( const attributeName of attributeNames ) {
436
- if ( node.hasAttribute( attributeName ) ) {
437
- events.push( `attribute:${ attributeName }` );
438
- }
439
- }
440
-
441
- if ( !events.every( event => consumable.test( node, event ) !== false ) ) {
442
- return false;
443
- }
444
-
445
- events.forEach( event => consumable.consume( node, event ) );
446
-
447
- return true;
448
- };
322
+ function createAttributesConsumer(attributeNames) {
323
+ return (node, consumable) => {
324
+ const events = [];
325
+ // Collect all set attributes that are triggering conversion.
326
+ for (const attributeName of attributeNames) {
327
+ if (node.hasAttribute(attributeName)) {
328
+ events.push(`attribute:${attributeName}`);
329
+ }
330
+ }
331
+ if (!events.every(event => consumable.test(node, event) !== false)) {
332
+ return false;
333
+ }
334
+ events.forEach(event => consumable.consume(node, event));
335
+ return true;
336
+ };
449
337
  }
450
-
451
338
  // Whether the given item should be rendered as a bogus paragraph.
452
- function shouldUseBogusParagraph( item, attributeNames, blocks = getAllListItemBlocks( item ) ) {
453
- if ( !isListItemBlock( item ) ) {
454
- return false;
455
- }
456
-
457
- for ( const attributeKey of item.getAttributeKeys() ) {
458
- // Ignore selection attributes stored on block elements.
459
- if ( attributeKey.startsWith( 'selection:' ) ) {
460
- continue;
461
- }
462
-
463
- // Don't use bogus paragraph if there are attributes from other features.
464
- if ( !attributeNames.includes( attributeKey ) ) {
465
- return false;
466
- }
467
- }
468
-
469
- return blocks.length < 2;
339
+ function shouldUseBogusParagraph(item, attributeNames, blocks = getAllListItemBlocks(item)) {
340
+ if (!isListItemBlock(item)) {
341
+ return false;
342
+ }
343
+ for (const attributeKey of item.getAttributeKeys()) {
344
+ // Ignore selection attributes stored on block elements.
345
+ if (attributeKey.startsWith('selection:')) {
346
+ continue;
347
+ }
348
+ // Don't use bogus paragraph if there are attributes from other features.
349
+ if (!attributeNames.includes(attributeKey)) {
350
+ return false;
351
+ }
352
+ }
353
+ return blocks.length < 2;
470
354
  }