@ckeditor/ckeditor5-list 39.0.2 → 40.1.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.
- package/LICENSE.md +2 -2
- package/build/list.js +1 -1
- package/build/translations/fi.js +1 -1
- package/build/translations/pt-br.js +1 -1
- package/ckeditor5-metadata.json +44 -0
- package/lang/translations/fi.po +1 -1
- package/lang/translations/pt-br.po +10 -10
- package/package.json +3 -3
- package/src/augmentation.d.ts +5 -3
- package/src/documentlist/converters.d.ts +3 -1
- package/src/documentlist/converters.js +106 -19
- package/src/documentlist/documentlistcommand.d.ts +2 -2
- package/src/documentlist/documentlistcommand.js +12 -7
- package/src/documentlist/documentlistediting.d.ts +65 -7
- package/src/documentlist/documentlistediting.js +157 -76
- package/src/documentlist/utils/listwalker.d.ts +4 -0
- package/src/documentlist/utils/listwalker.js +21 -1
- package/src/documentlist/utils/model.d.ts +11 -2
- package/src/documentlist/utils/model.js +21 -1
- package/src/documentlist/utils/postfixers.js +8 -0
- package/src/documentlist/utils/view.d.ts +3 -3
- package/src/documentlistproperties/documentlistpropertiesediting.js +11 -34
- package/src/index.d.ts +3 -0
- package/src/index.js +2 -0
- package/src/listconfig.d.ts +10 -0
- package/src/tododocumentlist/checktododocumentlistcommand.d.ts +49 -0
- package/src/tododocumentlist/checktododocumentlistcommand.js +82 -0
- package/src/tododocumentlist/todocheckboxchangeobserver.d.ts +41 -0
- package/src/tododocumentlist/todocheckboxchangeobserver.js +37 -0
- package/src/tododocumentlist/tododocumentlistediting.d.ts +38 -0
- package/src/tododocumentlist/tododocumentlistediting.js +399 -0
- package/src/tododocumentlist.d.ts +27 -0
- package/src/tododocumentlist.js +31 -0
- package/theme/todolist.css +101 -70
|
@@ -16,9 +16,10 @@ import DocumentListSplitCommand from './documentlistsplitcommand';
|
|
|
16
16
|
import DocumentListUtils from './documentlistutils';
|
|
17
17
|
import { bogusParagraphCreator, listItemDowncastConverter, listItemUpcastConverter, listUpcastCleanList, reconvertItemsOnDataChange } from './converters';
|
|
18
18
|
import { findAndAddListHeadToMap, fixListIndents, fixListItemIds } from './utils/postfixers';
|
|
19
|
-
import { getAllListItemBlocks, isFirstBlockOfListItem, isLastBlockOfListItem, isSingleListItem, getSelectedBlockObject, isListItemBlock, removeListAttributes } from './utils/model';
|
|
19
|
+
import { getAllListItemBlocks, isFirstBlockOfListItem, isLastBlockOfListItem, isSingleListItem, getSelectedBlockObject, isListItemBlock, removeListAttributes, ListItemUid } from './utils/model';
|
|
20
20
|
import { getViewElementIdForListType, getViewElementNameForListType } from './utils/view';
|
|
21
|
-
import ListWalker, {
|
|
21
|
+
import ListWalker, { ListBlocksIterable } from './utils/listwalker';
|
|
22
|
+
import { ClipboardPipeline } from 'ckeditor5/src/clipboard';
|
|
22
23
|
import '../../theme/documentlist.css';
|
|
23
24
|
import '../../theme/list.css';
|
|
24
25
|
/**
|
|
@@ -29,13 +30,6 @@ const LIST_BASE_ATTRIBUTES = ['listType', 'listIndent', 'listItemId'];
|
|
|
29
30
|
* The editing part of the document-list feature. It handles creating, editing and removing lists and list items.
|
|
30
31
|
*/
|
|
31
32
|
export default class DocumentListEditing extends Plugin {
|
|
32
|
-
constructor() {
|
|
33
|
-
super(...arguments);
|
|
34
|
-
/**
|
|
35
|
-
* The list of registered downcast strategies.
|
|
36
|
-
*/
|
|
37
|
-
this._downcastStrategies = [];
|
|
38
|
-
}
|
|
39
33
|
/**
|
|
40
34
|
* @inheritDoc
|
|
41
35
|
*/
|
|
@@ -46,7 +40,18 @@ export default class DocumentListEditing extends Plugin {
|
|
|
46
40
|
* @inheritDoc
|
|
47
41
|
*/
|
|
48
42
|
static get requires() {
|
|
49
|
-
return [Enter, Delete, DocumentListUtils];
|
|
43
|
+
return [Enter, Delete, DocumentListUtils, ClipboardPipeline];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* @inheritDoc
|
|
47
|
+
*/
|
|
48
|
+
constructor(editor) {
|
|
49
|
+
super(editor);
|
|
50
|
+
/**
|
|
51
|
+
* The list of registered downcast strategies.
|
|
52
|
+
*/
|
|
53
|
+
this._downcastStrategies = [];
|
|
54
|
+
editor.config.define('list.multiBlock', true);
|
|
50
55
|
}
|
|
51
56
|
/**
|
|
52
57
|
* @inheritDoc
|
|
@@ -54,6 +59,7 @@ export default class DocumentListEditing extends Plugin {
|
|
|
54
59
|
init() {
|
|
55
60
|
const editor = this.editor;
|
|
56
61
|
const model = editor.model;
|
|
62
|
+
const multiBlock = editor.config.get('list.multiBlock');
|
|
57
63
|
if (editor.plugins.has('ListEditing')) {
|
|
58
64
|
/**
|
|
59
65
|
* The `DocumentList` feature can not be loaded together with the `List` plugin.
|
|
@@ -63,9 +69,18 @@ export default class DocumentListEditing extends Plugin {
|
|
|
63
69
|
*/
|
|
64
70
|
throw new CKEditorError('document-list-feature-conflict', this, { conflictPlugin: 'ListEditing' });
|
|
65
71
|
}
|
|
66
|
-
model.schema.
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
model.schema.register('$listItem', { allowAttributes: LIST_BASE_ATTRIBUTES });
|
|
73
|
+
if (multiBlock) {
|
|
74
|
+
model.schema.extend('$container', { allowAttributesOf: '$listItem' });
|
|
75
|
+
model.schema.extend('$block', { allowAttributesOf: '$listItem' });
|
|
76
|
+
model.schema.extend('$blockObject', { allowAttributesOf: '$listItem' });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
model.schema.register('listItem', {
|
|
80
|
+
inheritAllFrom: '$block',
|
|
81
|
+
allowAttributesOf: '$listItem'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
69
84
|
for (const attribute of LIST_BASE_ATTRIBUTES) {
|
|
70
85
|
model.schema.setAttributeProperties(attribute, {
|
|
71
86
|
copyOnReplace: true
|
|
@@ -76,10 +91,12 @@ export default class DocumentListEditing extends Plugin {
|
|
|
76
91
|
editor.commands.add('bulletedList', new DocumentListCommand(editor, 'bulleted'));
|
|
77
92
|
editor.commands.add('indentList', new DocumentListIndentCommand(editor, 'forward'));
|
|
78
93
|
editor.commands.add('outdentList', new DocumentListIndentCommand(editor, 'backward'));
|
|
79
|
-
editor.commands.add('mergeListItemBackward', new DocumentListMergeCommand(editor, 'backward'));
|
|
80
|
-
editor.commands.add('mergeListItemForward', new DocumentListMergeCommand(editor, 'forward'));
|
|
81
94
|
editor.commands.add('splitListItemBefore', new DocumentListSplitCommand(editor, 'before'));
|
|
82
95
|
editor.commands.add('splitListItemAfter', new DocumentListSplitCommand(editor, 'after'));
|
|
96
|
+
if (multiBlock) {
|
|
97
|
+
editor.commands.add('mergeListItemBackward', new DocumentListMergeCommand(editor, 'backward'));
|
|
98
|
+
editor.commands.add('mergeListItemForward', new DocumentListMergeCommand(editor, 'forward'));
|
|
99
|
+
}
|
|
83
100
|
this._setupDeleteIntegration();
|
|
84
101
|
this._setupEnterIntegration();
|
|
85
102
|
this._setupTabIntegration();
|
|
@@ -121,7 +138,7 @@ export default class DocumentListEditing extends Plugin {
|
|
|
121
138
|
/**
|
|
122
139
|
* Returns list of model attribute names that should affect downcast conversion.
|
|
123
140
|
*/
|
|
124
|
-
|
|
141
|
+
getListAttributeNames() {
|
|
125
142
|
return [
|
|
126
143
|
...LIST_BASE_ATTRIBUTES,
|
|
127
144
|
...this._downcastStrategies.map(strategy => strategy.attributeName)
|
|
@@ -164,7 +181,7 @@ export default class DocumentListEditing extends Plugin {
|
|
|
164
181
|
}
|
|
165
182
|
// Merge block with previous one (on the block level or on the content level).
|
|
166
183
|
else {
|
|
167
|
-
if (!mergeBackwardCommand.isEnabled) {
|
|
184
|
+
if (!mergeBackwardCommand || !mergeBackwardCommand.isEnabled) {
|
|
168
185
|
return;
|
|
169
186
|
}
|
|
170
187
|
mergeBackwardCommand.execute({
|
|
@@ -180,7 +197,7 @@ export default class DocumentListEditing extends Plugin {
|
|
|
180
197
|
if (selection.isCollapsed && !selection.getLastPosition().isAtEnd) {
|
|
181
198
|
return;
|
|
182
199
|
}
|
|
183
|
-
if (!mergeForwardCommand.isEnabled) {
|
|
200
|
+
if (!mergeForwardCommand || !mergeForwardCommand.isEnabled) {
|
|
184
201
|
return;
|
|
185
202
|
}
|
|
186
203
|
mergeForwardCommand.execute({
|
|
@@ -281,29 +298,63 @@ export default class DocumentListEditing extends Plugin {
|
|
|
281
298
|
_setupConversion() {
|
|
282
299
|
const editor = this.editor;
|
|
283
300
|
const model = editor.model;
|
|
284
|
-
const attributeNames = this.
|
|
301
|
+
const attributeNames = this.getListAttributeNames();
|
|
302
|
+
const multiBlock = editor.config.get('list.multiBlock');
|
|
303
|
+
const elementName = multiBlock ? 'paragraph' : 'listItem';
|
|
285
304
|
editor.conversion.for('upcast')
|
|
286
|
-
|
|
305
|
+
// Convert <li> to a generic paragraph (or listItem element) so the content of <li> is always inside a block.
|
|
306
|
+
// Setting the listType attribute to let other features (to-do list) know that this is part of a list item.
|
|
307
|
+
// This is also important to properly handle simple lists so that paragraphs inside a list item won't break the list item.
|
|
308
|
+
// <li> <-- converted to listItem
|
|
309
|
+
// <p></p> <-- should be also converted to listItem, so it won't split and replace the listItem generated from the above li.
|
|
310
|
+
.elementToElement({
|
|
311
|
+
view: 'li',
|
|
312
|
+
model: (viewElement, { writer }) => writer.createElement(elementName, { listType: '' })
|
|
313
|
+
})
|
|
314
|
+
// Convert paragraph to the list block (without list type defined yet).
|
|
315
|
+
// This is important to properly handle bogus paragraph and to-do lists.
|
|
316
|
+
// Most of the time the bogus paragraph should not appear in the data of to-do list,
|
|
317
|
+
// but if there is any marker or an attribute on the paragraph then the bogus paragraph
|
|
318
|
+
// is preserved in the data, and we need to be able to detect this case.
|
|
319
|
+
.elementToElement({
|
|
320
|
+
view: 'p',
|
|
321
|
+
model: (viewElement, { writer }) => {
|
|
322
|
+
if (viewElement.parent && viewElement.parent.is('element', 'li')) {
|
|
323
|
+
return writer.createElement(elementName, { listType: '' });
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
},
|
|
327
|
+
converterPriority: 'high'
|
|
328
|
+
})
|
|
287
329
|
.add(dispatcher => {
|
|
288
330
|
dispatcher.on('element:li', listItemUpcastConverter());
|
|
289
331
|
dispatcher.on('element:ul', listUpcastCleanList(), { priority: 'high' });
|
|
290
332
|
dispatcher.on('element:ol', listUpcastCleanList(), { priority: 'high' });
|
|
291
333
|
});
|
|
334
|
+
if (!multiBlock) {
|
|
335
|
+
editor.conversion.for('downcast')
|
|
336
|
+
.elementToElement({
|
|
337
|
+
model: 'listItem',
|
|
338
|
+
view: 'p'
|
|
339
|
+
});
|
|
340
|
+
}
|
|
292
341
|
editor.conversion.for('editingDowncast')
|
|
293
342
|
.elementToElement({
|
|
294
|
-
model:
|
|
343
|
+
model: elementName,
|
|
295
344
|
view: bogusParagraphCreator(attributeNames),
|
|
296
345
|
converterPriority: 'high'
|
|
346
|
+
})
|
|
347
|
+
.add(dispatcher => {
|
|
348
|
+
dispatcher.on('attribute', listItemDowncastConverter(attributeNames, this._downcastStrategies, model));
|
|
297
349
|
});
|
|
298
350
|
editor.conversion.for('dataDowncast')
|
|
299
351
|
.elementToElement({
|
|
300
|
-
model:
|
|
352
|
+
model: elementName,
|
|
301
353
|
view: bogusParagraphCreator(attributeNames, { dataPipeline: true }),
|
|
302
354
|
converterPriority: 'high'
|
|
303
|
-
})
|
|
304
|
-
editor.conversion.for('downcast')
|
|
355
|
+
})
|
|
305
356
|
.add(dispatcher => {
|
|
306
|
-
dispatcher.on('attribute', listItemDowncastConverter(attributeNames, this._downcastStrategies, model));
|
|
357
|
+
dispatcher.on('attribute', listItemDowncastConverter(attributeNames, this._downcastStrategies, model, { dataPipeline: true }));
|
|
307
358
|
});
|
|
308
359
|
this.listenTo(model.document, 'change:data', reconvertItemsOnDataChange(model, editor.editing, attributeNames, this), { priority: 'high' });
|
|
309
360
|
// For LI verify if an ID of the attribute element is correct.
|
|
@@ -327,7 +378,7 @@ export default class DocumentListEditing extends Plugin {
|
|
|
327
378
|
*/
|
|
328
379
|
_setupModelPostFixing() {
|
|
329
380
|
const model = this.editor.model;
|
|
330
|
-
const attributeNames = this.
|
|
381
|
+
const attributeNames = this.getListAttributeNames();
|
|
331
382
|
// Register list fixing.
|
|
332
383
|
// First the low level handler.
|
|
333
384
|
model.document.registerPostFixer(writer => modelChangePostFixer(model, writer, attributeNames, this));
|
|
@@ -347,6 +398,7 @@ export default class DocumentListEditing extends Plugin {
|
|
|
347
398
|
*/
|
|
348
399
|
_setupClipboardIntegration() {
|
|
349
400
|
const model = this.editor.model;
|
|
401
|
+
const clipboardPipeline = this.editor.plugins.get('ClipboardPipeline');
|
|
350
402
|
this.listenTo(model, 'insertContent', createModelIndentPasteFixer(model), { priority: 'high' });
|
|
351
403
|
// To enhance the UX, the editor should not copy list attributes to the clipboard if the selection
|
|
352
404
|
// started and ended in the same list item.
|
|
@@ -375,12 +427,27 @@ export default class DocumentListEditing extends Plugin {
|
|
|
375
427
|
// │ * bar] │ * bar │
|
|
376
428
|
// └─────────────────────┴───────────────────┘
|
|
377
429
|
//
|
|
378
|
-
// See https://github.com/ckeditor/ckeditor5/issues/11608.
|
|
379
|
-
this.listenTo(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
430
|
+
// See https://github.com/ckeditor/ckeditor5/issues/11608, https://github.com/ckeditor/ckeditor5/issues/14969
|
|
431
|
+
this.listenTo(clipboardPipeline, 'outputTransformation', (evt, data) => {
|
|
432
|
+
model.change(writer => {
|
|
433
|
+
// Remove last block if it's empty.
|
|
434
|
+
const allContentChildren = Array.from(data.content.getChildren());
|
|
435
|
+
const lastItem = allContentChildren[allContentChildren.length - 1];
|
|
436
|
+
if (allContentChildren.length > 1 && lastItem.is('element') && lastItem.isEmpty) {
|
|
437
|
+
const contentChildrenExceptLastItem = allContentChildren.slice(0, -1);
|
|
438
|
+
if (contentChildrenExceptLastItem.every(isListItemBlock)) {
|
|
439
|
+
writer.remove(lastItem);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Copy/cut only content of a list item (for drag-drop move the whole list item).
|
|
443
|
+
if (data.method == 'copy' || data.method == 'cut') {
|
|
444
|
+
const allChildren = Array.from(data.content.getChildren());
|
|
445
|
+
const isSingleListItemSelected = isSingleListItem(allChildren);
|
|
446
|
+
if (isSingleListItemSelected) {
|
|
447
|
+
removeListAttributes(allChildren, writer);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
384
451
|
});
|
|
385
452
|
}
|
|
386
453
|
}
|
|
@@ -419,6 +486,7 @@ export default class DocumentListEditing extends Plugin {
|
|
|
419
486
|
function modelChangePostFixer(model, writer, attributeNames, documentListEditing) {
|
|
420
487
|
const changes = model.document.differ.getChanges();
|
|
421
488
|
const itemToListHead = new Map();
|
|
489
|
+
const multiBlock = documentListEditing.editor.config.get('list.multiBlock');
|
|
422
490
|
let applied = false;
|
|
423
491
|
for (const entry of changes) {
|
|
424
492
|
if (entry.type == 'insert' && entry.name != '$text') {
|
|
@@ -455,6 +523,18 @@ function modelChangePostFixer(model, writer, attributeNames, documentListEditing
|
|
|
455
523
|
findAndAddListHeadToMap(entry.range.start.getShiftedBy(1), itemToListHead);
|
|
456
524
|
}
|
|
457
525
|
}
|
|
526
|
+
// Make sure that there is no left over listItem element without attributes or a block with list attributes that is not a listItem.
|
|
527
|
+
if (!multiBlock && entry.type == 'attribute' && LIST_BASE_ATTRIBUTES.includes(entry.attributeKey)) {
|
|
528
|
+
const element = entry.range.start.nodeAfter;
|
|
529
|
+
if (entry.attributeNewValue === null && element && element.is('element', 'listItem')) {
|
|
530
|
+
writer.rename(element, 'paragraph');
|
|
531
|
+
applied = true;
|
|
532
|
+
}
|
|
533
|
+
else if (entry.attributeOldValue === null && element && element.is('element') && element.name != 'listItem') {
|
|
534
|
+
writer.rename(element, 'listItem');
|
|
535
|
+
applied = true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
458
538
|
}
|
|
459
539
|
// Make sure that IDs are not shared by split list.
|
|
460
540
|
const seenIds = new Set();
|
|
@@ -476,65 +556,66 @@ function modelChangePostFixer(model, writer, attributeNames, documentListEditing
|
|
|
476
556
|
* Example:
|
|
477
557
|
*
|
|
478
558
|
* ```xml
|
|
479
|
-
* <paragraph listType="bulleted" listItemId="a" listIndent=0>A</paragraph>
|
|
480
|
-
* <paragraph listType="bulleted" listItemId="b" listIndent=1>B^</paragraph>
|
|
481
|
-
* // At ^ paste: <paragraph listType="
|
|
482
|
-
* // <paragraph listType="
|
|
483
|
-
* <paragraph listType="bulleted" listItemId="c" listIndent=2>C</paragraph>
|
|
559
|
+
* <paragraph listType="bulleted" listItemId="a" listIndent="0">A</paragraph>
|
|
560
|
+
* <paragraph listType="bulleted" listItemId="b" listIndent="1">B^</paragraph>
|
|
561
|
+
* // At ^ paste: <paragraph listType="numbered" listItemId="x" listIndent="0">X</paragraph>
|
|
562
|
+
* // <paragraph listType="numbered" listItemId="y" listIndent="1">Y</paragraph>
|
|
563
|
+
* <paragraph listType="bulleted" listItemId="c" listIndent="2">C</paragraph>
|
|
484
564
|
* ```
|
|
485
565
|
*
|
|
486
566
|
* Should become:
|
|
487
567
|
*
|
|
488
568
|
* ```xml
|
|
489
|
-
* <paragraph listType="bulleted" listItemId="a" listIndent=0>A</paragraph>
|
|
490
|
-
* <paragraph listType="bulleted" listItemId="b" listIndent=1>BX</paragraph>
|
|
491
|
-
* <paragraph listType="bulleted" listItemId="y" listIndent=2>Y/paragraph>
|
|
492
|
-
* <paragraph listType="bulleted" listItemId="c" listIndent=2>C</paragraph>
|
|
569
|
+
* <paragraph listType="bulleted" listItemId="a" listIndent="0">A</paragraph>
|
|
570
|
+
* <paragraph listType="bulleted" listItemId="b" listIndent="1">BX</paragraph>
|
|
571
|
+
* <paragraph listType="bulleted" listItemId="y" listIndent="2">Y/paragraph>
|
|
572
|
+
* <paragraph listType="bulleted" listItemId="c" listIndent="2">C</paragraph>
|
|
493
573
|
* ```
|
|
494
574
|
*/
|
|
495
575
|
function createModelIndentPasteFixer(model) {
|
|
496
576
|
return (evt, [content, selectable]) => {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
// would create incorrect model.
|
|
502
|
-
const item = content.is('documentFragment') ? content.getChild(0) : content;
|
|
503
|
-
if (!isListItemBlock(item)) {
|
|
577
|
+
const items = content.is('documentFragment') ?
|
|
578
|
+
Array.from(content.getChildren()) :
|
|
579
|
+
[content];
|
|
580
|
+
if (!items.length) {
|
|
504
581
|
return;
|
|
505
582
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const pos = selection.getFirstPosition();
|
|
515
|
-
let refItem = null;
|
|
516
|
-
if (isListItemBlock(pos.parent)) {
|
|
517
|
-
refItem = pos.parent;
|
|
583
|
+
const selection = selectable ?
|
|
584
|
+
model.createSelection(selectable) :
|
|
585
|
+
model.document.selection;
|
|
586
|
+
const position = selection.getFirstPosition();
|
|
587
|
+
// Get a reference list item. Attributes of the inserted list items will be fixed according to that item.
|
|
588
|
+
let refItem;
|
|
589
|
+
if (isListItemBlock(position.parent)) {
|
|
590
|
+
refItem = position.parent;
|
|
518
591
|
}
|
|
519
|
-
else if (isListItemBlock(
|
|
520
|
-
refItem =
|
|
592
|
+
else if (isListItemBlock(position.nodeBefore)) {
|
|
593
|
+
refItem = position.nodeBefore;
|
|
521
594
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
// First list item in `data` has indent equal to 0 (it is a first list item). It should have indent equal
|
|
527
|
-
// to the indent of reference item. We have to fix the first item and all of it's children and following siblings.
|
|
528
|
-
// Indent of all those items has to be adjusted to reference item.
|
|
529
|
-
const indentChange = refItem.getAttribute('listIndent') - item.getAttribute('listIndent');
|
|
530
|
-
// Fix only if there is anything to fix.
|
|
531
|
-
if (indentChange <= 0) {
|
|
532
|
-
return;
|
|
595
|
+
else {
|
|
596
|
+
return; // Content is not copied into a list.
|
|
533
597
|
}
|
|
534
598
|
model.change(writer => {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
599
|
+
const refType = refItem.getAttribute('listType');
|
|
600
|
+
const refIndent = refItem.getAttribute('listIndent');
|
|
601
|
+
const firstElementIndent = items[0].getAttribute('listIndent') || 0;
|
|
602
|
+
const indentDiff = Math.max(refIndent - firstElementIndent, 0);
|
|
603
|
+
for (const item of items) {
|
|
604
|
+
const isListItem = isListItemBlock(item);
|
|
605
|
+
if (refItem.is('element', 'listItem') && item.is('element', 'paragraph')) {
|
|
606
|
+
/**
|
|
607
|
+
* When paragraphs or a plain text list is pasted into a simple list, convert
|
|
608
|
+
* the `<paragraphs>' to `<listItem>' to avoid breaking the target list.
|
|
609
|
+
*
|
|
610
|
+
* See https://github.com/ckeditor/ckeditor5/issues/13826.
|
|
611
|
+
*/
|
|
612
|
+
writer.rename(item, 'listItem');
|
|
613
|
+
}
|
|
614
|
+
writer.setAttributes({
|
|
615
|
+
listIndent: (isListItem ? item.getAttribute('listIndent') : 0) + indentDiff,
|
|
616
|
+
listItemId: isListItem ? item.getAttribute('listItemId') : ListItemUid.next(),
|
|
617
|
+
listType: refType
|
|
618
|
+
}, item);
|
|
538
619
|
}
|
|
539
620
|
});
|
|
540
621
|
};
|
|
@@ -132,9 +132,29 @@ export default class ListWalker {
|
|
|
132
132
|
*/
|
|
133
133
|
export function* iterateSiblingListBlocks(node, direction = 'forward') {
|
|
134
134
|
const isForward = direction == 'forward';
|
|
135
|
+
const previousNodesByIndent = []; // Last seen nodes of lower indented lists.
|
|
135
136
|
let previous = null;
|
|
136
137
|
while (isListItemBlock(node)) {
|
|
137
|
-
|
|
138
|
+
let previousNodeInList = null; // It's like `previous` but has the same indent as current node.
|
|
139
|
+
if (previous) {
|
|
140
|
+
const nodeIndent = node.getAttribute('listIndent');
|
|
141
|
+
const previousNodeIndent = previous.getAttribute('listIndent');
|
|
142
|
+
// Let's find previous node for the same indent.
|
|
143
|
+
// We're going to need that when we get back to previous indent.
|
|
144
|
+
if (nodeIndent > previousNodeIndent) {
|
|
145
|
+
previousNodesByIndent[previousNodeIndent] = previous;
|
|
146
|
+
}
|
|
147
|
+
// Restore the one for given indent.
|
|
148
|
+
else if (nodeIndent < previousNodeIndent) {
|
|
149
|
+
previousNodeInList = previousNodesByIndent[nodeIndent];
|
|
150
|
+
previousNodesByIndent.length = nodeIndent;
|
|
151
|
+
}
|
|
152
|
+
// Same indent.
|
|
153
|
+
else {
|
|
154
|
+
previousNodeInList = previous;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
yield { node, previous, previousNodeInList };
|
|
138
158
|
previous = node;
|
|
139
159
|
node = isForward ? node.nextSibling : node.previousSibling;
|
|
140
160
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @module list/documentlist/utils/model
|
|
7
7
|
*/
|
|
8
|
-
import type { DocumentFragment, Element, Model, Node, Writer, Item } from 'ckeditor5/src/engine';
|
|
8
|
+
import type { DocumentFragment, Element, Model, Node, Writer, Item, Schema } from 'ckeditor5/src/engine';
|
|
9
9
|
import { type ArrayOrItem } from 'ckeditor5/src/utils';
|
|
10
10
|
/**
|
|
11
11
|
* The list item ID generator.
|
|
@@ -28,7 +28,7 @@ export declare class ListItemUid {
|
|
|
28
28
|
export interface ListElement extends Element {
|
|
29
29
|
getAttribute(key: 'listItemId'): string;
|
|
30
30
|
getAttribute(key: 'listIndent'): number;
|
|
31
|
-
getAttribute(key: 'listType'): 'numbered' | 'bulleted';
|
|
31
|
+
getAttribute(key: 'listType'): 'numbered' | 'bulleted' | 'todo';
|
|
32
32
|
getAttribute(key: string): unknown;
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
@@ -191,3 +191,12 @@ export declare function sortBlocks<T extends Element>(blocks: Iterable<T>): Arra
|
|
|
191
191
|
* @returns Selected block object or `null`.
|
|
192
192
|
*/
|
|
193
193
|
export declare function getSelectedBlockObject(model: Model): Element | null;
|
|
194
|
+
/**
|
|
195
|
+
* Checks whether the given block can be replaced by a listItem.
|
|
196
|
+
*
|
|
197
|
+
* Note that this is possible only when multiBlock = false option is set in feature config.
|
|
198
|
+
*
|
|
199
|
+
* @param block A block to be tested.
|
|
200
|
+
* @param schema The schema of the document.
|
|
201
|
+
*/
|
|
202
|
+
export declare function canBecomeSimpleListItem(block: Element, schema: Schema): boolean;
|
|
@@ -282,6 +282,13 @@ export function outdentBlocksWithMerge(blocks, writer) {
|
|
|
282
282
|
*/
|
|
283
283
|
export function removeListAttributes(blocks, writer) {
|
|
284
284
|
blocks = toArray(blocks);
|
|
285
|
+
// Convert simple list items to plain paragraphs.
|
|
286
|
+
for (const block of blocks) {
|
|
287
|
+
if (block.is('element', 'listItem')) {
|
|
288
|
+
writer.rename(block, 'paragraph');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Remove list attributes.
|
|
285
292
|
for (const block of blocks) {
|
|
286
293
|
for (const attributeKey of block.getAttributeKeys()) {
|
|
287
294
|
if (attributeKey.startsWith('list')) {
|
|
@@ -416,7 +423,20 @@ export function getSelectedBlockObject(model) {
|
|
|
416
423
|
}
|
|
417
424
|
return null;
|
|
418
425
|
}
|
|
419
|
-
|
|
426
|
+
/**
|
|
427
|
+
* Checks whether the given block can be replaced by a listItem.
|
|
428
|
+
*
|
|
429
|
+
* Note that this is possible only when multiBlock = false option is set in feature config.
|
|
430
|
+
*
|
|
431
|
+
* @param block A block to be tested.
|
|
432
|
+
* @param schema The schema of the document.
|
|
433
|
+
*/
|
|
434
|
+
export function canBecomeSimpleListItem(block, schema) {
|
|
435
|
+
return schema.checkChild(block.parent, 'listItem') && schema.checkChild(block, '$text') && !schema.isObject(block);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Merges a given block to the given parent block if parent is a list item and there is no more blocks in the same item.
|
|
439
|
+
*/
|
|
420
440
|
function mergeListItemIfNotLast(block, parentBlock, writer) {
|
|
421
441
|
const parentItemBlocks = getListItemBlocks(parentBlock, { direction: 'forward' });
|
|
422
442
|
// Merge with parent only if outdented item wasn't the last one in its parent.
|
|
@@ -101,6 +101,14 @@ export function fixListItemIds(listNodes, seenIds, writer) {
|
|
|
101
101
|
listItemId = ListItemUid.next();
|
|
102
102
|
}
|
|
103
103
|
seenIds.add(listItemId);
|
|
104
|
+
// Make sure that all items in a simple list have unique IDs.
|
|
105
|
+
if (node.is('element', 'listItem')) {
|
|
106
|
+
if (node.getAttribute('listItemId') != listItemId) {
|
|
107
|
+
writer.setAttribute('listItemId', listItemId, node);
|
|
108
|
+
applied = true;
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
104
112
|
for (const block of getListItemBlocks(node, { direction: 'forward' })) {
|
|
105
113
|
visited.add(block);
|
|
106
114
|
// Use a new ID if a block of a bigger list item has different type.
|
|
@@ -60,7 +60,7 @@ export declare function getIndent(listItem: ViewElement): number;
|
|
|
60
60
|
*
|
|
61
61
|
* @internal
|
|
62
62
|
*/
|
|
63
|
-
export declare function createListElement(writer: DowncastWriter, indent: number, type: 'bulleted' | 'numbered', id?: string): ViewAttributeElement;
|
|
63
|
+
export declare function createListElement(writer: DowncastWriter, indent: number, type: 'bulleted' | 'numbered' | 'todo', id?: string): ViewAttributeElement;
|
|
64
64
|
/**
|
|
65
65
|
* Creates a list item attribute element (li).
|
|
66
66
|
*
|
|
@@ -72,10 +72,10 @@ export declare function createListItemElement(writer: DowncastWriter, indent: nu
|
|
|
72
72
|
*
|
|
73
73
|
* @internal
|
|
74
74
|
*/
|
|
75
|
-
export declare function getViewElementNameForListType(type?: 'bulleted' | 'numbered'): 'ol' | 'ul';
|
|
75
|
+
export declare function getViewElementNameForListType(type?: 'bulleted' | 'numbered' | 'todo'): 'ol' | 'ul';
|
|
76
76
|
/**
|
|
77
77
|
* Returns a view element ID for the given list type and indent.
|
|
78
78
|
*
|
|
79
79
|
* @internal
|
|
80
80
|
*/
|
|
81
|
-
export declare function getViewElementIdForListType(type?: 'bulleted' | 'numbered', indent?: number): string;
|
|
81
|
+
export declare function getViewElementIdForListType(type?: 'bulleted' | 'numbered' | 'todo', indent?: number): string;
|
|
@@ -38,12 +38,10 @@ export default class DocumentListPropertiesEditing extends Plugin {
|
|
|
38
38
|
*/
|
|
39
39
|
constructor(editor) {
|
|
40
40
|
super(editor);
|
|
41
|
-
editor.config.define('list', {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
reversed: false
|
|
46
|
-
}
|
|
41
|
+
editor.config.define('list.properties', {
|
|
42
|
+
styles: true,
|
|
43
|
+
startIndex: false,
|
|
44
|
+
reversed: false
|
|
47
45
|
});
|
|
48
46
|
}
|
|
49
47
|
/**
|
|
@@ -57,9 +55,7 @@ export default class DocumentListPropertiesEditing extends Plugin {
|
|
|
57
55
|
const strategies = createAttributeStrategies(enabledProperties);
|
|
58
56
|
for (const strategy of strategies) {
|
|
59
57
|
strategy.addCommand(editor);
|
|
60
|
-
model.schema.extend('$
|
|
61
|
-
model.schema.extend('$block', { allowAttributes: strategy.attributeName });
|
|
62
|
-
model.schema.extend('$blockObject', { allowAttributes: strategy.attributeName });
|
|
58
|
+
model.schema.extend('$listItem', { allowAttributes: strategy.attributeName });
|
|
63
59
|
// Register downcast strategy.
|
|
64
60
|
documentListEditing.registerDowncastStrategy({
|
|
65
61
|
scope: 'list',
|
|
@@ -122,29 +118,7 @@ export default class DocumentListPropertiesEditing extends Plugin {
|
|
|
122
118
|
});
|
|
123
119
|
// Make sure that all items in a single list (items at the same level & listType) have the same properties.
|
|
124
120
|
documentListEditing.on('postFixer', (evt, { listNodes, writer }) => {
|
|
125
|
-
const
|
|
126
|
-
for (const { node, previous } of listNodes) {
|
|
127
|
-
// For the first list block there is nothing to compare with.
|
|
128
|
-
if (!previous) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const nodeIndent = node.getAttribute('listIndent');
|
|
132
|
-
const previousNodeIndent = previous.getAttribute('listIndent');
|
|
133
|
-
let previousNodeInList = null; // It's like `previous` but has the same indent as current node.
|
|
134
|
-
// Let's find previous node for the same indent.
|
|
135
|
-
// We're going to need that when we get back to previous indent.
|
|
136
|
-
if (nodeIndent > previousNodeIndent) {
|
|
137
|
-
previousNodesByIndent[previousNodeIndent] = previous;
|
|
138
|
-
}
|
|
139
|
-
// Restore the one for given indent.
|
|
140
|
-
else if (nodeIndent < previousNodeIndent) {
|
|
141
|
-
previousNodeInList = previousNodesByIndent[nodeIndent];
|
|
142
|
-
previousNodesByIndent.length = nodeIndent;
|
|
143
|
-
}
|
|
144
|
-
// Same indent.
|
|
145
|
-
else {
|
|
146
|
-
previousNodeInList = previous;
|
|
147
|
-
}
|
|
121
|
+
for (const { node, previousNodeInList } of listNodes) {
|
|
148
122
|
// This is a first item of a nested list.
|
|
149
123
|
if (!previousNodeInList) {
|
|
150
124
|
continue;
|
|
@@ -187,10 +161,13 @@ function createAttributeStrategies(enabledProperties) {
|
|
|
187
161
|
}
|
|
188
162
|
editor.commands.add('listStyle', new DocumentListStyleCommand(editor, DEFAULT_LIST_TYPE, supportedTypes));
|
|
189
163
|
},
|
|
190
|
-
appliesToListItem() {
|
|
191
|
-
return
|
|
164
|
+
appliesToListItem(item) {
|
|
165
|
+
return item.getAttribute('listType') == 'numbered' || item.getAttribute('listType') == 'bulleted';
|
|
192
166
|
},
|
|
193
167
|
hasValidAttribute(item) {
|
|
168
|
+
if (!this.appliesToListItem(item)) {
|
|
169
|
+
return !item.hasAttribute('listStyle');
|
|
170
|
+
}
|
|
194
171
|
if (!item.hasAttribute('listStyle')) {
|
|
195
172
|
return false;
|
|
196
173
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export { default as ListPropertiesUI } from './listproperties/listpropertiesui';
|
|
|
24
24
|
export { default as TodoList } from './todolist';
|
|
25
25
|
export { default as TodoListEditing } from './todolist/todolistediting';
|
|
26
26
|
export { default as TodoListUI } from './todolist/todolistui';
|
|
27
|
+
export { default as TodoDocumentList } from './tododocumentlist';
|
|
28
|
+
export { default as TodoDocumentListEditing } from './tododocumentlist/tododocumentlistediting';
|
|
27
29
|
export type { ListConfig, ListPropertiesConfig } from './listconfig';
|
|
28
30
|
export type { default as ListStyle } from './liststyle';
|
|
29
31
|
export type { default as DocumentListCommand } from './documentlist/documentlistcommand';
|
|
@@ -37,4 +39,5 @@ export type { default as ListReversedCommand } from './listproperties/listrevers
|
|
|
37
39
|
export type { default as ListStartCommand } from './listproperties/liststartcommand';
|
|
38
40
|
export type { default as ListStyleCommand } from './listproperties/liststylecommand';
|
|
39
41
|
export type { default as CheckTodoListCommand } from './todolist/checktodolistcommand';
|
|
42
|
+
export type { default as CheckTodoDocumentListCommand } from './tododocumentlist/checktododocumentlistcommand';
|
|
40
43
|
import './augmentation';
|
package/src/index.js
CHANGED
|
@@ -24,4 +24,6 @@ export { default as ListPropertiesUI } from './listproperties/listpropertiesui';
|
|
|
24
24
|
export { default as TodoList } from './todolist';
|
|
25
25
|
export { default as TodoListEditing } from './todolist/todolistediting';
|
|
26
26
|
export { default as TodoListUI } from './todolist/todolistui';
|
|
27
|
+
export { default as TodoDocumentList } from './tododocumentlist';
|
|
28
|
+
export { default as TodoDocumentListEditing } from './tododocumentlist/tododocumentlistediting';
|
|
27
29
|
import './augmentation';
|
package/src/listconfig.d.ts
CHANGED
|
@@ -30,6 +30,16 @@ export interface ListConfig {
|
|
|
30
30
|
* Read more in {@link module:list/listconfig~ListPropertiesConfig}.
|
|
31
31
|
*/
|
|
32
32
|
properties?: ListPropertiesConfig;
|
|
33
|
+
/**
|
|
34
|
+
* Allows multiple blocks in single list item.
|
|
35
|
+
*
|
|
36
|
+
* With this option enabled you can have block widgets, for example images or even tables, within a list item.
|
|
37
|
+
*
|
|
38
|
+
* **Note:** This is enabled by default.
|
|
39
|
+
*
|
|
40
|
+
* @default true
|
|
41
|
+
*/
|
|
42
|
+
multiBlock?: boolean;
|
|
33
43
|
}
|
|
34
44
|
/**
|
|
35
45
|
* The configuration of the {@link module:list/listproperties~ListProperties list properties} feature and the
|