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 +461 -112
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +461 -112
- package/dist/index.esm.js.map +1 -1
- package/package.json +8 -1
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:
|
|
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(() =>
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
1197
|
+
var _img$closest;
|
|
1198
|
+
if (img.closest(".rte-modal")) return;
|
|
936
1199
|
const existingWrapper = img.closest(".image-container");
|
|
937
1200
|
if (existingWrapper) {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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(
|
|
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 ?
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
img.
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
if (
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
979
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1470
|
+
if (range.collapsed && isCursorAtEndOfListItem(range, listItem)) {
|
|
1182
1471
|
// If it's empty, create a regular paragraph instead
|
|
1183
|
-
if (listItem
|
|
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
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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 =
|
|
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:
|
|
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", {
|