react-lite-rich-text-editor 1.1.4 → 1.1.6

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.esm.js CHANGED
@@ -388,7 +388,7 @@ function styleInject(css, ref) {
388
388
  }
389
389
  }
390
390
 
391
- var css_248z = ".rte-container{background-color:#fff;border:1px solid #e5e7eb;border-radius:12px;box-shadow:0 1px 3px rgba(0,0,0,.05);overflow:hidden;transition:all .2s cubic-bezier(.4,0,.2,1)}.rte-container:focus-within{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.1)}.rte-toolbar{align-items:center;background-color:#f9fafb;border-bottom:1px solid #f3f4f6;display:flex;flex-wrap:wrap;gap:4px;padding:8px}.rte-toolbar-button{align-items:center;background:transparent;border:none;border-radius:6px;color:#4b5563;cursor:pointer;display:flex;height:32px;justify-content:center;padding:0;transition:all .15s ease;width:32px}.rte-toolbar-button:hover{background-color:#f3f4f6;color:#111827}.rte-toolbar-button.active{background-color:#eff6ff;color:#2563eb}.rte-toolbar-button:disabled{cursor:not-allowed;opacity:.5}.rte-toolbar-button-danger:hover{background-color:#fef2f2!important;color:#dc2626!important}.rte-toolbar-select{background-color:#fff;border:1px solid #e5e7eb;border-radius:6px;color:#374151;cursor:pointer;font-size:14px;height:32px;outline:none;padding:0 8px;transition:border-color .15s ease}.rte-toolbar-select:hover{border-color:#d1d5db}.rte-toolbar-select:focus{border-color:#3b82f6}.rte-color-picker-label{align-items:center;border-radius:6px;cursor:pointer;display:flex;height:32px;justify-content:center;position:relative;transition:background-color .15s ease;width:32px}.rte-color-picker-label:hover{background-color:#f3f4f6}.rte-color-input{cursor:pointer;height:100%;inset:0;opacity:0;position:absolute;width:100%}.rte-content{color:#1f2937;font-family:inherit;font-size:16px;line-height:1.6;min-height:150px;outline:none;overflow-y:auto;padding:12px;word-break:break-word}.rte-content ul{list-style-type:disc;margin-left:1.5rem}.rte-content ol{list-style-type:decimal;margin-left:1.5rem}.rte-content img{border-radius:8px;display:block;height:auto;max-width:100%}.rte-content table{border-collapse:collapse;margin:16px 0;width:100%}.rte-content td,.rte-content th{border:1px solid #e5e7eb;min-width:40px;padding:12px;word-break:break-word}.video-container{border-radius:12px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1);height:0;margin:16px 0;overflow:hidden;padding-bottom:56.25%;position:relative}.video-container iframe{height:100%;left:0;position:absolute;top:0;width:100%}.rte-modal-overlay{align-items:center;animation:rte-fade-in .2s ease-out;backdrop-filter:blur(4px);background-color:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;position:fixed;z-index:9999}.rte-modal{animation:rte-zoom-in .2s ease-out;background-color:#fff;border:1px solid #f3f4f6;border-radius:16px;box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 10px 10px -5px rgba(0,0,0,.04);display:flex;flex-direction:column;gap:16px;max-width:400px;padding:0;width:100%}.rte-modal-title{color:#111827;flex:1;font-size:18px;font-weight:600;margin:0;text-align:center}.rte-modal-header{align-items:center;border-bottom:1px solid #f3f4f6;display:flex;justify-content:space-between;padding:20px 24px 16px}.rte-form-group{display:flex;flex-direction:column;gap:8px;padding:16px 24px}.rte-label{color:#374151;font-size:14px;font-weight:600}.rte-input{border:1px solid #d1d5db;border-radius:8px;outline:none;padding:8px 12px;transition:all .15s ease;width:93%}.rte-input:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.1)}.rte-modal-actions{border-top:1px solid #f3f4f6;display:flex;gap:12px;justify-content:flex-end;padding:16px 24px 20px}.rte-button{border:none;border-radius:8px;cursor:pointer;font-weight:500;padding:8px 16px;transition:all .15s ease}.rte-button-secondary{background-color:#f3f4f6;color:#4b5563}.rte-button-secondary:hover{background-color:#e5e7eb}.rte-button-primary{background-color:#2563eb;box-shadow:0 1px 2px rgba(0,0,0,.05);color:#fff}.rte-button-primary:hover{background-color:#1d4ed8}.rte-button-primary:disabled{cursor:not-allowed;opacity:.5}.rte-spinner-container{align-items:center;display:flex;justify-content:center;padding:16px}.rte-spinner{animation:rte-spin .8s linear infinite;border:3px solid #eff6ff;border-radius:50%;border-top-color:#3b82f6;height:32px;width:32px}@keyframes rte-spin{to{transform:rotate(1turn)}}@keyframes rte-fade-in{0%{opacity:0}to{opacity:1}}@keyframes rte-zoom-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.image-container{display:inline-block;line-height:0;margin:15px;max-width:100%;position:relative}.image-container.image-align-center{display:block;margin:24px auto;text-align:center}.image-container.image-align-left,.image-container.image-align-right{display:block}.image-container.image-align-left,.image-container.image-align-right{margin:15px 0}.image-container.image-align-left img{margin-left:0!important;margin-right:auto!important}.image-container.image-align-center img{margin-left:auto!important;margin-right:auto!important}.image-container.image-align-right img{margin-left:auto!important;margin-right:0!important}.image-container img{border-radius:12px;display:block;height:auto;margin:0;max-width:100%;width:auto}.image-container.image-small img{width:50%!important}.image-delete-button{align-items:center;background:#ef4444;border:3px solid #fff;border-radius:9999px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);color:#fff;cursor:pointer;display:flex;font-size:18px;font-weight:700;height:26px;justify-content:center;line-height:1;padding:0;position:absolute;right:0;top:0;transform:translate(50%,-50%);transition:all .2s cubic-bezier(.4,0,.2,1);width:26px;z-index:50}.image-delete-button:hover{background:#b91c1c;box-shadow:0 10px 15px -3px rgba(0,0,0,.1);transform:translate(50%,-50%) scale(1.1)}.rte-table-delete-btn,.rte-table-delete-hover{align-items:center;display:flex;justify-content:center}.rte-table-delete-btn{background:#fff;border:1px solid #ef4444;border-radius:6px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);color:#ef4444;cursor:pointer;height:28px;padding:0;transition:all .2s ease;width:28px}.rte-table-delete-btn:hover{background:#ef4444;color:#fff;transform:scale(1.1)}.rte-table-delete-btn:active{transform:scale(.95)}.rte-image-toolbar{background:#fff!important;border:1px solid #e5e7eb!important;border-radius:8px!important;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06)!important;display:flex;gap:4px!important;padding:4px!important;pointer-events:auto!important}.rte-image-toolbar button{align-items:center;background:transparent;border:none;border-radius:4px;color:#4b5563;cursor:pointer;display:flex;font-size:11px;font-weight:600;height:28px;justify-content:center;min-width:32px;padding:0 4px;transition:all .15s ease}.rte-image-toolbar button:hover{background-color:#f3f4f6;color:#111827}.rte-image-toolbar button.danger{color:#ef4444!important}.rte-image-toolbar button.danger:hover{background-color:#fef2f2!important}.image-container:after{clear:both;content:\"\";display:table}.rte-footer{background-color:#fcfcfd;border-top:1px solid #f3f4f6;display:flex;justify-content:flex-end;padding:6px 16px;user-select:none}.rte-footer-content{align-items:center;color:#9ca3af;display:flex;font-size:11px;gap:10px;letter-spacing:.025em}.rte-footer-separator{color:#e5e7eb;font-size:14px;line-height:1}.rte-footer-item b{color:#6b7280;font-weight:600}";
391
+ var css_248z = ".rte-container{background-color:#fff;border:1px solid #e5e7eb;border-radius:12px;box-shadow:0 1px 3px rgba(0,0,0,.05);overflow:hidden;transition:all .2s cubic-bezier(.4,0,.2,1)}.rte-container:focus-within{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.1)}.rte-toolbar{align-items:center;background-color:#f9fafb;border-bottom:1px solid #f3f4f6;display:flex;flex-wrap:wrap;gap:4px;padding:8px}.rte-toolbar-button{align-items:center;background:transparent;border:none;border-radius:6px;color:#4b5563;cursor:pointer;display:flex;height:32px;justify-content:center;padding:0;transition:all .15s ease;width:32px}.rte-toolbar-button:hover{background-color:#f3f4f6;color:#111827}.rte-toolbar-button.active{background-color:#eff6ff;color:#2563eb}.rte-toolbar-button:disabled{cursor:not-allowed;opacity:.5}.rte-toolbar-button-danger:hover{background-color:#fef2f2!important;color:#dc2626!important}.rte-toolbar-select{background-color:#fff;border:1px solid #e5e7eb;border-radius:6px;color:#374151;cursor:pointer;font-size:14px;height:32px;outline:none;padding:0 8px;transition:border-color .15s ease}.rte-toolbar-select:hover{border-color:#d1d5db}.rte-toolbar-select:focus{border-color:#3b82f6}.rte-color-picker-label{align-items:center;border-radius:6px;cursor:pointer;display:flex;height:32px;justify-content:center;position:relative;transition:background-color .15s ease;width:32px}.rte-color-picker-label:hover{background-color:#f3f4f6}.rte-color-input{cursor:pointer;height:100%;inset:0;opacity:0;position:absolute;width:100%}.rte-content{color:#1f2937;font-family:inherit;font-size:16px;line-height:1.6;min-height:150px;outline:none;overflow-y:auto;padding:12px;word-break:break-word}.rte-content ul{list-style-type:disc;margin-left:1.5rem}.rte-content ol{list-style-type:decimal;margin-left:1.5rem}.rte-content img{border-radius:8px;display:block;height:auto;max-width:100%}.rte-content table{border-collapse:collapse;margin:16px 0;width:100%}.rte-content td,.rte-content th{border:1px solid #e5e7eb;min-width:40px;padding:12px;word-break:break-word}.video-container{border-radius:12px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1);height:0;margin:16px 0;max-width:100%;overflow:hidden;padding-bottom:56.25%;position:relative}.video-container iframe{height:100%;left:0;position:absolute;top:0;width:100%}.rte-modal-overlay{align-items:center;animation:rte-fade-in .2s ease-out;backdrop-filter:blur(4px);background-color:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;position:fixed;z-index:9999}.rte-modal{animation:rte-zoom-in .2s ease-out;background-color:#fff;border:1px solid #f3f4f6;border-radius:16px;box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 10px 10px -5px rgba(0,0,0,.04);display:flex;flex-direction:column;gap:16px;max-width:400px;padding:0;width:100%}.rte-modal-title{color:#111827;flex:1;font-size:18px;font-weight:600;margin:0;text-align:center}.rte-modal-header{align-items:center;border-bottom:1px solid #f3f4f6;display:flex;justify-content:space-between;padding:20px 24px 16px}.rte-form-group{display:flex;flex-direction:column;gap:8px;padding:16px 24px}.rte-label{color:#374151;font-size:14px;font-weight:600}.rte-input{border:1px solid #d1d5db;border-radius:8px;outline:none;padding:8px 12px;transition:all .15s ease;width:93%}.rte-input:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.1)}.rte-modal-actions{border-top:1px solid #f3f4f6;display:flex;gap:12px;justify-content:flex-end;padding:16px 24px 20px}.rte-button{border:none;border-radius:8px;cursor:pointer;font-weight:500;padding:8px 16px;transition:all .15s ease}.rte-button-secondary{background-color:#f3f4f6;color:#4b5563}.rte-button-secondary:hover{background-color:#e5e7eb}.rte-button-primary{background-color:#2563eb;box-shadow:0 1px 2px rgba(0,0,0,.05);color:#fff}.rte-button-primary:hover{background-color:#1d4ed8}.rte-button-primary:disabled{cursor:not-allowed;opacity:.5}.rte-spinner-container{align-items:center;display:flex;justify-content:center;padding:16px}.rte-spinner{animation:rte-spin .8s linear infinite;border:3px solid #eff6ff;border-radius:50%;border-top-color:#3b82f6;height:32px;width:32px}@keyframes rte-spin{to{transform:rotate(1turn)}}@keyframes rte-fade-in{0%{opacity:0}to{opacity:1}}@keyframes rte-zoom-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.image-container{display:block;line-height:0;margin:16px 0;max-width:100%;width:fit-content}.image-container.image-align-center{margin-left:auto;margin-right:auto}.image-container.image-align-left{margin-left:0;margin-right:auto}.image-container.image-align-right{margin-left:auto;margin-right:0}.image-media-frame{display:block;line-height:0;max-width:100%;position:relative;width:fit-content}.image-media-frame img{border-radius:12px;display:block;height:auto;margin:0;max-width:100%;width:auto}.image-media-frame[style*=width] img{width:100%}.image-media-frame[data-explicit-height=true] img,.image-media-frame[style*=height] img{height:100%;object-fit:contain}.image-container.image-small .image-media-frame img{width:50%!important}.image-delete-button{align-items:center;background:#ef4444;border:3px solid #fff;border-radius:9999px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);color:#fff;cursor:pointer;display:none;font-size:18px;font-weight:700;height:26px;justify-content:center;line-height:1;padding:0;pointer-events:none;position:absolute;right:0;top:0;transform:translate(50%,-50%);transition:all .2s cubic-bezier(.4,0,.2,1);width:26px;z-index:50}.rte-content.rte-is-editable.rte-is-focused .image-delete-button,.rte-content.rte-is-editable.rte-is-focused .video-delete-button{display:flex;pointer-events:auto}.image-delete-button:hover{background:#b91c1c;box-shadow:0 10px 15px -3px rgba(0,0,0,.1);transform:translate(50%,-50%) scale(1.1)}.media-resize-handles{inset:0;pointer-events:none;position:absolute;z-index:55}.media-resize-handle{background:#dbeafe;border:2px solid #fff;border-radius:4px;box-shadow:0 1px 3px rgba(15,23,42,.12);pointer-events:auto;position:absolute;z-index:60}.media-resize-handle:hover{background:#bfdbfe}.media-resize-handle-left,.media-resize-handle-right{cursor:ew-resize;height:28px;top:50%;transform:translateY(-50%);width:10px}.media-resize-handle-right{right:-5px}.media-resize-handle-left{left:-5px}.media-resize-handle-bottom,.media-resize-handle-top{cursor:ns-resize;height:10px;left:50%;transform:translateX(-50%);width:28px}.media-resize-handle-top{top:-5px}.media-resize-handle-bottom{bottom:-5px}.video-container .media-resize-handle-right{right:4px}.video-container .media-resize-handle-left{left:4px}.video-container .media-resize-handle-top{top:4px}.video-container .media-resize-handle-bottom{bottom:4px}.video-container .video-delete-button{z-index:70}.rte-table-delete-btn,.rte-table-delete-hover{align-items:center;display:flex;justify-content:center}.rte-table-delete-btn{background:#fff;border:1px solid #ef4444;border-radius:6px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);color:#ef4444;cursor:pointer;height:28px;padding:0;transition:all .2s ease;width:28px}.rte-table-delete-btn:hover{background:#ef4444;color:#fff;transform:scale(1.1)}.rte-table-delete-btn:active{transform:scale(.95)}.rte-image-toolbar{background:#fff!important;border:1px solid #e5e7eb!important;border-radius:8px!important;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06)!important;display:flex;gap:4px!important;padding:4px!important;pointer-events:auto!important}.rte-image-toolbar button{align-items:center;background:transparent;border:none;border-radius:4px;color:#4b5563;cursor:pointer;display:flex;font-size:11px;font-weight:600;height:28px;justify-content:center;min-width:32px;padding:0 4px;transition:all .15s ease}.rte-image-toolbar button:hover{background-color:#f3f4f6;color:#111827}.rte-image-toolbar button.danger{color:#ef4444!important}.rte-image-toolbar button.danger:hover{background-color:#fef2f2!important}.image-container:after{clear:both;content:\"\";display:table}.rte-footer{background-color:#fcfcfd;border-top:1px solid #f3f4f6;display:flex;justify-content:flex-end;padding:6px 16px;user-select:none}.rte-footer-content{align-items:center;color:#9ca3af;display:flex;font-size:11px;gap:10px;letter-spacing:.025em}.rte-footer-separator{color:#e5e7eb;font-size:14px;line-height:1}.rte-footer-item b{color:#6b7280;font-weight:600}";
392
392
  styleInject(css_248z);
393
393
 
394
394
  // Helper functions for HTML escaping
@@ -416,9 +416,6 @@ function RichTextEditor({
416
416
  maxHeight,
417
417
  onImageUpload
418
418
  }) {
419
- if (isLoading) {
420
- return /*#__PURE__*/React.createElement(Spinner, null);
421
- }
422
419
  const editorRef = useRef(null);
423
420
  const fileInputRef = useRef(null);
424
421
  const scrollTopRef = useRef(0);
@@ -428,7 +425,9 @@ function RichTextEditor({
428
425
  const [linkText, setLinkText] = useState("");
429
426
  const selectionRangeRef = useRef(null);
430
427
  const [editable, setEditable] = useState(initialEditable);
428
+ const [editorFocused, setEditorFocused] = useState(false);
431
429
  const lastSynchronizedHtmlRef = useRef("");
430
+ const syncProcessedMediaRef = useRef(() => {});
432
431
  useEffect(() => {
433
432
  setEditable(initialEditable);
434
433
  }, [initialEditable]);
@@ -533,40 +532,25 @@ function RichTextEditor({
533
532
  window.removeEventListener('wheel', handleWheel);
534
533
  };
535
534
  }, [imageModalOpen]);
536
- useEffect(() => {
537
- const handleClick = e => {
538
- // Trigger selection update for toolbar reactivity
539
- setSelectionVersion(v => v + 1);
540
- const deleteBtn = e.target.closest('button[title="Remove image"]');
541
- if (deleteBtn && editable) {
542
- e.preventDefault();
543
- e.stopPropagation();
544
- const wrapper = deleteBtn.closest('.image-container');
545
- if (wrapper && wrapper.parentNode) {
546
- wrapper.parentNode.removeChild(wrapper);
547
- triggerChange && triggerChange();
548
- }
549
- }
550
- };
551
- const editor = editorRef.current;
552
- if (editor) {
553
- editor.addEventListener('click', handleClick);
554
- return () => {
555
- editor.removeEventListener('click', handleClick);
556
- };
557
- }
558
- // Removed dependency on editable to minimize listener churn
559
- }, []);
560
535
  useEffect(() => {
561
536
  if (editorRef.current && value && value !== lastSynchronizedHtmlRef.current) {
562
- requestAnimationFrame(() => processExistingImages(editorRef.current));
537
+ requestAnimationFrame(() => syncProcessedMediaRef.current(editorRef.current));
563
538
  }
564
539
  }, [value]);
565
540
 
566
541
  // Runs whenever editable changes (toggles delete icon visibility)
567
542
  useEffect(() => {
568
- processExistingImages(editorRef.current);
543
+ if (!editable) {
544
+ setEditorFocused(false);
545
+ }
546
+ syncProcessedMediaRef.current(editorRef.current);
569
547
  }, [editable]);
548
+ useEffect(() => {
549
+ if (!editorRef.current) return;
550
+ editorRef.current.querySelectorAll(".image-container, .video-container").forEach(container => {
551
+ updateMediaControlVisibility(container);
552
+ });
553
+ }, [editorFocused, editable]);
570
554
  useEffect(() => {
571
555
  // Only update if value is different from our last known synced state
572
556
  if (value && value !== lastSynchronizedHtmlRef.current) {
@@ -590,6 +574,7 @@ function RichTextEditor({
590
574
  if (editorRef.current && editorRef.current.innerHTML !== newContent) {
591
575
  editorRef.current.innerHTML = newContent;
592
576
  }
577
+ requestAnimationFrame(() => syncProcessedMediaRef.current(editorRef.current));
593
578
  updateMetrics();
594
579
  }
595
580
  } catch (e) {
@@ -604,6 +589,54 @@ function RichTextEditor({
604
589
  }
605
590
  }
606
591
  }, [value, initialEditable, updateMetrics]);
592
+ const LIST_BLOCK_MEDIA_SELECTOR = ".video-container, .image-container, table";
593
+ const isListItemEffectivelyEmpty = listItem => {
594
+ if (!listItem) return true;
595
+ const clone = listItem.cloneNode(true);
596
+ clone.querySelectorAll(LIST_BLOCK_MEDIA_SELECTOR).forEach(el => el.remove());
597
+ clone.querySelectorAll("br").forEach(el => el.remove());
598
+ return clone.textContent.replace(/[\u200B\u00A0\s]/g, "").length === 0;
599
+ };
600
+ const hoistBlockMediaOutOfListItems = container => {
601
+ if (!container) return false;
602
+ let changed = false;
603
+ container.querySelectorAll("ol, ul").forEach(list => {
604
+ const items = Array.from(list.children).filter(child => child.tagName === "LI");
605
+ items.forEach(listItem => {
606
+ const blockMedia = Array.from(listItem.querySelectorAll(LIST_BLOCK_MEDIA_SELECTOR));
607
+ if (blockMedia.length === 0) return;
608
+ const hadText = !isListItemEffectivelyEmpty(listItem);
609
+ blockMedia.forEach(media => {
610
+ listItem.removeChild(media);
611
+ if (list.parentNode) {
612
+ list.parentNode.insertBefore(media, list.nextSibling);
613
+ }
614
+ changed = true;
615
+ });
616
+ if (!hadText || isListItemEffectivelyEmpty(listItem)) {
617
+ listItem.remove();
618
+ changed = true;
619
+ }
620
+ });
621
+ if (list.children.length === 0 && list.parentNode) {
622
+ list.remove();
623
+ changed = true;
624
+ }
625
+ });
626
+ return changed;
627
+ };
628
+ const processExistingMedia = container => {
629
+ if (!container) return false;
630
+ processExistingImages(container);
631
+ processExistingVideos(container);
632
+ return hoistBlockMediaOutOfListItems(container);
633
+ };
634
+ const getCleanHtml = () => {
635
+ if (!editorRef.current) return "";
636
+ const clone = editorRef.current.cloneNode(true);
637
+ stripEditorChrome(clone);
638
+ return clone.innerHTML;
639
+ };
607
640
 
608
641
  // Trigger change manually
609
642
  const triggerChange = useCallback(() => {
@@ -612,6 +645,11 @@ function RichTextEditor({
612
645
  lastSynchronizedHtmlRef.current = next;
613
646
  onChange && onChange(next);
614
647
  }, [onChange]);
648
+ syncProcessedMediaRef.current = container => {
649
+ if (processExistingMedia(container)) {
650
+ triggerChange();
651
+ }
652
+ };
615
653
 
616
654
  // Helper to walk up DOM to find style tags or CSS style:
617
655
  const isParentStyle = (node, ...tagNames) => {
@@ -647,14 +685,220 @@ function RichTextEditor({
647
685
  return hex.length === 1 ? "0" + hex : hex;
648
686
  }).join("");
649
687
  }
688
+ const getColorAtCursor = () => {
689
+ const sel = window.getSelection();
690
+ if (!sel || !sel.rangeCount || !editorRef.current) return null;
691
+ let node = sel.anchorNode;
692
+ if (node.nodeType === 3) node = node.parentNode;
693
+ while (node && node !== editorRef.current) {
694
+ if (node.nodeType === 1) {
695
+ if (node.style && node.style.color) {
696
+ return rgbToHex(node.style.color);
697
+ }
698
+ if (node.tagName === "FONT" && node.getAttribute("color")) {
699
+ return node.getAttribute("color");
700
+ }
701
+ }
702
+ node = node.parentNode;
703
+ }
704
+ const computedColor = window.getComputedStyle(sel.anchorNode.nodeType === 3 ? sel.anchorNode.parentNode : sel.anchorNode).color;
705
+ return rgbToHex(computedColor);
706
+ };
707
+ const stripEditorChrome = root => {
708
+ root.querySelectorAll(".image-delete-button, .video-delete-button, .video-edit-overlay, .media-resize-handles, .media-resize-handle").forEach(element => element.remove());
709
+ return root;
710
+ };
711
+ const getMediaSizeLimits = () => {
712
+ const maxWidth = editorRef.current ? editorRef.current.getBoundingClientRect().width - 24 : 800;
713
+ return {
714
+ minWidth: 120,
715
+ minHeight: 80,
716
+ maxWidth,
717
+ maxHeight: 720
718
+ };
719
+ };
720
+ const ensureImageMediaFrame = imageContainer => {
721
+ if (!imageContainer) return null;
722
+ let frame = imageContainer.querySelector(":scope > .image-media-frame");
723
+ if (frame) return frame;
724
+ frame = document.createElement("div");
725
+ frame.className = "image-media-frame";
726
+ ["width", "height", "marginLeft", "marginTop", "maxWidth"].forEach(prop => {
727
+ if (imageContainer.style[prop]) {
728
+ frame.style[prop] = imageContainer.style[prop];
729
+ imageContainer.style[prop] = "";
730
+ }
731
+ });
732
+ if (imageContainer.dataset.explicitHeight) {
733
+ frame.dataset.explicitHeight = imageContainer.dataset.explicitHeight;
734
+ delete imageContainer.dataset.explicitHeight;
735
+ }
736
+ const children = Array.from(imageContainer.children);
737
+ imageContainer.appendChild(frame);
738
+ children.forEach(child => frame.appendChild(child));
739
+ return frame;
740
+ };
741
+ const getImageMediaTarget = imageContainer => ensureImageMediaFrame(imageContainer) || imageContainer;
742
+ const applyImageMediaSize = (frame, width, height, edge) => {
743
+ const img = frame.querySelector("img");
744
+ const isVertical = edge === "top" || edge === "bottom";
745
+ frame.style.width = `${Math.round(width)}px`;
746
+ frame.style.maxWidth = "100%";
747
+ if (isVertical) {
748
+ frame.style.height = `${Math.round(height)}px`;
749
+ frame.dataset.explicitHeight = "true";
750
+ if (img) {
751
+ img.style.width = "100%";
752
+ img.style.height = "100%";
753
+ img.style.objectFit = "contain";
754
+ }
755
+ return;
756
+ }
757
+ if (!frame.dataset.explicitHeight) {
758
+ frame.style.height = "";
759
+ }
760
+ if (img) {
761
+ img.style.width = "100%";
762
+ if (frame.dataset.explicitHeight) {
763
+ img.style.height = "100%";
764
+ img.style.objectFit = "contain";
765
+ } else {
766
+ img.style.height = "auto";
767
+ img.style.objectFit = "";
768
+ }
769
+ }
770
+ };
771
+ const applyVideoMediaSize = (container, width, height) => {
772
+ container.style.paddingBottom = "0";
773
+ container.style.width = `${Math.round(width)}px`;
774
+ container.style.maxWidth = "100%";
775
+ container.style.height = `${Math.round(height)}px`;
776
+ };
777
+ const attachMediaResizeHandle = container => {
778
+ if (!container || container.querySelector(".media-resize-handles")) return;
779
+ const isVideo = container.classList.contains("video-container");
780
+ const resizeTarget = isVideo ? container : getImageMediaTarget(container);
781
+ if (!resizeTarget) return;
782
+ const handlesWrapper = document.createElement("div");
783
+ handlesWrapper.className = "media-resize-handles";
784
+ handlesWrapper.setAttribute("contenteditable", "false");
785
+ const edges = [{
786
+ edge: "left",
787
+ title: "Drag to resize width"
788
+ }, {
789
+ edge: "right",
790
+ title: "Drag to resize width"
791
+ }, {
792
+ edge: "top",
793
+ title: "Drag to resize height"
794
+ }, {
795
+ edge: "bottom",
796
+ title: "Drag to resize height"
797
+ }];
798
+ edges.forEach(({
799
+ edge,
800
+ title
801
+ }) => {
802
+ const handle = document.createElement("div");
803
+ handle.className = `media-resize-handle media-resize-handle-${edge}`;
804
+ handle.title = title;
805
+ handle.setAttribute("contenteditable", "false");
806
+ handle.dataset.edge = edge;
807
+ handle.addEventListener("mousedown", event => {
808
+ if (!editable) return;
809
+ event.preventDefault();
810
+ event.stopPropagation();
811
+ const limits = getMediaSizeLimits();
812
+ const rect = resizeTarget.getBoundingClientRect();
813
+ const startX = event.clientX;
814
+ const startY = event.clientY;
815
+ const startWidth = rect.width;
816
+ const startHeight = rect.height;
817
+ const startMarginLeft = Number.parseFloat(resizeTarget.style.marginLeft) || 0;
818
+ const startMarginTop = Number.parseFloat(resizeTarget.style.marginTop) || 0;
819
+ if (isVideo) {
820
+ resizeTarget.style.paddingBottom = "0";
821
+ }
822
+ const onMouseMove = moveEvent => {
823
+ const deltaX = moveEvent.clientX - startX;
824
+ const deltaY = moveEvent.clientY - startY;
825
+ let nextWidth = startWidth;
826
+ let nextHeight = startHeight;
827
+ if (edge === "right") {
828
+ nextWidth = startWidth + deltaX;
829
+ } else if (edge === "left") {
830
+ nextWidth = startWidth - deltaX;
831
+ } else if (edge === "bottom") {
832
+ nextHeight = startHeight + deltaY;
833
+ } else if (edge === "top") {
834
+ nextHeight = startHeight - deltaY;
835
+ }
836
+ nextWidth = Math.max(limits.minWidth, Math.min(nextWidth, limits.maxWidth));
837
+ nextHeight = Math.max(limits.minHeight, Math.min(nextHeight, limits.maxHeight));
838
+ if (edge === "left") {
839
+ resizeTarget.style.marginLeft = `${Math.round(startMarginLeft + (startWidth - nextWidth))}px`;
840
+ }
841
+ if (edge === "top") {
842
+ resizeTarget.style.marginTop = `${Math.round(startMarginTop + (startHeight - nextHeight))}px`;
843
+ }
844
+ if (isVideo) {
845
+ applyVideoMediaSize(resizeTarget, nextWidth, nextHeight);
846
+ } else {
847
+ applyImageMediaSize(resizeTarget, nextWidth, nextHeight, edge);
848
+ }
849
+ };
850
+ const onMouseUp = () => {
851
+ document.removeEventListener("mousemove", onMouseMove);
852
+ document.removeEventListener("mouseup", onMouseUp);
853
+ triggerChange();
854
+ };
855
+ document.addEventListener("mousemove", onMouseMove);
856
+ document.addEventListener("mouseup", onMouseUp);
857
+ });
858
+ handlesWrapper.appendChild(handle);
859
+ });
860
+ resizeTarget.appendChild(handlesWrapper);
861
+ };
862
+ const handleEditorFocus = () => {
863
+ setEditorFocused(true);
864
+ };
865
+ const handleEditorBlur = () => {
866
+ requestAnimationFrame(() => {
867
+ var _editorRef$current;
868
+ if (!((_editorRef$current = editorRef.current) !== null && _editorRef$current !== void 0 && _editorRef$current.contains(document.activeElement))) {
869
+ setEditorFocused(false);
870
+ }
871
+ });
872
+ };
873
+ const updateMediaControlVisibility = container => {
874
+ const handles = container.querySelector(".media-resize-handles");
875
+ if (handles instanceof HTMLElement) {
876
+ handles.style.display = editable ? "block" : "none";
877
+ handles.style.pointerEvents = editable ? "auto" : "none";
878
+ }
879
+ };
880
+ const createMediaDeleteButton = (title, className, onRemove) => {
881
+ const deleteBtn = document.createElement("button");
882
+ deleteBtn.type = "button";
883
+ deleteBtn.innerHTML = "×";
884
+ deleteBtn.className = className;
885
+ deleteBtn.title = title;
886
+ deleteBtn.setAttribute("contenteditable", "false");
887
+ deleteBtn.onclick = event => {
888
+ event.preventDefault();
889
+ event.stopPropagation();
890
+ onRemove();
891
+ };
892
+ return deleteBtn;
893
+ };
650
894
 
651
895
  // Listen for selection changes globally to update styles and list type in one pass
652
896
  useEffect(() => {
653
897
  const handleGlobalSelectionSync = () => {
654
- var _editorRef$current;
898
+ var _editorRef$current2;
655
899
  // Only sync if the editor has focus
656
900
  const sel = window.getSelection();
657
- if (!sel || !sel.rangeCount || !((_editorRef$current = editorRef.current) !== null && _editorRef$current !== void 0 && _editorRef$current.contains(sel.anchorNode))) {
901
+ if (!sel || !sel.rangeCount || !((_editorRef$current2 = editorRef.current) !== null && _editorRef$current2 !== void 0 && _editorRef$current2.contains(sel.anchorNode))) {
658
902
  return;
659
903
  }
660
904
 
@@ -715,6 +959,7 @@ function RichTextEditor({
715
959
  focus();
716
960
  };
717
961
  const [fontColor, setFontColor] = useState("#000000");
962
+ const getActiveTextColor = () => getColorAtCursor() || fontColor;
718
963
  const handleColorChange = color => {
719
964
  setFontColor(color);
720
965
  exec("foreColor", color);
@@ -924,59 +1169,85 @@ function RichTextEditor({
924
1169
  }
925
1170
  setVideoModalOpen(false);
926
1171
  setVideoUrl("");
927
- triggerChange && triggerChange();
1172
+ requestAnimationFrame(() => {
1173
+ processExistingMedia(editorRef.current);
1174
+ triggerChange();
1175
+ });
928
1176
  } else {
929
1177
  console.warn("Invalid Video URL or Platform not supported");
930
1178
  }
931
1179
  };
1180
+ const processExistingVideos = container => {
1181
+ if (!container) return;
1182
+ container.querySelectorAll(".video-container").forEach(videoContainer => {
1183
+ if (!videoContainer.querySelector(".video-delete-button")) {
1184
+ const deleteBtn = createMediaDeleteButton("Remove video", "video-delete-button image-delete-button", () => {
1185
+ videoContainer.remove();
1186
+ triggerChange && triggerChange();
1187
+ });
1188
+ videoContainer.appendChild(deleteBtn);
1189
+ }
1190
+ attachMediaResizeHandle(videoContainer);
1191
+ updateMediaControlVisibility(videoContainer);
1192
+ });
1193
+ };
932
1194
  const processExistingImages = container => {
933
1195
  if (!container) return;
934
1196
  container.querySelectorAll("img").forEach(img => {
935
- // ONLY wrap if it's not already inside a wrapper
1197
+ var _img$closest;
1198
+ if (img.closest(".rte-modal")) return;
936
1199
  const existingWrapper = img.closest(".image-container");
937
1200
  if (existingWrapper) {
938
- // Just update existing wrapper state if needed
939
- existingWrapper.style.cursor = editable ? 'pointer' : 'default';
940
- const deleteBtn = existingWrapper.querySelector('.image-delete-button');
941
- if (deleteBtn) {
942
- deleteBtn.style.display = editable ? 'flex' : 'none';
1201
+ existingWrapper.style.cursor = editable ? "pointer" : "default";
1202
+ const frame = ensureImageMediaFrame(existingWrapper);
1203
+ if (frame && !frame.querySelector(".image-delete-button")) {
1204
+ frame.appendChild(createMediaDeleteButton("Remove image", "image-delete-button", () => {
1205
+ existingWrapper.remove();
1206
+ triggerChange && triggerChange();
1207
+ }));
943
1208
  }
1209
+ attachMediaResizeHandle(existingWrapper);
1210
+ updateMediaControlVisibility(existingWrapper);
944
1211
  return;
945
1212
  }
946
1213
  const wrapper = document.createElement("div");
947
- const align = img.getAttribute('data-align') || 'center';
1214
+ const align = img.getAttribute("data-align") || ((_img$closest = img.closest("[data-align]")) === null || _img$closest === void 0 ? void 0 : _img$closest.getAttribute("data-align")) || "left";
948
1215
  wrapper.className = `image-container image-align-${align}`;
949
- wrapper.style.cursor = editable ? 'pointer' : 'default';
950
- img.className = "rte-image";
951
- img.style.cssText = ""; // Reset inline styles
952
- img.setAttribute('data-align', align);
953
- img.dataset.hasDeleteButton = "true";
954
-
955
- // Add click listener to open modal
956
- img.addEventListener("click", () => openImageModal(img.src));
957
- const deleteBtn = document.createElement("button");
958
- deleteBtn.innerHTML = "×";
959
- deleteBtn.className = "image-delete-button";
960
- deleteBtn.style.display = editable ? 'flex' : 'none';
961
- deleteBtn.style.pointerEvents = editable ? 'auto' : 'none';
962
- deleteBtn.title = "Remove image";
963
- deleteBtn.onclick = e => {
964
- e.preventDefault();
965
- e.stopPropagation();
966
- const wrapper = e.currentTarget.closest(".image-container");
967
- if (wrapper && wrapper.parentNode) {
968
- wrapper.parentNode.removeChild(wrapper);
969
- triggerChange && triggerChange();
970
- }
971
- };
1216
+ wrapper.style.cursor = editable ? "pointer" : "default";
1217
+ const frame = document.createElement("div");
1218
+ frame.className = "image-media-frame";
1219
+ if (img.getAttribute("width") && !frame.style.width) {
1220
+ frame.style.width = `${img.getAttribute("width")}px`;
1221
+ } else if (img.style.width && img.style.width.endsWith("px")) {
1222
+ frame.style.width = img.style.width;
1223
+ }
1224
+ img.classList.add("rte-image");
1225
+ img.setAttribute("data-align", align);
1226
+ if (frame.style.width) {
1227
+ img.style.width = "100%";
1228
+ img.style.height = frame.dataset.explicitHeight ? "100%" : "auto";
1229
+ } else {
1230
+ img.style.width = "";
1231
+ img.style.height = "auto";
1232
+ }
1233
+ img.addEventListener("click", event => {
1234
+ if (event.target.closest(".image-delete-button, .media-resize-handle")) return;
1235
+ openImageModal(img.src);
1236
+ });
1237
+ const deleteBtn = createMediaDeleteButton("Remove image", "image-delete-button", () => {
1238
+ wrapper.remove();
1239
+ triggerChange && triggerChange();
1240
+ });
972
1241
  const {
973
1242
  parentNode,
974
1243
  nextSibling
975
1244
  } = img;
976
1245
  if (parentNode) {
977
1246
  parentNode.removeChild(img);
978
- wrapper.appendChild(img);
979
- wrapper.appendChild(deleteBtn);
1247
+ frame.appendChild(img);
1248
+ frame.appendChild(deleteBtn);
1249
+ wrapper.appendChild(frame);
1250
+ attachMediaResizeHandle(wrapper);
980
1251
  if (nextSibling) {
981
1252
  parentNode.insertBefore(wrapper, nextSibling);
982
1253
  } else {
@@ -985,11 +1256,6 @@ function RichTextEditor({
985
1256
  }
986
1257
  });
987
1258
  };
988
- useEffect(() => {
989
- if (editorRef.current && value) {
990
- requestAnimationFrame(() => processExistingImages(editorRef.current));
991
- }
992
- }, [value]);
993
1259
 
994
1260
  /*
995
1261
  Advanced Tip: Use the 'onImageUpload' prop to handle file uploads to a server
@@ -1000,24 +1266,23 @@ function RichTextEditor({
1000
1266
  if (editable) {
1001
1267
  // Create container for the image
1002
1268
  const container = document.createElement('div');
1003
- container.className = 'image-container';
1269
+ container.className = 'image-container image-align-left';
1270
+ const frame = document.createElement('div');
1271
+ frame.className = 'image-media-frame';
1004
1272
 
1005
1273
  // Create image element
1006
1274
  const img = document.createElement('img');
1007
1275
  img.src = dataUrl;
1008
1276
  img.alt = fileName || "image";
1009
1277
  img.addEventListener("click", () => openImageModal(dataUrl));
1010
-
1011
- // Add elements to container
1012
- container.appendChild(img);
1278
+ frame.appendChild(img);
1279
+ container.appendChild(frame);
1013
1280
 
1014
1281
  // Insert at cursor position
1015
1282
  insertNodeAtCursor(container);
1016
- triggerChange();
1017
-
1018
- // Immediately process newly inserted image so delete button appears
1019
1283
  requestAnimationFrame(() => {
1020
- processExistingImages(editorRef.current);
1284
+ processExistingMedia(editorRef.current);
1285
+ triggerChange();
1021
1286
  });
1022
1287
  }
1023
1288
  } catch (err) {
@@ -1146,10 +1411,6 @@ function RichTextEditor({
1146
1411
  }
1147
1412
  }
1148
1413
  };
1149
- const getCleanHtml = () => {
1150
- if (!editorRef.current) return "";
1151
- return editorRef.current.innerHTML;
1152
- };
1153
1414
 
1154
1415
  // Helper function to unescape HTML entities
1155
1416
  const unescapeHtml = html => {
@@ -1158,6 +1419,35 @@ function RichTextEditor({
1158
1419
  txt.innerHTML = html;
1159
1420
  return txt.value;
1160
1421
  };
1422
+ const isCursorAtStartOfListItem = (range, listItem) => {
1423
+ const prefixRange = document.createRange();
1424
+ prefixRange.setStart(listItem, 0);
1425
+ prefixRange.setEnd(range.startContainer, range.startOffset);
1426
+ return prefixRange.toString().replace(/[\u200B\u00A0\s]/g, "").length === 0;
1427
+ };
1428
+ const isCursorAtEndOfListItem = (range, listItem) => {
1429
+ const suffixRange = document.createRange();
1430
+ suffixRange.setStart(range.startContainer, range.startOffset);
1431
+ suffixRange.setEnd(listItem, listItem.childNodes.length);
1432
+ return suffixRange.toString().replace(/\u200B/g, "").length === 0;
1433
+ };
1434
+ const prepareListItemForTyping = (listItem, selection) => {
1435
+ const activeColor = getActiveTextColor();
1436
+ const newRange = document.createRange();
1437
+ if (activeColor && activeColor.toLowerCase() !== "#000000") {
1438
+ const span = document.createElement("span");
1439
+ span.style.color = activeColor;
1440
+ span.appendChild(document.createTextNode("\u200B"));
1441
+ listItem.appendChild(span);
1442
+ newRange.setStart(span.firstChild, 1);
1443
+ } else {
1444
+ listItem.appendChild(document.createTextNode("\u200B"));
1445
+ newRange.setStart(listItem.firstChild, 1);
1446
+ }
1447
+ newRange.collapse(true);
1448
+ selection.removeAllRanges();
1449
+ selection.addRange(newRange);
1450
+ };
1161
1451
  const handleKeyDown = useCallback(e => {
1162
1452
  // Handle Enter key
1163
1453
  if (e.key === 'Enter') {
@@ -1172,15 +1462,14 @@ function RichTextEditor({
1172
1462
  const listItem = parent.closest('li');
1173
1463
  if (listItem) {
1174
1464
  const list = listItem.parentNode;
1175
- list.tagName === 'OL';
1176
1465
 
1177
1466
  // Create a new list item
1178
1467
  const newItem = document.createElement('li');
1179
1468
 
1180
1469
  // If we're at the end of a list item, add a new one
1181
- if (range.collapsed && range.endOffset === node.length) {
1470
+ if (range.collapsed && isCursorAtEndOfListItem(range, listItem)) {
1182
1471
  // If it's empty, create a regular paragraph instead
1183
- if (listItem.textContent.trim() === '') {
1472
+ if (isListItemEffectivelyEmpty(listItem)) {
1184
1473
  document.execCommand('insertHTML', false, '<div><br></div>');
1185
1474
  // Move the cursor to the new line
1186
1475
  const newRange = document.createRange();
@@ -1189,6 +1478,7 @@ function RichTextEditor({
1189
1478
  newRange.collapse(true);
1190
1479
  selection.removeAllRanges();
1191
1480
  selection.addRange(newRange);
1481
+ triggerChange();
1192
1482
  return;
1193
1483
  }
1194
1484
 
@@ -1198,35 +1488,29 @@ function RichTextEditor({
1198
1488
  } else {
1199
1489
  list.appendChild(newItem);
1200
1490
  }
1201
-
1202
- // Move cursor to the new list item
1203
- const newRange = document.createRange();
1204
- newRange.setStart(newItem, 0);
1205
- newRange.collapse(true);
1206
- selection.removeAllRanges();
1207
- selection.addRange(newRange);
1491
+ prepareListItemForTyping(newItem, selection);
1208
1492
  } else {
1209
- // If we're in the middle of text, split the list item
1210
- const textBefore = node.textContent.substring(0, range.startOffset);
1211
- const textAfter = node.textContent.substring(range.startOffset);
1212
-
1213
- // Update current item
1214
- node.textContent = textBefore;
1215
-
1216
- // Insert new item after current one
1217
- newItem.textContent = textAfter;
1493
+ // If we're in the middle of text, split the list item while preserving formatting
1494
+ const afterRange = document.createRange();
1495
+ afterRange.setStart(range.startContainer, range.startOffset);
1496
+ afterRange.setEnd(listItem, listItem.childNodes.length);
1497
+ const movedFragment = afterRange.extractContents();
1498
+ newItem.appendChild(movedFragment);
1218
1499
  if (listItem.nextSibling) {
1219
1500
  list.insertBefore(newItem, listItem.nextSibling);
1220
1501
  } else {
1221
1502
  list.appendChild(newItem);
1222
1503
  }
1223
-
1224
- // Move cursor to the new list item
1225
- const newRange = document.createRange();
1226
- newRange.setStart(newItem.firstChild || newItem, 0);
1227
- newRange.collapse(true);
1228
- selection.removeAllRanges();
1229
- selection.addRange(newRange);
1504
+ if (isListItemEffectivelyEmpty(newItem)) {
1505
+ newItem.textContent = "";
1506
+ prepareListItemForTyping(newItem, selection);
1507
+ } else {
1508
+ const newRange = document.createRange();
1509
+ newRange.setStart(newItem, 0);
1510
+ newRange.collapse(true);
1511
+ selection.removeAllRanges();
1512
+ selection.addRange(newRange);
1513
+ }
1230
1514
  }
1231
1515
  } else {
1232
1516
  // Regular text, insert a new paragraph
@@ -1235,6 +1519,54 @@ function RichTextEditor({
1235
1519
  triggerChange();
1236
1520
  return;
1237
1521
  }
1522
+ if (e.key === "Backspace") {
1523
+ var _node$closest, _node, _editorRef$current3;
1524
+ const selection = window.getSelection();
1525
+ if (!(selection !== null && selection !== void 0 && selection.rangeCount)) return;
1526
+ const range = selection.getRangeAt(0);
1527
+ if (!range.collapsed) return;
1528
+ let node = range.startContainer;
1529
+ if (node.nodeType === 3) {
1530
+ node = node.parentNode;
1531
+ }
1532
+ const listItem = (_node$closest = (_node = node).closest) === null || _node$closest === void 0 ? void 0 : _node$closest.call(_node, "li");
1533
+ if (!listItem || !((_editorRef$current3 = editorRef.current) !== null && _editorRef$current3 !== void 0 && _editorRef$current3.contains(listItem))) return;
1534
+ const list = listItem.parentNode;
1535
+ if (isListItemEffectivelyEmpty(listItem)) {
1536
+ e.preventDefault();
1537
+ const prevLi = listItem.previousElementSibling;
1538
+ const blockMedia = Array.from(listItem.querySelectorAll(LIST_BLOCK_MEDIA_SELECTOR));
1539
+ blockMedia.forEach(media => {
1540
+ var _list$parentNode;
1541
+ (_list$parentNode = list.parentNode) === null || _list$parentNode === void 0 || _list$parentNode.insertBefore(media, list.nextSibling);
1542
+ });
1543
+ listItem.remove();
1544
+ if (list.children.length === 0) {
1545
+ list.remove();
1546
+ }
1547
+ if ((prevLi === null || prevLi === void 0 ? void 0 : prevLi.tagName) === "LI") {
1548
+ const newRange = document.createRange();
1549
+ newRange.selectNodeContents(prevLi);
1550
+ newRange.collapse(false);
1551
+ selection.removeAllRanges();
1552
+ selection.addRange(newRange);
1553
+ }
1554
+ triggerChange();
1555
+ return;
1556
+ }
1557
+ if (isCursorAtStartOfListItem(range, listItem)) {
1558
+ const prevLi = listItem.previousElementSibling;
1559
+ if ((prevLi === null || prevLi === void 0 ? void 0 : prevLi.tagName) === "LI" && isListItemEffectivelyEmpty(prevLi)) {
1560
+ e.preventDefault();
1561
+ prevLi.remove();
1562
+ if (list.children.length === 0) {
1563
+ list.remove();
1564
+ }
1565
+ triggerChange();
1566
+ }
1567
+ }
1568
+ return;
1569
+ }
1238
1570
 
1239
1571
  // Handle Ctrl/Cmd + B/I/U for bold/italic/underline
1240
1572
  if ((e.ctrlKey || e.metaKey) && e.key === "b") {
@@ -1247,7 +1579,7 @@ function RichTextEditor({
1247
1579
  e.preventDefault();
1248
1580
  exec("underline");
1249
1581
  }
1250
- }, [exec, triggerChange]);
1582
+ }, [exec, triggerChange, fontColor]);
1251
1583
  const confirmLink = () => {
1252
1584
  // Add protocol if missing
1253
1585
  let url = linkUrl.trim();
@@ -1420,7 +1752,7 @@ function RichTextEditor({
1420
1752
  };
1421
1753
  const handleInput = useCallback(() => {
1422
1754
  if (editorRef.current) {
1423
- const next = editorRef.current.innerHTML;
1755
+ const next = getCleanHtml();
1424
1756
  setHtml(next);
1425
1757
  lastSynchronizedHtmlRef.current = next;
1426
1758
  onChange && onChange(next);
@@ -1457,6 +1789,18 @@ function RichTextEditor({
1457
1789
  }, [disabled]);
1458
1790
  const handleEditorClick = useCallback(e => {
1459
1791
  setSelectionVersion(v => v + 1);
1792
+ const deleteBtn = e.target.closest('button[title="Remove image"], button[title="Remove video"]');
1793
+ if (deleteBtn && editable && editorFocused) {
1794
+ e.preventDefault();
1795
+ e.stopPropagation();
1796
+ const wrapper = deleteBtn.closest('.image-container, .video-container');
1797
+ if (wrapper) {
1798
+ wrapper.remove();
1799
+ triggerChange();
1800
+ }
1801
+ return;
1802
+ }
1803
+
1460
1804
  // Check if the click is on a link
1461
1805
  const clickedLink = e.target.closest('a');
1462
1806
  if (clickedLink) {
@@ -1489,7 +1833,7 @@ function RichTextEditor({
1489
1833
  }
1490
1834
  }, 0);
1491
1835
  }
1492
- }, [editable, disabled]);
1836
+ }, [editable, disabled, editorFocused, triggerChange]);
1493
1837
  const renderImageToolbar = () => {
1494
1838
  if (!selectedImage || !editorRef.current || !editable) return null;
1495
1839
  const editorRect = editorRef.current.getBoundingClientRect();
@@ -1559,6 +1903,9 @@ function RichTextEditor({
1559
1903
  title: "Remove Image"
1560
1904
  }, "\xD7"));
1561
1905
  };
1906
+ if (isLoading) {
1907
+ return /*#__PURE__*/React.createElement(Spinner, null);
1908
+ }
1562
1909
  return /*#__PURE__*/React.createElement("div", {
1563
1910
  className: "rte-main-wrapper",
1564
1911
  style: {
@@ -1984,12 +2331,14 @@ function RichTextEditor({
1984
2331
  onDragOver: e => e.preventDefault(),
1985
2332
  onKeyDown: handleKeyDown,
1986
2333
  onClick: handleEditorClick,
2334
+ onFocus: handleEditorFocus,
2335
+ onBlur: handleEditorBlur,
1987
2336
  style: {
1988
2337
  minHeight: minHeight || '150px',
1989
2338
  maxHeight: maxHeight || '500px',
1990
2339
  paddingLeft: paddingLeft || '12px'
1991
2340
  },
1992
- className: "rte-content"
2341
+ className: `rte-content${editable ? " rte-is-editable" : ""}${editorFocused ? " rte-is-focused" : ""}`
1993
2342
  }), renderImageToolbar(), /*#__PURE__*/React.createElement("div", {
1994
2343
  className: "rte-footer"
1995
2344
  }, /*#__PURE__*/React.createElement("div", {