@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.
- package/dist/index.mjs +360 -319
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/Editor/Editor.vue +22 -6
- package/src/plugins/list/ListRenderer/ChecklistRenderer.ts +1 -1
- package/src/plugins/list/ListTabulator/index.ts +1 -1
- package/src/plugins/list/index.ts +118 -123
- package/src/plugins/list/styles/.cdx-list .css +168 -0
- package/src/plugins/list/utils/normalizeData.ts +0 -1
- package/src/plugins/list-bak/ListRenderer/ChecklistRenderer.ts +208 -0
- package/src/plugins/list-bak/ListRenderer/ListRenderer.ts +73 -0
- package/src/plugins/list-bak/ListRenderer/OrderedListRenderer.ts +123 -0
- package/src/plugins/list-bak/ListRenderer/UnorderedListRenderer.ts +123 -0
- package/src/plugins/list-bak/ListRenderer/index.ts +6 -0
- package/src/plugins/list-bak/ListTabulator/index.ts +1179 -0
- package/src/plugins/list-bak/index.ts +485 -0
- package/src/plugins/list-bak/styles/CssPrefix.ts +4 -0
- package/src/plugins/list-bak/styles/input.css +36 -0
- package/src/plugins/list-bak/styles/list.css +165 -0
- package/src/plugins/list-bak/types/Elements.ts +14 -0
- package/src/plugins/list-bak/types/ItemMeta.ts +40 -0
- package/src/plugins/list-bak/types/ListParams.ts +102 -0
- package/src/plugins/list-bak/types/ListRenderer.ts +6 -0
- package/src/plugins/list-bak/types/OlCounterType.ts +63 -0
- package/src/plugins/list-bak/types/index.ts +14 -0
- package/src/plugins/list-bak/utils/focusItem.ts +18 -0
- package/src/plugins/list-bak/utils/getChildItems.ts +40 -0
- package/src/plugins/list-bak/utils/getItemChildWrapper.ts +10 -0
- package/src/plugins/list-bak/utils/getItemContentElement.ts +10 -0
- package/src/plugins/list-bak/utils/getSiblings.ts +52 -0
- package/src/plugins/list-bak/utils/isLastItem.ts +9 -0
- package/src/plugins/list-bak/utils/itemHasSublist.ts +10 -0
- package/src/plugins/list-bak/utils/normalizeData.ts +84 -0
- package/src/plugins/list-bak/utils/removeChildWrapperIfEmpty.ts +31 -0
- package/src/plugins/list-bak/utils/renderToolboxInput.ts +105 -0
- package/src/plugins/list-bak/utils/stripNumbers.ts +7 -0
- 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
|
+
}
|