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