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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -231,6 +231,27 @@ const FaFont = ({
231
231
  }
232
232
  });
233
233
  };
234
+ const FaLink = ({
235
+ className,
236
+ size,
237
+ color,
238
+ style
239
+ }) => {
240
+ return /*#__PURE__*/React.createElement("span", {
241
+ className: className,
242
+ style: {
243
+ display: 'inline-flex',
244
+ alignItems: 'center',
245
+ justifyContent: 'center',
246
+ fontSize: size || '1em',
247
+ color: color || 'inherit',
248
+ ...style
249
+ },
250
+ dangerouslySetInnerHTML: {
251
+ __html: `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z"></path></svg>`
252
+ }
253
+ });
254
+ };
234
255
  const FaTable = ({
235
256
  className,
236
257
  size,
@@ -388,7 +409,7 @@ function styleInject(css, ref) {
388
409
  }
389
410
  }
390
411
 
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}";
412
+ 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-heading-select{width:112px}.rte-toolbar-button-text{font-size:12px;font-weight:700;min-width:32px;padding:0 8px;width:auto}.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-wrapper{position:relative}.rte-placeholder{color:#9ca3af;font-size:16px;line-height:1.6;pointer-events:none;position:absolute;top:12px;user-select:none}.rte-content ul{list-style-type:disc;margin-left:1.5rem}.rte-content ol{list-style-type:decimal;margin-left:1.5rem}.rte-content blockquote{background:#f8fafc;border-left:4px solid #bfdbfe;border-radius:0 8px 8px 0;color:#475569;margin:12px 0;padding:8px 14px}.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);display:block;height:0;line-height:0;margin:16px 0;max-width:100%;overflow:hidden;padding-bottom:56.25%;position:relative;width:100%}.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;box-sizing:border-box;font-size:14px;outline:none;padding:8px 12px;transition:all .15s ease;width:100%}.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%;position:relative;width:fit-content}.image-container.image-align-center,.video-container.image-align-center{margin-left:auto;margin-right:auto}.image-container.image-align-left,.video-container.image-align-left{margin-left:0;margin-right:auto}.image-container.image-align-right,.video-container.image-align-right{margin-left:auto;margin-right:0}.image-container.rte-media-selected,.video-container.rte-media-selected{border-radius:12px;outline:2px solid #3b82f6;outline-offset:2px}.image-media-frame{display:block;line-height:0;max-width:100%;position:relative;width:100%}.image-media-frame img{border-radius:12px;display:block;height:auto;margin:0;max-width:100%;width:auto}.image-container[data-width-percent] .image-media-frame,.image-container[data-width-percent] .image-media-frame img{width:100%}.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}.rte-content.rte-is-editable .video-container.rte-media-selected iframe{pointer-events:none}.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-handle{background:#3b82f6;border:2px solid #fff;border-radius:3px;bottom:2px;box-shadow:0 1px 3px rgba(15,23,42,.2);cursor:nwse-resize;height:12px;pointer-events:auto;position:absolute;right:2px;width:12px;z-index:60}.media-resize-handle:hover{background:#2563eb}.video-container .media-resize-handle{bottom:6px;right:6px}.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-media-toolbar{align-items:center;background:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);display:flex;gap:2px;padding:4px;pointer-events:auto}.rte-media-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:28px;padding:0 6px;transition:all .15s ease}.rte-media-toolbar button.active,.rte-media-toolbar button:hover{background-color:#eff6ff;color:#2563eb}.rte-media-toolbar button.danger{color:#ef4444}.rte-media-toolbar button.danger:hover{background-color:#fef2f2;color:#dc2626}.rte-media-toolbar-divider{background:#e5e7eb;height:18px;margin:0 2px;width:1px}.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
413
  styleInject(css_248z);
393
414
 
394
415
  // Helper functions for HTML escaping
@@ -405,11 +426,12 @@ function RichTextEditor({
405
426
  showEditButton,
406
427
  onBlur,
407
428
  disabled = false,
408
- editable: initialEditable = false,
429
+ editable: initialEditable = true,
409
430
  value,
410
431
  isLoading,
411
432
  isList = false,
412
433
  label,
434
+ placeholder = "Type here...",
413
435
  showBorder = true,
414
436
  paddingLeft,
415
437
  minHeight,
@@ -427,6 +449,7 @@ function RichTextEditor({
427
449
  const [editable, setEditable] = useState(initialEditable);
428
450
  const [editorFocused, setEditorFocused] = useState(false);
429
451
  const lastSynchronizedHtmlRef = useRef("");
452
+ const syncProcessedMediaRef = useRef(() => {});
430
453
  useEffect(() => {
431
454
  setEditable(initialEditable);
432
455
  }, [initialEditable]);
@@ -445,6 +468,7 @@ function RichTextEditor({
445
468
  // NEW: Track current line height
446
469
  const [currentLineHeight, setCurrentLineHeight] = useState("");
447
470
  const [activeAlign, setActiveAlign] = useState(null);
471
+ const [currentBlockFormat, setCurrentBlockFormat] = useState("div");
448
472
  const [imageModalOpen, setImageModalOpen] = useState(false);
449
473
  const [selectedImageUrl, setSelectedImageUrl] = useState("");
450
474
  const [zoomLevel, setZoomLevel] = useState(1);
@@ -456,11 +480,12 @@ function RichTextEditor({
456
480
  const [tableRows, setTableRows] = useState(3);
457
481
  const [tableCols, setTableCols] = useState(3);
458
482
  const [selectionVersion, setSelectionVersion] = useState(0);
459
- const [selectedImage, setSelectedImage] = useState(null);
483
+ const [selectedMedia, setSelectedMedia] = useState(null);
460
484
  const [metrics, setMetrics] = useState({
461
485
  words: 0,
462
486
  chars: 0
463
487
  });
488
+ const [isEmpty, setIsEmpty] = useState(!value);
464
489
  const updateMetrics = useCallback(() => {
465
490
  if (!editorRef.current) return;
466
491
  // Calculate metrics immediately but outside of render path
@@ -472,6 +497,11 @@ function RichTextEditor({
472
497
  words,
473
498
  chars
474
499
  });
500
+
501
+ // Track emptiness for the placeholder. Account for media-only content.
502
+ const stripped = text.replace(/[\u200B\u00A0\s]/g, "");
503
+ const hasMedia = !!editorRef.current.querySelector("img, table, iframe");
504
+ setIsEmpty(stripped.length === 0 && !hasMedia);
475
505
  }, []);
476
506
  const openImageModal = url => {
477
507
  if (editorRef.current) {
@@ -533,16 +563,15 @@ function RichTextEditor({
533
563
  }, [imageModalOpen]);
534
564
  useEffect(() => {
535
565
  if (editorRef.current && value && value !== lastSynchronizedHtmlRef.current) {
536
- requestAnimationFrame(() => processExistingMedia(editorRef.current));
566
+ requestAnimationFrame(() => syncProcessedMediaRef.current(editorRef.current));
537
567
  }
538
568
  }, [value]);
539
-
540
- // Runs whenever editable changes (toggles delete icon visibility)
541
569
  useEffect(() => {
542
570
  if (!editable) {
543
571
  setEditorFocused(false);
572
+ clearMediaSelection();
544
573
  }
545
- processExistingMedia(editorRef.current);
574
+ syncProcessedMediaRef.current(editorRef.current);
546
575
  }, [editable]);
547
576
  useEffect(() => {
548
577
  if (!editorRef.current) return;
@@ -573,7 +602,7 @@ function RichTextEditor({
573
602
  if (editorRef.current && editorRef.current.innerHTML !== newContent) {
574
603
  editorRef.current.innerHTML = newContent;
575
604
  }
576
- requestAnimationFrame(() => processExistingMedia(editorRef.current));
605
+ requestAnimationFrame(() => syncProcessedMediaRef.current(editorRef.current));
577
606
  updateMetrics();
578
607
  }
579
608
  } catch (e) {
@@ -588,10 +617,47 @@ function RichTextEditor({
588
617
  }
589
618
  }
590
619
  }, [value, initialEditable, updateMetrics]);
620
+ const LIST_BLOCK_MEDIA_SELECTOR = ".video-container, .image-container, table";
621
+ const isListItemEffectivelyEmpty = listItem => {
622
+ if (!listItem) return true;
623
+ const clone = listItem.cloneNode(true);
624
+ clone.querySelectorAll(LIST_BLOCK_MEDIA_SELECTOR).forEach(el => el.remove());
625
+ clone.querySelectorAll("br").forEach(el => el.remove());
626
+ return clone.textContent.replace(/[\u200B\u00A0\s]/g, "").length === 0;
627
+ };
628
+ const hoistBlockMediaOutOfListItems = container => {
629
+ if (!container) return false;
630
+ let changed = false;
631
+ container.querySelectorAll("ol, ul").forEach(list => {
632
+ const items = Array.from(list.children).filter(child => child.tagName === "LI");
633
+ items.forEach(listItem => {
634
+ const blockMedia = Array.from(listItem.querySelectorAll(LIST_BLOCK_MEDIA_SELECTOR));
635
+ if (blockMedia.length === 0) return;
636
+ const hadText = !isListItemEffectivelyEmpty(listItem);
637
+ blockMedia.forEach(media => {
638
+ listItem.removeChild(media);
639
+ if (list.parentNode) {
640
+ list.parentNode.insertBefore(media, list.nextSibling);
641
+ }
642
+ changed = true;
643
+ });
644
+ if (!hadText || isListItemEffectivelyEmpty(listItem)) {
645
+ listItem.remove();
646
+ changed = true;
647
+ }
648
+ });
649
+ if (list.children.length === 0 && list.parentNode) {
650
+ list.remove();
651
+ changed = true;
652
+ }
653
+ });
654
+ return changed;
655
+ };
591
656
  const processExistingMedia = container => {
592
- if (!container) return;
657
+ if (!container) return false;
593
658
  processExistingImages(container);
594
659
  processExistingVideos(container);
660
+ return hoistBlockMediaOutOfListItems(container);
595
661
  };
596
662
  const getCleanHtml = () => {
597
663
  if (!editorRef.current) return "";
@@ -607,6 +673,11 @@ function RichTextEditor({
607
673
  lastSynchronizedHtmlRef.current = next;
608
674
  onChange && onChange(next);
609
675
  }, [onChange]);
676
+ syncProcessedMediaRef.current = container => {
677
+ if (processExistingMedia(container)) {
678
+ triggerChange();
679
+ }
680
+ };
610
681
 
611
682
  // Helper to walk up DOM to find style tags or CSS style:
612
683
  const isParentStyle = (node, ...tagNames) => {
@@ -662,17 +733,15 @@ function RichTextEditor({
662
733
  return rgbToHex(computedColor);
663
734
  };
664
735
  const stripEditorChrome = root => {
665
- root.querySelectorAll(".image-delete-button, .video-delete-button, .video-edit-overlay, .media-resize-handles, .media-resize-handle").forEach(element => element.remove());
736
+ root.querySelectorAll(".image-delete-button, .video-delete-button, .video-edit-overlay, .media-resize-handle").forEach(element => element.remove());
737
+ root.querySelectorAll(".rte-media-selected").forEach(element => {
738
+ element.classList.remove("rte-media-selected");
739
+ });
666
740
  return root;
667
741
  };
668
- const getMediaSizeLimits = () => {
669
- const maxWidth = editorRef.current ? editorRef.current.getBoundingClientRect().width - 24 : 800;
670
- return {
671
- minWidth: 120,
672
- minHeight: 80,
673
- maxWidth,
674
- maxHeight: 720
675
- };
742
+ const getEditorInnerWidth = () => {
743
+ if (!editorRef.current) return 800;
744
+ return Math.max(editorRef.current.clientWidth - 24, 200);
676
745
  };
677
746
  const ensureImageMediaFrame = imageContainer => {
678
747
  if (!imageContainer) return null;
@@ -680,160 +749,164 @@ function RichTextEditor({
680
749
  if (frame) return frame;
681
750
  frame = document.createElement("div");
682
751
  frame.className = "image-media-frame";
683
- ["width", "height", "marginLeft", "marginTop", "maxWidth"].forEach(prop => {
684
- if (imageContainer.style[prop]) {
685
- frame.style[prop] = imageContainer.style[prop];
686
- imageContainer.style[prop] = "";
687
- }
688
- });
689
- if (imageContainer.dataset.explicitHeight) {
690
- frame.dataset.explicitHeight = imageContainer.dataset.explicitHeight;
691
- delete imageContainer.dataset.explicitHeight;
692
- }
693
752
  const children = Array.from(imageContainer.children);
694
753
  imageContainer.appendChild(frame);
695
754
  children.forEach(child => frame.appendChild(child));
696
755
  return frame;
697
756
  };
698
757
  const getImageMediaTarget = imageContainer => ensureImageMediaFrame(imageContainer) || imageContainer;
699
- const applyImageMediaSize = (frame, width, height, edge) => {
758
+ const getMediaWidthPercent = container => {
759
+ if (!container) return 100;
760
+ if (container.dataset.widthPercent) {
761
+ return Number(container.dataset.widthPercent);
762
+ }
763
+ const width = container.style.width || "";
764
+ if (width.endsWith("%")) {
765
+ return parseInt(width, 10) || 100;
766
+ }
767
+ const editorWidth = getEditorInnerWidth();
768
+ const rect = container.getBoundingClientRect();
769
+ if (editorWidth > 0 && rect.width > 0) {
770
+ return Math.round(rect.width / editorWidth * 100);
771
+ }
772
+ return 100;
773
+ };
774
+ const applyMediaWidthPercent = (container, percent) => {
775
+ if (!container) return;
776
+ const clamped = Math.max(25, Math.min(100, Math.round(percent)));
777
+ container.dataset.widthPercent = String(clamped);
778
+ container.style.width = `${clamped}%`;
779
+ container.style.maxWidth = "100%";
780
+ container.style.marginLeft = "";
781
+ container.style.marginTop = "";
782
+ if (container.classList.contains("video-container")) {
783
+ container.style.height = "0";
784
+ container.style.paddingBottom = "56.25%";
785
+ return;
786
+ }
787
+ const frame = getImageMediaTarget(container);
788
+ if (!frame) return;
789
+ frame.style.width = "100%";
790
+ frame.style.height = "";
700
791
  const img = frame.querySelector("img");
701
- const isVertical = edge === "top" || edge === "bottom";
702
- frame.style.width = `${Math.round(width)}px`;
703
- frame.style.maxWidth = "100%";
704
- if (isVertical) {
705
- frame.style.height = `${Math.round(height)}px`;
706
- frame.dataset.explicitHeight = "true";
707
- if (img) {
708
- img.style.width = "100%";
709
- img.style.height = "100%";
710
- img.style.objectFit = "contain";
711
- }
792
+ if (img) {
793
+ img.style.height = "auto";
794
+ img.style.objectFit = "";
795
+ }
796
+ };
797
+ const normalizeMediaWidth = container => {
798
+ if (!container) return;
799
+ if (container.classList.contains("image-small")) {
800
+ container.classList.remove("image-small");
801
+ applyMediaWidthPercent(container, 50);
712
802
  return;
713
803
  }
714
- if (!frame.dataset.explicitHeight) {
715
- frame.style.height = "";
804
+ if (container.dataset.widthPercent) {
805
+ applyMediaWidthPercent(container, Number(container.dataset.widthPercent));
806
+ return;
716
807
  }
717
- if (img) {
718
- img.style.width = "100%";
719
- if (frame.dataset.explicitHeight) {
720
- img.style.height = "100%";
721
- img.style.objectFit = "contain";
722
- } else {
723
- img.style.height = "auto";
724
- img.style.objectFit = "";
808
+ const width = container.style.width || "";
809
+ if (width.endsWith("%")) {
810
+ applyMediaWidthPercent(container, parseInt(width, 10) || 100);
811
+ return;
812
+ }
813
+ if (width.endsWith("px")) {
814
+ const editorWidth = getEditorInnerWidth();
815
+ const px = parseFloat(width);
816
+ if (editorWidth > 0 && px > 0) {
817
+ applyMediaWidthPercent(container, Math.round(px / editorWidth * 100));
725
818
  }
819
+ return;
820
+ }
821
+ if (container.classList.contains("video-container")) {
822
+ applyMediaWidthPercent(container, 100);
726
823
  }
727
824
  };
728
- const applyVideoMediaSize = (container, width, height) => {
729
- container.style.paddingBottom = "0";
730
- container.style.width = `${Math.round(width)}px`;
731
- container.style.maxWidth = "100%";
732
- container.style.height = `${Math.round(height)}px`;
825
+ const clearMediaSelection = () => {
826
+ var _editorRef$current;
827
+ (_editorRef$current = editorRef.current) === null || _editorRef$current === void 0 || _editorRef$current.querySelectorAll(".rte-media-selected").forEach(element => {
828
+ element.classList.remove("rte-media-selected");
829
+ });
830
+ setSelectedMedia(null);
831
+ };
832
+ const selectMediaContainer = container => {
833
+ var _editorRef$current2;
834
+ if (!container || !((_editorRef$current2 = editorRef.current) !== null && _editorRef$current2 !== void 0 && _editorRef$current2.contains(container))) return;
835
+ editorRef.current.querySelectorAll(".rte-media-selected").forEach(element => {
836
+ element.classList.remove("rte-media-selected");
837
+ });
838
+ container.classList.add("rte-media-selected");
839
+ setSelectedMedia(container);
733
840
  };
734
841
  const attachMediaResizeHandle = container => {
735
- if (!container || container.querySelector(".media-resize-handles")) return;
736
- const isVideo = container.classList.contains("video-container");
737
- const resizeTarget = isVideo ? container : getImageMediaTarget(container);
842
+ var _resizeTarget$querySe;
843
+ if (!container) return;
844
+ const resizeTarget = container.classList.contains("video-container") ? container : getImageMediaTarget(container);
738
845
  if (!resizeTarget) return;
739
- const handlesWrapper = document.createElement("div");
740
- handlesWrapper.className = "media-resize-handles";
741
- handlesWrapper.setAttribute("contenteditable", "false");
742
- const edges = [{
743
- edge: "left",
744
- title: "Drag to resize width"
745
- }, {
746
- edge: "right",
747
- title: "Drag to resize width"
748
- }, {
749
- edge: "top",
750
- title: "Drag to resize height"
751
- }, {
752
- edge: "bottom",
753
- title: "Drag to resize height"
754
- }];
755
- edges.forEach(({
756
- edge,
757
- title
758
- }) => {
759
- const handle = document.createElement("div");
760
- handle.className = `media-resize-handle media-resize-handle-${edge}`;
761
- handle.title = title;
762
- handle.setAttribute("contenteditable", "false");
763
- handle.dataset.edge = edge;
764
- handle.addEventListener("mousedown", event => {
765
- if (!editable) return;
766
- event.preventDefault();
767
- event.stopPropagation();
768
- const limits = getMediaSizeLimits();
769
- const rect = resizeTarget.getBoundingClientRect();
770
- const startX = event.clientX;
771
- const startY = event.clientY;
772
- const startWidth = rect.width;
773
- const startHeight = rect.height;
774
- const startMarginLeft = Number.parseFloat(resizeTarget.style.marginLeft) || 0;
775
- const startMarginTop = Number.parseFloat(resizeTarget.style.marginTop) || 0;
776
- if (isVideo) {
777
- resizeTarget.style.paddingBottom = "0";
778
- }
779
- const onMouseMove = moveEvent => {
780
- const deltaX = moveEvent.clientX - startX;
781
- const deltaY = moveEvent.clientY - startY;
782
- let nextWidth = startWidth;
783
- let nextHeight = startHeight;
784
- if (edge === "right") {
785
- nextWidth = startWidth + deltaX;
786
- } else if (edge === "left") {
787
- nextWidth = startWidth - deltaX;
788
- } else if (edge === "bottom") {
789
- nextHeight = startHeight + deltaY;
790
- } else if (edge === "top") {
791
- nextHeight = startHeight - deltaY;
792
- }
793
- nextWidth = Math.max(limits.minWidth, Math.min(nextWidth, limits.maxWidth));
794
- nextHeight = Math.max(limits.minHeight, Math.min(nextHeight, limits.maxHeight));
795
- if (edge === "left") {
796
- resizeTarget.style.marginLeft = `${Math.round(startMarginLeft + (startWidth - nextWidth))}px`;
797
- }
798
- if (edge === "top") {
799
- resizeTarget.style.marginTop = `${Math.round(startMarginTop + (startHeight - nextHeight))}px`;
800
- }
801
- if (isVideo) {
802
- applyVideoMediaSize(resizeTarget, nextWidth, nextHeight);
803
- } else {
804
- applyImageMediaSize(resizeTarget, nextWidth, nextHeight, edge);
805
- }
806
- };
807
- const onMouseUp = () => {
808
- document.removeEventListener("mousemove", onMouseMove);
809
- document.removeEventListener("mouseup", onMouseUp);
810
- triggerChange();
811
- };
812
- document.addEventListener("mousemove", onMouseMove);
813
- document.addEventListener("mouseup", onMouseUp);
814
- });
815
- handlesWrapper.appendChild(handle);
846
+ (_resizeTarget$querySe = resizeTarget.querySelector(".media-resize-handle")) === null || _resizeTarget$querySe === void 0 || _resizeTarget$querySe.remove();
847
+ const handle = document.createElement("div");
848
+ handle.className = "media-resize-handle";
849
+ handle.title = "Drag to resize";
850
+ handle.setAttribute("contenteditable", "false");
851
+ handle.addEventListener("mousedown", event => {
852
+ if (!editable) return;
853
+ event.preventDefault();
854
+ event.stopPropagation();
855
+ selectMediaContainer(container);
856
+ const editorWidth = getEditorInnerWidth();
857
+ const startX = event.clientX;
858
+ const startWidth = container.getBoundingClientRect().width;
859
+ const onMouseMove = moveEvent => {
860
+ const nextWidth = Math.max(60, startWidth + (moveEvent.clientX - startX));
861
+ const percent = Math.round(nextWidth / editorWidth * 100);
862
+ applyMediaWidthPercent(container, percent);
863
+ };
864
+ const onMouseUp = () => {
865
+ document.removeEventListener("mousemove", onMouseMove);
866
+ document.removeEventListener("mouseup", onMouseUp);
867
+ triggerChange();
868
+ };
869
+ document.addEventListener("mousemove", onMouseMove);
870
+ document.addEventListener("mouseup", onMouseUp);
816
871
  });
817
- resizeTarget.appendChild(handlesWrapper);
872
+ resizeTarget.appendChild(handle);
818
873
  };
819
874
  const handleEditorFocus = () => {
820
875
  setEditorFocused(true);
821
876
  };
822
877
  const handleEditorBlur = () => {
823
878
  requestAnimationFrame(() => {
824
- var _editorRef$current;
825
- if (!((_editorRef$current = editorRef.current) !== null && _editorRef$current !== void 0 && _editorRef$current.contains(document.activeElement))) {
879
+ var _editorRef$current3;
880
+ if (!((_editorRef$current3 = editorRef.current) !== null && _editorRef$current3 !== void 0 && _editorRef$current3.contains(document.activeElement))) {
826
881
  setEditorFocused(false);
827
882
  }
828
883
  });
829
884
  };
830
885
  const updateMediaControlVisibility = container => {
831
- const handles = container.querySelector(".media-resize-handles");
832
- if (handles instanceof HTMLElement) {
833
- handles.style.display = editable ? "block" : "none";
834
- handles.style.pointerEvents = editable ? "auto" : "none";
886
+ const handle = container.querySelector(".media-resize-handle");
887
+ if (handle instanceof HTMLElement) {
888
+ handle.style.display = editable ? "block" : "none";
889
+ handle.style.pointerEvents = editable ? "auto" : "none";
835
890
  }
836
891
  };
892
+ const BLOCK_TAGS = ["P", "DIV", "H1", "H2", "H3", "BLOCKQUOTE", "LI"];
893
+ const getActiveBlock = node => {
894
+ if (!editorRef.current || !node) return null;
895
+ let current = node.nodeType === 3 ? node.parentNode : node;
896
+ while (current && current !== editorRef.current) {
897
+ if (current.nodeType === 1 && BLOCK_TAGS.includes(current.tagName)) {
898
+ return current;
899
+ }
900
+ current = current.parentNode;
901
+ }
902
+ return editorRef.current;
903
+ };
904
+ const getBlockFormat = node => {
905
+ const block = getActiveBlock(node);
906
+ if (!block || block === editorRef.current) return "div";
907
+ const tag = block.tagName.toLowerCase();
908
+ return tag === "p" || tag === "li" ? "div" : tag;
909
+ };
837
910
  const createMediaDeleteButton = (title, className, onRemove) => {
838
911
  const deleteBtn = document.createElement("button");
839
912
  deleteBtn.type = "button";
@@ -852,10 +925,10 @@ function RichTextEditor({
852
925
  // Listen for selection changes globally to update styles and list type in one pass
853
926
  useEffect(() => {
854
927
  const handleGlobalSelectionSync = () => {
855
- var _editorRef$current2;
928
+ var _editorRef$current4;
856
929
  // Only sync if the editor has focus
857
930
  const sel = window.getSelection();
858
- if (!sel || !sel.rangeCount || !((_editorRef$current2 = editorRef.current) !== null && _editorRef$current2 !== void 0 && _editorRef$current2.contains(sel.anchorNode))) {
931
+ if (!sel || !sel.rangeCount || !((_editorRef$current4 = editorRef.current) !== null && _editorRef$current4 !== void 0 && _editorRef$current4.contains(sel.anchorNode))) {
859
932
  return;
860
933
  }
861
934
 
@@ -903,6 +976,7 @@ function RichTextEditor({
903
976
  } else {
904
977
  setCurrentFontSize("16");
905
978
  }
979
+ setCurrentBlockFormat(getBlockFormat(sel.anchorNode));
906
980
  };
907
981
  document.addEventListener("selectionchange", handleGlobalSelectionSync);
908
982
  return () => {
@@ -1126,8 +1200,10 @@ function RichTextEditor({
1126
1200
  }
1127
1201
  setVideoModalOpen(false);
1128
1202
  setVideoUrl("");
1129
- triggerChange && triggerChange();
1130
- requestAnimationFrame(() => processExistingMedia(editorRef.current));
1203
+ requestAnimationFrame(() => {
1204
+ processExistingMedia(editorRef.current);
1205
+ triggerChange();
1206
+ });
1131
1207
  } else {
1132
1208
  console.warn("Invalid Video URL or Platform not supported");
1133
1209
  }
@@ -1138,11 +1214,25 @@ function RichTextEditor({
1138
1214
  if (!videoContainer.querySelector(".video-delete-button")) {
1139
1215
  const deleteBtn = createMediaDeleteButton("Remove video", "video-delete-button image-delete-button", () => {
1140
1216
  videoContainer.remove();
1217
+ clearMediaSelection();
1141
1218
  triggerChange && triggerChange();
1142
1219
  });
1143
1220
  videoContainer.appendChild(deleteBtn);
1144
1221
  }
1222
+ if (!videoContainer.dataset.mediaEnhanced) {
1223
+ videoContainer.dataset.mediaEnhanced = "true";
1224
+ if (!videoContainer.classList.contains("image-align-left") && !videoContainer.classList.contains("image-align-center") && !videoContainer.classList.contains("image-align-right")) {
1225
+ videoContainer.classList.add("image-align-left");
1226
+ }
1227
+ videoContainer.addEventListener("click", event => {
1228
+ if (event.target.closest(".image-delete-button, .media-resize-handle")) return;
1229
+ event.preventDefault();
1230
+ event.stopPropagation();
1231
+ selectMediaContainer(videoContainer);
1232
+ });
1233
+ }
1145
1234
  attachMediaResizeHandle(videoContainer);
1235
+ normalizeMediaWidth(videoContainer);
1146
1236
  updateMediaControlVisibility(videoContainer);
1147
1237
  });
1148
1238
  };
@@ -1162,6 +1252,7 @@ function RichTextEditor({
1162
1252
  }));
1163
1253
  }
1164
1254
  attachMediaResizeHandle(existingWrapper);
1255
+ normalizeMediaWidth(existingWrapper);
1165
1256
  updateMediaControlVisibility(existingWrapper);
1166
1257
  return;
1167
1258
  }
@@ -1171,24 +1262,21 @@ function RichTextEditor({
1171
1262
  wrapper.style.cursor = editable ? "pointer" : "default";
1172
1263
  const frame = document.createElement("div");
1173
1264
  frame.className = "image-media-frame";
1174
- if (img.getAttribute("width") && !frame.style.width) {
1175
- frame.style.width = `${img.getAttribute("width")}px`;
1176
- } else if (img.style.width && img.style.width.endsWith("px")) {
1177
- frame.style.width = img.style.width;
1178
- }
1179
1265
  img.classList.add("rte-image");
1180
1266
  img.setAttribute("data-align", align);
1181
- if (frame.style.width) {
1182
- img.style.width = "100%";
1183
- img.style.height = frame.dataset.explicitHeight ? "100%" : "auto";
1184
- } else {
1185
- img.style.width = "";
1186
- img.style.height = "auto";
1187
- }
1188
- img.addEventListener("click", event => {
1267
+ img.style.height = "auto";
1268
+ img.addEventListener("dblclick", event => {
1189
1269
  if (event.target.closest(".image-delete-button, .media-resize-handle")) return;
1270
+ event.preventDefault();
1271
+ event.stopPropagation();
1190
1272
  openImageModal(img.src);
1191
1273
  });
1274
+ img.addEventListener("click", event => {
1275
+ if (event.target.closest(".image-delete-button, .media-resize-handle")) return;
1276
+ event.preventDefault();
1277
+ event.stopPropagation();
1278
+ selectMediaContainer(wrapper);
1279
+ });
1192
1280
  const deleteBtn = createMediaDeleteButton("Remove image", "image-delete-button", () => {
1193
1281
  wrapper.remove();
1194
1282
  triggerChange && triggerChange();
@@ -1203,6 +1291,7 @@ function RichTextEditor({
1203
1291
  frame.appendChild(deleteBtn);
1204
1292
  wrapper.appendChild(frame);
1205
1293
  attachMediaResizeHandle(wrapper);
1294
+ normalizeMediaWidth(wrapper);
1206
1295
  if (nextSibling) {
1207
1296
  parentNode.insertBefore(wrapper, nextSibling);
1208
1297
  } else {
@@ -1229,9 +1318,19 @@ function RichTextEditor({
1229
1318
  const img = document.createElement('img');
1230
1319
  img.src = dataUrl;
1231
1320
  img.alt = fileName || "image";
1232
- img.addEventListener("click", () => openImageModal(dataUrl));
1321
+ img.addEventListener("dblclick", event => {
1322
+ event.preventDefault();
1323
+ event.stopPropagation();
1324
+ openImageModal(dataUrl);
1325
+ });
1326
+ img.addEventListener("click", event => {
1327
+ event.preventDefault();
1328
+ event.stopPropagation();
1329
+ selectMediaContainer(container);
1330
+ });
1233
1331
  frame.appendChild(img);
1234
1332
  container.appendChild(frame);
1333
+ applyMediaWidthPercent(container, 25);
1235
1334
 
1236
1335
  // Insert at cursor position
1237
1336
  insertNodeAtCursor(container);
@@ -1374,6 +1473,12 @@ function RichTextEditor({
1374
1473
  txt.innerHTML = html;
1375
1474
  return txt.value;
1376
1475
  };
1476
+ const isCursorAtStartOfListItem = (range, listItem) => {
1477
+ const prefixRange = document.createRange();
1478
+ prefixRange.setStart(listItem, 0);
1479
+ prefixRange.setEnd(range.startContainer, range.startOffset);
1480
+ return prefixRange.toString().replace(/[\u200B\u00A0\s]/g, "").length === 0;
1481
+ };
1377
1482
  const isCursorAtEndOfListItem = (range, listItem) => {
1378
1483
  const suffixRange = document.createRange();
1379
1484
  suffixRange.setStart(range.startContainer, range.startOffset);
@@ -1398,6 +1503,8 @@ function RichTextEditor({
1398
1503
  selection.addRange(newRange);
1399
1504
  };
1400
1505
  const handleKeyDown = useCallback(e => {
1506
+ if (applyMarkdownShortcut(e)) return;
1507
+
1401
1508
  // Handle Enter key
1402
1509
  if (e.key === 'Enter') {
1403
1510
  e.preventDefault();
@@ -1418,7 +1525,7 @@ function RichTextEditor({
1418
1525
  // If we're at the end of a list item, add a new one
1419
1526
  if (range.collapsed && isCursorAtEndOfListItem(range, listItem)) {
1420
1527
  // If it's empty, create a regular paragraph instead
1421
- if (listItem.textContent.replace(/\u200B/g, '').trim() === '') {
1528
+ if (isListItemEffectivelyEmpty(listItem)) {
1422
1529
  document.execCommand('insertHTML', false, '<div><br></div>');
1423
1530
  // Move the cursor to the new line
1424
1531
  const newRange = document.createRange();
@@ -1450,7 +1557,7 @@ function RichTextEditor({
1450
1557
  } else {
1451
1558
  list.appendChild(newItem);
1452
1559
  }
1453
- if (!newItem.textContent.replace(/\u200B/g, '').trim()) {
1560
+ if (isListItemEffectivelyEmpty(newItem)) {
1454
1561
  newItem.textContent = "";
1455
1562
  prepareListItemForTyping(newItem, selection);
1456
1563
  } else {
@@ -1468,6 +1575,54 @@ function RichTextEditor({
1468
1575
  triggerChange();
1469
1576
  return;
1470
1577
  }
1578
+ if (e.key === "Backspace") {
1579
+ var _node$closest, _node, _editorRef$current5;
1580
+ const selection = window.getSelection();
1581
+ if (!(selection !== null && selection !== void 0 && selection.rangeCount)) return;
1582
+ const range = selection.getRangeAt(0);
1583
+ if (!range.collapsed) return;
1584
+ let node = range.startContainer;
1585
+ if (node.nodeType === 3) {
1586
+ node = node.parentNode;
1587
+ }
1588
+ const listItem = (_node$closest = (_node = node).closest) === null || _node$closest === void 0 ? void 0 : _node$closest.call(_node, "li");
1589
+ if (!listItem || !((_editorRef$current5 = editorRef.current) !== null && _editorRef$current5 !== void 0 && _editorRef$current5.contains(listItem))) return;
1590
+ const list = listItem.parentNode;
1591
+ if (isListItemEffectivelyEmpty(listItem)) {
1592
+ e.preventDefault();
1593
+ const prevLi = listItem.previousElementSibling;
1594
+ const blockMedia = Array.from(listItem.querySelectorAll(LIST_BLOCK_MEDIA_SELECTOR));
1595
+ blockMedia.forEach(media => {
1596
+ var _list$parentNode;
1597
+ (_list$parentNode = list.parentNode) === null || _list$parentNode === void 0 || _list$parentNode.insertBefore(media, list.nextSibling);
1598
+ });
1599
+ listItem.remove();
1600
+ if (list.children.length === 0) {
1601
+ list.remove();
1602
+ }
1603
+ if ((prevLi === null || prevLi === void 0 ? void 0 : prevLi.tagName) === "LI") {
1604
+ const newRange = document.createRange();
1605
+ newRange.selectNodeContents(prevLi);
1606
+ newRange.collapse(false);
1607
+ selection.removeAllRanges();
1608
+ selection.addRange(newRange);
1609
+ }
1610
+ triggerChange();
1611
+ return;
1612
+ }
1613
+ if (isCursorAtStartOfListItem(range, listItem)) {
1614
+ const prevLi = listItem.previousElementSibling;
1615
+ if ((prevLi === null || prevLi === void 0 ? void 0 : prevLi.tagName) === "LI" && isListItemEffectivelyEmpty(prevLi)) {
1616
+ e.preventDefault();
1617
+ prevLi.remove();
1618
+ if (list.children.length === 0) {
1619
+ list.remove();
1620
+ }
1621
+ triggerChange();
1622
+ }
1623
+ }
1624
+ return;
1625
+ }
1471
1626
 
1472
1627
  // Handle Ctrl/Cmd + B/I/U for bold/italic/underline
1473
1628
  if ((e.ctrlKey || e.metaKey) && e.key === "b") {
@@ -1508,6 +1663,65 @@ function RichTextEditor({
1508
1663
  const handleSelect = type => {
1509
1664
  exec(type === "unordered" ? "insertUnorderedList" : "insertOrderedList");
1510
1665
  };
1666
+ const applyBlockFormat = format => {
1667
+ document.execCommand("formatBlock", false, format);
1668
+ setCurrentBlockFormat(format);
1669
+ triggerChange();
1670
+ focus();
1671
+ };
1672
+ const clearFormatting = () => {
1673
+ document.execCommand("removeFormat", false, null);
1674
+ document.execCommand("unlink", false, null);
1675
+ document.execCommand("formatBlock", false, "div");
1676
+ setCurrentBlockFormat("div");
1677
+ setCurrentFontSize("16");
1678
+ setCurrentLineHeight("");
1679
+ setFontColor("#000000");
1680
+ triggerChange();
1681
+ focus();
1682
+ };
1683
+ const deleteTextBeforeCursorInBlock = (block, range, selection) => {
1684
+ const prefixRange = document.createRange();
1685
+ prefixRange.setStart(block, 0);
1686
+ prefixRange.setEnd(range.startContainer, range.startOffset);
1687
+ prefixRange.deleteContents();
1688
+ const nextRange = document.createRange();
1689
+ nextRange.setStart(block, 0);
1690
+ nextRange.collapse(true);
1691
+ selection.removeAllRanges();
1692
+ selection.addRange(nextRange);
1693
+ };
1694
+ const applyMarkdownShortcut = event => {
1695
+ if (event.key !== " " || event.ctrlKey || event.metaKey || event.altKey) {
1696
+ return false;
1697
+ }
1698
+ const selection = window.getSelection();
1699
+ if (!(selection !== null && selection !== void 0 && selection.rangeCount) || !selection.isCollapsed || !editorRef.current) {
1700
+ return false;
1701
+ }
1702
+ const range = selection.getRangeAt(0);
1703
+ const block = getActiveBlock(range.startContainer);
1704
+ if (!block || !editorRef.current.contains(block)) return false;
1705
+ const prefixRange = document.createRange();
1706
+ prefixRange.setStart(block, 0);
1707
+ prefixRange.setEnd(range.startContainer, range.startOffset);
1708
+ const textBeforeCursor = prefixRange.toString().replace(/\u00A0/g, " ").trim();
1709
+ const shortcuts = {
1710
+ "#": () => applyBlockFormat("h1"),
1711
+ "##": () => applyBlockFormat("h2"),
1712
+ "###": () => applyBlockFormat("h3"),
1713
+ ">": () => applyBlockFormat("blockquote"),
1714
+ "-": () => handleSelect("unordered"),
1715
+ "*": () => handleSelect("unordered"),
1716
+ "1.": () => handleSelect("ordered")
1717
+ };
1718
+ const action = shortcuts[textBeforeCursor];
1719
+ if (!action) return false;
1720
+ event.preventDefault();
1721
+ deleteTextBeforeCursorInBlock(block, range, selection);
1722
+ action();
1723
+ return true;
1724
+ };
1511
1725
  const onLineHeightChange = value => {
1512
1726
  if (!value) return;
1513
1727
  const sel = window.getSelection();
@@ -1689,6 +1903,7 @@ function RichTextEditor({
1689
1903
  }
1690
1904
  }, [disabled]);
1691
1905
  const handleEditorClick = useCallback(e => {
1906
+ var _editorRef$current6;
1692
1907
  setSelectionVersion(v => v + 1);
1693
1908
  const deleteBtn = e.target.closest('button[title="Remove image"], button[title="Remove video"]');
1694
1909
  if (deleteBtn && editable && editorFocused) {
@@ -1697,10 +1912,15 @@ function RichTextEditor({
1697
1912
  const wrapper = deleteBtn.closest('.image-container, .video-container');
1698
1913
  if (wrapper) {
1699
1914
  wrapper.remove();
1915
+ clearMediaSelection();
1700
1916
  triggerChange();
1701
1917
  }
1702
1918
  return;
1703
1919
  }
1920
+ const clickedMedia = e.target.closest('.image-container, .video-container');
1921
+ if (clickedMedia && (_editorRef$current6 = editorRef.current) !== null && _editorRef$current6 !== void 0 && _editorRef$current6.contains(clickedMedia) && editable) {
1922
+ return;
1923
+ }
1704
1924
 
1705
1925
  // Check if the click is on a link
1706
1926
  const clickedLink = e.target.closest('a');
@@ -1710,13 +1930,8 @@ function RichTextEditor({
1710
1930
  window.open(clickedLink.href, '_blank');
1711
1931
  return;
1712
1932
  }
1713
-
1714
- // NEW: Check if click is on an image for resizing
1715
- const clickedImg = e.target.closest('img');
1716
- if (clickedImg && !clickedImg.closest('.rte-modal')) {
1717
- setSelectedImage(clickedImg);
1718
- } else if (!e.target.closest('.rte-image-toolbar')) {
1719
- setSelectedImage(null);
1933
+ if (!e.target.closest('.rte-media-toolbar')) {
1934
+ clearMediaSelection();
1720
1935
  }
1721
1936
 
1722
1937
  // If disabled is true, prevent editing
@@ -1735,73 +1950,72 @@ function RichTextEditor({
1735
1950
  }, 0);
1736
1951
  }
1737
1952
  }, [editable, disabled, editorFocused, triggerChange]);
1738
- const renderImageToolbar = () => {
1739
- if (!selectedImage || !editorRef.current || !editable) return null;
1953
+ const renderMediaToolbar = () => {
1954
+ if (!selectedMedia || !editorRef.current || !editable) return null;
1740
1955
  const editorRect = editorRef.current.getBoundingClientRect();
1741
- const imgRect = selectedImage.getBoundingClientRect();
1742
- const top = imgRect.top - editorRect.top + editorRef.current.scrollTop;
1743
- const left = imgRect.left - editorRect.left + editorRef.current.scrollLeft;
1744
- const width = imgRect.width;
1956
+ const mediaRect = selectedMedia.getBoundingClientRect();
1957
+ const top = mediaRect.top - editorRect.top + editorRef.current.scrollTop;
1958
+ const left = mediaRect.left - editorRect.left + editorRef.current.scrollLeft;
1959
+ const width = mediaRect.width;
1960
+ const currentPercent = getMediaWidthPercent(selectedMedia);
1961
+ const widthPresets = [25, 50, 75, 100];
1745
1962
  const handleAlignment = align => {
1746
- const wrapper = selectedImage.closest('.image-container');
1747
- if (wrapper) {
1748
- // Remove all alignment classes first
1749
- wrapper.classList.remove('image-align-left', 'image-align-center', 'image-align-right');
1750
- // Add the new alignment class
1751
- wrapper.classList.add(`image-align-${align}`);
1752
- selectedImage.setAttribute('data-align', align);
1753
- triggerChange();
1754
- }
1963
+ selectedMedia.classList.remove("image-align-left", "image-align-center", "image-align-right");
1964
+ selectedMedia.classList.add(`image-align-${align}`);
1965
+ const img = selectedMedia.querySelector("img");
1966
+ if (img) img.setAttribute("data-align", align);
1967
+ triggerChange();
1755
1968
  };
1756
- const removeImage = () => {
1757
- const wrapper = selectedImage.closest('.image-container');
1758
- if (wrapper) {
1759
- wrapper.remove();
1760
- setSelectedImage(null);
1761
- triggerChange();
1762
- }
1969
+ const setWidth = percent => {
1970
+ applyMediaWidthPercent(selectedMedia, percent);
1971
+ triggerChange();
1763
1972
  };
1764
- const toggleSize = () => {
1765
- const wrapper = selectedImage.closest('.image-container');
1766
- if (wrapper) {
1767
- const isSmall = wrapper.classList.contains('image-small');
1768
- if (isSmall) {
1769
- wrapper.classList.remove('image-small');
1770
- } else {
1771
- wrapper.classList.add('image-small');
1772
- }
1773
- triggerChange();
1973
+ const removeMedia = () => {
1974
+ selectedMedia.remove();
1975
+ clearMediaSelection();
1976
+ triggerChange();
1977
+ };
1978
+ const isActivePercent = percent => {
1979
+ if (!selectedMedia.dataset.widthPercent && !(selectedMedia.style.width || "").endsWith("%")) {
1980
+ return false;
1774
1981
  }
1982
+ return Math.abs(currentPercent - percent) <= 3;
1775
1983
  };
1776
1984
  return /*#__PURE__*/React.createElement("div", {
1777
- className: "rte-image-toolbar",
1985
+ className: "rte-media-toolbar",
1778
1986
  style: {
1779
- position: 'absolute',
1780
- top: Math.max(0, top - 45),
1781
- left: Math.max(0, left + width / 2 - 80),
1987
+ position: "absolute",
1988
+ top: Math.max(0, top - 44),
1989
+ left: Math.max(8, left + width / 2 - 156),
1782
1990
  zIndex: 1000
1783
1991
  }
1784
1992
  }, /*#__PURE__*/React.createElement("button", {
1785
1993
  type: "button",
1786
- onClick: () => handleAlignment('left'),
1994
+ onClick: () => handleAlignment("left"),
1787
1995
  title: "Align Left"
1788
1996
  }, "L"), /*#__PURE__*/React.createElement("button", {
1789
1997
  type: "button",
1790
- onClick: () => handleAlignment('center'),
1998
+ onClick: () => handleAlignment("center"),
1791
1999
  title: "Align Center"
1792
2000
  }, "C"), /*#__PURE__*/React.createElement("button", {
1793
2001
  type: "button",
1794
- onClick: () => handleAlignment('right'),
2002
+ onClick: () => handleAlignment("right"),
1795
2003
  title: "Align Right"
1796
- }, "R"), /*#__PURE__*/React.createElement("button", {
2004
+ }, "R"), /*#__PURE__*/React.createElement("span", {
2005
+ className: "rte-media-toolbar-divider"
2006
+ }), widthPresets.map(percent => /*#__PURE__*/React.createElement("button", {
2007
+ key: percent,
1797
2008
  type: "button",
1798
- onClick: toggleSize,
1799
- title: "Toggle 50% Width"
1800
- }, "50%"), /*#__PURE__*/React.createElement("button", {
2009
+ className: isActivePercent(percent) ? "active" : "",
2010
+ onClick: () => setWidth(percent),
2011
+ title: `${percent}% width`
2012
+ }, percent, "%")), /*#__PURE__*/React.createElement("span", {
2013
+ className: "rte-media-toolbar-divider"
2014
+ }), /*#__PURE__*/React.createElement("button", {
1801
2015
  type: "button",
1802
- onClick: removeImage,
2016
+ onClick: removeMedia,
1803
2017
  className: "danger",
1804
- title: "Remove Image"
2018
+ title: "Remove"
1805
2019
  }, "\xD7"));
1806
2020
  };
1807
2021
  if (isLoading) {
@@ -1837,7 +2051,7 @@ function RichTextEditor({
1837
2051
  e.preventDefault();
1838
2052
  e.stopPropagation();
1839
2053
  }
1840
- }, /*#__PURE__*/React.createElement("div", {
2054
+ }, !disabled && /*#__PURE__*/React.createElement("div", {
1841
2055
  className: "rte-toolbar"
1842
2056
  }, /*#__PURE__*/React.createElement("button", {
1843
2057
  type: "button",
@@ -1885,6 +2099,41 @@ function RichTextEditor({
1885
2099
  backgroundColor: '#e5e7eb',
1886
2100
  margin: '0 4px'
1887
2101
  }
2102
+ }), /*#__PURE__*/React.createElement("select", {
2103
+ value: currentBlockFormat,
2104
+ onMouseDown: e => e.stopPropagation(),
2105
+ onChange: e => {
2106
+ e.preventDefault();
2107
+ e.stopPropagation();
2108
+ applyBlockFormat(e.target.value);
2109
+ },
2110
+ className: "rte-toolbar-select rte-heading-select",
2111
+ title: "Text style"
2112
+ }, /*#__PURE__*/React.createElement("option", {
2113
+ value: "div"
2114
+ }, "Paragraph"), /*#__PURE__*/React.createElement("option", {
2115
+ value: "h1"
2116
+ }, "Heading 1"), /*#__PURE__*/React.createElement("option", {
2117
+ value: "h2"
2118
+ }, "Heading 2"), /*#__PURE__*/React.createElement("option", {
2119
+ value: "h3"
2120
+ }, "Heading 3"), /*#__PURE__*/React.createElement("option", {
2121
+ value: "blockquote"
2122
+ }, "Quote")), /*#__PURE__*/React.createElement("button", {
2123
+ type: "button",
2124
+ title: "Clear Formatting",
2125
+ className: "rte-toolbar-button rte-toolbar-button-text",
2126
+ onMouseDown: e => {
2127
+ e.preventDefault();
2128
+ clearFormatting();
2129
+ }
2130
+ }, "Tx"), /*#__PURE__*/React.createElement("div", {
2131
+ style: {
2132
+ width: '1px',
2133
+ height: '20px',
2134
+ backgroundColor: '#e5e7eb',
2135
+ margin: '0 4px'
2136
+ }
1888
2137
  }), /*#__PURE__*/React.createElement("select", {
1889
2138
  value: currentFontSize,
1890
2139
  onMouseDown: e => e.stopPropagation(),
@@ -2051,11 +2300,9 @@ function RichTextEditor({
2051
2300
  e.preventDefault();
2052
2301
  addLink();
2053
2302
  }
2054
- }, /*#__PURE__*/React.createElement("span", {
2055
- style: {
2056
- fontSize: '16px'
2057
- }
2058
- }, "\uD83D\uDD17")), /*#__PURE__*/React.createElement("input", {
2303
+ }, /*#__PURE__*/React.createElement(FaLink, {
2304
+ size: 14
2305
+ })), /*#__PURE__*/React.createElement("input", {
2059
2306
  ref: fileInputRef,
2060
2307
  type: "file",
2061
2308
  accept: "image/*",
@@ -2222,9 +2469,17 @@ function RichTextEditor({
2222
2469
  }
2223
2470
  return null;
2224
2471
  })()), /*#__PURE__*/React.createElement("div", {
2472
+ className: "rte-content-wrapper",
2473
+ style: {
2474
+ position: 'relative'
2475
+ }
2476
+ }, /*#__PURE__*/React.createElement("div", {
2225
2477
  ref: editorRef,
2226
2478
  contentEditable: editable && disabled !== true,
2227
2479
  suppressContentEditableWarning: true,
2480
+ role: "textbox",
2481
+ "aria-multiline": "true",
2482
+ "aria-label": label || "Rich text editor",
2228
2483
  onInput: handleInput,
2229
2484
  onPaste: handlePaste,
2230
2485
  onDrop: handleDrop,
@@ -2240,7 +2495,13 @@ function RichTextEditor({
2240
2495
  paddingLeft: paddingLeft || '12px'
2241
2496
  },
2242
2497
  className: `rte-content${editable ? " rte-is-editable" : ""}${editorFocused ? " rte-is-focused" : ""}`
2243
- }), renderImageToolbar(), /*#__PURE__*/React.createElement("div", {
2498
+ }), isEmpty && editable && disabled !== true && /*#__PURE__*/React.createElement("div", {
2499
+ className: "rte-placeholder",
2500
+ style: {
2501
+ left: paddingLeft || '12px'
2502
+ },
2503
+ "aria-hidden": "true"
2504
+ }, placeholder)), renderMediaToolbar(), /*#__PURE__*/React.createElement("div", {
2244
2505
  className: "rte-footer"
2245
2506
  }, /*#__PURE__*/React.createElement("div", {
2246
2507
  className: "rte-footer-content"
@@ -2256,13 +2517,11 @@ function RichTextEditor({
2256
2517
  }, /*#__PURE__*/React.createElement("div", {
2257
2518
  className: "rte-modal",
2258
2519
  onClick: e => e.stopPropagation()
2520
+ }, /*#__PURE__*/React.createElement("div", {
2521
+ className: "rte-modal-header"
2259
2522
  }, /*#__PURE__*/React.createElement("h3", {
2260
2523
  className: "rte-modal-title"
2261
- }, "Insert Link"), /*#__PURE__*/React.createElement("div", {
2262
- className: "rte-modal-divider"
2263
- }), /*#__PURE__*/React.createElement("div", {
2264
- className: "rte-modal-body"
2265
- }, /*#__PURE__*/React.createElement("div", {
2524
+ }, "Insert Link")), /*#__PURE__*/React.createElement("div", {
2266
2525
  className: "rte-form-group"
2267
2526
  }, /*#__PURE__*/React.createElement("label", {
2268
2527
  className: "rte-label"
@@ -2284,20 +2543,15 @@ function RichTextEditor({
2284
2543
  onChange: e => setLinkUrl(e.target.value),
2285
2544
  onKeyDown: e => e.key === 'Enter' && confirmLink(),
2286
2545
  autoFocus: true
2287
- }))), /*#__PURE__*/React.createElement("div", {
2288
- className: "rte-modal-divider",
2289
- style: {
2290
- margin: '8px 0 20px 0'
2291
- }
2292
- }), /*#__PURE__*/React.createElement("div", {
2293
- className: "rte-modal-footer"
2546
+ })), /*#__PURE__*/React.createElement("div", {
2547
+ className: "rte-modal-actions"
2294
2548
  }, /*#__PURE__*/React.createElement("button", {
2295
2549
  type: "button",
2296
- className: "rte-btn rte-btn-secondary",
2550
+ className: "rte-button rte-button-secondary",
2297
2551
  onClick: cancelLink
2298
2552
  }, "Cancel"), /*#__PURE__*/React.createElement("button", {
2299
2553
  type: "button",
2300
- className: "rte-btn rte-btn-primary",
2554
+ className: "rte-button rte-button-primary",
2301
2555
  onClick: confirmLink,
2302
2556
  disabled: !linkUrl
2303
2557
  }, "Insert")))), tableModalOpen && /*#__PURE__*/React.createElement("div", {