@ckeditor/ckeditor5-list 35.3.2 → 36.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE.md +1 -1
  2. package/build/list.js +2 -2
  3. package/package.json +43 -39
  4. package/src/documentlist/converters.js +303 -419
  5. package/src/documentlist/documentlistcommand.js +136 -207
  6. package/src/documentlist/documentlistediting.js +538 -697
  7. package/src/documentlist/documentlistindentcommand.js +115 -168
  8. package/src/documentlist/documentlistmergecommand.js +161 -222
  9. package/src/documentlist/documentlistsplitcommand.js +59 -103
  10. package/src/documentlist/documentlistutils.js +41 -0
  11. package/src/documentlist/utils/listwalker.js +138 -236
  12. package/src/documentlist/utils/model.js +322 -421
  13. package/src/documentlist/utils/postfixers.js +98 -118
  14. package/src/documentlist/utils/view.js +74 -105
  15. package/src/documentlist.js +13 -19
  16. package/src/documentlistproperties/converters.js +33 -47
  17. package/src/documentlistproperties/documentlistpropertiesediting.js +266 -354
  18. package/src/documentlistproperties/documentlistpropertiesutils.js +44 -0
  19. package/src/documentlistproperties/documentlistreversedcommand.js +40 -61
  20. package/src/documentlistproperties/documentliststartcommand.js +42 -61
  21. package/src/documentlistproperties/documentliststylecommand.js +97 -147
  22. package/src/documentlistproperties/utils/style.js +27 -47
  23. package/src/documentlistproperties.js +13 -19
  24. package/src/index.js +4 -3
  25. package/src/list/converters.js +772 -929
  26. package/src/list/indentcommand.js +105 -140
  27. package/src/list/listcommand.js +262 -315
  28. package/src/list/listediting.js +142 -200
  29. package/src/list/listui.js +16 -25
  30. package/src/list/listutils.js +46 -0
  31. package/src/list/utils.js +295 -378
  32. package/src/list.js +13 -44
  33. package/src/listcommands.js +5 -0
  34. package/src/listconfig.js +5 -0
  35. package/src/listproperties/listpropertiesediting.js +656 -801
  36. package/src/listproperties/listpropertiesui.js +244 -296
  37. package/src/listproperties/listreversedcommand.js +37 -49
  38. package/src/listproperties/liststartcommand.js +37 -49
  39. package/src/listproperties/liststylecommand.js +82 -115
  40. package/src/listproperties/ui/collapsibleview.js +75 -138
  41. package/src/listproperties/ui/listpropertiesview.js +289 -414
  42. package/src/listproperties.js +13 -118
  43. package/src/liststyle.js +18 -24
  44. package/src/todolist/checktodolistcommand.js +60 -102
  45. package/src/todolist/todolistconverters.js +189 -271
  46. package/src/todolist/todolistediting.js +141 -206
  47. package/src/todolist/todolistui.js +14 -21
  48. package/src/todolist.js +13 -19
  49. package/theme/collapsible.css +1 -1
  50. package/theme/documentlist.css +1 -1
  51. package/theme/list.css +40 -0
  52. package/theme/listproperties.css +1 -1
  53. package/theme/liststyles.css +1 -37
  54. package/theme/todolist.css +1 -1
  55. package/build/list.js.map +0 -1
@@ -1,724 +1,565 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module list/documentlist/documentlistediting
8
7
  */
9
-
10
8
  import { Plugin } from 'ckeditor5/src/core';
11
- import { Enter } from 'ckeditor5/src/enter';
12
9
  import { Delete } from 'ckeditor5/src/typing';
10
+ import { Enter } from 'ckeditor5/src/enter';
13
11
  import { CKEditorError } from 'ckeditor5/src/utils';
14
-
15
12
  import DocumentListIndentCommand from './documentlistindentcommand';
16
13
  import DocumentListCommand from './documentlistcommand';
17
14
  import DocumentListMergeCommand from './documentlistmergecommand';
18
15
  import DocumentListSplitCommand from './documentlistsplitcommand';
19
- import {
20
- bogusParagraphCreator,
21
- listItemDowncastConverter,
22
- listItemUpcastConverter,
23
- listUpcastCleanList,
24
- reconvertItemsOnDataChange
25
- } from './converters';
26
- import {
27
- findAndAddListHeadToMap,
28
- fixListIndents,
29
- fixListItemIds
30
- } from './utils/postfixers';
31
- import {
32
- getAllListItemBlocks,
33
- isFirstBlockOfListItem,
34
- isLastBlockOfListItem,
35
- isSingleListItem,
36
- getSelectedBlockObject,
37
- isListItemBlock,
38
- removeListAttributes
39
- } from './utils/model';
40
- import {
41
- getViewElementIdForListType,
42
- getViewElementNameForListType
43
- } from './utils/view';
44
- import ListWalker, {
45
- iterateSiblingListBlocks,
46
- ListBlocksIterable
47
- } from './utils/listwalker';
48
-
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';
49
22
  import '../../theme/documentlist.css';
50
-
23
+ import '../../theme/list.css';
51
24
  /**
52
25
  * A list of base list model attributes.
53
- *
54
- * @private
55
26
  */
56
- const LIST_BASE_ATTRIBUTES = [ 'listType', 'listIndent', 'listItemId' ];
57
-
27
+ const LIST_BASE_ATTRIBUTES = ['listType', 'listIndent', 'listItemId'];
58
28
  /**
59
29
  * The editing part of the document-list feature. It handles creating, editing and removing lists and list items.
60
- *
61
- * @extends module:core/plugin~Plugin
62
30
  */
63
31
  export default class DocumentListEditing extends Plugin {
64
- /**
65
- * @inheritDoc
66
- */
67
- static get pluginName() {
68
- return 'DocumentListEditing';
69
- }
70
-
71
- /**
72
- * @inheritDoc
73
- */
74
- static get requires() {
75
- return [ Enter, Delete ];
76
- }
77
-
78
- /**
79
- * @inheritDoc
80
- */
81
- constructor( editor ) {
82
- super( editor );
83
-
84
- /**
85
- * The list of registered downcast strategies.
86
- *
87
- * @private
88
- * @type {Array.<module:list/documentlist/documentlistediting~DowncastStrategy>}
89
- */
90
- this._downcastStrategies = [];
91
- }
92
-
93
- /**
94
- * @inheritDoc
95
- */
96
- init() {
97
- const editor = this.editor;
98
- const model = editor.model;
99
-
100
- if ( editor.plugins.has( 'ListEditing' ) ) {
101
- /**
102
- * The `DocumentList` feature can not be loaded together with the `List` plugin.
103
- *
104
- * @error document-list-feature-conflict
105
- * @param {String} conflictPlugin Name of the plugin.
106
- */
107
- throw new CKEditorError( 'document-list-feature-conflict', this, { conflictPlugin: 'ListEditing' } );
108
- }
109
-
110
- model.schema.extend( '$container', { allowAttributes: LIST_BASE_ATTRIBUTES } );
111
- model.schema.extend( '$block', { allowAttributes: LIST_BASE_ATTRIBUTES } );
112
- model.schema.extend( '$blockObject', { allowAttributes: LIST_BASE_ATTRIBUTES } );
113
-
114
- for ( const attribute of LIST_BASE_ATTRIBUTES ) {
115
- model.schema.setAttributeProperties( attribute, {
116
- copyOnReplace: true
117
- } );
118
- }
119
-
120
- // Register commands.
121
- editor.commands.add( 'numberedList', new DocumentListCommand( editor, 'numbered' ) );
122
- editor.commands.add( 'bulletedList', new DocumentListCommand( editor, 'bulleted' ) );
123
-
124
- editor.commands.add( 'indentList', new DocumentListIndentCommand( editor, 'forward' ) );
125
- editor.commands.add( 'outdentList', new DocumentListIndentCommand( editor, 'backward' ) );
126
-
127
- editor.commands.add( 'mergeListItemBackward', new DocumentListMergeCommand( editor, 'backward' ) );
128
- editor.commands.add( 'mergeListItemForward', new DocumentListMergeCommand( editor, 'forward' ) );
129
-
130
- editor.commands.add( 'splitListItemBefore', new DocumentListSplitCommand( editor, 'before' ) );
131
- editor.commands.add( 'splitListItemAfter', new DocumentListSplitCommand( editor, 'after' ) );
132
-
133
- this._setupDeleteIntegration();
134
- this._setupEnterIntegration();
135
- this._setupTabIntegration();
136
- this._setupClipboardIntegration();
137
- }
138
-
139
- /**
140
- * @inheritDoc
141
- */
142
- afterInit() {
143
- const editor = this.editor;
144
- const commands = editor.commands;
145
- const indent = commands.get( 'indent' );
146
- const outdent = commands.get( 'outdent' );
147
-
148
- if ( indent ) {
149
- // Priority is high due to integration with `IndentBlock` plugin. We want to indent list first and if it's not possible
150
- // user can indent content with `IndentBlock` plugin.
151
- indent.registerChildCommand( commands.get( 'indentList' ), { priority: 'high' } );
152
- }
153
-
154
- if ( outdent ) {
155
- // Priority is lowest due to integration with `IndentBlock` and `IndentCode` plugins.
156
- // First we want to allow user to outdent all indendations from other features then he can oudent list item.
157
- outdent.registerChildCommand( commands.get( 'outdentList' ), { priority: 'lowest' } );
158
- }
159
-
160
- // Register conversion and model post-fixer after other plugins had a chance to register their attribute strategies.
161
- this._setupModelPostFixing();
162
- this._setupConversion();
163
- }
164
-
165
- /**
166
- * Registers a downcast strategy.
167
- *
168
- * **Note**: Strategies must be registered in the `Plugin#init()` phase so that it can be applied
169
- * in the `DocumentListEditing#afterInit()`.
170
- *
171
- * @param {module:list/documentlist/documentlistediting~DowncastStrategy} strategy The downcast strategy to register.
172
- */
173
- registerDowncastStrategy( strategy ) {
174
- this._downcastStrategies.push( strategy );
175
- }
176
-
177
- /**
178
- * Returns list of model attribute names that should affect downcast conversion.
179
- *
180
- * @private
181
- */
182
- _getListAttributeNames() {
183
- return [
184
- ...LIST_BASE_ATTRIBUTES,
185
- ...this._downcastStrategies.map( strategy => strategy.attributeName )
186
- ];
187
- }
188
-
189
- /**
190
- * Attaches the listener to the {@link module:engine/view/document~Document#event:delete} event and handles backspace/delete
191
- * keys in and around document lists.
192
- *
193
- * @private
194
- */
195
- _setupDeleteIntegration() {
196
- const editor = this.editor;
197
- const mergeBackwardCommand = editor.commands.get( 'mergeListItemBackward' );
198
- const mergeForwardCommand = editor.commands.get( 'mergeListItemForward' );
199
-
200
- this.listenTo( editor.editing.view.document, 'delete', ( evt, data ) => {
201
- const selection = editor.model.document.selection;
202
-
203
- // Let the Widget plugin take care of block widgets while deleting (https://github.com/ckeditor/ckeditor5/issues/11346).
204
- if ( getSelectedBlockObject( editor.model ) ) {
205
- return;
206
- }
207
-
208
- editor.model.change( () => {
209
- const firstPosition = selection.getFirstPosition();
210
-
211
- if ( selection.isCollapsed && data.direction == 'backward' ) {
212
- if ( !firstPosition.isAtStart ) {
213
- return;
214
- }
215
-
216
- const positionParent = firstPosition.parent;
217
-
218
- if ( !isListItemBlock( positionParent ) ) {
219
- return;
220
- }
221
-
222
- const previousBlock = ListWalker.first( positionParent, {
223
- sameAttributes: 'listType',
224
- sameIndent: true
225
- } );
226
-
227
- // Outdent the first block of a first list item.
228
- if ( !previousBlock && positionParent.getAttribute( 'listIndent' ) === 0 ) {
229
- if ( !isLastBlockOfListItem( positionParent ) ) {
230
- editor.execute( 'splitListItemAfter' );
231
- }
232
-
233
- editor.execute( 'outdentList' );
234
- }
235
- // Merge block with previous one (on the block level or on the content level).
236
- else {
237
- if ( !mergeBackwardCommand.isEnabled ) {
238
- return;
239
- }
240
-
241
- mergeBackwardCommand.execute( {
242
- shouldMergeOnBlocksContentLevel: shouldMergeOnBlocksContentLevel( editor.model, 'backward' )
243
- } );
244
- }
245
-
246
- data.preventDefault();
247
- evt.stop();
248
- }
249
- // Non-collapsed selection or forward delete.
250
- else {
251
- // Collapsed selection should trigger forward merging only if at the end of a block.
252
- if ( selection.isCollapsed && !selection.getLastPosition().isAtEnd ) {
253
- return;
254
- }
255
-
256
- if ( !mergeForwardCommand.isEnabled ) {
257
- return;
258
- }
259
-
260
- mergeForwardCommand.execute( {
261
- shouldMergeOnBlocksContentLevel: shouldMergeOnBlocksContentLevel( editor.model, 'forward' )
262
- } );
263
-
264
- data.preventDefault();
265
- evt.stop();
266
- }
267
- } );
268
- }, { context: 'li' } );
269
- }
270
-
271
- /**
272
- * Attaches a listener to the {@link module:engine/view/document~Document#event:enter} event and handles enter key press
273
- * in document lists.
274
- *
275
- * @private
276
- */
277
- _setupEnterIntegration() {
278
- const editor = this.editor;
279
- const model = editor.model;
280
- const commands = editor.commands;
281
- const enterCommand = commands.get( 'enter' );
282
-
283
- // Overwrite the default Enter key behavior: outdent or split the list in certain cases.
284
- this.listenTo( editor.editing.view.document, 'enter', ( evt, data ) => {
285
- const doc = model.document;
286
- const positionParent = doc.selection.getFirstPosition().parent;
287
-
288
- if (
289
- doc.selection.isCollapsed &&
290
- isListItemBlock( positionParent ) &&
291
- positionParent.isEmpty &&
292
- !data.isSoft
293
- ) {
294
- const isFirstBlock = isFirstBlockOfListItem( positionParent );
295
- const isLastBlock = isLastBlockOfListItem( positionParent );
296
-
297
- // * a → * a
298
- // * [] → []
299
- if ( isFirstBlock && isLastBlock ) {
300
- editor.execute( 'outdentList' );
301
-
302
- data.preventDefault();
303
- evt.stop();
304
- }
305
- // * [] → * []
306
- // a → * a
307
- else if ( isFirstBlock && !isLastBlock ) {
308
- editor.execute( 'splitListItemAfter' );
309
-
310
- data.preventDefault();
311
- evt.stop();
312
- }
313
- // * a → * a
314
- // [] → * []
315
- else if ( isLastBlock ) {
316
- editor.execute( 'splitListItemBefore' );
317
-
318
- data.preventDefault();
319
- evt.stop();
320
- }
321
- }
322
- }, { context: 'li' } );
323
-
324
- // In some cases, after the default block splitting, we want to modify the new block to become a new list item
325
- // instead of an additional block in the same list item.
326
- this.listenTo( enterCommand, 'afterExecute', () => {
327
- const splitCommand = commands.get( 'splitListItemBefore' );
328
-
329
- // The command has not refreshed because the change block related to EnterCommand#execute() is not over yet.
330
- // Let's keep it up to date and take advantage of DocumentListSplitCommand#isEnabled.
331
- splitCommand.refresh();
332
-
333
- if ( !splitCommand.isEnabled ) {
334
- return;
335
- }
336
-
337
- const doc = editor.model.document;
338
- const positionParent = doc.selection.getLastPosition().parent;
339
- const listItemBlocks = getAllListItemBlocks( positionParent );
340
-
341
- // Keep in mind this split happens after the default enter handler was executed. For instance:
342
- //
343
- // │ Initial state │ After default enter │ Here in #afterExecute │
344
- // ├───────────────────────────┼───────────────────────────┼───────────────────────────┤
345
- // │ * a[] │ * a │ * a │
346
- // │ │ [] │ * [] │
347
- if ( listItemBlocks.length === 2 ) {
348
- splitCommand.execute();
349
- }
350
- } );
351
- }
352
-
353
- /**
354
- * Attaches a listener to the {@link module:engine/view/document~Document#event:tab} event and handles tab key and tab+shift keys
355
- * presses in document lists.
356
- *
357
- * @private
358
- */
359
- _setupTabIntegration() {
360
- const editor = this.editor;
361
-
362
- this.listenTo( editor.editing.view.document, 'tab', ( evt, data ) => {
363
- const commandName = data.shiftKey ? 'outdentList' : 'indentList';
364
- const command = this.editor.commands.get( commandName );
365
-
366
- if ( command.isEnabled ) {
367
- editor.execute( commandName );
368
-
369
- data.stopPropagation();
370
- data.preventDefault();
371
- evt.stop();
372
- }
373
- }, { context: 'li' } );
374
- }
375
-
376
- /**
377
- * Registers the conversion helpers for the document-list feature.
378
- * @private
379
- */
380
- _setupConversion() {
381
- const editor = this.editor;
382
- const model = editor.model;
383
- const attributeNames = this._getListAttributeNames();
384
-
385
- editor.conversion.for( 'upcast' )
386
- .elementToElement( { view: 'li', model: 'paragraph' } )
387
- .add( dispatcher => {
388
- dispatcher.on( 'element:li', listItemUpcastConverter() );
389
- dispatcher.on( 'element:ul', listUpcastCleanList(), { priority: 'high' } );
390
- dispatcher.on( 'element:ol', listUpcastCleanList(), { priority: 'high' } );
391
- } );
392
-
393
- editor.conversion.for( 'editingDowncast' )
394
- .elementToElement( {
395
- model: 'paragraph',
396
- view: bogusParagraphCreator( attributeNames ),
397
- converterPriority: 'high'
398
- } );
399
-
400
- editor.conversion.for( 'dataDowncast' )
401
- .elementToElement( {
402
- model: 'paragraph',
403
- view: bogusParagraphCreator( attributeNames, { dataPipeline: true } ),
404
- converterPriority: 'high'
405
- } );
406
-
407
- editor.conversion.for( 'downcast' )
408
- .add( dispatcher => {
409
- dispatcher.on( 'attribute', listItemDowncastConverter( attributeNames, this._downcastStrategies, model ) );
410
- } );
411
-
412
- this.listenTo( model.document, 'change:data', reconvertItemsOnDataChange( model, editor.editing, attributeNames, this ) );
413
-
414
- // For LI verify if an ID of the attribute element is correct.
415
- this.on( 'checkAttributes:item', ( evt, { viewElement, modelAttributes } ) => {
416
- if ( viewElement.id != modelAttributes.listItemId ) {
417
- evt.return = true;
418
- evt.stop();
419
- }
420
- } );
421
-
422
- // For UL and OL check if the name and ID of element is correct.
423
- this.on( 'checkAttributes:list', ( evt, { viewElement, modelAttributes } ) => {
424
- if (
425
- viewElement.name != getViewElementNameForListType( modelAttributes.listType ) ||
426
- viewElement.id != getViewElementIdForListType( modelAttributes.listType, modelAttributes.listIndent )
427
- ) {
428
- evt.return = true;
429
- evt.stop();
430
- }
431
- } );
432
- }
433
-
434
- /**
435
- * Registers model post-fixers.
436
- *
437
- * @private
438
- */
439
- _setupModelPostFixing() {
440
- const model = this.editor.model;
441
- const attributeNames = this._getListAttributeNames();
442
-
443
- // Register list fixing.
444
- // First the low level handler.
445
- model.document.registerPostFixer( writer => modelChangePostFixer( model, writer, attributeNames, this ) );
446
-
447
- // Then the callbacks for the specific lists.
448
- // The indentation fixing must be the first one...
449
- this.on( 'postFixer', ( evt, { listNodes, writer } ) => {
450
- evt.return = fixListIndents( listNodes, writer ) || evt.return;
451
- }, { priority: 'high' } );
452
-
453
- // ...then the item ids... and after that other fixers that rely on the correct indentation and ids.
454
- this.on( 'postFixer', ( evt, { listNodes, writer, seenIds } ) => {
455
- evt.return = fixListItemIds( listNodes, seenIds, writer ) || evt.return;
456
- }, { priority: 'high' } );
457
- }
458
-
459
- /**
460
- * Integrates the feature with the clipboard via {@link module:engine/model/model~Model#insertContent} and
461
- * {@link module:engine/model/model~Model#getSelectedContent}.
462
- *
463
- * @private
464
- */
465
- _setupClipboardIntegration() {
466
- const model = this.editor.model;
467
-
468
- this.listenTo( model, 'insertContent', createModelIndentPasteFixer( model ), { priority: 'high' } );
469
-
470
- // To enhance the UX, the editor should not copy list attributes to the clipboard if the selection
471
- // started and ended in the same list item.
472
- //
473
- // If the selection was enclosed in a single list item, there is a good chance the user did not want it
474
- // copied as a list item but plain blocks.
475
- //
476
- // This avoids pasting orphaned list items instead of paragraphs, for instance, straight into the root.
477
- //
478
- // ┌─────────────────────┬───────────────────┐
479
- // │ Selection │ Clipboard content │
480
- // ├─────────────────────┼───────────────────┤
481
- // │ [* <Widget />] │ <Widget /> │
482
- // ├─────────────────────┼───────────────────┤
483
- // │ [* Foo] │ Foo │
484
- // ├─────────────────────┼───────────────────┤
485
- // │ * Foo [bar] baz │ bar │
486
- // ├─────────────────────┼───────────────────┤
487
- // │ * Fo[o │ o │
488
- // │ ba]r │ ba │
489
- // ├─────────────────────┼───────────────────┤
490
- // │ * Fo[o │ * o │
491
- // │ * ba]r │ * ba │
492
- // ├─────────────────────┼───────────────────┤
493
- // │ [* Foo │ * Foo │
494
- // │ * bar] │ * bar │
495
- // └─────────────────────┴───────────────────┘
496
- //
497
- // See https://github.com/ckeditor/ckeditor5/issues/11608.
498
- this.listenTo( model, 'getSelectedContent', ( evt, [ selection ] ) => {
499
- const isSingleListItemSelected = isSingleListItem( Array.from( selection.getSelectedBlocks() ) );
500
-
501
- if ( isSingleListItemSelected ) {
502
- model.change( writer => removeListAttributes( Array.from( evt.return.getChildren() ), writer ) );
503
- }
504
- } );
505
- }
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
+ }
506
386
  }
507
-
508
387
  /**
509
- * @typedef {Object} module:list/documentlist/documentlistediting~DowncastStrategy
510
- * @property {'list'|'item'} scope The scope of the downcast (whether it applies to LI or OL/UL).
511
- * @property {String} attributeName The model attribute name.
512
- * @property {Function} setAttributeOnDowncast Sets the property on the view element.
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.
513
418
  */
514
-
515
- // Post-fixer that reacts to changes on document and fixes incorrect model states (invalid `listItemId` and `listIndent` values).
516
- //
517
- // In the example below, there is a correct list structure.
518
- // Then the middle element is removed so the list structure will become incorrect:
519
- //
520
- // <paragraph listType="bulleted" listItemId="a" listIndent=0>Item 1</paragraph>
521
- // <paragraph listType="bulleted" listItemId="b" listIndent=1>Item 2</paragraph> <--- this is removed.
522
- // <paragraph listType="bulleted" listItemId="c" listIndent=2>Item 3</paragraph>
523
- //
524
- // The list structure after the middle element is removed:
525
- //
526
- // <paragraph listType="bulleted" listItemId="a" listIndent=0>Item 1</paragraph>
527
- // <paragraph listType="bulleted" listItemId="c" listIndent=2>Item 3</paragraph>
528
- //
529
- // Should become:
530
- //
531
- // <paragraph listType="bulleted" listItemId="a" listIndent=0>Item 1</paragraph>
532
- // <paragraph listType="bulleted" listItemId="c" listIndent=1>Item 3</paragraph> <--- note that indent got post-fixed.
533
- //
534
- // @param {module:engine/model/model~Model} model The data model.
535
- // @param {module:engine/model/writer~Writer} writer The writer to do changes with.
536
- // @param {Array.<String>} attributeNames The list of all model list attributes (including registered strategies).
537
- // @param {module:list/documentlist/documentlistediting~DocumentListEditing} documentListEditing The document list editing plugin.
538
- // @returns {Boolean} `true` if any change has been applied, `false` otherwise.
539
- function modelChangePostFixer( model, writer, attributeNames, documentListEditing ) {
540
- const changes = model.document.differ.getChanges();
541
- const itemToListHead = new Map();
542
-
543
- let applied = false;
544
-
545
- for ( const entry of changes ) {
546
- if ( entry.type == 'insert' && entry.name != '$text' ) {
547
- const item = entry.position.nodeAfter;
548
-
549
- // Remove attributes in case of renamed element.
550
- if ( !model.schema.checkAttribute( item, 'listItemId' ) ) {
551
- for ( const attributeName of Array.from( item.getAttributeKeys() ) ) {
552
- if ( attributeNames.includes( attributeName ) ) {
553
- writer.removeAttribute( attributeName, item );
554
-
555
- applied = true;
556
- }
557
- }
558
- }
559
-
560
- findAndAddListHeadToMap( entry.position, itemToListHead );
561
-
562
- // Insert of a non-list item - check if there is a list after it.
563
- if ( !entry.attributes.has( 'listItemId' ) ) {
564
- findAndAddListHeadToMap( entry.position.getShiftedBy( entry.length ), itemToListHead );
565
- }
566
-
567
- // Check if there is no nested list.
568
- for ( const { item: innerItem, previousPosition } of model.createRangeIn( item ) ) {
569
- if ( isListItemBlock( innerItem ) ) {
570
- findAndAddListHeadToMap( previousPosition, itemToListHead );
571
- }
572
- }
573
- }
574
- // Removed list item or block adjacent to a list.
575
- else if ( entry.type == 'remove' ) {
576
- findAndAddListHeadToMap( entry.position, itemToListHead );
577
- }
578
- // Changed list item indent or type.
579
- else if ( entry.type == 'attribute' && attributeNames.includes( entry.attributeKey ) ) {
580
- findAndAddListHeadToMap( entry.range.start, itemToListHead );
581
-
582
- if ( entry.attributeNewValue === null ) {
583
- findAndAddListHeadToMap( entry.range.start.getShiftedBy( 1 ), itemToListHead );
584
- }
585
- }
586
- }
587
-
588
- // Make sure that IDs are not shared by split list.
589
- const seenIds = new Set();
590
-
591
- for ( const listHead of itemToListHead.values() ) {
592
- /**
593
- * Event fired on changes detected on the model list element to verify if the view representation of a list element
594
- * is representing those attributes.
595
- *
596
- * It allows triggering a re-wrapping of a list item.
597
- *
598
- * **Note**: For convenience this event is namespaced and could be captured as `checkAttributes:list` or `checkAttributes:item`.
599
- *
600
- * @protected
601
- * @event module:list/documentlist/documentlistediting~DocumentListEditing#event:postFixer
602
- * @param {module:engine/model/element~Element} listHead The head element of a list.
603
- * @param {module:engine/model/writer~Writer} writer The writer to do changes with.
604
- * @param {Set.<String>} seenIds The set of already known IDs.
605
- * @param {Object} modelAttributes
606
- * @returns {Boolean} If a post-fixer made a change of the model tree, it should return `true`.
607
- */
608
- applied = documentListEditing.fire( 'postFixer', {
609
- listNodes: new ListBlocksIterable( listHead ),
610
- listHead,
611
- writer,
612
- seenIds
613
- } ) || applied;
614
- }
615
-
616
- return applied;
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;
617
470
  }
618
-
619
- // A fixer for pasted content that includes list items.
620
- //
621
- // It fixes indentation of pasted list items so the pasted items match correctly to the context they are pasted into.
622
- //
623
- // Example:
624
- //
625
- // <paragraph listType="bulleted" listItemId="a" listIndent=0>A</paragraph>
626
- // <paragraph listType="bulleted" listItemId="b" listIndent=1>B^</paragraph>
627
- // // At ^ paste: <paragraph listType="bulleted" listItemId="x" listIndent=4>X</paragraph>
628
- // // <paragraph listType="bulleted" listItemId="y" listIndent=5>Y</paragraph>
629
- // <paragraph listType="bulleted" listItemId="c" listIndent=2>C</paragraph>
630
- //
631
- // Should become:
632
- //
633
- // <paragraph listType="bulleted" listItemId="a" listIndent=0>A</paragraph>
634
- // <paragraph listType="bulleted" listItemId="b" listIndent=1>BX</paragraph>
635
- // <paragraph listType="bulleted" listItemId="y" listIndent=2>Y/paragraph>
636
- // <paragraph listType="bulleted" listItemId="c" listIndent=2>C</paragraph>
637
- //
638
- function createModelIndentPasteFixer( model ) {
639
- return ( evt, [ content, selectable ] ) => {
640
- // Check whether inserted content starts from a `listItem`. If it does not, it means that there are some other
641
- // elements before it and there is no need to fix indents, because even if we insert that content into a list,
642
- // that list will be broken.
643
- // Note: we also need to handle singular elements because inserting item with indent 0 into 0,1,[],2
644
- // would create incorrect model.
645
- const item = content.is( 'documentFragment' ) ? content.getChild( 0 ) : content;
646
-
647
- if ( !isListItemBlock( item ) ) {
648
- return;
649
- }
650
-
651
- let selection;
652
-
653
- if ( !selectable ) {
654
- selection = model.document.selection;
655
- } else {
656
- selection = model.createSelection( selectable );
657
- }
658
-
659
- // Get a reference list item. Inserted list items will be fixed according to that item.
660
- const pos = selection.getFirstPosition();
661
- let refItem = null;
662
-
663
- if ( isListItemBlock( pos.parent ) ) {
664
- refItem = pos.parent;
665
- } else if ( isListItemBlock( pos.nodeBefore ) ) {
666
- refItem = pos.nodeBefore;
667
- }
668
-
669
- // If there is `refItem` it means that we do insert list items into an existing list.
670
- if ( !refItem ) {
671
- return;
672
- }
673
-
674
- // First list item in `data` has indent equal to 0 (it is a first list item). It should have indent equal
675
- // to the indent of reference item. We have to fix the first item and all of it's children and following siblings.
676
- // Indent of all those items has to be adjusted to reference item.
677
- const indentChange = refItem.getAttribute( 'listIndent' ) - item.getAttribute( 'listIndent' );
678
-
679
- // Fix only if there is anything to fix.
680
- if ( indentChange <= 0 ) {
681
- return;
682
- }
683
-
684
- model.change( writer => {
685
- // Adjust indent of all "first" list items in inserted data.
686
- for ( const { node } of iterateSiblingListBlocks( item, 'forward' ) ) {
687
- writer.setAttribute( 'listIndent', node.getAttribute( 'listIndent' ) + indentChange, node );
688
- }
689
- } );
690
- };
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
+ };
691
541
  }
692
-
693
- // Decides whether the merge should be accompanied by the model's `deleteContent()`, for instance, to get rid of the inline
694
- // content in the selection or take advantage of the heuristics in `deleteContent()` that helps convert lists into paragraphs
695
- // in certain cases.
696
- //
697
- // @param {module:engine/model/model~Model} model
698
- // @param {'backward'|'forward'} direction
699
- // @returns {Boolean}
700
- function shouldMergeOnBlocksContentLevel( model, direction ) {
701
- const selection = model.document.selection;
702
-
703
- if ( !selection.isCollapsed ) {
704
- return !getSelectedBlockObject( model );
705
- }
706
-
707
- if ( direction === 'forward' ) {
708
- return true;
709
- }
710
-
711
- const firstPosition = selection.getFirstPosition();
712
- const positionParent = firstPosition.parent;
713
- const previousSibling = positionParent.previousSibling;
714
-
715
- if ( model.schema.isObject( previousSibling ) ) {
716
- return false;
717
- }
718
-
719
- if ( previousSibling.isEmpty ) {
720
- return true;
721
- }
722
-
723
- return isSingleListItem( [ positionParent, previousSibling ] );
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]);
724
565
  }