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

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,6 +427,7 @@ 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("");
434
432
  React.useEffect(() => {
435
433
  setEditable(initialEditable);
@@ -535,40 +533,25 @@ function RichTextEditor({
535
533
  window.removeEventListener('wheel', handleWheel);
536
534
  };
537
535
  }, [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
536
  React.useEffect(() => {
563
537
  if (editorRef.current && value && value !== lastSynchronizedHtmlRef.current) {
564
- requestAnimationFrame(() => processExistingImages(editorRef.current));
538
+ requestAnimationFrame(() => processExistingMedia(editorRef.current));
565
539
  }
566
540
  }, [value]);
567
541
 
568
542
  // Runs whenever editable changes (toggles delete icon visibility)
569
543
  React.useEffect(() => {
570
- processExistingImages(editorRef.current);
544
+ if (!editable) {
545
+ setEditorFocused(false);
546
+ }
547
+ processExistingMedia(editorRef.current);
571
548
  }, [editable]);
549
+ React.useEffect(() => {
550
+ if (!editorRef.current) return;
551
+ editorRef.current.querySelectorAll(".image-container, .video-container").forEach(container => {
552
+ updateMediaControlVisibility(container);
553
+ });
554
+ }, [editorFocused, editable]);
572
555
  React.useEffect(() => {
573
556
  // Only update if value is different from our last known synced state
574
557
  if (value && value !== lastSynchronizedHtmlRef.current) {
@@ -592,6 +575,7 @@ function RichTextEditor({
592
575
  if (editorRef.current && editorRef.current.innerHTML !== newContent) {
593
576
  editorRef.current.innerHTML = newContent;
594
577
  }
578
+ requestAnimationFrame(() => processExistingMedia(editorRef.current));
595
579
  updateMetrics();
596
580
  }
597
581
  } catch (e) {
@@ -606,6 +590,17 @@ function RichTextEditor({
606
590
  }
607
591
  }
608
592
  }, [value, initialEditable, updateMetrics]);
593
+ const processExistingMedia = container => {
594
+ if (!container) return;
595
+ processExistingImages(container);
596
+ processExistingVideos(container);
597
+ };
598
+ const getCleanHtml = () => {
599
+ if (!editorRef.current) return "";
600
+ const clone = editorRef.current.cloneNode(true);
601
+ stripEditorChrome(clone);
602
+ return clone.innerHTML;
603
+ };
609
604
 
610
605
  // Trigger change manually
611
606
  const triggerChange = React.useCallback(() => {
@@ -649,14 +644,220 @@ function RichTextEditor({
649
644
  return hex.length === 1 ? "0" + hex : hex;
650
645
  }).join("");
651
646
  }
647
+ const getColorAtCursor = () => {
648
+ const sel = window.getSelection();
649
+ if (!sel || !sel.rangeCount || !editorRef.current) return null;
650
+ let node = sel.anchorNode;
651
+ if (node.nodeType === 3) node = node.parentNode;
652
+ while (node && node !== editorRef.current) {
653
+ if (node.nodeType === 1) {
654
+ if (node.style && node.style.color) {
655
+ return rgbToHex(node.style.color);
656
+ }
657
+ if (node.tagName === "FONT" && node.getAttribute("color")) {
658
+ return node.getAttribute("color");
659
+ }
660
+ }
661
+ node = node.parentNode;
662
+ }
663
+ const computedColor = window.getComputedStyle(sel.anchorNode.nodeType === 3 ? sel.anchorNode.parentNode : sel.anchorNode).color;
664
+ return rgbToHex(computedColor);
665
+ };
666
+ const stripEditorChrome = root => {
667
+ root.querySelectorAll(".image-delete-button, .video-delete-button, .video-edit-overlay, .media-resize-handles, .media-resize-handle").forEach(element => element.remove());
668
+ return root;
669
+ };
670
+ const getMediaSizeLimits = () => {
671
+ const maxWidth = editorRef.current ? editorRef.current.getBoundingClientRect().width - 24 : 800;
672
+ return {
673
+ minWidth: 120,
674
+ minHeight: 80,
675
+ maxWidth,
676
+ maxHeight: 720
677
+ };
678
+ };
679
+ const ensureImageMediaFrame = imageContainer => {
680
+ if (!imageContainer) return null;
681
+ let frame = imageContainer.querySelector(":scope > .image-media-frame");
682
+ if (frame) return frame;
683
+ frame = document.createElement("div");
684
+ frame.className = "image-media-frame";
685
+ ["width", "height", "marginLeft", "marginTop", "maxWidth"].forEach(prop => {
686
+ if (imageContainer.style[prop]) {
687
+ frame.style[prop] = imageContainer.style[prop];
688
+ imageContainer.style[prop] = "";
689
+ }
690
+ });
691
+ if (imageContainer.dataset.explicitHeight) {
692
+ frame.dataset.explicitHeight = imageContainer.dataset.explicitHeight;
693
+ delete imageContainer.dataset.explicitHeight;
694
+ }
695
+ const children = Array.from(imageContainer.children);
696
+ imageContainer.appendChild(frame);
697
+ children.forEach(child => frame.appendChild(child));
698
+ return frame;
699
+ };
700
+ const getImageMediaTarget = imageContainer => ensureImageMediaFrame(imageContainer) || imageContainer;
701
+ const applyImageMediaSize = (frame, width, height, edge) => {
702
+ const img = frame.querySelector("img");
703
+ const isVertical = edge === "top" || edge === "bottom";
704
+ frame.style.width = `${Math.round(width)}px`;
705
+ frame.style.maxWidth = "100%";
706
+ if (isVertical) {
707
+ frame.style.height = `${Math.round(height)}px`;
708
+ frame.dataset.explicitHeight = "true";
709
+ if (img) {
710
+ img.style.width = "100%";
711
+ img.style.height = "100%";
712
+ img.style.objectFit = "contain";
713
+ }
714
+ return;
715
+ }
716
+ if (!frame.dataset.explicitHeight) {
717
+ frame.style.height = "";
718
+ }
719
+ if (img) {
720
+ img.style.width = "100%";
721
+ if (frame.dataset.explicitHeight) {
722
+ img.style.height = "100%";
723
+ img.style.objectFit = "contain";
724
+ } else {
725
+ img.style.height = "auto";
726
+ img.style.objectFit = "";
727
+ }
728
+ }
729
+ };
730
+ const applyVideoMediaSize = (container, width, height) => {
731
+ container.style.paddingBottom = "0";
732
+ container.style.width = `${Math.round(width)}px`;
733
+ container.style.maxWidth = "100%";
734
+ container.style.height = `${Math.round(height)}px`;
735
+ };
736
+ const attachMediaResizeHandle = container => {
737
+ if (!container || container.querySelector(".media-resize-handles")) return;
738
+ const isVideo = container.classList.contains("video-container");
739
+ const resizeTarget = isVideo ? container : getImageMediaTarget(container);
740
+ if (!resizeTarget) return;
741
+ const handlesWrapper = document.createElement("div");
742
+ handlesWrapper.className = "media-resize-handles";
743
+ handlesWrapper.setAttribute("contenteditable", "false");
744
+ const edges = [{
745
+ edge: "left",
746
+ title: "Drag to resize width"
747
+ }, {
748
+ edge: "right",
749
+ title: "Drag to resize width"
750
+ }, {
751
+ edge: "top",
752
+ title: "Drag to resize height"
753
+ }, {
754
+ edge: "bottom",
755
+ title: "Drag to resize height"
756
+ }];
757
+ edges.forEach(({
758
+ edge,
759
+ title
760
+ }) => {
761
+ const handle = document.createElement("div");
762
+ handle.className = `media-resize-handle media-resize-handle-${edge}`;
763
+ handle.title = title;
764
+ handle.setAttribute("contenteditable", "false");
765
+ handle.dataset.edge = edge;
766
+ handle.addEventListener("mousedown", event => {
767
+ if (!editable) return;
768
+ event.preventDefault();
769
+ event.stopPropagation();
770
+ const limits = getMediaSizeLimits();
771
+ const rect = resizeTarget.getBoundingClientRect();
772
+ const startX = event.clientX;
773
+ const startY = event.clientY;
774
+ const startWidth = rect.width;
775
+ const startHeight = rect.height;
776
+ const startMarginLeft = Number.parseFloat(resizeTarget.style.marginLeft) || 0;
777
+ const startMarginTop = Number.parseFloat(resizeTarget.style.marginTop) || 0;
778
+ if (isVideo) {
779
+ resizeTarget.style.paddingBottom = "0";
780
+ }
781
+ const onMouseMove = moveEvent => {
782
+ const deltaX = moveEvent.clientX - startX;
783
+ const deltaY = moveEvent.clientY - startY;
784
+ let nextWidth = startWidth;
785
+ let nextHeight = startHeight;
786
+ if (edge === "right") {
787
+ nextWidth = startWidth + deltaX;
788
+ } else if (edge === "left") {
789
+ nextWidth = startWidth - deltaX;
790
+ } else if (edge === "bottom") {
791
+ nextHeight = startHeight + deltaY;
792
+ } else if (edge === "top") {
793
+ nextHeight = startHeight - deltaY;
794
+ }
795
+ nextWidth = Math.max(limits.minWidth, Math.min(nextWidth, limits.maxWidth));
796
+ nextHeight = Math.max(limits.minHeight, Math.min(nextHeight, limits.maxHeight));
797
+ if (edge === "left") {
798
+ resizeTarget.style.marginLeft = `${Math.round(startMarginLeft + (startWidth - nextWidth))}px`;
799
+ }
800
+ if (edge === "top") {
801
+ resizeTarget.style.marginTop = `${Math.round(startMarginTop + (startHeight - nextHeight))}px`;
802
+ }
803
+ if (isVideo) {
804
+ applyVideoMediaSize(resizeTarget, nextWidth, nextHeight);
805
+ } else {
806
+ applyImageMediaSize(resizeTarget, nextWidth, nextHeight, edge);
807
+ }
808
+ };
809
+ const onMouseUp = () => {
810
+ document.removeEventListener("mousemove", onMouseMove);
811
+ document.removeEventListener("mouseup", onMouseUp);
812
+ triggerChange();
813
+ };
814
+ document.addEventListener("mousemove", onMouseMove);
815
+ document.addEventListener("mouseup", onMouseUp);
816
+ });
817
+ handlesWrapper.appendChild(handle);
818
+ });
819
+ resizeTarget.appendChild(handlesWrapper);
820
+ };
821
+ const handleEditorFocus = () => {
822
+ setEditorFocused(true);
823
+ };
824
+ const handleEditorBlur = () => {
825
+ requestAnimationFrame(() => {
826
+ var _editorRef$current;
827
+ if (!((_editorRef$current = editorRef.current) !== null && _editorRef$current !== void 0 && _editorRef$current.contains(document.activeElement))) {
828
+ setEditorFocused(false);
829
+ }
830
+ });
831
+ };
832
+ const updateMediaControlVisibility = container => {
833
+ const handles = container.querySelector(".media-resize-handles");
834
+ if (handles instanceof HTMLElement) {
835
+ handles.style.display = editable ? "block" : "none";
836
+ handles.style.pointerEvents = editable ? "auto" : "none";
837
+ }
838
+ };
839
+ const createMediaDeleteButton = (title, className, onRemove) => {
840
+ const deleteBtn = document.createElement("button");
841
+ deleteBtn.type = "button";
842
+ deleteBtn.innerHTML = "×";
843
+ deleteBtn.className = className;
844
+ deleteBtn.title = title;
845
+ deleteBtn.setAttribute("contenteditable", "false");
846
+ deleteBtn.onclick = event => {
847
+ event.preventDefault();
848
+ event.stopPropagation();
849
+ onRemove();
850
+ };
851
+ return deleteBtn;
852
+ };
652
853
 
653
854
  // Listen for selection changes globally to update styles and list type in one pass
654
855
  React.useEffect(() => {
655
856
  const handleGlobalSelectionSync = () => {
656
- var _editorRef$current;
857
+ var _editorRef$current2;
657
858
  // Only sync if the editor has focus
658
859
  const sel = window.getSelection();
659
- if (!sel || !sel.rangeCount || !((_editorRef$current = editorRef.current) !== null && _editorRef$current !== void 0 && _editorRef$current.contains(sel.anchorNode))) {
860
+ if (!sel || !sel.rangeCount || !((_editorRef$current2 = editorRef.current) !== null && _editorRef$current2 !== void 0 && _editorRef$current2.contains(sel.anchorNode))) {
660
861
  return;
661
862
  }
662
863
 
@@ -717,6 +918,7 @@ function RichTextEditor({
717
918
  focus();
718
919
  };
719
920
  const [fontColor, setFontColor] = React.useState("#000000");
921
+ const getActiveTextColor = () => getColorAtCursor() || fontColor;
720
922
  const handleColorChange = color => {
721
923
  setFontColor(color);
722
924
  exec("foreColor", color);
@@ -927,58 +1129,82 @@ function RichTextEditor({
927
1129
  setVideoModalOpen(false);
928
1130
  setVideoUrl("");
929
1131
  triggerChange && triggerChange();
1132
+ requestAnimationFrame(() => processExistingMedia(editorRef.current));
930
1133
  } else {
931
1134
  console.warn("Invalid Video URL or Platform not supported");
932
1135
  }
933
1136
  };
1137
+ const processExistingVideos = container => {
1138
+ if (!container) return;
1139
+ container.querySelectorAll(".video-container").forEach(videoContainer => {
1140
+ if (!videoContainer.querySelector(".video-delete-button")) {
1141
+ const deleteBtn = createMediaDeleteButton("Remove video", "video-delete-button image-delete-button", () => {
1142
+ videoContainer.remove();
1143
+ triggerChange && triggerChange();
1144
+ });
1145
+ videoContainer.appendChild(deleteBtn);
1146
+ }
1147
+ attachMediaResizeHandle(videoContainer);
1148
+ updateMediaControlVisibility(videoContainer);
1149
+ });
1150
+ };
934
1151
  const processExistingImages = container => {
935
1152
  if (!container) return;
936
1153
  container.querySelectorAll("img").forEach(img => {
937
- // ONLY wrap if it's not already inside a wrapper
1154
+ var _img$closest;
1155
+ if (img.closest(".rte-modal")) return;
938
1156
  const existingWrapper = img.closest(".image-container");
939
1157
  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';
1158
+ existingWrapper.style.cursor = editable ? "pointer" : "default";
1159
+ const frame = ensureImageMediaFrame(existingWrapper);
1160
+ if (frame && !frame.querySelector(".image-delete-button")) {
1161
+ frame.appendChild(createMediaDeleteButton("Remove image", "image-delete-button", () => {
1162
+ existingWrapper.remove();
1163
+ triggerChange && triggerChange();
1164
+ }));
945
1165
  }
1166
+ attachMediaResizeHandle(existingWrapper);
1167
+ updateMediaControlVisibility(existingWrapper);
946
1168
  return;
947
1169
  }
948
1170
  const wrapper = document.createElement("div");
949
- const align = img.getAttribute('data-align') || 'center';
1171
+ 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
1172
  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
- };
1173
+ wrapper.style.cursor = editable ? "pointer" : "default";
1174
+ const frame = document.createElement("div");
1175
+ frame.className = "image-media-frame";
1176
+ if (img.getAttribute("width") && !frame.style.width) {
1177
+ frame.style.width = `${img.getAttribute("width")}px`;
1178
+ } else if (img.style.width && img.style.width.endsWith("px")) {
1179
+ frame.style.width = img.style.width;
1180
+ }
1181
+ img.classList.add("rte-image");
1182
+ img.setAttribute("data-align", align);
1183
+ if (frame.style.width) {
1184
+ img.style.width = "100%";
1185
+ img.style.height = frame.dataset.explicitHeight ? "100%" : "auto";
1186
+ } else {
1187
+ img.style.width = "";
1188
+ img.style.height = "auto";
1189
+ }
1190
+ img.addEventListener("click", event => {
1191
+ if (event.target.closest(".image-delete-button, .media-resize-handle")) return;
1192
+ openImageModal(img.src);
1193
+ });
1194
+ const deleteBtn = createMediaDeleteButton("Remove image", "image-delete-button", () => {
1195
+ wrapper.remove();
1196
+ triggerChange && triggerChange();
1197
+ });
974
1198
  const {
975
1199
  parentNode,
976
1200
  nextSibling
977
1201
  } = img;
978
1202
  if (parentNode) {
979
1203
  parentNode.removeChild(img);
980
- wrapper.appendChild(img);
981
- wrapper.appendChild(deleteBtn);
1204
+ frame.appendChild(img);
1205
+ frame.appendChild(deleteBtn);
1206
+ wrapper.appendChild(frame);
1207
+ attachMediaResizeHandle(wrapper);
982
1208
  if (nextSibling) {
983
1209
  parentNode.insertBefore(wrapper, nextSibling);
984
1210
  } else {
@@ -987,11 +1213,6 @@ function RichTextEditor({
987
1213
  }
988
1214
  });
989
1215
  };
990
- React.useEffect(() => {
991
- if (editorRef.current && value) {
992
- requestAnimationFrame(() => processExistingImages(editorRef.current));
993
- }
994
- }, [value]);
995
1216
 
996
1217
  /*
997
1218
  Advanced Tip: Use the 'onImageUpload' prop to handle file uploads to a server
@@ -1002,24 +1223,23 @@ function RichTextEditor({
1002
1223
  if (editable) {
1003
1224
  // Create container for the image
1004
1225
  const container = document.createElement('div');
1005
- container.className = 'image-container';
1226
+ container.className = 'image-container image-align-left';
1227
+ const frame = document.createElement('div');
1228
+ frame.className = 'image-media-frame';
1006
1229
 
1007
1230
  // Create image element
1008
1231
  const img = document.createElement('img');
1009
1232
  img.src = dataUrl;
1010
1233
  img.alt = fileName || "image";
1011
1234
  img.addEventListener("click", () => openImageModal(dataUrl));
1012
-
1013
- // Add elements to container
1014
- container.appendChild(img);
1235
+ frame.appendChild(img);
1236
+ container.appendChild(frame);
1015
1237
 
1016
1238
  // Insert at cursor position
1017
1239
  insertNodeAtCursor(container);
1018
- triggerChange();
1019
-
1020
- // Immediately process newly inserted image so delete button appears
1021
1240
  requestAnimationFrame(() => {
1022
- processExistingImages(editorRef.current);
1241
+ processExistingMedia(editorRef.current);
1242
+ triggerChange();
1023
1243
  });
1024
1244
  }
1025
1245
  } catch (err) {
@@ -1148,10 +1368,6 @@ function RichTextEditor({
1148
1368
  }
1149
1369
  }
1150
1370
  };
1151
- const getCleanHtml = () => {
1152
- if (!editorRef.current) return "";
1153
- return editorRef.current.innerHTML;
1154
- };
1155
1371
 
1156
1372
  // Helper function to unescape HTML entities
1157
1373
  const unescapeHtml = html => {
@@ -1160,6 +1376,29 @@ function RichTextEditor({
1160
1376
  txt.innerHTML = html;
1161
1377
  return txt.value;
1162
1378
  };
1379
+ const isCursorAtEndOfListItem = (range, listItem) => {
1380
+ const suffixRange = document.createRange();
1381
+ suffixRange.setStart(range.startContainer, range.startOffset);
1382
+ suffixRange.setEnd(listItem, listItem.childNodes.length);
1383
+ return suffixRange.toString().replace(/\u200B/g, "").length === 0;
1384
+ };
1385
+ const prepareListItemForTyping = (listItem, selection) => {
1386
+ const activeColor = getActiveTextColor();
1387
+ const newRange = document.createRange();
1388
+ if (activeColor && activeColor.toLowerCase() !== "#000000") {
1389
+ const span = document.createElement("span");
1390
+ span.style.color = activeColor;
1391
+ span.appendChild(document.createTextNode("\u200B"));
1392
+ listItem.appendChild(span);
1393
+ newRange.setStart(span.firstChild, 1);
1394
+ } else {
1395
+ listItem.appendChild(document.createTextNode("\u200B"));
1396
+ newRange.setStart(listItem.firstChild, 1);
1397
+ }
1398
+ newRange.collapse(true);
1399
+ selection.removeAllRanges();
1400
+ selection.addRange(newRange);
1401
+ };
1163
1402
  const handleKeyDown = React.useCallback(e => {
1164
1403
  // Handle Enter key
1165
1404
  if (e.key === 'Enter') {
@@ -1174,15 +1413,14 @@ function RichTextEditor({
1174
1413
  const listItem = parent.closest('li');
1175
1414
  if (listItem) {
1176
1415
  const list = listItem.parentNode;
1177
- list.tagName === 'OL';
1178
1416
 
1179
1417
  // Create a new list item
1180
1418
  const newItem = document.createElement('li');
1181
1419
 
1182
1420
  // If we're at the end of a list item, add a new one
1183
- if (range.collapsed && range.endOffset === node.length) {
1421
+ if (range.collapsed && isCursorAtEndOfListItem(range, listItem)) {
1184
1422
  // If it's empty, create a regular paragraph instead
1185
- if (listItem.textContent.trim() === '') {
1423
+ if (listItem.textContent.replace(/\u200B/g, '').trim() === '') {
1186
1424
  document.execCommand('insertHTML', false, '<div><br></div>');
1187
1425
  // Move the cursor to the new line
1188
1426
  const newRange = document.createRange();
@@ -1191,6 +1429,7 @@ function RichTextEditor({
1191
1429
  newRange.collapse(true);
1192
1430
  selection.removeAllRanges();
1193
1431
  selection.addRange(newRange);
1432
+ triggerChange();
1194
1433
  return;
1195
1434
  }
1196
1435
 
@@ -1200,35 +1439,29 @@ function RichTextEditor({
1200
1439
  } else {
1201
1440
  list.appendChild(newItem);
1202
1441
  }
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);
1442
+ prepareListItemForTyping(newItem, selection);
1210
1443
  } 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;
1444
+ // If we're in the middle of text, split the list item while preserving formatting
1445
+ const afterRange = document.createRange();
1446
+ afterRange.setStart(range.startContainer, range.startOffset);
1447
+ afterRange.setEnd(listItem, listItem.childNodes.length);
1448
+ const movedFragment = afterRange.extractContents();
1449
+ newItem.appendChild(movedFragment);
1220
1450
  if (listItem.nextSibling) {
1221
1451
  list.insertBefore(newItem, listItem.nextSibling);
1222
1452
  } else {
1223
1453
  list.appendChild(newItem);
1224
1454
  }
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);
1455
+ if (!newItem.textContent.replace(/\u200B/g, '').trim()) {
1456
+ newItem.textContent = "";
1457
+ prepareListItemForTyping(newItem, selection);
1458
+ } else {
1459
+ const newRange = document.createRange();
1460
+ newRange.setStart(newItem, 0);
1461
+ newRange.collapse(true);
1462
+ selection.removeAllRanges();
1463
+ selection.addRange(newRange);
1464
+ }
1232
1465
  }
1233
1466
  } else {
1234
1467
  // Regular text, insert a new paragraph
@@ -1249,7 +1482,7 @@ function RichTextEditor({
1249
1482
  e.preventDefault();
1250
1483
  exec("underline");
1251
1484
  }
1252
- }, [exec, triggerChange]);
1485
+ }, [exec, triggerChange, fontColor]);
1253
1486
  const confirmLink = () => {
1254
1487
  // Add protocol if missing
1255
1488
  let url = linkUrl.trim();
@@ -1422,7 +1655,7 @@ function RichTextEditor({
1422
1655
  };
1423
1656
  const handleInput = React.useCallback(() => {
1424
1657
  if (editorRef.current) {
1425
- const next = editorRef.current.innerHTML;
1658
+ const next = getCleanHtml();
1426
1659
  setHtml(next);
1427
1660
  lastSynchronizedHtmlRef.current = next;
1428
1661
  onChange && onChange(next);
@@ -1459,6 +1692,18 @@ function RichTextEditor({
1459
1692
  }, [disabled]);
1460
1693
  const handleEditorClick = React.useCallback(e => {
1461
1694
  setSelectionVersion(v => v + 1);
1695
+ const deleteBtn = e.target.closest('button[title="Remove image"], button[title="Remove video"]');
1696
+ if (deleteBtn && editable && editorFocused) {
1697
+ e.preventDefault();
1698
+ e.stopPropagation();
1699
+ const wrapper = deleteBtn.closest('.image-container, .video-container');
1700
+ if (wrapper) {
1701
+ wrapper.remove();
1702
+ triggerChange();
1703
+ }
1704
+ return;
1705
+ }
1706
+
1462
1707
  // Check if the click is on a link
1463
1708
  const clickedLink = e.target.closest('a');
1464
1709
  if (clickedLink) {
@@ -1491,7 +1736,7 @@ function RichTextEditor({
1491
1736
  }
1492
1737
  }, 0);
1493
1738
  }
1494
- }, [editable, disabled]);
1739
+ }, [editable, disabled, editorFocused, triggerChange]);
1495
1740
  const renderImageToolbar = () => {
1496
1741
  if (!selectedImage || !editorRef.current || !editable) return null;
1497
1742
  const editorRect = editorRef.current.getBoundingClientRect();
@@ -1561,6 +1806,9 @@ function RichTextEditor({
1561
1806
  title: "Remove Image"
1562
1807
  }, "\xD7"));
1563
1808
  };
1809
+ if (isLoading) {
1810
+ return /*#__PURE__*/React.createElement(Spinner, null);
1811
+ }
1564
1812
  return /*#__PURE__*/React.createElement("div", {
1565
1813
  className: "rte-main-wrapper",
1566
1814
  style: {
@@ -1986,12 +2234,14 @@ function RichTextEditor({
1986
2234
  onDragOver: e => e.preventDefault(),
1987
2235
  onKeyDown: handleKeyDown,
1988
2236
  onClick: handleEditorClick,
2237
+ onFocus: handleEditorFocus,
2238
+ onBlur: handleEditorBlur,
1989
2239
  style: {
1990
2240
  minHeight: minHeight || '150px',
1991
2241
  maxHeight: maxHeight || '500px',
1992
2242
  paddingLeft: paddingLeft || '12px'
1993
2243
  },
1994
- className: "rte-content"
2244
+ className: `rte-content${editable ? " rte-is-editable" : ""}${editorFocused ? " rte-is-focused" : ""}`
1995
2245
  }), renderImageToolbar(), /*#__PURE__*/React.createElement("div", {
1996
2246
  className: "rte-footer"
1997
2247
  }, /*#__PURE__*/React.createElement("div", {