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.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:
|
|
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(() =>
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
1199
|
+
var _img$closest;
|
|
1200
|
+
if (img.closest(".rte-modal")) return;
|
|
938
1201
|
const existingWrapper = img.closest(".image-container");
|
|
939
1202
|
if (existingWrapper) {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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(
|
|
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 ?
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
img.
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
if (
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
981
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1472
|
+
if (range.collapsed && isCursorAtEndOfListItem(range, listItem)) {
|
|
1184
1473
|
// If it's empty, create a regular paragraph instead
|
|
1185
|
-
if (listItem
|
|
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
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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 =
|
|
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:
|
|
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", {
|