@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.
Files changed (34) hide show
  1. package/LICENSE.md +2 -2
  2. package/build/list.js +1 -1
  3. package/build/translations/fi.js +1 -1
  4. package/build/translations/pt-br.js +1 -1
  5. package/ckeditor5-metadata.json +44 -0
  6. package/lang/translations/fi.po +1 -1
  7. package/lang/translations/pt-br.po +10 -10
  8. package/package.json +3 -3
  9. package/src/augmentation.d.ts +5 -3
  10. package/src/documentlist/converters.d.ts +3 -1
  11. package/src/documentlist/converters.js +106 -19
  12. package/src/documentlist/documentlistcommand.d.ts +2 -2
  13. package/src/documentlist/documentlistcommand.js +12 -7
  14. package/src/documentlist/documentlistediting.d.ts +65 -7
  15. package/src/documentlist/documentlistediting.js +157 -76
  16. package/src/documentlist/utils/listwalker.d.ts +4 -0
  17. package/src/documentlist/utils/listwalker.js +21 -1
  18. package/src/documentlist/utils/model.d.ts +11 -2
  19. package/src/documentlist/utils/model.js +21 -1
  20. package/src/documentlist/utils/postfixers.js +8 -0
  21. package/src/documentlist/utils/view.d.ts +3 -3
  22. package/src/documentlistproperties/documentlistpropertiesediting.js +11 -34
  23. package/src/index.d.ts +3 -0
  24. package/src/index.js +2 -0
  25. package/src/listconfig.d.ts +10 -0
  26. package/src/tododocumentlist/checktododocumentlistcommand.d.ts +49 -0
  27. package/src/tododocumentlist/checktododocumentlistcommand.js +82 -0
  28. package/src/tododocumentlist/todocheckboxchangeobserver.d.ts +41 -0
  29. package/src/tododocumentlist/todocheckboxchangeobserver.js +37 -0
  30. package/src/tododocumentlist/tododocumentlistediting.d.ts +38 -0
  31. package/src/tododocumentlist/tododocumentlistediting.js +399 -0
  32. package/src/tododocumentlist.d.ts +27 -0
  33. package/src/tododocumentlist.js +31 -0
  34. 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, { iterateSiblingListBlocks, ListBlocksIterable } from './utils/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.extend('$container', { allowAttributes: LIST_BASE_ATTRIBUTES });
67
- model.schema.extend('$block', { allowAttributes: LIST_BASE_ATTRIBUTES });
68
- model.schema.extend('$blockObject', { allowAttributes: LIST_BASE_ATTRIBUTES });
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
- _getListAttributeNames() {
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._getListAttributeNames();
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
- .elementToElement({ view: 'li', model: 'paragraph' })
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: 'paragraph',
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: 'paragraph',
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._getListAttributeNames();
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(model, 'getSelectedContent', (evt, [selection]) => {
380
- const isSingleListItemSelected = isSingleListItem(Array.from(selection.getSelectedBlocks()));
381
- if (isSingleListItemSelected) {
382
- model.change(writer => removeListAttributes(Array.from(evt.return.getChildren()), writer));
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="bulleted" listItemId="x" listIndent=4>X</paragraph>
482
- * // <paragraph listType="bulleted" listItemId="y" listIndent=5>Y</paragraph>
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
- // Check whether inserted content starts from a `listItem`. If it does not, it means that there are some other
498
- // elements before it and there is no need to fix indents, because even if we insert that content into a list,
499
- // that list will be broken.
500
- // Note: we also need to handle singular elements because inserting item with indent 0 into 0,1,[],2
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
- let selection;
507
- if (!selectable) {
508
- selection = model.document.selection;
509
- }
510
- else {
511
- selection = model.createSelection(selectable);
512
- }
513
- // Get a reference list item. Inserted list items will be fixed according to that item.
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(pos.nodeBefore)) {
520
- refItem = pos.nodeBefore;
592
+ else if (isListItemBlock(position.nodeBefore)) {
593
+ refItem = position.nodeBefore;
521
594
  }
522
- // If there is `refItem` it means that we do insert list items into an existing list.
523
- if (!refItem) {
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
- // Adjust indent of all "first" list items in inserted data.
536
- for (const { node } of iterateSiblingListBlocks(item, 'forward')) {
537
- writer.setAttribute('listIndent', node.getAttribute('listIndent') + indentChange, node);
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
  };
@@ -138,4 +138,8 @@ export interface ListIteratorValue {
138
138
  * The previous list node.
139
139
  */
140
140
  previous: ListElement | null;
141
+ /**
142
+ * The previous list node at the same indent as current node.
143
+ */
144
+ previousNodeInList: ListElement | null;
141
145
  }
@@ -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
- yield { node, previous };
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
- // 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.
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
- properties: {
43
- styles: true,
44
- startIndex: false,
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('$container', { allowAttributes: strategy.attributeName });
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 previousNodesByIndent = []; // Last seen nodes of lower indented lists.
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 true;
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';
@@ -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