leksy-editor 2.4.0 → 2.5.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/README.md CHANGED
@@ -110,6 +110,7 @@ const app = createApp({
110
110
  | `showTabs` | Enables or disables the display of tabs. |
111
111
  | `enableFindAndReplace` | To enabled find and replace feature|
112
112
  | `customTooltip` | Customize tooltip according to your need, set using key-value pair|
113
+ | `customIcon` | Customize icon according to your need, set using key-value pair|
113
114
 
114
115
  ### CSS Customization
115
116
 
@@ -177,10 +178,10 @@ Similarly, Pexels and Tenor can be integrated using `pexels` and `tenor` plugins
177
178
 
178
179
  | Function | Description |
179
180
  |----------|-------------|
180
- | `setContents(html)` | Sets the editor's content. |
181
- | `getContents()` | Returns the current HTML content of the editor. |
182
- | `onChange(html)` | Triggered when content changes. |
183
- | `onBlur(html)` | Triggered when the editor loses focus. |
181
+ | `setContents(tabs || html)` | Sets the editor's content. |
182
+ | `getContents()` | Returns the array of tabs containing HTML content of the editor. |
183
+ | `onChange(tabs)` | Triggered when content changes. |
184
+ | `onBlur(tabs)` | Triggered when the editor loses focus. |
184
185
  | `onAttachment(files)` | Fires when files are attached. |
185
186
  | `manipulateImage()` | Custom function for manipulating images. |
186
187
  | `handleFilePicker()` | Custom function for file upload, useful for Android and iOS file upload. |
package/constant.js CHANGED
@@ -932,6 +932,7 @@ const UNORDERED_LIST_OPTIONS = {
932
932
 
933
933
  const RESIZE_MARGIN = 10;
934
934
 
935
+ const BLOCK_FORMAT_TAGS = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P'];
935
936
  const LIST_STYLES_BY_LEVEL = ['1', 'a', 'i', 'A'];
936
937
  const UNORDERED_LIST_STYLES_BY_LEVEL = ['disc', 'circle', 'square'];
937
938
 
@@ -958,6 +959,7 @@ export {
958
959
  CSS_VARIABLES,
959
960
  TAB_CATEGORIES,
960
961
  RESIZE_MARGIN,
962
+ BLOCK_FORMAT_TAGS,
961
963
  LIST_STYLES_BY_LEVEL,
962
964
  UNORDERED_LIST_STYLES_BY_LEVEL,
963
965
  UNORDERED_LIST_OPTIONS,
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import './style.css'
2
2
  import { CLASSES, CSS, CSS_VARIABLES, ERRORS, REGEX, SVG } from "./constant"
3
3
  import PLUGINS, { applyTextFormat } from './plugin';
4
- import { showAnchorPopover, changeAllToolbarState, changeToolbarStateByName, changeToolbarValueByName, cleanHTML, debounce, destroyImageResizer, destroyTableEditPlugin, initImageResizer, initTableEditPlugin, makeToolbarButton, makeToolbarColor, makeToolbarDropdown, makeToolbarSelect, rgbToHex, updateTableResizerPosition, destroyAnchorPopover, changeToolbarHtmlByName, showRemoteCursor, syncRemoteChangesDebounce, applyRemoteChanges, updateCursorPositionDebounce, buildTributeValues, updateHeight, getTableGrid, getCellPosition, makeUnorderedList, makeOrderedList, makeHeading, makeBlockQuote, makeStrikethrough, makeCodeBlock, makeSublist, showTooltip, makeToolbarButtonSelect, navigateToHeading, updateOutlineItems, highlightActiveOutline, findAndReplace, handleBackspaceInList } from './utilities';
4
+ import { showAnchorPopover, changeAllToolbarState, changeToolbarStateByName, changeToolbarValueByName, cleanHTML, debounce, destroyImageResizer, destroyTableEditPlugin, initImageResizer, initTableEditPlugin, makeToolbarButton, makeToolbarColor, makeToolbarDropdown, makeToolbarSelect, rgbToHex, updateTableResizerPosition, destroyAnchorPopover, changeToolbarHtmlByName, showRemoteCursor, syncRemoteChangesDebounce, applyRemoteChanges, updateCursorPositionDebounce, buildTributeValues, updateHeight, getTableGrid, getCellPosition, makeUnorderedList, makeOrderedList, makeHeading, makeBlockQuote, makeStrikethrough, makeCodeBlock, makeSublist, showTooltip, makeToolbarButtonSelect, navigateToHeading, updateOutlineItems, highlightActiveOutline, findAndReplace, handleBackspaceInList, trackListSelectionDeletion, cleanupListSelectionDeletion } from './utilities';
5
5
  import { initTabs, findTab, renderTabs, prepareTabs } from './tab';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
 
@@ -32,7 +32,9 @@ class LeksyEditor {
32
32
  * @property {Boolean} disabled
33
33
  * @property {Function} onFullScreen
34
34
  * @property {Boolean} showTabs
35
- * @property {Boolean} enableFindAndReplace
35
+ * @property {Boolean} enableFindAndReplace
36
+ * @property {Array<Object>} customTooltip
37
+ * @property {Array<Object>} customIcon
36
38
  */
37
39
  /**
38
40
  *
@@ -675,6 +677,7 @@ class LeksyEditor {
675
677
  contentEditableDiv.className = 'content-editable';
676
678
 
677
679
  contentEditableDiv.oninput = (e) => {
680
+ cleanupListSelectionDeletion(core);
678
681
  core.onChange(e.target.innerHTML)
679
682
  }
680
683
  contentEditableDiv.onbeforeinput = (e) => {
@@ -858,6 +861,7 @@ class LeksyEditor {
858
861
  }
859
862
 
860
863
  handleBackspaceInList(event, core);
864
+ trackListSelectionDeletion(event, core);
861
865
 
862
866
  if (!isLinkCreated) core.elements.lastCreatedLink = null
863
867
 
@@ -1015,4 +1019,4 @@ export default LeksyEditor
1015
1019
  export {
1016
1020
  convertHtmlToText,
1017
1021
  isHTMLEmpty,
1018
- }
1022
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leksy-editor",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Leksy Editor is an alternative to traditional WYSIWYG editors, designed primarily for creating mail templates, blogs, and documents without any content manipulation.",
5
5
  "main": "index.js",
6
6
  "directories": {
package/plugin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EMOJI_CATEGORIES, FONT_SIZE_OPTIONS, FONTS, FORMAT_OPTIONS, MIMETYPE, REGEX, SPECIAL_CHARACTERS, SVG, UNORDERED_LIST_OPTIONS, ORDERED_LIST_OPTIONS, CLASSES } from "./constant"
2
2
  import { giphy, pexels, tenor } from "./gallery";
3
- import { formatLink, changeAllToolbarState, changeToolbarHtmlByName, changeToolbarStateByName, changeToolbarValueByName, cleanHTML, constructEmbedUrl, extractSocialMediaId, isLinkValid, openModal, transformTextStyle, insertTOC, } from "./utilities";
3
+ import { formatLink, changeAllToolbarState, changeToolbarHtmlByName, changeToolbarStateByName, changeToolbarValueByName, cleanHTML, constructEmbedUrl, extractSocialMediaId, isLinkValid, openModal, transformTextStyle, insertTOC, applyFormatBlockInListItem, normalizeHeadingWrappedLists, } from "./utilities";
4
4
 
5
5
  const applyTextFormat = (core, command) => {
6
6
  core.elements.editor.focus();
@@ -12,27 +12,28 @@ const applyTextFormat = (core, command) => {
12
12
  else changeToolbarStateByName(core, 'inactive', [command])
13
13
  }
14
14
 
15
- const applyOrderList = (core) => {
16
- core.elements.iframeWindow.execCommand('insertOrderedList');
15
+ const applyList = (core, { command, activePlugin, inactivePlugin }) => {
16
+ core.elements.iframeWindow.execCommand(command);
17
+ normalizeHeadingWrappedLists(core);
17
18
  core.elements.editor.focus();
18
19
  core.updateCaretPosition()
19
20
 
20
- const isActive = core.elements.iframeWindow.queryCommandState('insertOrderedList');
21
- if (isActive) changeToolbarStateByName(core, 'active', ['ordered_list'])
22
- else changeToolbarStateByName(core, 'inactive', ['ordered_list'])
23
- changeToolbarStateByName(core, 'inactive', ['unordered_list'])
21
+ const isActive = core.elements.iframeWindow.queryCommandState(command);
22
+ changeToolbarStateByName(core, isActive ? 'active' : 'inactive', [activePlugin])
23
+ changeToolbarStateByName(core, 'inactive', [inactivePlugin])
24
24
  }
25
25
 
26
- const applyUnorderedList = (core) => {
27
- core.elements.iframeWindow.execCommand('insertUnorderedList');
28
- core.elements.editor.focus();
29
- core.updateCaretPosition()
26
+ const applyOrderList = (core) => applyList(core, {
27
+ command: 'insertOrderedList',
28
+ activePlugin: 'ordered_list',
29
+ inactivePlugin: 'unordered_list'
30
+ })
30
31
 
31
- const isActive = core.elements.iframeWindow.queryCommandState('insertUnorderedList');
32
- if (isActive) changeToolbarStateByName(core, 'active', ['unordered_list'])
33
- else changeToolbarStateByName(core, 'inactive', ['unordered_list'])
34
- changeToolbarStateByName(core, 'inactive', ['ordered_list'])
35
- }
32
+ const applyUnorderedList = (core) => applyList(core, {
33
+ command: 'insertUnorderedList',
34
+ activePlugin: 'unordered_list',
35
+ inactivePlugin: 'ordered_list'
36
+ })
36
37
 
37
38
  const PLUGINS = {
38
39
  'undo': {
@@ -258,7 +259,11 @@ const PLUGINS = {
258
259
  width: '110px',
259
260
  click: (tag, core, options) => {
260
261
  core.elements.editor.focus();
261
- core.elements.iframeWindow.execCommand('formatBlock', false, tag);
262
+ const isFormattedInList = applyFormatBlockInListItem(core, tag);
263
+ if (!isFormattedInList) {
264
+ core.elements.iframeWindow.execCommand('formatBlock', false, tag);
265
+ }
266
+ normalizeHeadingWrappedLists(core);
262
267
 
263
268
  core.updateCaretPosition()
264
269
  changeToolbarValueByName(core, 'format-block', tag)
@@ -276,6 +281,7 @@ const PLUGINS = {
276
281
  const isActive = core.elements.iframeWindow.queryCommandState('insertOrderedList');
277
282
  if (!isActive) {
278
283
  core.elements.iframeWindow.execCommand('insertOrderedList');
284
+ normalizeHeadingWrappedLists(core);
279
285
  }
280
286
  const selection = core.elements.iframeWindow.getSelection();
281
287
  if (selection.rangeCount > 0) {
@@ -308,6 +314,7 @@ const PLUGINS = {
308
314
  const isActive = core.elements.iframeWindow.queryCommandState('insertUnorderedList');
309
315
  if (!isActive) {
310
316
  core.elements.iframeWindow.execCommand('insertUnorderedList');
317
+ normalizeHeadingWrappedLists(core);
311
318
  }
312
319
  const selection = core.elements.iframeWindow.getSelection();
313
320
  if (selection.rangeCount > 0) {
@@ -1345,4 +1352,4 @@ export {
1345
1352
  applyTextFormat,
1346
1353
  applyOrderList,
1347
1354
  applyUnorderedList
1348
- }
1355
+ }
package/utilities.js CHANGED
@@ -1,8 +1,8 @@
1
- import { CLASSES, FONT_SIZES, FONTS, FORMATS, GIPHY_POWERED_IMAGE, REGEX, RESIZER_PLUGINS, SOCIAL_MEDIA_BASEURLS, SOCIAL_MEDIA_PATTERNS, SVG, TABLE_PLUGINS, TAB_CATEGORIES, RESIZE_MARGIN, LIST_STYLES_BY_LEVEL, UNORDERED_LIST_STYLES_BY_LEVEL } from "./constant";
1
+ import { BLOCK_FORMAT_TAGS, CLASSES, FONT_SIZES, FONTS, FORMATS, GIPHY_POWERED_IMAGE, REGEX, RESIZER_PLUGINS, SOCIAL_MEDIA_BASEURLS, SOCIAL_MEDIA_PATTERNS, SVG, TABLE_PLUGINS, TAB_CATEGORIES, RESIZE_MARGIN, LIST_STYLES_BY_LEVEL, UNORDERED_LIST_STYLES_BY_LEVEL } from "./constant";
2
2
  import { DiffDOM } from 'diff-dom';
3
- import { applyUnorderedList, applyOrderList } from "./plugin";
4
3
  import { v4 as uuidv4 } from 'uuid';
5
4
  import { findTab } from "./tab";
5
+ import { applyOrderList, applyUnorderedList } from "./plugin";
6
6
 
7
7
  const dd = new DiffDOM();
8
8
 
@@ -204,6 +204,10 @@ const getTooltip = (plugin, options) => {
204
204
  return options?.customTooltip?.[plugin.pluginId] ?? plugin.title
205
205
  }
206
206
 
207
+ const getToolbarIcon = (plugin, options) => {
208
+ return options?.customIcon?.[plugin.pluginId] ?? plugin.icon
209
+ }
210
+
207
211
  const makeToolbarButton = (_plugin, options, core) => {
208
212
  const pluginButton = document.createElement('button');
209
213
  pluginButton.type = "button"
@@ -217,7 +221,7 @@ const makeToolbarButton = (_plugin, options, core) => {
217
221
  };
218
222
 
219
223
  const pluginIcon = document.createElement('div');
220
- pluginIcon.innerHTML = _plugin.icon;
224
+ pluginIcon.innerHTML = getToolbarIcon(_plugin, options);
221
225
  pluginButton.appendChild(pluginIcon)
222
226
  return pluginButton
223
227
  }
@@ -236,7 +240,7 @@ const makeToolbarButtonSelect = (_plugin, options, core) => {
236
240
  };
237
241
 
238
242
  const pluginIcon = document.createElement('div');
239
- pluginIcon.innerHTML = _plugin.icon;
243
+ pluginIcon.innerHTML = getToolbarIcon(_plugin, options);
240
244
  pluginButton.appendChild(pluginIcon)
241
245
  const pluginSelect = makeToolbarSelect({ ..._plugin, icon: '' }, options, core);
242
246
 
@@ -265,7 +269,7 @@ const makeToolbarColor = (_plugin, options, core) => {
265
269
  };
266
270
 
267
271
  const pluginIcon = document.createElement('div');
268
- pluginIcon.innerHTML = _plugin.icon;
272
+ pluginIcon.innerHTML = getToolbarIcon(_plugin, options);
269
273
  pluginIcon.onclick = () => pluginButton.click()
270
274
  pluginContainer.append(pluginButton, pluginIcon)
271
275
  return pluginContainer
@@ -348,7 +352,7 @@ const makeToolbarDropdown = (_plugin, options, core) => {
348
352
  dropdownButton.setAttribute('data-title', getTooltip(_plugin, options))
349
353
 
350
354
  const pluginIcon = document.createElement('div');
351
- pluginIcon.innerHTML = _plugin.icon;
355
+ pluginIcon.innerHTML = getToolbarIcon(_plugin, options);
352
356
  dropdownButton.appendChild(pluginIcon)
353
357
 
354
358
  const dropdownContent = document.createElement('div');
@@ -597,7 +601,7 @@ const makeToolbarSelect = (_plugin, options, core) => {
597
601
  pluginIcon.style.justifyContent = 'space-between'
598
602
 
599
603
  const iconName = document.createElement('span');
600
- iconName.innerHTML = _plugin.icon;
604
+ iconName.innerHTML = getToolbarIcon(_plugin, options);
601
605
 
602
606
  const arrow = document.createElement('span');
603
607
  if (_plugin.type == 'button-select') {
@@ -2870,6 +2874,98 @@ const isInsideTableCell = (node) => {
2870
2874
  return element?.closest('td, th');
2871
2875
  };
2872
2876
 
2877
+ const formatListItemContent = (li, tag) => {
2878
+ const formattedBlock = document.createElement(tag);
2879
+ const childNodes = Array.from(li.childNodes);
2880
+
2881
+ childNodes.forEach(child => {
2882
+ if (child.nodeType === Node.ELEMENT_NODE && ['UL', 'OL'].includes(child.tagName)) return;
2883
+ formattedBlock.appendChild(child);
2884
+ });
2885
+
2886
+ if (!formattedBlock.childNodes.length) formattedBlock.innerHTML = '<br>';
2887
+
2888
+ const firstNestedList = Array.from(li.children).find(child => ['UL', 'OL'].includes(child.tagName));
2889
+ li.insertBefore(formattedBlock, firstNestedList || null);
2890
+
2891
+ return formattedBlock;
2892
+ }
2893
+
2894
+ const applyFormatBlockInListItem = (core, tag) => {
2895
+ const selection = core.elements.iframeWindow.getSelection();
2896
+ const node = selection?.rangeCount ? selection.getRangeAt(0).startContainer : null;
2897
+ const element = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
2898
+ const li = element?.closest('li');
2899
+
2900
+ if (!li || !core.elements.editor.contains(li)) return false;
2901
+
2902
+ const currentBlock = element.closest(BLOCK_FORMAT_TAGS.join(','));
2903
+ const isCurrentBlockInLi = currentBlock && currentBlock.closest('li') === li;
2904
+ let formattedBlock;
2905
+
2906
+ const placeCaretAtEnd = (core, element) => {
2907
+ const range = document.createRange();
2908
+ const selection = core.elements.iframeWindow.getSelection();
2909
+
2910
+ range.selectNodeContents(element);
2911
+ range.collapse(false);
2912
+ selection.removeAllRanges();
2913
+ selection.addRange(range);
2914
+ }
2915
+
2916
+ if (tag === 'p') {
2917
+ if (isCurrentBlockInLi) {
2918
+ const fragment = document.createDocumentFragment();
2919
+
2920
+ while (currentBlock.firstChild) fragment.appendChild(currentBlock.firstChild);
2921
+
2922
+ li.insertBefore(fragment, currentBlock);
2923
+ currentBlock.remove();
2924
+ }
2925
+ placeCaretAtEnd(core, li);
2926
+ return true;
2927
+ }
2928
+
2929
+ if (isCurrentBlockInLi) {
2930
+ formattedBlock = document.createElement(tag);
2931
+ formattedBlock.innerHTML = currentBlock.innerHTML || '<br>';
2932
+ currentBlock.replaceWith(formattedBlock);
2933
+ } else {
2934
+ formattedBlock = formatListItemContent(li, tag);
2935
+ }
2936
+
2937
+ placeCaretAtEnd(core, formattedBlock);
2938
+ return true;
2939
+ }
2940
+
2941
+ const normalizeHeadingWrappedLists = (core) => {
2942
+ core.elements.editor.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
2943
+ const lists = Array.from(heading.children).filter(child => ['UL', 'OL'].includes(child.tagName));
2944
+ if (!lists.length) return;
2945
+
2946
+ const headingTag = heading.tagName.toLowerCase();
2947
+ const fragment = document.createDocumentFragment();
2948
+
2949
+ lists.forEach(list => {
2950
+ list.querySelectorAll('li').forEach(li => {
2951
+ const hasBlock = Array.from(li.children).some(child => BLOCK_FORMAT_TAGS.includes(child.tagName));
2952
+ if (!hasBlock) formatListItemContent(li, headingTag);
2953
+ });
2954
+ fragment.appendChild(list);
2955
+ });
2956
+
2957
+ heading.parentNode.insertBefore(fragment, heading);
2958
+
2959
+ if (heading.textContent.replace(/\u200B/g, '').trim() || heading.querySelector('img, br')) {
2960
+ const paragraph = document.createElement('p');
2961
+ paragraph.innerHTML = heading.innerHTML;
2962
+ heading.replaceWith(paragraph);
2963
+ } else {
2964
+ heading.remove();
2965
+ }
2966
+ });
2967
+ }
2968
+
2873
2969
  const makeUnorderedList = (event, core) => {
2874
2970
  if (event.key !== ' ') return;
2875
2971
 
@@ -3201,7 +3297,7 @@ const indentListItem = (li, core) => {
3201
3297
  const selection = core.elements.iframeWindow.getSelection();
3202
3298
  const range = document.createRange();
3203
3299
  range.selectNodeContents(li);
3204
- range.collapse(false);
3300
+ range.collapse(true);
3205
3301
  selection.removeAllRanges();
3206
3302
  selection.addRange(range);
3207
3303
  core.elements.editor.focus();
@@ -3241,7 +3337,109 @@ const outdentListItem = (li, core) => {
3241
3337
  const isEmptyLi = (li) => {
3242
3338
  const text = li.textContent.replace(/\u200B/g, '').trim();
3243
3339
  const hasOnlySublist = li.children.length === 1 && li.querySelector('ul, ol');
3244
- return text === '' && !hasOnlySublist;
3340
+ const hasMeaningfulElement = li.querySelector('img, iframe, audio, video, table, hr, figure');
3341
+ return text === '' && !hasOnlySublist && !hasMeaningfulElement;
3342
+ }
3343
+
3344
+ const trackListSelectionDeletion = (event, core) => {
3345
+ if (event.defaultPrevented) {
3346
+ delete core.state.pendingListSelectionDeletion;
3347
+ return;
3348
+ }
3349
+
3350
+ if (!['Backspace', 'Delete'].includes(event.key)) {
3351
+ delete core.state.pendingListSelectionDeletion;
3352
+ return;
3353
+ }
3354
+
3355
+ const selection = core.elements.iframeWindow.getSelection();
3356
+ if (!selection?.rangeCount) return;
3357
+
3358
+ const range = selection.getRangeAt(0);
3359
+ if (range.collapsed) {
3360
+ delete core.state.pendingListSelectionDeletion;
3361
+ return;
3362
+ }
3363
+
3364
+ const getElementFromNode = (node) => {
3365
+ if (!node) return null;
3366
+ return node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
3367
+ }
3368
+
3369
+ const startElement = getElementFromNode(range.startContainer);
3370
+ const endElement = getElementFromNode(range.endContainer);
3371
+ const startLi = startElement?.closest('li');
3372
+ const endLi = endElement?.closest('li');
3373
+
3374
+ if (!startLi || !endLi || startLi === endLi) {
3375
+ delete core.state.pendingListSelectionDeletion;
3376
+ return;
3377
+ }
3378
+
3379
+ const listItems = [];
3380
+
3381
+ core.elements.editor.querySelectorAll('li').forEach(li => {
3382
+ if (range.intersectsNode(li)) listItems.push(li);
3383
+ });
3384
+
3385
+ if (listItems.length < 2) {
3386
+ delete core.state.pendingListSelectionDeletion;
3387
+ return;
3388
+ }
3389
+
3390
+ core.state.pendingListSelectionDeletion = {
3391
+ listItems,
3392
+ lists: Array.from(new Set(listItems.map(li => li.parentElement).filter(Boolean))),
3393
+ };
3394
+ }
3395
+
3396
+ const cleanupListSelectionDeletion = (core) => {
3397
+ const pendingDeletion = core.state.pendingListSelectionDeletion;
3398
+ if (!pendingDeletion) return false;
3399
+
3400
+ delete core.state.pendingListSelectionDeletion;
3401
+
3402
+ let didChange = false;
3403
+ const lists = new Set(pendingDeletion.lists);
3404
+
3405
+ pendingDeletion.listItems.forEach(li => {
3406
+ if (!li.isConnected || !core.elements.editor.contains(li)) return;
3407
+ if (!isEmptyLi(li)) return;
3408
+
3409
+ const parentList = li.parentElement;
3410
+ if (parentList) lists.add(parentList);
3411
+ li.remove();
3412
+ didChange = true;
3413
+ });
3414
+
3415
+ Array.from(lists).reverse().forEach(list => {
3416
+ if (!list?.isConnected || !core.elements.editor.contains(list)) return;
3417
+ if (list.querySelector('li')) return;
3418
+
3419
+ list.remove();
3420
+ didChange = true;
3421
+ });
3422
+
3423
+ if (didChange) {
3424
+ if (!core.elements.editor.textContent.replace(/\u200B/g, '').trim() && !core.elements.editor.querySelector('img, iframe, audio, video, table, hr, figure')) {
3425
+ core.elements.editor.innerHTML = '<div><br></div>';
3426
+ }
3427
+
3428
+ const selection = core.elements.iframeWindow.getSelection();
3429
+ if (!selection?.rangeCount || !core.elements.editor.contains(selection.anchorNode)) {
3430
+ const range = document.createRange();
3431
+ const selection = core.elements.iframeWindow.getSelection();
3432
+
3433
+ range.selectNodeContents(core.elements.editor);
3434
+ range.collapse(false);
3435
+ selection.removeAllRanges();
3436
+ selection.addRange(range);
3437
+ }
3438
+
3439
+ core.updateCaretPosition();
3440
+ }
3441
+
3442
+ return didChange;
3245
3443
  }
3246
3444
 
3247
3445
  const makeSublist = (event, core) => {
@@ -3251,6 +3449,7 @@ const makeSublist = (event, core) => {
3251
3449
  if (!selection.rangeCount) return;
3252
3450
 
3253
3451
  const range = selection.getRangeAt(0);
3452
+ const isAtStart = range.startOffset === 0;
3254
3453
 
3255
3454
  let container = range.commonAncestorContainer;
3256
3455
 
@@ -3274,7 +3473,7 @@ const makeSublist = (event, core) => {
3274
3473
 
3275
3474
  if (!event.shiftKey) {
3276
3475
  const hasContent = selectedLis.some(li => !isEmptyLi(li));
3277
- if (hasContent) return;
3476
+ if (hasContent && !isAtStart) return;
3278
3477
  }
3279
3478
 
3280
3479
  event.preventDefault();
@@ -3419,7 +3618,74 @@ const insertTOC = (core) => {
3419
3618
  });
3420
3619
 
3421
3620
  tocContainer.appendChild(ul);
3422
- core.insertNode(tocContainer);
3621
+
3622
+ const selection = core.elements.iframeWindow.getSelection();
3623
+
3624
+ if (!selection.rangeCount) return;
3625
+
3626
+ const range = selection.getRangeAt(0);
3627
+
3628
+ if (!range.collapsed) {
3629
+ // Replace selected content with TOC
3630
+ range.deleteContents();
3631
+ range.insertNode(tocContainer);
3632
+
3633
+ // Remove empty list items
3634
+ editor.querySelectorAll('li').forEach(li => {
3635
+ const text = li.textContent.replace(/\u00A0/g, '').trim();
3636
+
3637
+ if (
3638
+ !text &&
3639
+ !li.querySelector(
3640
+ 'img, video, table, ul, ol, iframe, blockquote'
3641
+ )
3642
+ ) {
3643
+ li.remove();
3644
+ }
3645
+ });
3646
+
3647
+ // Place cursor after TOC
3648
+ const p = document.createElement('p');
3649
+ p.innerHTML = '<br>';
3650
+ tocContainer.after(p);
3651
+
3652
+ const newRange = document.createRange();
3653
+ newRange.setStart(p, 0);
3654
+ newRange.collapse(true);
3655
+
3656
+ selection.removeAllRanges();
3657
+ selection.addRange(newRange);
3658
+ } else {
3659
+ // No selection -> insert after current block
3660
+ let node = range.startContainer;
3661
+
3662
+ if (node.nodeType === Node.TEXT_NODE) {
3663
+ node = node.parentElement;
3664
+ }
3665
+
3666
+ const block = node.closest(
3667
+ 'p, div, h1, h2, h3, h4, h5, h6, blockquote, pre, li'
3668
+ );
3669
+
3670
+ if (block) {
3671
+ block.insertAdjacentElement('afterend', tocContainer);
3672
+ } else {
3673
+ editor.appendChild(tocContainer);
3674
+ }
3675
+
3676
+ // Place cursor after TOC
3677
+ const p = document.createElement('p');
3678
+ p.innerHTML = '<br>';
3679
+ tocContainer.after(p);
3680
+
3681
+ const newRange = document.createRange();
3682
+ newRange.setStart(p, 0);
3683
+ newRange.collapse(true);
3684
+
3685
+ selection.removeAllRanges();
3686
+ selection.addRange(newRange);
3687
+ }
3688
+
3423
3689
  core.updateCaretPosition();
3424
3690
  };
3425
3691
 
@@ -4069,6 +4335,8 @@ export {
4069
4335
  updateHeight,
4070
4336
  getTableGrid,
4071
4337
  getCellPosition,
4338
+ applyFormatBlockInListItem,
4339
+ normalizeHeadingWrappedLists,
4072
4340
  makeUnorderedList,
4073
4341
  makeOrderedList,
4074
4342
  showTooltip,
@@ -4086,4 +4354,6 @@ export {
4086
4354
  highlightActiveOutline,
4087
4355
  findAndReplace,
4088
4356
  handleBackspaceInList,
4357
+ trackListSelectionDeletion,
4358
+ cleanupListSelectionDeletion,
4089
4359
  }