@ebl-vue/editor-full 2.31.11 → 2.31.12

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 (37) hide show
  1. package/dist/index.mjs +360 -319
  2. package/dist/index.mjs.map +1 -1
  3. package/package.json +1 -1
  4. package/src/components/Editor/Editor.vue +22 -6
  5. package/src/plugins/list/ListRenderer/ChecklistRenderer.ts +1 -1
  6. package/src/plugins/list/ListTabulator/index.ts +1 -1
  7. package/src/plugins/list/index.ts +118 -123
  8. package/src/plugins/list/styles/.cdx-list .css +168 -0
  9. package/src/plugins/list/utils/normalizeData.ts +0 -1
  10. package/src/plugins/list-bak/ListRenderer/ChecklistRenderer.ts +208 -0
  11. package/src/plugins/list-bak/ListRenderer/ListRenderer.ts +73 -0
  12. package/src/plugins/list-bak/ListRenderer/OrderedListRenderer.ts +123 -0
  13. package/src/plugins/list-bak/ListRenderer/UnorderedListRenderer.ts +123 -0
  14. package/src/plugins/list-bak/ListRenderer/index.ts +6 -0
  15. package/src/plugins/list-bak/ListTabulator/index.ts +1179 -0
  16. package/src/plugins/list-bak/index.ts +485 -0
  17. package/src/plugins/list-bak/styles/CssPrefix.ts +4 -0
  18. package/src/plugins/list-bak/styles/input.css +36 -0
  19. package/src/plugins/list-bak/styles/list.css +165 -0
  20. package/src/plugins/list-bak/types/Elements.ts +14 -0
  21. package/src/plugins/list-bak/types/ItemMeta.ts +40 -0
  22. package/src/plugins/list-bak/types/ListParams.ts +102 -0
  23. package/src/plugins/list-bak/types/ListRenderer.ts +6 -0
  24. package/src/plugins/list-bak/types/OlCounterType.ts +63 -0
  25. package/src/plugins/list-bak/types/index.ts +14 -0
  26. package/src/plugins/list-bak/utils/focusItem.ts +18 -0
  27. package/src/plugins/list-bak/utils/getChildItems.ts +40 -0
  28. package/src/plugins/list-bak/utils/getItemChildWrapper.ts +10 -0
  29. package/src/plugins/list-bak/utils/getItemContentElement.ts +10 -0
  30. package/src/plugins/list-bak/utils/getSiblings.ts +52 -0
  31. package/src/plugins/list-bak/utils/isLastItem.ts +9 -0
  32. package/src/plugins/list-bak/utils/itemHasSublist.ts +10 -0
  33. package/src/plugins/list-bak/utils/normalizeData.ts +84 -0
  34. package/src/plugins/list-bak/utils/removeChildWrapperIfEmpty.ts +31 -0
  35. package/src/plugins/list-bak/utils/renderToolboxInput.ts +105 -0
  36. package/src/plugins/list-bak/utils/stripNumbers.ts +7 -0
  37. package/src/plugins/list-bak/utils/type-guards.ts +8 -0
@@ -0,0 +1,1179 @@
1
+ import { OrderedListRenderer } from '../ListRenderer/OrderedListRenderer';
2
+ import { UnorderedListRenderer } from '../ListRenderer/UnorderedListRenderer';
3
+ import type { ListConfig, ListData, ListDataStyle } from '../types/ListParams';
4
+ import type { ListItem } from '../types/ListParams';
5
+ import type { ItemElement, ItemChildWrapperElement } from '../types/Elements';
6
+ import { isHtmlElement } from '../utils/type-guards';
7
+ import { getContenteditableSlice, getCaretNodeAndOffset, isCaretAtStartOfInput } from '@editorjs/caret';
8
+ import { DefaultListCssClasses } from '../ListRenderer';
9
+ import type { PasteEvent } from '../types';
10
+ import type { API, BlockAPI, PasteConfig } from '@editorjs/editorjs';
11
+ import type { ListParams } from '..';
12
+ import type { ChecklistItemMeta, ItemMeta, OrderedListItemMeta, UnorderedListItemMeta } from '../types/ItemMeta';
13
+ import type { ListRenderer } from '../types/ListRenderer';
14
+ import { getSiblings } from '../utils/getSiblings';
15
+ import { getChildItems } from '../utils/getChildItems';
16
+ import { isLastItem } from '../utils/isLastItem';
17
+ import { itemHasSublist } from '../utils/itemHasSublist';
18
+ import { getItemChildWrapper } from '../utils/getItemChildWrapper';
19
+ import { removeChildWrapperIfEmpty } from '../utils/removeChildWrapperIfEmpty';
20
+ import { getItemContentElement } from '../utils/getItemContentElement';
21
+ import { focusItem } from '../utils/focusItem';
22
+ import type { OlCounterType } from '../types/OlCounterType';
23
+
24
+ /**
25
+ * Class that is responsible for list tabulation
26
+ */
27
+ export default class ListTabulator<Renderer extends ListRenderer> {
28
+ /**
29
+ * The Editor.js API
30
+ */
31
+ private api: API;
32
+
33
+ /**
34
+ * Is Editorjs List Tool read-only option
35
+ */
36
+ private readOnly: boolean;
37
+
38
+ /**
39
+ * Tool's configuration
40
+ */
41
+ private config?: ListConfig;
42
+
43
+ /**
44
+ * Full content of the list
45
+ */
46
+ private data: ListData;
47
+
48
+ /**
49
+ * Editor block api
50
+ */
51
+ private block: BlockAPI;
52
+
53
+ /**
54
+ * Rendered list of items
55
+ */
56
+ private renderer: Renderer;
57
+
58
+ /**
59
+ * Wrapper of the whole list
60
+ */
61
+ private listWrapper: ItemChildWrapperElement | undefined;
62
+
63
+ /**
64
+ * Getter method to get current item
65
+ * @returns current list item or null if caret position is not undefined
66
+ */
67
+ private get currentItem(): ItemElement | null {
68
+ const selection = window.getSelection();
69
+
70
+ if (!selection) {
71
+ return null;
72
+ }
73
+
74
+ let currentNode = selection.anchorNode;
75
+
76
+ if (!currentNode) {
77
+ return null;
78
+ }
79
+
80
+ if (!isHtmlElement(currentNode)) {
81
+ currentNode = currentNode.parentNode;
82
+ }
83
+ if (!currentNode) {
84
+ return null;
85
+ }
86
+ if (!isHtmlElement(currentNode)) {
87
+ return null;
88
+ }
89
+
90
+ return currentNode.closest(`.${DefaultListCssClasses.item}`);
91
+ }
92
+
93
+ /**
94
+ * Method that returns nesting level of the current item, null if there is no selection
95
+ */
96
+ private get currentItemLevel(): number | null {
97
+ const currentItem = this.currentItem;
98
+
99
+ if (currentItem === null) {
100
+ return null;
101
+ }
102
+
103
+ let parentNode = currentItem.parentNode;
104
+
105
+ let levelCounter = 0;
106
+
107
+ while (parentNode !== null && parentNode !== this.listWrapper) {
108
+ if (isHtmlElement(parentNode) && parentNode.classList.contains(DefaultListCssClasses.item)) {
109
+ levelCounter += 1;
110
+ }
111
+
112
+ parentNode = parentNode.parentNode;
113
+ }
114
+
115
+ /**
116
+ * Level counter is number of the parent element, so it should be increased by one
117
+ */
118
+ return levelCounter + 1;
119
+ }
120
+
121
+ /**
122
+ * Assign all passed params and renderer to relevant class properties
123
+ * @param params - tool constructor options
124
+ * @param params.data - previously saved data
125
+ * @param params.config - user config for Tool
126
+ * @param params.api - Editor.js API
127
+ * @param params.readOnly - read-only mode flag
128
+ * @param renderer - renderer instance initialized in tool class
129
+ */
130
+ constructor({ data, config, api, readOnly, block }: ListParams, renderer: Renderer) {
131
+ this.config = config;
132
+ this.data = data as ListData;
133
+ this.readOnly = readOnly;
134
+ this.api = api;
135
+ this.block = block;
136
+
137
+ this.renderer = renderer;
138
+ }
139
+
140
+ /**
141
+ * Function that is responsible for rendering list with contents
142
+ * @returns Filled with content wrapper element of the list
143
+ */
144
+ public render(): ItemChildWrapperElement {
145
+ this.listWrapper = this.renderer.renderWrapper(true);
146
+
147
+ // fill with data
148
+ if (this.data.items.length) {
149
+ this.appendItems(this.data.items, this.listWrapper);
150
+ } else {
151
+ this.appendItems(
152
+ [
153
+ {
154
+ content: '',
155
+ meta: {},
156
+ items: [],
157
+ },
158
+ ],
159
+ this.listWrapper
160
+ );
161
+ }
162
+
163
+ if (!this.readOnly) {
164
+ // detect keydown on the last item to escape List
165
+ this.listWrapper.addEventListener(
166
+ 'keydown',
167
+ (event) => {
168
+ switch (event.key) {
169
+ case 'Enter':
170
+ if (!event.shiftKey) {
171
+ this.enterPressed(event);
172
+ }
173
+ break;
174
+ case 'Backspace':
175
+ this.backspace(event);
176
+ break;
177
+ case 'Tab':
178
+ if (event.shiftKey) {
179
+ this.shiftTab(event);
180
+ } else {
181
+ this.addTab(event);
182
+ }
183
+ break;
184
+ }
185
+ },
186
+ false
187
+ );
188
+ }
189
+
190
+ /**
191
+ * Set start property value from initial data
192
+ */
193
+ if ('start' in this.data.meta && this.data.meta.start !== undefined) {
194
+ this.changeStartWith(this.data.meta.start);
195
+ }
196
+
197
+ /**
198
+ * Set counterType value from initial data
199
+ */
200
+ if ('counterType' in this.data.meta && this.data.meta.counterType !== undefined) {
201
+ this.changeCounters(this.data.meta.counterType);
202
+ }
203
+
204
+ return this.listWrapper;
205
+ }
206
+
207
+ /**
208
+ * Function that is responsible for list content saving
209
+ * @param wrapper - optional argument wrapper
210
+ * @returns whole list saved data if wrapper not passes, otherwise will return data of the passed wrapper
211
+ */
212
+ public save(wrapper?: ItemChildWrapperElement): ListData {
213
+ const listWrapper = wrapper ?? this.listWrapper;
214
+
215
+ /**
216
+ * The method for recursive collecting of the child items
217
+ * @param parent - where to find items
218
+ */
219
+ const getItems = (parent: ItemChildWrapperElement): ListItem[] => {
220
+ const children = getChildItems(parent);
221
+
222
+ return children.map((el) => {
223
+ const subItemsWrapper = getItemChildWrapper(el);
224
+ const content = this.renderer.getItemContent(el);
225
+ const meta = this.renderer.getItemMeta(el);
226
+ const subItems = subItemsWrapper ? getItems(subItemsWrapper) : [];
227
+
228
+ return {
229
+ content,
230
+ meta,
231
+ items: subItems,
232
+ };
233
+ });
234
+ };
235
+
236
+ const composedListItems = listWrapper ? getItems(listWrapper) : [];
237
+
238
+ let dataToSave: ListData = {
239
+ style: this.data.style,
240
+ meta: {} as ItemMeta,
241
+ items: composedListItems,
242
+ };
243
+
244
+ if (this.data.style === 'ordered') {
245
+ dataToSave.meta = {
246
+ start: (this.data.meta as OrderedListItemMeta).start,
247
+ counterType: (this.data.meta as OrderedListItemMeta).counterType,
248
+ };
249
+ }
250
+
251
+ return dataToSave;
252
+ }
253
+
254
+ /**
255
+ * On paste sanitzation config. Allow only tags that are allowed in the Tool.
256
+ * @returns - config that determines tags supposted by paste handler
257
+ * @todo - refactor and move to list instance
258
+ */
259
+ public static get pasteConfig(): PasteConfig {
260
+ return {
261
+ tags: ['OL', 'UL', 'LI'],
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Method that specified hot to merge two List blocks.
267
+ * Called by Editor.js by backspace at the beginning of the Block
268
+ *
269
+ * Content of the first item of the next List would be merged with deepest item in current list
270
+ * Other items of the next List would be appended to the current list without any changes in nesting levels
271
+ * @param data - data of the second list to be merged with current
272
+ */
273
+ public merge(data: ListData): void {
274
+ /**
275
+ * Get list of all levels children of the previous item
276
+ */
277
+ const items = this.block.holder.querySelectorAll<ItemElement>(`.${DefaultListCssClasses.item}`);
278
+
279
+ const deepestBlockItem = items[items.length - 1];
280
+ const deepestBlockItemContentElement = getItemContentElement(deepestBlockItem);
281
+
282
+ if (deepestBlockItem === null || deepestBlockItemContentElement === null) {
283
+ return;
284
+ }
285
+
286
+ /**
287
+ * Insert trailing html to the deepest block item content
288
+ */
289
+ deepestBlockItemContentElement.insertAdjacentHTML('beforeend', data.items[0].content);
290
+
291
+ if (this.listWrapper === undefined) {
292
+ return;
293
+ }
294
+
295
+ const firstLevelItems = getChildItems(this.listWrapper);
296
+
297
+ if (firstLevelItems.length === 0) {
298
+ return;
299
+ }
300
+
301
+ /**
302
+ * Get last item of the first level of the list
303
+ */
304
+ const lastFirstLevelItem = firstLevelItems[firstLevelItems.length - 1];
305
+
306
+ /**
307
+ * Get child items wrapper of the last item
308
+ */
309
+ let lastFirstLevelItemChildWrapper = getItemChildWrapper(lastFirstLevelItem);
310
+
311
+ /**
312
+ * Get first item of the list to be merged with current one
313
+ */
314
+ const firstItem = data.items.shift();
315
+
316
+ /**
317
+ * Check that first item exists
318
+ */
319
+ if (firstItem === undefined) {
320
+ return;
321
+ }
322
+
323
+ /**
324
+ * Append child items of the first element
325
+ */
326
+ if (firstItem.items.length !== 0) {
327
+ /**
328
+ * Render child wrapper of the last item if it does not exist
329
+ */
330
+ if (lastFirstLevelItemChildWrapper === null) {
331
+ lastFirstLevelItemChildWrapper = this.renderer.renderWrapper(false);
332
+ }
333
+
334
+ this.appendItems(firstItem.items, lastFirstLevelItemChildWrapper);
335
+ }
336
+
337
+ if (data.items.length > 0) {
338
+ this.appendItems(data.items, this.listWrapper);
339
+ }
340
+ }
341
+
342
+ /**
343
+ * On paste callback that is fired from Editor.
344
+ * @param event - event with pasted data
345
+ * @todo - refactor and move to list instance
346
+ */
347
+ public onPaste(event: PasteEvent): void {
348
+ const list = event.detail.data;
349
+
350
+ this.data = this.pasteHandler(list);
351
+
352
+ // render new list
353
+ const oldView = this.listWrapper;
354
+
355
+ if (oldView && oldView.parentNode) {
356
+ oldView.parentNode.replaceChild(this.render(), oldView);
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Handle UL, OL and LI tags paste and returns List data
362
+ * @param element - html element that contains whole list
363
+ * @todo - refactor and move to list instance
364
+ */
365
+ public pasteHandler(element: PasteEvent['detail']['data']): ListData {
366
+ const { tagName: tag } = element;
367
+ let style: ListDataStyle = 'unordered';
368
+ let tagToSearch: string;
369
+
370
+ // set list style and tag to search.
371
+ switch (tag) {
372
+ case 'OL':
373
+ style = 'ordered';
374
+ tagToSearch = 'ol';
375
+ break;
376
+ case 'UL':
377
+ case 'LI':
378
+ style = 'unordered';
379
+ tagToSearch = 'ul';
380
+ }
381
+
382
+ const data: ListData = {
383
+ style,
384
+ meta: {} as ItemMeta,
385
+ items: [],
386
+ };
387
+
388
+ /**
389
+ * Set default ordered list atributes if style is ordered
390
+ */
391
+ if (style === 'ordered') {
392
+ (this.data.meta as OrderedListItemMeta).counterType = 'numeric';
393
+ (this.data.meta as OrderedListItemMeta).start = 1;
394
+ }
395
+
396
+ // get pasted items from the html.
397
+ const getPastedItems = (parent: Element): ListItem[] => {
398
+ // get first level li elements.
399
+ const children = Array.from(parent.querySelectorAll(`:scope > li`));
400
+
401
+ return children.map((child) => {
402
+ // get subitems if they exist.
403
+ const subItemsWrapper = child.querySelector(`:scope > ${tagToSearch}`);
404
+ // get subitems.
405
+ const subItems = subItemsWrapper ? getPastedItems(subItemsWrapper) : [];
406
+ // get text content of the li element.
407
+ const content = child.innerHTML ?? '';
408
+
409
+ return {
410
+ content,
411
+ meta: {},
412
+ items: subItems,
413
+ };
414
+ });
415
+ };
416
+
417
+ // get pasted items.
418
+ data.items = getPastedItems(element);
419
+
420
+ return data;
421
+ }
422
+
423
+ /**
424
+ * Changes ordered list start property value
425
+ * @param index - new value of the start property
426
+ */
427
+ public changeStartWith(index: number): void {
428
+ this.listWrapper!.style.setProperty('counter-reset', `item ${index - 1}`);
429
+
430
+ (this.data.meta as OrderedListItemMeta).start = index;
431
+ }
432
+
433
+ /**
434
+ * Changes ordered list counterType property value
435
+ * @param counterType - new value of the counterType value
436
+ */
437
+ public changeCounters(counterType: OlCounterType): void {
438
+ this.listWrapper!.style.setProperty('--list-counter-type', counterType);
439
+
440
+ (this.data.meta as OrderedListItemMeta).counterType = counterType;
441
+ }
442
+
443
+ /**
444
+ * Handles Enter keypress
445
+ * @param event - keydown
446
+ */
447
+ private enterPressed(event: KeyboardEvent): void {
448
+ const currentItem = this.currentItem;
449
+
450
+ /**
451
+ * Prevent editor.js behaviour
452
+ */
453
+ event.stopPropagation();
454
+
455
+ /**
456
+ * Prevent browser behaviour
457
+ */
458
+ event.preventDefault();
459
+
460
+ /**
461
+ * Prevent duplicated event in Chinese, Japanese and Korean languages
462
+ */
463
+ if (event.isComposing) {
464
+ return;
465
+ }
466
+ if (currentItem === null) {
467
+ return;
468
+ }
469
+
470
+ const isEmpty = this.renderer?.getItemContent(currentItem).trim().length === 0;
471
+ const isFirstLevelItem = currentItem.parentNode === this.listWrapper;
472
+ const isFirstItem = currentItem.previousElementSibling === null;
473
+
474
+ const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
475
+
476
+ /**
477
+ * On Enter in the last empty item, get out of list
478
+ */
479
+ if (isFirstLevelItem && isEmpty) {
480
+ if (isLastItem(currentItem) && !itemHasSublist(currentItem)) {
481
+ /**
482
+ * If current item is first and last item of the list, then empty list should be deleted after deletion of the item
483
+ */
484
+ if (isFirstItem) {
485
+ this.convertItemToDefaultBlock(currentBlockIndex, true);
486
+ } else {
487
+ /**
488
+ * If there are other items in the list, just remove current item and get out of the list
489
+ */
490
+ this.convertItemToDefaultBlock();
491
+ }
492
+
493
+ return;
494
+ } else {
495
+ /**
496
+ * If enter is pressed in the сenter of the list item we should split it
497
+ */
498
+ this.splitList(currentItem);
499
+
500
+ return;
501
+ }
502
+ } else if (isEmpty) {
503
+ /**
504
+ * If currnet item is empty and is in the middle of the list
505
+ * And if current item is not on the first level
506
+ * Then unshift current item
507
+ */
508
+ this.unshiftItem(currentItem);
509
+
510
+ return;
511
+ } else {
512
+ /**
513
+ * If current item is not empty than split current item
514
+ */
515
+ this.splitItem(currentItem);
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Handle backspace
521
+ * @param event - keydown
522
+ */
523
+ private backspace(event: KeyboardEvent): void {
524
+ const currentItem = this.currentItem;
525
+
526
+ if (currentItem === null) {
527
+ return;
528
+ }
529
+
530
+ /**
531
+ * Caret is not at start of the item
532
+ * Then backspace button should remove letter as usual
533
+ */
534
+ if (!isCaretAtStartOfInput(currentItem)) {
535
+ return;
536
+ }
537
+
538
+ /**
539
+ * If backspace is pressed with selection, it should be handled as usual
540
+ */
541
+ if (window.getSelection()?.isCollapsed === false) {
542
+ return;
543
+ }
544
+
545
+ /**
546
+ * Prevent Editor.js backspace handling
547
+ */
548
+ event.stopPropagation();
549
+
550
+ /**
551
+ * First item of the list should become paragraph on backspace
552
+ */
553
+ if (currentItem.parentNode === this.listWrapper && currentItem.previousElementSibling === null) {
554
+ /**
555
+ * If current item is first item of the list, then we need to merge first item content with previous block
556
+ */
557
+ this.convertFirstItemToDefaultBlock();
558
+
559
+ return;
560
+ }
561
+
562
+ /**
563
+ * Prevent default backspace behaviour
564
+ */
565
+ event.preventDefault();
566
+
567
+ this.mergeItemWithPrevious(currentItem);
568
+ }
569
+
570
+ /**
571
+ * Reduce indentation for current item
572
+ * @param event - keydown
573
+ */
574
+ private shiftTab(event: KeyboardEvent): void {
575
+ /**
576
+ * Prevent editor.js behaviour
577
+ */
578
+ event.stopPropagation();
579
+
580
+ /**
581
+ * Prevent browser tab behaviour
582
+ */
583
+ event.preventDefault();
584
+
585
+ /**
586
+ * Check that current item exists
587
+ */
588
+ if (this.currentItem === null) {
589
+ return;
590
+ }
591
+
592
+ /**
593
+ * Move item from current list to parent list
594
+ */
595
+ this.unshiftItem(this.currentItem);
596
+ }
597
+
598
+ /**
599
+ * Decrease indentation of the passed item
600
+ * @param item - list item to be unshifted
601
+ */
602
+ private unshiftItem(item: ItemElement): void {
603
+ if (!item.parentNode) {
604
+ return;
605
+ }
606
+ if (!isHtmlElement(item.parentNode)) {
607
+ return;
608
+ }
609
+
610
+ const parentItem = item.parentNode.closest<ItemElement>(`.${DefaultListCssClasses.item}`);
611
+
612
+ /**
613
+ * If item in the first-level list then no need to do anything
614
+ */
615
+ if (!parentItem) {
616
+ return;
617
+ }
618
+
619
+ let currentItemChildWrapper = getItemChildWrapper(item);
620
+
621
+ if (item.parentElement === null) {
622
+ return;
623
+ }
624
+
625
+ const siblings = getSiblings(item);
626
+
627
+ /**
628
+ * If item has any siblings, they should be appended to item child wrapper
629
+ */
630
+ if (siblings !== null) {
631
+ /**
632
+ * Render child wrapper if it does no exist
633
+ */
634
+ if (currentItemChildWrapper === null) {
635
+ currentItemChildWrapper = this.renderer.renderWrapper(false);
636
+ }
637
+
638
+ /**
639
+ * Append siblings to item child wrapper
640
+ */
641
+ siblings.forEach((sibling) => {
642
+ currentItemChildWrapper!.appendChild(sibling);
643
+ });
644
+
645
+ item.appendChild(currentItemChildWrapper);
646
+ }
647
+
648
+ parentItem.after(item);
649
+
650
+ focusItem(item, false);
651
+
652
+ /**
653
+ * If parent item has empty child wrapper after unshifting of the current item, then we need to remove child wrapper
654
+ * This case could be reached if the only child item of the parent was unshifted
655
+ */
656
+ removeChildWrapperIfEmpty(parentItem);
657
+ }
658
+
659
+ /**
660
+ * Method that is used for list splitting and moving trailing items to the new separated list
661
+ * @param item - current item html element
662
+ */
663
+ private splitList(item: ItemElement): void {
664
+ const currentItemChildrenList = getChildItems(item);
665
+
666
+ /**
667
+ * Get current list block index
668
+ */
669
+ const currentBlock = this.block;
670
+
671
+ const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
672
+
673
+ /**
674
+ * First child item should be unshifted because separated list should start
675
+ * with item with first nesting level
676
+ */
677
+ if (currentItemChildrenList.length !== 0) {
678
+ const firstChildItem = currentItemChildrenList[0];
679
+
680
+ this.unshiftItem(firstChildItem);
681
+
682
+ /**
683
+ * If first child item was been unshifted, that caret would be set to the end of the first child item
684
+ * Then we should set caret to the actual current item
685
+ */
686
+ focusItem(item, false);
687
+ }
688
+
689
+ /**
690
+ * If item is first item of the list, we should just get out of the list
691
+ * It means, that we would not split on two lists, if one of them would be empty
692
+ */
693
+ if (item.previousElementSibling === null && item.parentNode === this.listWrapper) {
694
+ this.convertItemToDefaultBlock(currentBlockIndex);
695
+
696
+ return;
697
+ }
698
+
699
+ /**
700
+ * Get trailing siblings of the current item
701
+ */
702
+ const newListItems = getSiblings(item);
703
+
704
+ if (newListItems === null) {
705
+ return;
706
+ }
707
+
708
+ /**
709
+ * Render new wrapper for list that would be separated
710
+ */
711
+ const newListWrapper = this.renderer.renderWrapper(true);
712
+
713
+ /**
714
+ * Append new list wrapper with trailing elements
715
+ */
716
+ newListItems.forEach((newListItem) => {
717
+ newListWrapper.appendChild(newListItem);
718
+ });
719
+
720
+ const newListContent = this.save(newListWrapper);
721
+
722
+ (newListContent.meta as OrderedListItemMeta).start = this.data.style == 'ordered' ? 1 : undefined;
723
+
724
+ /**
725
+ * Insert separated list with trailing items
726
+ */
727
+ this.api.blocks.insert(currentBlock?.name, newListContent, this.config, currentBlockIndex + 1);
728
+
729
+ /**
730
+ * Insert paragraph
731
+ */
732
+ this.convertItemToDefaultBlock(currentBlockIndex + 1);
733
+
734
+ /**
735
+ * Remove temporary new list wrapper used for content save
736
+ */
737
+ newListWrapper.remove();
738
+ }
739
+
740
+ /**
741
+ * Method that is used for splitting item content and moving trailing content to the new sibling item
742
+ * @param currentItem - current item html element
743
+ */
744
+ private splitItem(currentItem: ItemElement): void {
745
+ const [currentNode, offset] = getCaretNodeAndOffset();
746
+
747
+ if (currentNode === null) {
748
+ return;
749
+ }
750
+
751
+ const currentItemContent = getItemContentElement(currentItem);
752
+
753
+ let endingHTML: string;
754
+
755
+ /**
756
+ * If current item has no content, we should pass an empty string to the next created list item
757
+ */
758
+ if (currentItemContent === null) {
759
+ endingHTML = '';
760
+ } else {
761
+ /**
762
+ * On other Enters, get content from caret till the end of the block
763
+ * And move it to the new item
764
+ */
765
+ endingHTML = getContenteditableSlice(currentItemContent, currentNode, offset, 'right', true);
766
+ }
767
+
768
+ const itemChildren = getItemChildWrapper(currentItem);
769
+ /**
770
+ * Create the new list item
771
+ */
772
+ const itemEl = this.renderItem(endingHTML);
773
+
774
+ /**
775
+ * Move new item after current
776
+ */
777
+ currentItem?.after(itemEl);
778
+
779
+ /**
780
+ * If current item has children, move them to the new item
781
+ */
782
+ if (itemChildren) {
783
+ itemEl.appendChild(itemChildren);
784
+ }
785
+
786
+ focusItem(itemEl);
787
+ }
788
+
789
+ /**
790
+ * Method that is used for merging current item with previous one
791
+ * Content of the current item would be appended to the previous item
792
+ * Current item children would not change nesting level
793
+ * @param item - current item html element
794
+ */
795
+ private mergeItemWithPrevious(item: ItemElement): void {
796
+ const previousItem = item.previousElementSibling;
797
+
798
+ const currentItemParentNode = item.parentNode;
799
+
800
+ /**
801
+ * Check that parent node of the current element exists
802
+ */
803
+ if (currentItemParentNode === null) {
804
+ return;
805
+ }
806
+ if (!isHtmlElement(currentItemParentNode)) {
807
+ return;
808
+ }
809
+
810
+ const parentItem = currentItemParentNode.closest<ItemElement>(`.${DefaultListCssClasses.item}`);
811
+
812
+ /**
813
+ * Check that current item has any previous siblings to be merged with
814
+ */
815
+ if (!previousItem && !parentItem) {
816
+ return;
817
+ }
818
+
819
+ /**
820
+ * Make sure previousItem is an HTMLElement
821
+ */
822
+ if (previousItem && !isHtmlElement(previousItem)) {
823
+ return;
824
+ }
825
+
826
+ /**
827
+ * Lets compute the item which will be merged with current item text
828
+ */
829
+ let targetItem: ItemElement | null;
830
+
831
+ /**
832
+ * If there is a previous item then we get a deepest item in its sublists
833
+ *
834
+ * Otherwise we will use the parent item
835
+ */
836
+ if (previousItem) {
837
+ /**
838
+ * Get list of all levels children of the previous item
839
+ */
840
+ const childrenOfPreviousItem = getChildItems(previousItem, false);
841
+
842
+ /**
843
+ * Target item would be deepest child of the previous item or previous item itself
844
+ */
845
+ if (childrenOfPreviousItem.length !== 0 && childrenOfPreviousItem.length !== 0) {
846
+ targetItem = childrenOfPreviousItem[childrenOfPreviousItem.length - 1];
847
+ } else {
848
+ targetItem = previousItem;
849
+ }
850
+ } else {
851
+ targetItem = parentItem;
852
+ }
853
+
854
+ /**
855
+ * Get current item content
856
+ */
857
+ const currentItemContent = this.renderer.getItemContent(item);
858
+
859
+ /**
860
+ * Get the target item content element
861
+ */
862
+ if (!targetItem) {
863
+ return;
864
+ }
865
+
866
+ /**
867
+ * Set caret to the end of the target item
868
+ */
869
+ focusItem(targetItem, false);
870
+
871
+ /**
872
+ * Get target item content element
873
+ */
874
+ const targetItemContentElement = getItemContentElement(targetItem);
875
+
876
+ /**
877
+ * Set a new place for caret
878
+ */
879
+ if (targetItemContentElement === null) {
880
+ return;
881
+ }
882
+
883
+ /**
884
+ * Update target item content by merging with current item html content
885
+ */
886
+ targetItemContentElement.insertAdjacentHTML('beforeend', currentItemContent);
887
+
888
+ /**
889
+ * Get child list of the currentItem
890
+ */
891
+ const currentItemChildrenList = getChildItems(item);
892
+
893
+ /**
894
+ * If item has no children, just remove item
895
+ * Else children of the item should be prepended to the target item child list
896
+ */
897
+ if (currentItemChildrenList.length === 0) {
898
+ /**
899
+ * Remove current item element
900
+ */
901
+ item.remove();
902
+
903
+ /**
904
+ * If target item has empty child wrapper after merge, we need to remove child wrapper
905
+ * This case could be reached if the only child item of the target was merged with target
906
+ */
907
+ removeChildWrapperIfEmpty(targetItem);
908
+
909
+ return;
910
+ }
911
+
912
+ /**
913
+ * Get target for child list of the currentItem
914
+ * Note that previous item and parent item could not be null at the same time
915
+ * This case is checked before
916
+ */
917
+ const targetForChildItems = previousItem ? previousItem : parentItem!;
918
+
919
+ const targetChildWrapper = getItemChildWrapper(targetForChildItems) ?? this.renderer.renderWrapper(false);
920
+
921
+ /**
922
+ * Add child current item children to the target childWrapper
923
+ */
924
+ if (previousItem) {
925
+ currentItemChildrenList.forEach((childItem) => {
926
+ targetChildWrapper.appendChild(childItem);
927
+ });
928
+ } else {
929
+ currentItemChildrenList.forEach((childItem) => {
930
+ targetChildWrapper.prepend(childItem);
931
+ });
932
+ }
933
+
934
+ /**
935
+ * If we created new wrapper, then append childWrapper to the target item
936
+ */
937
+ if (getItemChildWrapper(targetForChildItems) === null) {
938
+ targetItem.appendChild(targetChildWrapper);
939
+ }
940
+
941
+ /**
942
+ * Remove current item element
943
+ */
944
+ item.remove();
945
+ }
946
+
947
+ /**
948
+ * Add indentation to current item
949
+ * @param event - keydown
950
+ */
951
+ private addTab(event: KeyboardEvent): void {
952
+ /**
953
+ * Prevent editor.js behaviour
954
+ */
955
+ event.stopPropagation();
956
+
957
+ /**
958
+ * Prevent browser tab behaviour
959
+ */
960
+ event.preventDefault();
961
+
962
+ const currentItem = this.currentItem;
963
+
964
+ if (!currentItem) {
965
+ return;
966
+ }
967
+
968
+ /**
969
+ * Check that maxLevel specified in config
970
+ */
971
+ if (this.config?.maxLevel !== undefined) {
972
+ const currentItemLevel = this.currentItemLevel;
973
+
974
+ /**
975
+ * Check that current item is not in the maximum nesting level
976
+ */
977
+ if (currentItemLevel !== null && currentItemLevel === this.config.maxLevel) {
978
+ return;
979
+ }
980
+ }
981
+
982
+ /**
983
+ * Check that the item has potential parent
984
+ * Previous sibling is potential parent in case of adding tab
985
+ * After adding tab current item would be moved to the previous sibling's child list
986
+ */
987
+ const prevItem = currentItem.previousSibling;
988
+
989
+ if (prevItem === null) {
990
+ return;
991
+ }
992
+ if (!isHtmlElement(prevItem)) {
993
+ return;
994
+ }
995
+
996
+ const prevItemChildrenList = getItemChildWrapper(prevItem);
997
+
998
+ /**
999
+ * If prev item has child items, just append current to them
1000
+ * Else render new child wrapper for previous item
1001
+ */
1002
+ if (prevItemChildrenList) {
1003
+ /**
1004
+ * Previous item would be appended with current item and it's sublists
1005
+ * After that sublists would be moved one level back
1006
+ */
1007
+ prevItemChildrenList.appendChild(currentItem);
1008
+
1009
+ /**
1010
+ * Get all current item child to be moved to previous nesting level
1011
+ */
1012
+ const currentItemChildrenList = getChildItems(currentItem);
1013
+
1014
+ /**
1015
+ * Move current item sublists one level back
1016
+ */
1017
+ currentItemChildrenList.forEach((child) => {
1018
+ prevItemChildrenList.appendChild(child);
1019
+ });
1020
+ } else {
1021
+ const prevItemChildrenListWrapper = this.renderer.renderWrapper(false);
1022
+
1023
+ /**
1024
+ * Previous item would be appended with current item and it's sublists
1025
+ * After that sublists would be moved one level back
1026
+ */
1027
+ prevItemChildrenListWrapper.appendChild(currentItem);
1028
+
1029
+ /**
1030
+ * Get all current item child to be moved to previous nesting level
1031
+ */
1032
+ const currentItemChildrenList = getChildItems(currentItem);
1033
+
1034
+ /**
1035
+ * Move current item sublists one level back
1036
+ */
1037
+ currentItemChildrenList.forEach((child) => {
1038
+ prevItemChildrenListWrapper.appendChild(child);
1039
+ });
1040
+
1041
+ prevItem.appendChild(prevItemChildrenListWrapper);
1042
+ }
1043
+
1044
+ /**
1045
+ * Remove child wrapper of the current item if it is empty after adding the tab
1046
+ * This case would be reached, because after adding tab current item will have same nesting level with children
1047
+ * So its child wrapper would be empty
1048
+ */
1049
+ removeChildWrapperIfEmpty(currentItem);
1050
+
1051
+ focusItem(currentItem, false);
1052
+ }
1053
+
1054
+ /**
1055
+ * Convert current item to default block with passed index
1056
+ * @param newBloxkIndex - optional parameter represents index, where would be inseted default block
1057
+ * @param removeList - optional parameter, that represents condition, if List should be removed
1058
+ */
1059
+ private convertItemToDefaultBlock(newBloxkIndex?: number, removeList?: boolean): void {
1060
+ let newBlock;
1061
+
1062
+ const currentItem = this.currentItem;
1063
+
1064
+ const currentItemContent = currentItem !== null ? this.renderer.getItemContent(currentItem) : '';
1065
+
1066
+ if (removeList === true) {
1067
+ this.api.blocks.delete();
1068
+ }
1069
+
1070
+ /**
1071
+ * Check that index have passed
1072
+ */
1073
+ if (newBloxkIndex !== undefined) {
1074
+ newBlock = this.api.blocks.insert(undefined, { text: currentItemContent }, undefined, newBloxkIndex);
1075
+ } else {
1076
+ newBlock = this.api.blocks.insert();
1077
+ }
1078
+
1079
+ currentItem?.remove();
1080
+ this.api.caret.setToBlock(newBlock, 'start');
1081
+ }
1082
+
1083
+ /**
1084
+ * Convert first item of the list to default block
1085
+ * This method could be called when backspace button pressed at start of the first item of the list
1086
+ * First item of the list would be converted to the paragraph and first item children would be unshifted
1087
+ */
1088
+ private convertFirstItemToDefaultBlock(): void {
1089
+ const currentItem = this.currentItem;
1090
+
1091
+ if (currentItem === null) {
1092
+ return;
1093
+ }
1094
+
1095
+ const currentItemChildren = getChildItems(currentItem);
1096
+
1097
+ /**
1098
+ * Check that current item have at least one child
1099
+ * If current item have no children, we can guarantee,
1100
+ * that after deletion of the first item of the list, children would not be removed
1101
+ */
1102
+ if (currentItemChildren.length !== 0) {
1103
+ const firstChildItem = currentItemChildren[0];
1104
+
1105
+ /**
1106
+ * Unshift first child item, to guarantee, that after deletion of the first item
1107
+ * list will start with first level of nesting
1108
+ */
1109
+ this.unshiftItem(firstChildItem);
1110
+
1111
+ /**
1112
+ * Set focus back to the current item after unshifting child
1113
+ */
1114
+ focusItem(currentItem);
1115
+ }
1116
+
1117
+ /**
1118
+ * Get all first level items of the list
1119
+ */
1120
+ const currentItemSiblings = getSiblings(currentItem);
1121
+
1122
+ const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();
1123
+
1124
+ /**
1125
+ * If current item has no siblings, than List is empty, and it should be deleted
1126
+ */
1127
+ const removeList = currentItemSiblings === null;
1128
+
1129
+ this.convertItemToDefaultBlock(currentBlockIndex, removeList);
1130
+ }
1131
+
1132
+ /**
1133
+ * Method that calls render function of the renderer with a necessary item meta cast
1134
+ * @param itemContent - content to be rendered in new item
1135
+ * @param meta - meta used in list item rendering
1136
+ * @returns html element of the rendered item
1137
+ */
1138
+ private renderItem(itemContent: ListItem['content'], meta?: ListItem['meta']): ItemElement {
1139
+ const itemMeta = meta ?? this.renderer.composeDefaultMeta();
1140
+
1141
+ switch (true) {
1142
+ case this.renderer instanceof OrderedListRenderer:
1143
+ return this.renderer.renderItem(itemContent, itemMeta as OrderedListItemMeta);
1144
+
1145
+ case this.renderer instanceof UnorderedListRenderer:
1146
+ return this.renderer.renderItem(itemContent, itemMeta as UnorderedListItemMeta);
1147
+
1148
+ default:
1149
+ return this.renderer.renderItem(itemContent, itemMeta as ChecklistItemMeta);
1150
+ }
1151
+ }
1152
+
1153
+ /**
1154
+ * Renders children list
1155
+ * @param items - list data used in item rendering
1156
+ * @param parentElement - where to append passed items
1157
+ */
1158
+ private appendItems(items: ListItem[], parentElement: Element): void {
1159
+ items.forEach((item) => {
1160
+ const itemEl = this.renderItem(item.content, item.meta);
1161
+
1162
+ parentElement.appendChild(itemEl);
1163
+
1164
+ /**
1165
+ * Check if there are child items
1166
+ */
1167
+ if (item.items.length) {
1168
+ const sublistWrapper = this.renderer?.renderWrapper(false);
1169
+
1170
+ /**
1171
+ * Recursively render child items
1172
+ */
1173
+ this.appendItems(item.items, sublistWrapper);
1174
+
1175
+ itemEl.appendChild(sublistWrapper);
1176
+ }
1177
+ });
1178
+ }
1179
+ }