@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.
- package/LICENSE.md +1 -1
- package/build/list.js +2 -2
- package/package.json +43 -39
- package/src/documentlist/converters.js +303 -419
- package/src/documentlist/documentlistcommand.js +136 -207
- package/src/documentlist/documentlistediting.js +538 -697
- package/src/documentlist/documentlistindentcommand.js +115 -168
- package/src/documentlist/documentlistmergecommand.js +161 -222
- package/src/documentlist/documentlistsplitcommand.js +59 -103
- package/src/documentlist/documentlistutils.js +41 -0
- package/src/documentlist/utils/listwalker.js +138 -236
- package/src/documentlist/utils/model.js +322 -421
- package/src/documentlist/utils/postfixers.js +98 -118
- package/src/documentlist/utils/view.js +74 -105
- package/src/documentlist.js +13 -19
- package/src/documentlistproperties/converters.js +33 -47
- package/src/documentlistproperties/documentlistpropertiesediting.js +266 -354
- package/src/documentlistproperties/documentlistpropertiesutils.js +44 -0
- package/src/documentlistproperties/documentlistreversedcommand.js +40 -61
- package/src/documentlistproperties/documentliststartcommand.js +42 -61
- package/src/documentlistproperties/documentliststylecommand.js +97 -147
- package/src/documentlistproperties/utils/style.js +27 -47
- package/src/documentlistproperties.js +13 -19
- package/src/index.js +4 -3
- package/src/list/converters.js +772 -929
- package/src/list/indentcommand.js +105 -140
- package/src/list/listcommand.js +262 -315
- package/src/list/listediting.js +142 -200
- package/src/list/listui.js +16 -25
- package/src/list/listutils.js +46 -0
- package/src/list/utils.js +295 -378
- package/src/list.js +13 -44
- package/src/listcommands.js +5 -0
- package/src/listconfig.js +5 -0
- package/src/listproperties/listpropertiesediting.js +656 -801
- package/src/listproperties/listpropertiesui.js +244 -296
- package/src/listproperties/listreversedcommand.js +37 -49
- package/src/listproperties/liststartcommand.js +37 -49
- package/src/listproperties/liststylecommand.js +82 -115
- package/src/listproperties/ui/collapsibleview.js +75 -138
- package/src/listproperties/ui/listpropertiesview.js +289 -414
- package/src/listproperties.js +13 -118
- package/src/liststyle.js +18 -24
- package/src/todolist/checktodolistcommand.js +60 -102
- package/src/todolist/todolistconverters.js +189 -271
- package/src/todolist/todolistediting.js +141 -206
- package/src/todolist/todolistui.js +14 -21
- package/src/todolist.js +13 -19
- package/theme/collapsible.css +1 -1
- package/theme/documentlist.css +1 -1
- package/theme/list.css +40 -0
- package/theme/listproperties.css +1 -1
- package/theme/liststyles.css +1 -37
- package/theme/todolist.css +1 -1
- package/build/list.js.map +0 -1
|
@@ -1,724 +1,565 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @license Copyright (c) 2003-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 = [
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
//
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
}
|