@ckeditor/ckeditor5-list 38.1.0 → 38.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/build/list.js +1 -1
  2. package/build/list.js.map +1 -0
  3. package/package.json +3 -3
  4. package/src/augmentation.d.ts +50 -50
  5. package/src/augmentation.js +5 -5
  6. package/src/documentlist/adjacentlistssupport.d.ts +15 -15
  7. package/src/documentlist/adjacentlistssupport.js +81 -81
  8. package/src/documentlist/converters.d.ts +63 -63
  9. package/src/documentlist/converters.js +354 -354
  10. package/src/documentlist/documentlistcommand.d.ts +80 -80
  11. package/src/documentlist/documentlistcommand.js +145 -145
  12. package/src/documentlist/documentlistediting.d.ts +154 -154
  13. package/src/documentlist/documentlistediting.js +565 -565
  14. package/src/documentlist/documentlistindentcommand.d.ts +62 -62
  15. package/src/documentlist/documentlistindentcommand.js +129 -129
  16. package/src/documentlist/documentlistmergecommand.d.ts +76 -76
  17. package/src/documentlist/documentlistmergecommand.js +174 -174
  18. package/src/documentlist/documentlistsplitcommand.d.ts +67 -67
  19. package/src/documentlist/documentlistsplitcommand.js +70 -70
  20. package/src/documentlist/documentlistutils.d.ts +46 -46
  21. package/src/documentlist/documentlistutils.js +50 -50
  22. package/src/documentlist/utils/listwalker.d.ts +141 -141
  23. package/src/documentlist/utils/listwalker.js +162 -162
  24. package/src/documentlist/utils/model.d.ts +193 -193
  25. package/src/documentlist/utils/model.js +435 -435
  26. package/src/documentlist/utils/postfixers.d.ts +37 -37
  27. package/src/documentlist/utils/postfixers.js +118 -118
  28. package/src/documentlist/utils/view.d.ts +81 -81
  29. package/src/documentlist/utils/view.js +117 -117
  30. package/src/documentlist.d.ts +26 -26
  31. package/src/documentlist.js +30 -30
  32. package/src/documentlistproperties/converters.d.ts +19 -19
  33. package/src/documentlistproperties/converters.js +43 -43
  34. package/src/documentlistproperties/documentlistpropertiesediting.d.ts +88 -88
  35. package/src/documentlistproperties/documentlistpropertiesediting.js +289 -289
  36. package/src/documentlistproperties/documentlistpropertiesutils.d.ts +33 -33
  37. package/src/documentlistproperties/documentlistpropertiesutils.js +44 -44
  38. package/src/documentlistproperties/documentlistreversedcommand.d.ts +36 -36
  39. package/src/documentlistproperties/documentlistreversedcommand.js +55 -55
  40. package/src/documentlistproperties/documentliststartcommand.d.ts +38 -38
  41. package/src/documentlistproperties/documentliststartcommand.js +57 -57
  42. package/src/documentlistproperties/documentliststylecommand.d.ts +72 -72
  43. package/src/documentlistproperties/documentliststylecommand.js +113 -113
  44. package/src/documentlistproperties/utils/style.d.ts +20 -20
  45. package/src/documentlistproperties/utils/style.js +54 -54
  46. package/src/documentlistproperties.d.ts +27 -27
  47. package/src/documentlistproperties.js +31 -31
  48. package/src/index.d.ts +40 -40
  49. package/src/index.js +27 -27
  50. package/src/list/converters.d.ts +196 -196
  51. package/src/list/converters.js +905 -905
  52. package/src/list/indentcommand.d.ts +37 -37
  53. package/src/list/indentcommand.js +107 -107
  54. package/src/list/listcommand.d.ts +55 -55
  55. package/src/list/listcommand.js +274 -274
  56. package/src/list/listediting.d.ts +32 -32
  57. package/src/list/listediting.js +161 -161
  58. package/src/list/listui.d.ts +19 -19
  59. package/src/list/listui.js +32 -32
  60. package/src/list/listutils.d.ts +41 -41
  61. package/src/list/listutils.js +46 -46
  62. package/src/list/utils.d.ts +112 -112
  63. package/src/list/utils.js +374 -374
  64. package/src/list.d.ts +26 -26
  65. package/src/list.js +30 -30
  66. package/src/listconfig.d.ts +122 -122
  67. package/src/listconfig.js +5 -5
  68. package/src/listproperties/listpropertiesediting.d.ts +72 -72
  69. package/src/listproperties/listpropertiesediting.js +696 -696
  70. package/src/listproperties/listpropertiesui.d.ts +23 -23
  71. package/src/listproperties/listpropertiesui.js +277 -277
  72. package/src/listproperties/listreversedcommand.d.ts +38 -38
  73. package/src/listproperties/listreversedcommand.js +52 -52
  74. package/src/listproperties/liststartcommand.d.ts +37 -37
  75. package/src/listproperties/liststartcommand.js +51 -51
  76. package/src/listproperties/liststylecommand.d.ts +67 -67
  77. package/src/listproperties/liststylecommand.js +99 -99
  78. package/src/listproperties/ui/collapsibleview.d.ts +63 -63
  79. package/src/listproperties/ui/collapsibleview.js +89 -89
  80. package/src/listproperties/ui/listpropertiesview.d.ts +157 -157
  81. package/src/listproperties/ui/listpropertiesview.js +299 -299
  82. package/src/listproperties.d.ts +26 -26
  83. package/src/listproperties.js +30 -30
  84. package/src/liststyle.d.ts +28 -28
  85. package/src/liststyle.js +36 -36
  86. package/src/todolist/checktodolistcommand.d.ts +52 -52
  87. package/src/todolist/checktodolistcommand.js +76 -76
  88. package/src/todolist/todolistconverters.d.ts +82 -82
  89. package/src/todolist/todolistconverters.js +260 -260
  90. package/src/todolist/todolistediting.d.ts +39 -39
  91. package/src/todolist/todolistediting.js +161 -161
  92. package/src/todolist/todolistui.d.ts +19 -19
  93. package/src/todolist/todolistui.js +29 -29
  94. package/src/todolist.d.ts +27 -27
  95. package/src/todolist.js +31 -31
@@ -1,565 +1,565 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
- */
5
- /**
6
- * @module list/documentlist/documentlistediting
7
- */
8
- import { Plugin } from 'ckeditor5/src/core';
9
- import { Delete } from 'ckeditor5/src/typing';
10
- import { Enter } from 'ckeditor5/src/enter';
11
- import { CKEditorError } from 'ckeditor5/src/utils';
12
- import DocumentListIndentCommand from './documentlistindentcommand';
13
- import DocumentListCommand from './documentlistcommand';
14
- import DocumentListMergeCommand from './documentlistmergecommand';
15
- import DocumentListSplitCommand from './documentlistsplitcommand';
16
- import DocumentListUtils from './documentlistutils';
17
- import { bogusParagraphCreator, listItemDowncastConverter, listItemUpcastConverter, listUpcastCleanList, reconvertItemsOnDataChange } from './converters';
18
- import { findAndAddListHeadToMap, fixListIndents, fixListItemIds } from './utils/postfixers';
19
- import { getAllListItemBlocks, isFirstBlockOfListItem, isLastBlockOfListItem, isSingleListItem, getSelectedBlockObject, isListItemBlock, removeListAttributes } from './utils/model';
20
- import { getViewElementIdForListType, getViewElementNameForListType } from './utils/view';
21
- import ListWalker, { iterateSiblingListBlocks, ListBlocksIterable } from './utils/listwalker';
22
- import '../../theme/documentlist.css';
23
- import '../../theme/list.css';
24
- /**
25
- * A list of base list model attributes.
26
- */
27
- const LIST_BASE_ATTRIBUTES = ['listType', 'listIndent', 'listItemId'];
28
- /**
29
- * The editing part of the document-list feature. It handles creating, editing and removing lists and list items.
30
- */
31
- 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
- /**
40
- * @inheritDoc
41
- */
42
- static get pluginName() {
43
- return 'DocumentListEditing';
44
- }
45
- /**
46
- * @inheritDoc
47
- */
48
- static get requires() {
49
- return [Enter, Delete, DocumentListUtils];
50
- }
51
- /**
52
- * @inheritDoc
53
- */
54
- init() {
55
- const editor = this.editor;
56
- const model = editor.model;
57
- if (editor.plugins.has('ListEditing')) {
58
- /**
59
- * The `DocumentList` feature can not be loaded together with the `List` plugin.
60
- *
61
- * @error document-list-feature-conflict
62
- * @param conflictPlugin Name of the plugin.
63
- */
64
- throw new CKEditorError('document-list-feature-conflict', this, { conflictPlugin: 'ListEditing' });
65
- }
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 });
69
- for (const attribute of LIST_BASE_ATTRIBUTES) {
70
- model.schema.setAttributeProperties(attribute, {
71
- copyOnReplace: true
72
- });
73
- }
74
- // Register commands.
75
- editor.commands.add('numberedList', new DocumentListCommand(editor, 'numbered'));
76
- editor.commands.add('bulletedList', new DocumentListCommand(editor, 'bulleted'));
77
- editor.commands.add('indentList', new DocumentListIndentCommand(editor, 'forward'));
78
- 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
- editor.commands.add('splitListItemBefore', new DocumentListSplitCommand(editor, 'before'));
82
- editor.commands.add('splitListItemAfter', new DocumentListSplitCommand(editor, 'after'));
83
- this._setupDeleteIntegration();
84
- this._setupEnterIntegration();
85
- this._setupTabIntegration();
86
- this._setupClipboardIntegration();
87
- }
88
- /**
89
- * @inheritDoc
90
- */
91
- afterInit() {
92
- const editor = this.editor;
93
- const commands = editor.commands;
94
- const indent = commands.get('indent');
95
- const outdent = commands.get('outdent');
96
- if (indent) {
97
- // Priority is high due to integration with `IndentBlock` plugin. We want to indent list first and if it's not possible
98
- // user can indent content with `IndentBlock` plugin.
99
- indent.registerChildCommand(commands.get('indentList'), { priority: 'high' });
100
- }
101
- if (outdent) {
102
- // Priority is lowest due to integration with `IndentBlock` and `IndentCode` plugins.
103
- // First we want to allow user to outdent all indendations from other features then he can oudent list item.
104
- outdent.registerChildCommand(commands.get('outdentList'), { priority: 'lowest' });
105
- }
106
- // Register conversion and model post-fixer after other plugins had a chance to register their attribute strategies.
107
- this._setupModelPostFixing();
108
- this._setupConversion();
109
- }
110
- /**
111
- * Registers a downcast strategy.
112
- *
113
- * **Note**: Strategies must be registered in the `Plugin#init()` phase so that it can be applied
114
- * in the `DocumentListEditing#afterInit()`.
115
- *
116
- * @param strategy The downcast strategy to register.
117
- */
118
- registerDowncastStrategy(strategy) {
119
- this._downcastStrategies.push(strategy);
120
- }
121
- /**
122
- * Returns list of model attribute names that should affect downcast conversion.
123
- */
124
- _getListAttributeNames() {
125
- return [
126
- ...LIST_BASE_ATTRIBUTES,
127
- ...this._downcastStrategies.map(strategy => strategy.attributeName)
128
- ];
129
- }
130
- /**
131
- * Attaches the listener to the {@link module:engine/view/document~Document#event:delete} event and handles backspace/delete
132
- * keys in and around document lists.
133
- */
134
- _setupDeleteIntegration() {
135
- const editor = this.editor;
136
- const mergeBackwardCommand = editor.commands.get('mergeListItemBackward');
137
- const mergeForwardCommand = editor.commands.get('mergeListItemForward');
138
- this.listenTo(editor.editing.view.document, 'delete', (evt, data) => {
139
- const selection = editor.model.document.selection;
140
- // Let the Widget plugin take care of block widgets while deleting (https://github.com/ckeditor/ckeditor5/issues/11346).
141
- if (getSelectedBlockObject(editor.model)) {
142
- return;
143
- }
144
- editor.model.change(() => {
145
- const firstPosition = selection.getFirstPosition();
146
- if (selection.isCollapsed && data.direction == 'backward') {
147
- if (!firstPosition.isAtStart) {
148
- return;
149
- }
150
- const positionParent = firstPosition.parent;
151
- if (!isListItemBlock(positionParent)) {
152
- return;
153
- }
154
- const previousBlock = ListWalker.first(positionParent, {
155
- sameAttributes: 'listType',
156
- sameIndent: true
157
- });
158
- // Outdent the first block of a first list item.
159
- if (!previousBlock && positionParent.getAttribute('listIndent') === 0) {
160
- if (!isLastBlockOfListItem(positionParent)) {
161
- editor.execute('splitListItemAfter');
162
- }
163
- editor.execute('outdentList');
164
- }
165
- // Merge block with previous one (on the block level or on the content level).
166
- else {
167
- if (!mergeBackwardCommand.isEnabled) {
168
- return;
169
- }
170
- mergeBackwardCommand.execute({
171
- shouldMergeOnBlocksContentLevel: shouldMergeOnBlocksContentLevel(editor.model, 'backward')
172
- });
173
- }
174
- data.preventDefault();
175
- evt.stop();
176
- }
177
- // Non-collapsed selection or forward delete.
178
- else {
179
- // Collapsed selection should trigger forward merging only if at the end of a block.
180
- if (selection.isCollapsed && !selection.getLastPosition().isAtEnd) {
181
- return;
182
- }
183
- if (!mergeForwardCommand.isEnabled) {
184
- return;
185
- }
186
- mergeForwardCommand.execute({
187
- shouldMergeOnBlocksContentLevel: shouldMergeOnBlocksContentLevel(editor.model, 'forward')
188
- });
189
- data.preventDefault();
190
- evt.stop();
191
- }
192
- });
193
- }, { context: 'li' });
194
- }
195
- /**
196
- * Attaches a listener to the {@link module:engine/view/document~Document#event:enter} event and handles enter key press
197
- * in document lists.
198
- */
199
- _setupEnterIntegration() {
200
- const editor = this.editor;
201
- const model = editor.model;
202
- const commands = editor.commands;
203
- const enterCommand = commands.get('enter');
204
- // Overwrite the default Enter key behavior: outdent or split the list in certain cases.
205
- this.listenTo(editor.editing.view.document, 'enter', (evt, data) => {
206
- const doc = model.document;
207
- const positionParent = doc.selection.getFirstPosition().parent;
208
- if (doc.selection.isCollapsed &&
209
- isListItemBlock(positionParent) &&
210
- positionParent.isEmpty &&
211
- !data.isSoft) {
212
- const isFirstBlock = isFirstBlockOfListItem(positionParent);
213
- const isLastBlock = isLastBlockOfListItem(positionParent);
214
- // * a → * a
215
- // * [] → []
216
- if (isFirstBlock && isLastBlock) {
217
- editor.execute('outdentList');
218
- data.preventDefault();
219
- evt.stop();
220
- }
221
- // * [] → * []
222
- // a → * a
223
- else if (isFirstBlock && !isLastBlock) {
224
- editor.execute('splitListItemAfter');
225
- data.preventDefault();
226
- evt.stop();
227
- }
228
- // * a → * a
229
- // [] → * []
230
- else if (isLastBlock) {
231
- editor.execute('splitListItemBefore');
232
- data.preventDefault();
233
- evt.stop();
234
- }
235
- }
236
- }, { context: 'li' });
237
- // In some cases, after the default block splitting, we want to modify the new block to become a new list item
238
- // instead of an additional block in the same list item.
239
- this.listenTo(enterCommand, 'afterExecute', () => {
240
- const splitCommand = commands.get('splitListItemBefore');
241
- // The command has not refreshed because the change block related to EnterCommand#execute() is not over yet.
242
- // Let's keep it up to date and take advantage of DocumentListSplitCommand#isEnabled.
243
- splitCommand.refresh();
244
- if (!splitCommand.isEnabled) {
245
- return;
246
- }
247
- const doc = editor.model.document;
248
- const positionParent = doc.selection.getLastPosition().parent;
249
- const listItemBlocks = getAllListItemBlocks(positionParent);
250
- // Keep in mind this split happens after the default enter handler was executed. For instance:
251
- //
252
- // │ Initial state │ After default enter │ Here in #afterExecute │
253
- // ├───────────────────────────┼───────────────────────────┼───────────────────────────┤
254
- // │ * a[] │ * a │ * a │
255
- // │ │ [] │ * [] │
256
- if (listItemBlocks.length === 2) {
257
- splitCommand.execute();
258
- }
259
- });
260
- }
261
- /**
262
- * Attaches a listener to the {@link module:engine/view/document~Document#event:tab} event and handles tab key and tab+shift keys
263
- * presses in document lists.
264
- */
265
- _setupTabIntegration() {
266
- const editor = this.editor;
267
- this.listenTo(editor.editing.view.document, 'tab', (evt, data) => {
268
- const commandName = data.shiftKey ? 'outdentList' : 'indentList';
269
- const command = this.editor.commands.get(commandName);
270
- if (command.isEnabled) {
271
- editor.execute(commandName);
272
- data.stopPropagation();
273
- data.preventDefault();
274
- evt.stop();
275
- }
276
- }, { context: 'li' });
277
- }
278
- /**
279
- * Registers the conversion helpers for the document-list feature.
280
- */
281
- _setupConversion() {
282
- const editor = this.editor;
283
- const model = editor.model;
284
- const attributeNames = this._getListAttributeNames();
285
- editor.conversion.for('upcast')
286
- .elementToElement({ view: 'li', model: 'paragraph' })
287
- .add(dispatcher => {
288
- dispatcher.on('element:li', listItemUpcastConverter());
289
- dispatcher.on('element:ul', listUpcastCleanList(), { priority: 'high' });
290
- dispatcher.on('element:ol', listUpcastCleanList(), { priority: 'high' });
291
- });
292
- editor.conversion.for('editingDowncast')
293
- .elementToElement({
294
- model: 'paragraph',
295
- view: bogusParagraphCreator(attributeNames),
296
- converterPriority: 'high'
297
- });
298
- editor.conversion.for('dataDowncast')
299
- .elementToElement({
300
- model: 'paragraph',
301
- view: bogusParagraphCreator(attributeNames, { dataPipeline: true }),
302
- converterPriority: 'high'
303
- });
304
- editor.conversion.for('downcast')
305
- .add(dispatcher => {
306
- dispatcher.on('attribute', listItemDowncastConverter(attributeNames, this._downcastStrategies, model));
307
- });
308
- this.listenTo(model.document, 'change:data', reconvertItemsOnDataChange(model, editor.editing, attributeNames, this), { priority: 'high' });
309
- // For LI verify if an ID of the attribute element is correct.
310
- this.on('checkAttributes:item', (evt, { viewElement, modelAttributes }) => {
311
- if (viewElement.id != modelAttributes.listItemId) {
312
- evt.return = true;
313
- evt.stop();
314
- }
315
- });
316
- // For UL and OL check if the name and ID of element is correct.
317
- this.on('checkAttributes:list', (evt, { viewElement, modelAttributes }) => {
318
- if (viewElement.name != getViewElementNameForListType(modelAttributes.listType) ||
319
- viewElement.id != getViewElementIdForListType(modelAttributes.listType, modelAttributes.listIndent)) {
320
- evt.return = true;
321
- evt.stop();
322
- }
323
- });
324
- }
325
- /**
326
- * Registers model post-fixers.
327
- */
328
- _setupModelPostFixing() {
329
- const model = this.editor.model;
330
- const attributeNames = this._getListAttributeNames();
331
- // Register list fixing.
332
- // First the low level handler.
333
- model.document.registerPostFixer(writer => modelChangePostFixer(model, writer, attributeNames, this));
334
- // Then the callbacks for the specific lists.
335
- // The indentation fixing must be the first one...
336
- this.on('postFixer', (evt, { listNodes, writer }) => {
337
- evt.return = fixListIndents(listNodes, writer) || evt.return;
338
- }, { priority: 'high' });
339
- // ...then the item ids... and after that other fixers that rely on the correct indentation and ids.
340
- this.on('postFixer', (evt, { listNodes, writer, seenIds }) => {
341
- evt.return = fixListItemIds(listNodes, seenIds, writer) || evt.return;
342
- }, { priority: 'high' });
343
- }
344
- /**
345
- * Integrates the feature with the clipboard via {@link module:engine/model/model~Model#insertContent} and
346
- * {@link module:engine/model/model~Model#getSelectedContent}.
347
- */
348
- _setupClipboardIntegration() {
349
- const model = this.editor.model;
350
- this.listenTo(model, 'insertContent', createModelIndentPasteFixer(model), { priority: 'high' });
351
- // To enhance the UX, the editor should not copy list attributes to the clipboard if the selection
352
- // started and ended in the same list item.
353
- //
354
- // If the selection was enclosed in a single list item, there is a good chance the user did not want it
355
- // copied as a list item but plain blocks.
356
- //
357
- // This avoids pasting orphaned list items instead of paragraphs, for instance, straight into the root.
358
- //
359
- // ┌─────────────────────┬───────────────────┐
360
- // │ Selection │ Clipboard content │
361
- // ├─────────────────────┼───────────────────┤
362
- // │ [* <Widget />] │ <Widget /> │
363
- // ├─────────────────────┼───────────────────┤
364
- // │ [* Foo] │ Foo │
365
- // ├─────────────────────┼───────────────────┤
366
- // │ * Foo [bar] baz │ bar │
367
- // ├─────────────────────┼───────────────────┤
368
- // │ * Fo[o │ o │
369
- // │ ba]r │ ba │
370
- // ├─────────────────────┼───────────────────┤
371
- // │ * Fo[o │ * o │
372
- // │ * ba]r │ * ba │
373
- // ├─────────────────────┼───────────────────┤
374
- // │ [* Foo │ * Foo │
375
- // │ * bar] │ * bar │
376
- // └─────────────────────┴───────────────────┘
377
- //
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
- }
384
- });
385
- }
386
- }
387
- /**
388
- * Post-fixer that reacts to changes on document and fixes incorrect model states (invalid `listItemId` and `listIndent` values).
389
- *
390
- * In the example below, there is a correct list structure.
391
- * Then the middle element is removed so the list structure will become incorrect:
392
- *
393
- * ```xml
394
- * <paragraph listType="bulleted" listItemId="a" listIndent=0>Item 1</paragraph>
395
- * <paragraph listType="bulleted" listItemId="b" listIndent=1>Item 2</paragraph> <--- this is removed.
396
- * <paragraph listType="bulleted" listItemId="c" listIndent=2>Item 3</paragraph>
397
- * ```
398
- *
399
- * The list structure after the middle element is removed:
400
- *
401
- * ```xml
402
- * <paragraph listType="bulleted" listItemId="a" listIndent=0>Item 1</paragraph>
403
- * <paragraph listType="bulleted" listItemId="c" listIndent=2>Item 3</paragraph>
404
- * ```
405
- *
406
- * Should become:
407
- *
408
- * ```xml
409
- * <paragraph listType="bulleted" listItemId="a" listIndent=0>Item 1</paragraph>
410
- * <paragraph listType="bulleted" listItemId="c" listIndent=1>Item 3</paragraph> <--- note that indent got post-fixed.
411
- * ```
412
- *
413
- * @param model The data model.
414
- * @param writer The writer to do changes with.
415
- * @param attributeNames The list of all model list attributes (including registered strategies).
416
- * @param documentListEditing The document list editing plugin.
417
- * @returns `true` if any change has been applied, `false` otherwise.
418
- */
419
- function modelChangePostFixer(model, writer, attributeNames, documentListEditing) {
420
- const changes = model.document.differ.getChanges();
421
- const itemToListHead = new Map();
422
- let applied = false;
423
- for (const entry of changes) {
424
- if (entry.type == 'insert' && entry.name != '$text') {
425
- const item = entry.position.nodeAfter;
426
- // Remove attributes in case of renamed element.
427
- if (!model.schema.checkAttribute(item, 'listItemId')) {
428
- for (const attributeName of Array.from(item.getAttributeKeys())) {
429
- if (attributeNames.includes(attributeName)) {
430
- writer.removeAttribute(attributeName, item);
431
- applied = true;
432
- }
433
- }
434
- }
435
- findAndAddListHeadToMap(entry.position, itemToListHead);
436
- // Insert of a non-list item - check if there is a list after it.
437
- if (!entry.attributes.has('listItemId')) {
438
- findAndAddListHeadToMap(entry.position.getShiftedBy(entry.length), itemToListHead);
439
- }
440
- // Check if there is no nested list.
441
- for (const { item: innerItem, previousPosition } of model.createRangeIn(item)) {
442
- if (isListItemBlock(innerItem)) {
443
- findAndAddListHeadToMap(previousPosition, itemToListHead);
444
- }
445
- }
446
- }
447
- // Removed list item or block adjacent to a list.
448
- else if (entry.type == 'remove') {
449
- findAndAddListHeadToMap(entry.position, itemToListHead);
450
- }
451
- // Changed list item indent or type.
452
- else if (entry.type == 'attribute' && attributeNames.includes(entry.attributeKey)) {
453
- findAndAddListHeadToMap(entry.range.start, itemToListHead);
454
- if (entry.attributeNewValue === null) {
455
- findAndAddListHeadToMap(entry.range.start.getShiftedBy(1), itemToListHead);
456
- }
457
- }
458
- }
459
- // Make sure that IDs are not shared by split list.
460
- const seenIds = new Set();
461
- for (const listHead of itemToListHead.values()) {
462
- applied = documentListEditing.fire('postFixer', {
463
- listNodes: new ListBlocksIterable(listHead),
464
- listHead,
465
- writer,
466
- seenIds
467
- }) || applied;
468
- }
469
- return applied;
470
- }
471
- /**
472
- * A fixer for pasted content that includes list items.
473
- *
474
- * It fixes indentation of pasted list items so the pasted items match correctly to the context they are pasted into.
475
- *
476
- * Example:
477
- *
478
- * ```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>
484
- * ```
485
- *
486
- * Should become:
487
- *
488
- * ```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>
493
- * ```
494
- */
495
- function createModelIndentPasteFixer(model) {
496
- 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)) {
504
- return;
505
- }
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;
518
- }
519
- else if (isListItemBlock(pos.nodeBefore)) {
520
- refItem = pos.nodeBefore;
521
- }
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;
533
- }
534
- 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);
538
- }
539
- });
540
- };
541
- }
542
- /**
543
- * Decides whether the merge should be accompanied by the model's `deleteContent()`, for instance, to get rid of the inline
544
- * content in the selection or take advantage of the heuristics in `deleteContent()` that helps convert lists into paragraphs
545
- * in certain cases.
546
- */
547
- function shouldMergeOnBlocksContentLevel(model, direction) {
548
- const selection = model.document.selection;
549
- if (!selection.isCollapsed) {
550
- return !getSelectedBlockObject(model);
551
- }
552
- if (direction === 'forward') {
553
- return true;
554
- }
555
- const firstPosition = selection.getFirstPosition();
556
- const positionParent = firstPosition.parent;
557
- const previousSibling = positionParent.previousSibling;
558
- if (model.schema.isObject(previousSibling)) {
559
- return false;
560
- }
561
- if (previousSibling.isEmpty) {
562
- return true;
563
- }
564
- return isSingleListItem([positionParent, previousSibling]);
565
- }
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module list/documentlist/documentlistediting
7
+ */
8
+ import { Plugin } from 'ckeditor5/src/core';
9
+ import { Delete } from 'ckeditor5/src/typing';
10
+ import { Enter } from 'ckeditor5/src/enter';
11
+ import { CKEditorError } from 'ckeditor5/src/utils';
12
+ import DocumentListIndentCommand from './documentlistindentcommand';
13
+ import DocumentListCommand from './documentlistcommand';
14
+ import DocumentListMergeCommand from './documentlistmergecommand';
15
+ import DocumentListSplitCommand from './documentlistsplitcommand';
16
+ import DocumentListUtils from './documentlistutils';
17
+ import { bogusParagraphCreator, listItemDowncastConverter, listItemUpcastConverter, listUpcastCleanList, reconvertItemsOnDataChange } from './converters';
18
+ import { findAndAddListHeadToMap, fixListIndents, fixListItemIds } from './utils/postfixers';
19
+ import { getAllListItemBlocks, isFirstBlockOfListItem, isLastBlockOfListItem, isSingleListItem, getSelectedBlockObject, isListItemBlock, removeListAttributes } from './utils/model';
20
+ import { getViewElementIdForListType, getViewElementNameForListType } from './utils/view';
21
+ import ListWalker, { iterateSiblingListBlocks, ListBlocksIterable } from './utils/listwalker';
22
+ import '../../theme/documentlist.css';
23
+ import '../../theme/list.css';
24
+ /**
25
+ * A list of base list model attributes.
26
+ */
27
+ const LIST_BASE_ATTRIBUTES = ['listType', 'listIndent', 'listItemId'];
28
+ /**
29
+ * The editing part of the document-list feature. It handles creating, editing and removing lists and list items.
30
+ */
31
+ 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
+ /**
40
+ * @inheritDoc
41
+ */
42
+ static get pluginName() {
43
+ return 'DocumentListEditing';
44
+ }
45
+ /**
46
+ * @inheritDoc
47
+ */
48
+ static get requires() {
49
+ return [Enter, Delete, DocumentListUtils];
50
+ }
51
+ /**
52
+ * @inheritDoc
53
+ */
54
+ init() {
55
+ const editor = this.editor;
56
+ const model = editor.model;
57
+ if (editor.plugins.has('ListEditing')) {
58
+ /**
59
+ * The `DocumentList` feature can not be loaded together with the `List` plugin.
60
+ *
61
+ * @error document-list-feature-conflict
62
+ * @param conflictPlugin Name of the plugin.
63
+ */
64
+ throw new CKEditorError('document-list-feature-conflict', this, { conflictPlugin: 'ListEditing' });
65
+ }
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 });
69
+ for (const attribute of LIST_BASE_ATTRIBUTES) {
70
+ model.schema.setAttributeProperties(attribute, {
71
+ copyOnReplace: true
72
+ });
73
+ }
74
+ // Register commands.
75
+ editor.commands.add('numberedList', new DocumentListCommand(editor, 'numbered'));
76
+ editor.commands.add('bulletedList', new DocumentListCommand(editor, 'bulleted'));
77
+ editor.commands.add('indentList', new DocumentListIndentCommand(editor, 'forward'));
78
+ 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
+ editor.commands.add('splitListItemBefore', new DocumentListSplitCommand(editor, 'before'));
82
+ editor.commands.add('splitListItemAfter', new DocumentListSplitCommand(editor, 'after'));
83
+ this._setupDeleteIntegration();
84
+ this._setupEnterIntegration();
85
+ this._setupTabIntegration();
86
+ this._setupClipboardIntegration();
87
+ }
88
+ /**
89
+ * @inheritDoc
90
+ */
91
+ afterInit() {
92
+ const editor = this.editor;
93
+ const commands = editor.commands;
94
+ const indent = commands.get('indent');
95
+ const outdent = commands.get('outdent');
96
+ if (indent) {
97
+ // Priority is high due to integration with `IndentBlock` plugin. We want to indent list first and if it's not possible
98
+ // user can indent content with `IndentBlock` plugin.
99
+ indent.registerChildCommand(commands.get('indentList'), { priority: 'high' });
100
+ }
101
+ if (outdent) {
102
+ // Priority is lowest due to integration with `IndentBlock` and `IndentCode` plugins.
103
+ // First we want to allow user to outdent all indendations from other features then he can oudent list item.
104
+ outdent.registerChildCommand(commands.get('outdentList'), { priority: 'lowest' });
105
+ }
106
+ // Register conversion and model post-fixer after other plugins had a chance to register their attribute strategies.
107
+ this._setupModelPostFixing();
108
+ this._setupConversion();
109
+ }
110
+ /**
111
+ * Registers a downcast strategy.
112
+ *
113
+ * **Note**: Strategies must be registered in the `Plugin#init()` phase so that it can be applied
114
+ * in the `DocumentListEditing#afterInit()`.
115
+ *
116
+ * @param strategy The downcast strategy to register.
117
+ */
118
+ registerDowncastStrategy(strategy) {
119
+ this._downcastStrategies.push(strategy);
120
+ }
121
+ /**
122
+ * Returns list of model attribute names that should affect downcast conversion.
123
+ */
124
+ _getListAttributeNames() {
125
+ return [
126
+ ...LIST_BASE_ATTRIBUTES,
127
+ ...this._downcastStrategies.map(strategy => strategy.attributeName)
128
+ ];
129
+ }
130
+ /**
131
+ * Attaches the listener to the {@link module:engine/view/document~Document#event:delete} event and handles backspace/delete
132
+ * keys in and around document lists.
133
+ */
134
+ _setupDeleteIntegration() {
135
+ const editor = this.editor;
136
+ const mergeBackwardCommand = editor.commands.get('mergeListItemBackward');
137
+ const mergeForwardCommand = editor.commands.get('mergeListItemForward');
138
+ this.listenTo(editor.editing.view.document, 'delete', (evt, data) => {
139
+ const selection = editor.model.document.selection;
140
+ // Let the Widget plugin take care of block widgets while deleting (https://github.com/ckeditor/ckeditor5/issues/11346).
141
+ if (getSelectedBlockObject(editor.model)) {
142
+ return;
143
+ }
144
+ editor.model.change(() => {
145
+ const firstPosition = selection.getFirstPosition();
146
+ if (selection.isCollapsed && data.direction == 'backward') {
147
+ if (!firstPosition.isAtStart) {
148
+ return;
149
+ }
150
+ const positionParent = firstPosition.parent;
151
+ if (!isListItemBlock(positionParent)) {
152
+ return;
153
+ }
154
+ const previousBlock = ListWalker.first(positionParent, {
155
+ sameAttributes: 'listType',
156
+ sameIndent: true
157
+ });
158
+ // Outdent the first block of a first list item.
159
+ if (!previousBlock && positionParent.getAttribute('listIndent') === 0) {
160
+ if (!isLastBlockOfListItem(positionParent)) {
161
+ editor.execute('splitListItemAfter');
162
+ }
163
+ editor.execute('outdentList');
164
+ }
165
+ // Merge block with previous one (on the block level or on the content level).
166
+ else {
167
+ if (!mergeBackwardCommand.isEnabled) {
168
+ return;
169
+ }
170
+ mergeBackwardCommand.execute({
171
+ shouldMergeOnBlocksContentLevel: shouldMergeOnBlocksContentLevel(editor.model, 'backward')
172
+ });
173
+ }
174
+ data.preventDefault();
175
+ evt.stop();
176
+ }
177
+ // Non-collapsed selection or forward delete.
178
+ else {
179
+ // Collapsed selection should trigger forward merging only if at the end of a block.
180
+ if (selection.isCollapsed && !selection.getLastPosition().isAtEnd) {
181
+ return;
182
+ }
183
+ if (!mergeForwardCommand.isEnabled) {
184
+ return;
185
+ }
186
+ mergeForwardCommand.execute({
187
+ shouldMergeOnBlocksContentLevel: shouldMergeOnBlocksContentLevel(editor.model, 'forward')
188
+ });
189
+ data.preventDefault();
190
+ evt.stop();
191
+ }
192
+ });
193
+ }, { context: 'li' });
194
+ }
195
+ /**
196
+ * Attaches a listener to the {@link module:engine/view/document~Document#event:enter} event and handles enter key press
197
+ * in document lists.
198
+ */
199
+ _setupEnterIntegration() {
200
+ const editor = this.editor;
201
+ const model = editor.model;
202
+ const commands = editor.commands;
203
+ const enterCommand = commands.get('enter');
204
+ // Overwrite the default Enter key behavior: outdent or split the list in certain cases.
205
+ this.listenTo(editor.editing.view.document, 'enter', (evt, data) => {
206
+ const doc = model.document;
207
+ const positionParent = doc.selection.getFirstPosition().parent;
208
+ if (doc.selection.isCollapsed &&
209
+ isListItemBlock(positionParent) &&
210
+ positionParent.isEmpty &&
211
+ !data.isSoft) {
212
+ const isFirstBlock = isFirstBlockOfListItem(positionParent);
213
+ const isLastBlock = isLastBlockOfListItem(positionParent);
214
+ // * a → * a
215
+ // * [] → []
216
+ if (isFirstBlock && isLastBlock) {
217
+ editor.execute('outdentList');
218
+ data.preventDefault();
219
+ evt.stop();
220
+ }
221
+ // * [] → * []
222
+ // a → * a
223
+ else if (isFirstBlock && !isLastBlock) {
224
+ editor.execute('splitListItemAfter');
225
+ data.preventDefault();
226
+ evt.stop();
227
+ }
228
+ // * a → * a
229
+ // [] → * []
230
+ else if (isLastBlock) {
231
+ editor.execute('splitListItemBefore');
232
+ data.preventDefault();
233
+ evt.stop();
234
+ }
235
+ }
236
+ }, { context: 'li' });
237
+ // In some cases, after the default block splitting, we want to modify the new block to become a new list item
238
+ // instead of an additional block in the same list item.
239
+ this.listenTo(enterCommand, 'afterExecute', () => {
240
+ const splitCommand = commands.get('splitListItemBefore');
241
+ // The command has not refreshed because the change block related to EnterCommand#execute() is not over yet.
242
+ // Let's keep it up to date and take advantage of DocumentListSplitCommand#isEnabled.
243
+ splitCommand.refresh();
244
+ if (!splitCommand.isEnabled) {
245
+ return;
246
+ }
247
+ const doc = editor.model.document;
248
+ const positionParent = doc.selection.getLastPosition().parent;
249
+ const listItemBlocks = getAllListItemBlocks(positionParent);
250
+ // Keep in mind this split happens after the default enter handler was executed. For instance:
251
+ //
252
+ // │ Initial state │ After default enter │ Here in #afterExecute │
253
+ // ├───────────────────────────┼───────────────────────────┼───────────────────────────┤
254
+ // │ * a[] │ * a │ * a │
255
+ // │ │ [] │ * [] │
256
+ if (listItemBlocks.length === 2) {
257
+ splitCommand.execute();
258
+ }
259
+ });
260
+ }
261
+ /**
262
+ * Attaches a listener to the {@link module:engine/view/document~Document#event:tab} event and handles tab key and tab+shift keys
263
+ * presses in document lists.
264
+ */
265
+ _setupTabIntegration() {
266
+ const editor = this.editor;
267
+ this.listenTo(editor.editing.view.document, 'tab', (evt, data) => {
268
+ const commandName = data.shiftKey ? 'outdentList' : 'indentList';
269
+ const command = this.editor.commands.get(commandName);
270
+ if (command.isEnabled) {
271
+ editor.execute(commandName);
272
+ data.stopPropagation();
273
+ data.preventDefault();
274
+ evt.stop();
275
+ }
276
+ }, { context: 'li' });
277
+ }
278
+ /**
279
+ * Registers the conversion helpers for the document-list feature.
280
+ */
281
+ _setupConversion() {
282
+ const editor = this.editor;
283
+ const model = editor.model;
284
+ const attributeNames = this._getListAttributeNames();
285
+ editor.conversion.for('upcast')
286
+ .elementToElement({ view: 'li', model: 'paragraph' })
287
+ .add(dispatcher => {
288
+ dispatcher.on('element:li', listItemUpcastConverter());
289
+ dispatcher.on('element:ul', listUpcastCleanList(), { priority: 'high' });
290
+ dispatcher.on('element:ol', listUpcastCleanList(), { priority: 'high' });
291
+ });
292
+ editor.conversion.for('editingDowncast')
293
+ .elementToElement({
294
+ model: 'paragraph',
295
+ view: bogusParagraphCreator(attributeNames),
296
+ converterPriority: 'high'
297
+ });
298
+ editor.conversion.for('dataDowncast')
299
+ .elementToElement({
300
+ model: 'paragraph',
301
+ view: bogusParagraphCreator(attributeNames, { dataPipeline: true }),
302
+ converterPriority: 'high'
303
+ });
304
+ editor.conversion.for('downcast')
305
+ .add(dispatcher => {
306
+ dispatcher.on('attribute', listItemDowncastConverter(attributeNames, this._downcastStrategies, model));
307
+ });
308
+ this.listenTo(model.document, 'change:data', reconvertItemsOnDataChange(model, editor.editing, attributeNames, this), { priority: 'high' });
309
+ // For LI verify if an ID of the attribute element is correct.
310
+ this.on('checkAttributes:item', (evt, { viewElement, modelAttributes }) => {
311
+ if (viewElement.id != modelAttributes.listItemId) {
312
+ evt.return = true;
313
+ evt.stop();
314
+ }
315
+ });
316
+ // For UL and OL check if the name and ID of element is correct.
317
+ this.on('checkAttributes:list', (evt, { viewElement, modelAttributes }) => {
318
+ if (viewElement.name != getViewElementNameForListType(modelAttributes.listType) ||
319
+ viewElement.id != getViewElementIdForListType(modelAttributes.listType, modelAttributes.listIndent)) {
320
+ evt.return = true;
321
+ evt.stop();
322
+ }
323
+ });
324
+ }
325
+ /**
326
+ * Registers model post-fixers.
327
+ */
328
+ _setupModelPostFixing() {
329
+ const model = this.editor.model;
330
+ const attributeNames = this._getListAttributeNames();
331
+ // Register list fixing.
332
+ // First the low level handler.
333
+ model.document.registerPostFixer(writer => modelChangePostFixer(model, writer, attributeNames, this));
334
+ // Then the callbacks for the specific lists.
335
+ // The indentation fixing must be the first one...
336
+ this.on('postFixer', (evt, { listNodes, writer }) => {
337
+ evt.return = fixListIndents(listNodes, writer) || evt.return;
338
+ }, { priority: 'high' });
339
+ // ...then the item ids... and after that other fixers that rely on the correct indentation and ids.
340
+ this.on('postFixer', (evt, { listNodes, writer, seenIds }) => {
341
+ evt.return = fixListItemIds(listNodes, seenIds, writer) || evt.return;
342
+ }, { priority: 'high' });
343
+ }
344
+ /**
345
+ * Integrates the feature with the clipboard via {@link module:engine/model/model~Model#insertContent} and
346
+ * {@link module:engine/model/model~Model#getSelectedContent}.
347
+ */
348
+ _setupClipboardIntegration() {
349
+ const model = this.editor.model;
350
+ this.listenTo(model, 'insertContent', createModelIndentPasteFixer(model), { priority: 'high' });
351
+ // To enhance the UX, the editor should not copy list attributes to the clipboard if the selection
352
+ // started and ended in the same list item.
353
+ //
354
+ // If the selection was enclosed in a single list item, there is a good chance the user did not want it
355
+ // copied as a list item but plain blocks.
356
+ //
357
+ // This avoids pasting orphaned list items instead of paragraphs, for instance, straight into the root.
358
+ //
359
+ // ┌─────────────────────┬───────────────────┐
360
+ // │ Selection │ Clipboard content │
361
+ // ├─────────────────────┼───────────────────┤
362
+ // │ [* <Widget />] │ <Widget /> │
363
+ // ├─────────────────────┼───────────────────┤
364
+ // │ [* Foo] │ Foo │
365
+ // ├─────────────────────┼───────────────────┤
366
+ // │ * Foo [bar] baz │ bar │
367
+ // ├─────────────────────┼───────────────────┤
368
+ // │ * Fo[o │ o │
369
+ // │ ba]r │ ba │
370
+ // ├─────────────────────┼───────────────────┤
371
+ // │ * Fo[o │ * o │
372
+ // │ * ba]r │ * ba │
373
+ // ├─────────────────────┼───────────────────┤
374
+ // │ [* Foo │ * Foo │
375
+ // │ * bar] │ * bar │
376
+ // └─────────────────────┴───────────────────┘
377
+ //
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
+ }
384
+ });
385
+ }
386
+ }
387
+ /**
388
+ * Post-fixer that reacts to changes on document and fixes incorrect model states (invalid `listItemId` and `listIndent` values).
389
+ *
390
+ * In the example below, there is a correct list structure.
391
+ * Then the middle element is removed so the list structure will become incorrect:
392
+ *
393
+ * ```xml
394
+ * <paragraph listType="bulleted" listItemId="a" listIndent=0>Item 1</paragraph>
395
+ * <paragraph listType="bulleted" listItemId="b" listIndent=1>Item 2</paragraph> <--- this is removed.
396
+ * <paragraph listType="bulleted" listItemId="c" listIndent=2>Item 3</paragraph>
397
+ * ```
398
+ *
399
+ * The list structure after the middle element is removed:
400
+ *
401
+ * ```xml
402
+ * <paragraph listType="bulleted" listItemId="a" listIndent=0>Item 1</paragraph>
403
+ * <paragraph listType="bulleted" listItemId="c" listIndent=2>Item 3</paragraph>
404
+ * ```
405
+ *
406
+ * Should become:
407
+ *
408
+ * ```xml
409
+ * <paragraph listType="bulleted" listItemId="a" listIndent=0>Item 1</paragraph>
410
+ * <paragraph listType="bulleted" listItemId="c" listIndent=1>Item 3</paragraph> <--- note that indent got post-fixed.
411
+ * ```
412
+ *
413
+ * @param model The data model.
414
+ * @param writer The writer to do changes with.
415
+ * @param attributeNames The list of all model list attributes (including registered strategies).
416
+ * @param documentListEditing The document list editing plugin.
417
+ * @returns `true` if any change has been applied, `false` otherwise.
418
+ */
419
+ function modelChangePostFixer(model, writer, attributeNames, documentListEditing) {
420
+ const changes = model.document.differ.getChanges();
421
+ const itemToListHead = new Map();
422
+ let applied = false;
423
+ for (const entry of changes) {
424
+ if (entry.type == 'insert' && entry.name != '$text') {
425
+ const item = entry.position.nodeAfter;
426
+ // Remove attributes in case of renamed element.
427
+ if (!model.schema.checkAttribute(item, 'listItemId')) {
428
+ for (const attributeName of Array.from(item.getAttributeKeys())) {
429
+ if (attributeNames.includes(attributeName)) {
430
+ writer.removeAttribute(attributeName, item);
431
+ applied = true;
432
+ }
433
+ }
434
+ }
435
+ findAndAddListHeadToMap(entry.position, itemToListHead);
436
+ // Insert of a non-list item - check if there is a list after it.
437
+ if (!entry.attributes.has('listItemId')) {
438
+ findAndAddListHeadToMap(entry.position.getShiftedBy(entry.length), itemToListHead);
439
+ }
440
+ // Check if there is no nested list.
441
+ for (const { item: innerItem, previousPosition } of model.createRangeIn(item)) {
442
+ if (isListItemBlock(innerItem)) {
443
+ findAndAddListHeadToMap(previousPosition, itemToListHead);
444
+ }
445
+ }
446
+ }
447
+ // Removed list item or block adjacent to a list.
448
+ else if (entry.type == 'remove') {
449
+ findAndAddListHeadToMap(entry.position, itemToListHead);
450
+ }
451
+ // Changed list item indent or type.
452
+ else if (entry.type == 'attribute' && attributeNames.includes(entry.attributeKey)) {
453
+ findAndAddListHeadToMap(entry.range.start, itemToListHead);
454
+ if (entry.attributeNewValue === null) {
455
+ findAndAddListHeadToMap(entry.range.start.getShiftedBy(1), itemToListHead);
456
+ }
457
+ }
458
+ }
459
+ // Make sure that IDs are not shared by split list.
460
+ const seenIds = new Set();
461
+ for (const listHead of itemToListHead.values()) {
462
+ applied = documentListEditing.fire('postFixer', {
463
+ listNodes: new ListBlocksIterable(listHead),
464
+ listHead,
465
+ writer,
466
+ seenIds
467
+ }) || applied;
468
+ }
469
+ return applied;
470
+ }
471
+ /**
472
+ * A fixer for pasted content that includes list items.
473
+ *
474
+ * It fixes indentation of pasted list items so the pasted items match correctly to the context they are pasted into.
475
+ *
476
+ * Example:
477
+ *
478
+ * ```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>
484
+ * ```
485
+ *
486
+ * Should become:
487
+ *
488
+ * ```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>
493
+ * ```
494
+ */
495
+ function createModelIndentPasteFixer(model) {
496
+ 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)) {
504
+ return;
505
+ }
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;
518
+ }
519
+ else if (isListItemBlock(pos.nodeBefore)) {
520
+ refItem = pos.nodeBefore;
521
+ }
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;
533
+ }
534
+ 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);
538
+ }
539
+ });
540
+ };
541
+ }
542
+ /**
543
+ * Decides whether the merge should be accompanied by the model's `deleteContent()`, for instance, to get rid of the inline
544
+ * content in the selection or take advantage of the heuristics in `deleteContent()` that helps convert lists into paragraphs
545
+ * in certain cases.
546
+ */
547
+ function shouldMergeOnBlocksContentLevel(model, direction) {
548
+ const selection = model.document.selection;
549
+ if (!selection.isCollapsed) {
550
+ return !getSelectedBlockObject(model);
551
+ }
552
+ if (direction === 'forward') {
553
+ return true;
554
+ }
555
+ const firstPosition = selection.getFirstPosition();
556
+ const positionParent = firstPosition.parent;
557
+ const previousSibling = positionParent.previousSibling;
558
+ if (model.schema.isObject(previousSibling)) {
559
+ return false;
560
+ }
561
+ if (previousSibling.isEmpty) {
562
+ return true;
563
+ }
564
+ return isSingleListItem([positionParent, previousSibling]);
565
+ }