leksy-editor 2.3.2 → 2.4.1

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
@@ -177,10 +177,10 @@ Similarly, Pexels and Tenor can be integrated using `pexels` and `tenor` plugins
177
177
 
178
178
  | Function | Description |
179
179
  |----------|-------------|
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. |
180
+ | `setContents(tabs || html)` | Sets the editor's content. |
181
+ | `getContents()` | Returns the array of tabs containing HTML content of the editor. |
182
+ | `onChange(tabs)` | Triggered when content changes. |
183
+ | `onBlur(tabs)` | Triggered when the editor loses focus. |
184
184
  | `onAttachment(files)` | Fires when files are attached. |
185
185
  | `manipulateImage()` | Custom function for manipulating images. |
186
186
  | `handleFilePicker()` | Custom function for file upload, useful for Android and iOS file upload. |
package/constant.js CHANGED
@@ -1,5 +1,43 @@
1
1
  const WHILE_LISTED_TAG = "br|p|div|pre|blockquote|h|h1|h2|h3|h4|h5|h6|ol|ul|li|hr|figure|figcaption|img|iframe|audio|video|source|table|thead|tbody|tr|th|td|a|b|strong|var|i|em|u|ins|s|span|strike|del|sub|sup|code|svg|path|details|summary|font|article|section|main|header|footer|aside|mark|cite|abbr|dfn|nav|caption|picture|dl|dt|dd|time|label|kbd|samp|wbr";
2
2
  const WHILE_LISTED_ATTRIBUTES = "align|alt|bgcolor|border|cellpadding|cellspacing|color|colspan|contenteditable|dir|download|face|height|href|lang|name|rowspan|size|src|style|target|title|valign|width|background|media|sizes|srcset|spellcheck";
3
+ const BLOCKED_STYLES = [
4
+ "position",
5
+ "inset",
6
+ "top",
7
+ "right",
8
+ "bottom",
9
+ "left",
10
+ "z-index",
11
+ "float",
12
+ "clear",
13
+ "overflow",
14
+ "overflow-x",
15
+ "overflow-y",
16
+ "transform",
17
+ "transform-origin",
18
+ "perspective",
19
+ "transition",
20
+ "transition-property",
21
+ "transition-duration",
22
+ "transition-timing-function",
23
+ "transition-delay",
24
+ "animation",
25
+ "animation-name",
26
+ "animation-duration",
27
+ "animation-timing-function",
28
+ "animation-delay",
29
+ "animation-iteration-count",
30
+ "animation-direction",
31
+ "animation-fill-mode",
32
+ "animation-play-state",
33
+ "flex-basis",
34
+ "flex-grow",
35
+ "flex-shrink",
36
+ "order",
37
+ "place-self",
38
+ "grid-column",
39
+ "grid-row",
40
+ ].join("|")
3
41
 
4
42
  const REGEX = {
5
43
  TEXT_URL: /(https?:\/\/[^\s]+|www\.[^\s]+)$/i,
@@ -10,6 +48,7 @@ const REGEX = {
10
48
  MOBILE: /^\+?[0-9\s()-]+$/,
11
49
  WHILE_LISTED_TAG: new RegExp('^(' + WHILE_LISTED_TAG.replace(/\|/g, '\\b|\\b') + ')$', 'i'),
12
50
  WHILE_LISTED_ATTRIBUTES: new RegExp('^(' + WHILE_LISTED_ATTRIBUTES.replace(/\|/g, '\\b|\\b') + ')$', 'i'),
51
+ BLOCKED_STYLES: new RegExp('^(' + BLOCKED_STYLES.replace(/\|/g, '\\b|\\b') + ')$', 'i'),
13
52
  }
14
53
 
15
54
  const ERRORS = {
@@ -893,6 +932,7 @@ const UNORDERED_LIST_OPTIONS = {
893
932
 
894
933
  const RESIZE_MARGIN = 10;
895
934
 
935
+ const BLOCK_FORMAT_TAGS = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P'];
896
936
  const LIST_STYLES_BY_LEVEL = ['1', 'a', 'i', 'A'];
897
937
  const UNORDERED_LIST_STYLES_BY_LEVEL = ['disc', 'circle', 'square'];
898
938
 
@@ -919,8 +959,9 @@ export {
919
959
  CSS_VARIABLES,
920
960
  TAB_CATEGORIES,
921
961
  RESIZE_MARGIN,
962
+ BLOCK_FORMAT_TAGS,
922
963
  LIST_STYLES_BY_LEVEL,
923
964
  UNORDERED_LIST_STYLES_BY_LEVEL,
924
965
  UNORDERED_LIST_OPTIONS,
925
966
  ORDERED_LIST_OPTIONS,
926
- }
967
+ }
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
 
@@ -675,6 +675,7 @@ class LeksyEditor {
675
675
  contentEditableDiv.className = 'content-editable';
676
676
 
677
677
  contentEditableDiv.oninput = (e) => {
678
+ cleanupListSelectionDeletion(core);
678
679
  core.onChange(e.target.innerHTML)
679
680
  }
680
681
  contentEditableDiv.onbeforeinput = (e) => {
@@ -858,6 +859,7 @@ class LeksyEditor {
858
859
  }
859
860
 
860
861
  handleBackspaceInList(event, core);
862
+ trackListSelectionDeletion(event, core);
861
863
 
862
864
  if (!isLinkCreated) core.elements.lastCreatedLink = null
863
865
 
@@ -1015,4 +1017,4 @@ export default LeksyEditor
1015
1017
  export {
1016
1018
  convertHtmlToText,
1017
1019
  isHTMLEmpty,
1018
- }
1020
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leksy-editor",
3
- "version": "2.3.2",
3
+ "version": "2.4.1",
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,11 +1,26 @@
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
 
9
+ const removeBlockedStyles = (node) => {
10
+ if (!node?.style) return;
11
+ const styleProperties = Array.from(node.style);
12
+
13
+ styleProperties.forEach((propertyName) => {
14
+ if (REGEX.BLOCKED_STYLES.test(propertyName)) {
15
+ node.style.removeProperty(propertyName);
16
+ }
17
+ });
18
+
19
+ if (!node.getAttribute('style')?.trim()) {
20
+ node.removeAttribute('style');
21
+ }
22
+ }
23
+
9
24
  const traverseAndClean = (element, options, core) => {
10
25
  if (!element?.childNodes) return;
11
26
 
@@ -51,6 +66,7 @@ const traverseAndClean = (element, options, core) => {
51
66
  node.style.removeProperty('background-color');
52
67
  node.style.removeProperty('color');
53
68
  }
69
+ removeBlockedStyles(node);
54
70
  attributesToRemove.forEach(attr => node.removeAttribute(attr));
55
71
 
56
72
  // Process child nodes
@@ -2854,6 +2870,98 @@ const isInsideTableCell = (node) => {
2854
2870
  return element?.closest('td, th');
2855
2871
  };
2856
2872
 
2873
+ const formatListItemContent = (li, tag) => {
2874
+ const formattedBlock = document.createElement(tag);
2875
+ const childNodes = Array.from(li.childNodes);
2876
+
2877
+ childNodes.forEach(child => {
2878
+ if (child.nodeType === Node.ELEMENT_NODE && ['UL', 'OL'].includes(child.tagName)) return;
2879
+ formattedBlock.appendChild(child);
2880
+ });
2881
+
2882
+ if (!formattedBlock.childNodes.length) formattedBlock.innerHTML = '<br>';
2883
+
2884
+ const firstNestedList = Array.from(li.children).find(child => ['UL', 'OL'].includes(child.tagName));
2885
+ li.insertBefore(formattedBlock, firstNestedList || null);
2886
+
2887
+ return formattedBlock;
2888
+ }
2889
+
2890
+ const applyFormatBlockInListItem = (core, tag) => {
2891
+ const selection = core.elements.iframeWindow.getSelection();
2892
+ const node = selection?.rangeCount ? selection.getRangeAt(0).startContainer : null;
2893
+ const element = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
2894
+ const li = element?.closest('li');
2895
+
2896
+ if (!li || !core.elements.editor.contains(li)) return false;
2897
+
2898
+ const currentBlock = element.closest(BLOCK_FORMAT_TAGS.join(','));
2899
+ const isCurrentBlockInLi = currentBlock && currentBlock.closest('li') === li;
2900
+ let formattedBlock;
2901
+
2902
+ const placeCaretAtEnd = (core, element) => {
2903
+ const range = document.createRange();
2904
+ const selection = core.elements.iframeWindow.getSelection();
2905
+
2906
+ range.selectNodeContents(element);
2907
+ range.collapse(false);
2908
+ selection.removeAllRanges();
2909
+ selection.addRange(range);
2910
+ }
2911
+
2912
+ if (tag === 'p') {
2913
+ if (isCurrentBlockInLi) {
2914
+ const fragment = document.createDocumentFragment();
2915
+
2916
+ while (currentBlock.firstChild) fragment.appendChild(currentBlock.firstChild);
2917
+
2918
+ li.insertBefore(fragment, currentBlock);
2919
+ currentBlock.remove();
2920
+ }
2921
+ placeCaretAtEnd(core, li);
2922
+ return true;
2923
+ }
2924
+
2925
+ if (isCurrentBlockInLi) {
2926
+ formattedBlock = document.createElement(tag);
2927
+ formattedBlock.innerHTML = currentBlock.innerHTML || '<br>';
2928
+ currentBlock.replaceWith(formattedBlock);
2929
+ } else {
2930
+ formattedBlock = formatListItemContent(li, tag);
2931
+ }
2932
+
2933
+ placeCaretAtEnd(core, formattedBlock);
2934
+ return true;
2935
+ }
2936
+
2937
+ const normalizeHeadingWrappedLists = (core) => {
2938
+ core.elements.editor.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
2939
+ const lists = Array.from(heading.children).filter(child => ['UL', 'OL'].includes(child.tagName));
2940
+ if (!lists.length) return;
2941
+
2942
+ const headingTag = heading.tagName.toLowerCase();
2943
+ const fragment = document.createDocumentFragment();
2944
+
2945
+ lists.forEach(list => {
2946
+ list.querySelectorAll('li').forEach(li => {
2947
+ const hasBlock = Array.from(li.children).some(child => BLOCK_FORMAT_TAGS.includes(child.tagName));
2948
+ if (!hasBlock) formatListItemContent(li, headingTag);
2949
+ });
2950
+ fragment.appendChild(list);
2951
+ });
2952
+
2953
+ heading.parentNode.insertBefore(fragment, heading);
2954
+
2955
+ if (heading.textContent.replace(/\u200B/g, '').trim() || heading.querySelector('img, br')) {
2956
+ const paragraph = document.createElement('p');
2957
+ paragraph.innerHTML = heading.innerHTML;
2958
+ heading.replaceWith(paragraph);
2959
+ } else {
2960
+ heading.remove();
2961
+ }
2962
+ });
2963
+ }
2964
+
2857
2965
  const makeUnorderedList = (event, core) => {
2858
2966
  if (event.key !== ' ') return;
2859
2967
 
@@ -3185,7 +3293,7 @@ const indentListItem = (li, core) => {
3185
3293
  const selection = core.elements.iframeWindow.getSelection();
3186
3294
  const range = document.createRange();
3187
3295
  range.selectNodeContents(li);
3188
- range.collapse(false);
3296
+ range.collapse(true);
3189
3297
  selection.removeAllRanges();
3190
3298
  selection.addRange(range);
3191
3299
  core.elements.editor.focus();
@@ -3225,7 +3333,109 @@ const outdentListItem = (li, core) => {
3225
3333
  const isEmptyLi = (li) => {
3226
3334
  const text = li.textContent.replace(/\u200B/g, '').trim();
3227
3335
  const hasOnlySublist = li.children.length === 1 && li.querySelector('ul, ol');
3228
- return text === '' && !hasOnlySublist;
3336
+ const hasMeaningfulElement = li.querySelector('img, iframe, audio, video, table, hr, figure');
3337
+ return text === '' && !hasOnlySublist && !hasMeaningfulElement;
3338
+ }
3339
+
3340
+ const trackListSelectionDeletion = (event, core) => {
3341
+ if (event.defaultPrevented) {
3342
+ delete core.state.pendingListSelectionDeletion;
3343
+ return;
3344
+ }
3345
+
3346
+ if (!['Backspace', 'Delete'].includes(event.key)) {
3347
+ delete core.state.pendingListSelectionDeletion;
3348
+ return;
3349
+ }
3350
+
3351
+ const selection = core.elements.iframeWindow.getSelection();
3352
+ if (!selection?.rangeCount) return;
3353
+
3354
+ const range = selection.getRangeAt(0);
3355
+ if (range.collapsed) {
3356
+ delete core.state.pendingListSelectionDeletion;
3357
+ return;
3358
+ }
3359
+
3360
+ const getElementFromNode = (node) => {
3361
+ if (!node) return null;
3362
+ return node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
3363
+ }
3364
+
3365
+ const startElement = getElementFromNode(range.startContainer);
3366
+ const endElement = getElementFromNode(range.endContainer);
3367
+ const startLi = startElement?.closest('li');
3368
+ const endLi = endElement?.closest('li');
3369
+
3370
+ if (!startLi || !endLi || startLi === endLi) {
3371
+ delete core.state.pendingListSelectionDeletion;
3372
+ return;
3373
+ }
3374
+
3375
+ const listItems = [];
3376
+
3377
+ core.elements.editor.querySelectorAll('li').forEach(li => {
3378
+ if (range.intersectsNode(li)) listItems.push(li);
3379
+ });
3380
+
3381
+ if (listItems.length < 2) {
3382
+ delete core.state.pendingListSelectionDeletion;
3383
+ return;
3384
+ }
3385
+
3386
+ core.state.pendingListSelectionDeletion = {
3387
+ listItems,
3388
+ lists: Array.from(new Set(listItems.map(li => li.parentElement).filter(Boolean))),
3389
+ };
3390
+ }
3391
+
3392
+ const cleanupListSelectionDeletion = (core) => {
3393
+ const pendingDeletion = core.state.pendingListSelectionDeletion;
3394
+ if (!pendingDeletion) return false;
3395
+
3396
+ delete core.state.pendingListSelectionDeletion;
3397
+
3398
+ let didChange = false;
3399
+ const lists = new Set(pendingDeletion.lists);
3400
+
3401
+ pendingDeletion.listItems.forEach(li => {
3402
+ if (!li.isConnected || !core.elements.editor.contains(li)) return;
3403
+ if (!isEmptyLi(li)) return;
3404
+
3405
+ const parentList = li.parentElement;
3406
+ if (parentList) lists.add(parentList);
3407
+ li.remove();
3408
+ didChange = true;
3409
+ });
3410
+
3411
+ Array.from(lists).reverse().forEach(list => {
3412
+ if (!list?.isConnected || !core.elements.editor.contains(list)) return;
3413
+ if (list.querySelector('li')) return;
3414
+
3415
+ list.remove();
3416
+ didChange = true;
3417
+ });
3418
+
3419
+ if (didChange) {
3420
+ if (!core.elements.editor.textContent.replace(/\u200B/g, '').trim() && !core.elements.editor.querySelector('img, iframe, audio, video, table, hr, figure')) {
3421
+ core.elements.editor.innerHTML = '<div><br></div>';
3422
+ }
3423
+
3424
+ const selection = core.elements.iframeWindow.getSelection();
3425
+ if (!selection?.rangeCount || !core.elements.editor.contains(selection.anchorNode)) {
3426
+ const range = document.createRange();
3427
+ const selection = core.elements.iframeWindow.getSelection();
3428
+
3429
+ range.selectNodeContents(core.elements.editor);
3430
+ range.collapse(false);
3431
+ selection.removeAllRanges();
3432
+ selection.addRange(range);
3433
+ }
3434
+
3435
+ core.updateCaretPosition();
3436
+ }
3437
+
3438
+ return didChange;
3229
3439
  }
3230
3440
 
3231
3441
  const makeSublist = (event, core) => {
@@ -3235,6 +3445,7 @@ const makeSublist = (event, core) => {
3235
3445
  if (!selection.rangeCount) return;
3236
3446
 
3237
3447
  const range = selection.getRangeAt(0);
3448
+ const isAtStart = range.startOffset === 0;
3238
3449
 
3239
3450
  let container = range.commonAncestorContainer;
3240
3451
 
@@ -3258,7 +3469,7 @@ const makeSublist = (event, core) => {
3258
3469
 
3259
3470
  if (!event.shiftKey) {
3260
3471
  const hasContent = selectedLis.some(li => !isEmptyLi(li));
3261
- if (hasContent) return;
3472
+ if (hasContent && !isAtStart) return;
3262
3473
  }
3263
3474
 
3264
3475
  event.preventDefault();
@@ -3403,7 +3614,74 @@ const insertTOC = (core) => {
3403
3614
  });
3404
3615
 
3405
3616
  tocContainer.appendChild(ul);
3406
- core.insertNode(tocContainer);
3617
+
3618
+ const selection = core.elements.iframeWindow.getSelection();
3619
+
3620
+ if (!selection.rangeCount) return;
3621
+
3622
+ const range = selection.getRangeAt(0);
3623
+
3624
+ if (!range.collapsed) {
3625
+ // Replace selected content with TOC
3626
+ range.deleteContents();
3627
+ range.insertNode(tocContainer);
3628
+
3629
+ // Remove empty list items
3630
+ editor.querySelectorAll('li').forEach(li => {
3631
+ const text = li.textContent.replace(/\u00A0/g, '').trim();
3632
+
3633
+ if (
3634
+ !text &&
3635
+ !li.querySelector(
3636
+ 'img, video, table, ul, ol, iframe, blockquote'
3637
+ )
3638
+ ) {
3639
+ li.remove();
3640
+ }
3641
+ });
3642
+
3643
+ // Place cursor after TOC
3644
+ const p = document.createElement('p');
3645
+ p.innerHTML = '<br>';
3646
+ tocContainer.after(p);
3647
+
3648
+ const newRange = document.createRange();
3649
+ newRange.setStart(p, 0);
3650
+ newRange.collapse(true);
3651
+
3652
+ selection.removeAllRanges();
3653
+ selection.addRange(newRange);
3654
+ } else {
3655
+ // No selection -> insert after current block
3656
+ let node = range.startContainer;
3657
+
3658
+ if (node.nodeType === Node.TEXT_NODE) {
3659
+ node = node.parentElement;
3660
+ }
3661
+
3662
+ const block = node.closest(
3663
+ 'p, div, h1, h2, h3, h4, h5, h6, blockquote, pre, li'
3664
+ );
3665
+
3666
+ if (block) {
3667
+ block.insertAdjacentElement('afterend', tocContainer);
3668
+ } else {
3669
+ editor.appendChild(tocContainer);
3670
+ }
3671
+
3672
+ // Place cursor after TOC
3673
+ const p = document.createElement('p');
3674
+ p.innerHTML = '<br>';
3675
+ tocContainer.after(p);
3676
+
3677
+ const newRange = document.createRange();
3678
+ newRange.setStart(p, 0);
3679
+ newRange.collapse(true);
3680
+
3681
+ selection.removeAllRanges();
3682
+ selection.addRange(newRange);
3683
+ }
3684
+
3407
3685
  core.updateCaretPosition();
3408
3686
  };
3409
3687
 
@@ -4053,6 +4331,8 @@ export {
4053
4331
  updateHeight,
4054
4332
  getTableGrid,
4055
4333
  getCellPosition,
4334
+ applyFormatBlockInListItem,
4335
+ normalizeHeadingWrappedLists,
4056
4336
  makeUnorderedList,
4057
4337
  makeOrderedList,
4058
4338
  showTooltip,
@@ -4070,4 +4350,6 @@ export {
4070
4350
  highlightActiveOutline,
4071
4351
  findAndReplace,
4072
4352
  handleBackspaceInList,
4073
- }
4353
+ trackListSelectionDeletion,
4354
+ cleanupListSelectionDeletion,
4355
+ }