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.cjs.js CHANGED
@@ -233,6 +233,27 @@ const FaFont = ({
233
233
  }
234
234
  });
235
235
  };
236
+ const FaLink = ({
237
+ className,
238
+ size,
239
+ color,
240
+ style
241
+ }) => {
242
+ return /*#__PURE__*/React.createElement("span", {
243
+ className: className,
244
+ style: {
245
+ display: 'inline-flex',
246
+ alignItems: 'center',
247
+ justifyContent: 'center',
248
+ fontSize: size || '1em',
249
+ color: color || 'inherit',
250
+ ...style
251
+ },
252
+ dangerouslySetInnerHTML: {
253
+ __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>`
254
+ }
255
+ });
256
+ };
236
257
  const FaTable = ({
237
258
  className,
238
259
  size,
@@ -390,7 +411,7 @@ function styleInject(css, ref) {
390
411
  }
391
412
  }
392
413
 
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}";
414
+ 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}";
394
415
  styleInject(css_248z);
395
416
 
396
417
  // Helper functions for HTML escaping
@@ -407,11 +428,12 @@ function RichTextEditor({
407
428
  showEditButton,
408
429
  onBlur,
409
430
  disabled = false,
410
- editable: initialEditable = false,
431
+ editable: initialEditable = true,
411
432
  value,
412
433
  isLoading,
413
434
  isList = false,
414
435
  label,
436
+ placeholder = "Type here...",
415
437
  showBorder = true,
416
438
  paddingLeft,
417
439
  minHeight,
@@ -429,6 +451,7 @@ function RichTextEditor({
429
451
  const [editable, setEditable] = React.useState(initialEditable);
430
452
  const [editorFocused, setEditorFocused] = React.useState(false);
431
453
  const lastSynchronizedHtmlRef = React.useRef("");
454
+ const syncProcessedMediaRef = React.useRef(() => {});
432
455
  React.useEffect(() => {
433
456
  setEditable(initialEditable);
434
457
  }, [initialEditable]);
@@ -447,6 +470,7 @@ function RichTextEditor({
447
470
  // NEW: Track current line height
448
471
  const [currentLineHeight, setCurrentLineHeight] = React.useState("");
449
472
  const [activeAlign, setActiveAlign] = React.useState(null);
473
+ const [currentBlockFormat, setCurrentBlockFormat] = React.useState("div");
450
474
  const [imageModalOpen, setImageModalOpen] = React.useState(false);
451
475
  const [selectedImageUrl, setSelectedImageUrl] = React.useState("");
452
476
  const [zoomLevel, setZoomLevel] = React.useState(1);
@@ -458,11 +482,12 @@ function RichTextEditor({
458
482
  const [tableRows, setTableRows] = React.useState(3);
459
483
  const [tableCols, setTableCols] = React.useState(3);
460
484
  const [selectionVersion, setSelectionVersion] = React.useState(0);
461
- const [selectedImage, setSelectedImage] = React.useState(null);
485
+ const [selectedMedia, setSelectedMedia] = React.useState(null);
462
486
  const [metrics, setMetrics] = React.useState({
463
487
  words: 0,
464
488
  chars: 0
465
489
  });
490
+ const [isEmpty, setIsEmpty] = React.useState(!value);
466
491
  const updateMetrics = React.useCallback(() => {
467
492
  if (!editorRef.current) return;
468
493
  // Calculate metrics immediately but outside of render path
@@ -474,6 +499,11 @@ function RichTextEditor({
474
499
  words,
475
500
  chars
476
501
  });
502
+
503
+ // Track emptiness for the placeholder. Account for media-only content.
504
+ const stripped = text.replace(/[\u200B\u00A0\s]/g, "");
505
+ const hasMedia = !!editorRef.current.querySelector("img, table, iframe");
506
+ setIsEmpty(stripped.length === 0 && !hasMedia);
477
507
  }, []);
478
508
  const openImageModal = url => {
479
509
  if (editorRef.current) {
@@ -535,16 +565,15 @@ function RichTextEditor({
535
565
  }, [imageModalOpen]);
536
566
  React.useEffect(() => {
537
567
  if (editorRef.current && value && value !== lastSynchronizedHtmlRef.current) {
538
- requestAnimationFrame(() => processExistingMedia(editorRef.current));
568
+ requestAnimationFrame(() => syncProcessedMediaRef.current(editorRef.current));
539
569
  }
540
570
  }, [value]);
541
-
542
- // Runs whenever editable changes (toggles delete icon visibility)
543
571
  React.useEffect(() => {
544
572
  if (!editable) {
545
573
  setEditorFocused(false);
574
+ clearMediaSelection();
546
575
  }
547
- processExistingMedia(editorRef.current);
576
+ syncProcessedMediaRef.current(editorRef.current);
548
577
  }, [editable]);
549
578
  React.useEffect(() => {
550
579
  if (!editorRef.current) return;
@@ -575,7 +604,7 @@ function RichTextEditor({
575
604
  if (editorRef.current && editorRef.current.innerHTML !== newContent) {
576
605
  editorRef.current.innerHTML = newContent;
577
606
  }
578
- requestAnimationFrame(() => processExistingMedia(editorRef.current));
607
+ requestAnimationFrame(() => syncProcessedMediaRef.current(editorRef.current));
579
608
  updateMetrics();
580
609
  }
581
610
  } catch (e) {
@@ -590,10 +619,47 @@ function RichTextEditor({
590
619
  }
591
620
  }
592
621
  }, [value, initialEditable, updateMetrics]);
622
+ const LIST_BLOCK_MEDIA_SELECTOR = ".video-container, .image-container, table";
623
+ const isListItemEffectivelyEmpty = listItem => {
624
+ if (!listItem) return true;
625
+ const clone = listItem.cloneNode(true);
626
+ clone.querySelectorAll(LIST_BLOCK_MEDIA_SELECTOR).forEach(el => el.remove());
627
+ clone.querySelectorAll("br").forEach(el => el.remove());
628
+ return clone.textContent.replace(/[\u200B\u00A0\s]/g, "").length === 0;
629
+ };
630
+ const hoistBlockMediaOutOfListItems = container => {
631
+ if (!container) return false;
632
+ let changed = false;
633
+ container.querySelectorAll("ol, ul").forEach(list => {
634
+ const items = Array.from(list.children).filter(child => child.tagName === "LI");
635
+ items.forEach(listItem => {
636
+ const blockMedia = Array.from(listItem.querySelectorAll(LIST_BLOCK_MEDIA_SELECTOR));
637
+ if (blockMedia.length === 0) return;
638
+ const hadText = !isListItemEffectivelyEmpty(listItem);
639
+ blockMedia.forEach(media => {
640
+ listItem.removeChild(media);
641
+ if (list.parentNode) {
642
+ list.parentNode.insertBefore(media, list.nextSibling);
643
+ }
644
+ changed = true;
645
+ });
646
+ if (!hadText || isListItemEffectivelyEmpty(listItem)) {
647
+ listItem.remove();
648
+ changed = true;
649
+ }
650
+ });
651
+ if (list.children.length === 0 && list.parentNode) {
652
+ list.remove();
653
+ changed = true;
654
+ }
655
+ });
656
+ return changed;
657
+ };
593
658
  const processExistingMedia = container => {
594
- if (!container) return;
659
+ if (!container) return false;
595
660
  processExistingImages(container);
596
661
  processExistingVideos(container);
662
+ return hoistBlockMediaOutOfListItems(container);
597
663
  };
598
664
  const getCleanHtml = () => {
599
665
  if (!editorRef.current) return "";
@@ -609,6 +675,11 @@ function RichTextEditor({
609
675
  lastSynchronizedHtmlRef.current = next;
610
676
  onChange && onChange(next);
611
677
  }, [onChange]);
678
+ syncProcessedMediaRef.current = container => {
679
+ if (processExistingMedia(container)) {
680
+ triggerChange();
681
+ }
682
+ };
612
683
 
613
684
  // Helper to walk up DOM to find style tags or CSS style:
614
685
  const isParentStyle = (node, ...tagNames) => {
@@ -664,17 +735,15 @@ function RichTextEditor({
664
735
  return rgbToHex(computedColor);
665
736
  };
666
737
  const stripEditorChrome = root => {
667
- root.querySelectorAll(".image-delete-button, .video-delete-button, .video-edit-overlay, .media-resize-handles, .media-resize-handle").forEach(element => element.remove());
738
+ root.querySelectorAll(".image-delete-button, .video-delete-button, .video-edit-overlay, .media-resize-handle").forEach(element => element.remove());
739
+ root.querySelectorAll(".rte-media-selected").forEach(element => {
740
+ element.classList.remove("rte-media-selected");
741
+ });
668
742
  return root;
669
743
  };
670
- const getMediaSizeLimits = () => {
671
- const maxWidth = editorRef.current ? editorRef.current.getBoundingClientRect().width - 24 : 800;
672
- return {
673
- minWidth: 120,
674
- minHeight: 80,
675
- maxWidth,
676
- maxHeight: 720
677
- };
744
+ const getEditorInnerWidth = () => {
745
+ if (!editorRef.current) return 800;
746
+ return Math.max(editorRef.current.clientWidth - 24, 200);
678
747
  };
679
748
  const ensureImageMediaFrame = imageContainer => {
680
749
  if (!imageContainer) return null;
@@ -682,160 +751,164 @@ function RichTextEditor({
682
751
  if (frame) return frame;
683
752
  frame = document.createElement("div");
684
753
  frame.className = "image-media-frame";
685
- ["width", "height", "marginLeft", "marginTop", "maxWidth"].forEach(prop => {
686
- if (imageContainer.style[prop]) {
687
- frame.style[prop] = imageContainer.style[prop];
688
- imageContainer.style[prop] = "";
689
- }
690
- });
691
- if (imageContainer.dataset.explicitHeight) {
692
- frame.dataset.explicitHeight = imageContainer.dataset.explicitHeight;
693
- delete imageContainer.dataset.explicitHeight;
694
- }
695
754
  const children = Array.from(imageContainer.children);
696
755
  imageContainer.appendChild(frame);
697
756
  children.forEach(child => frame.appendChild(child));
698
757
  return frame;
699
758
  };
700
759
  const getImageMediaTarget = imageContainer => ensureImageMediaFrame(imageContainer) || imageContainer;
701
- const applyImageMediaSize = (frame, width, height, edge) => {
760
+ const getMediaWidthPercent = container => {
761
+ if (!container) return 100;
762
+ if (container.dataset.widthPercent) {
763
+ return Number(container.dataset.widthPercent);
764
+ }
765
+ const width = container.style.width || "";
766
+ if (width.endsWith("%")) {
767
+ return parseInt(width, 10) || 100;
768
+ }
769
+ const editorWidth = getEditorInnerWidth();
770
+ const rect = container.getBoundingClientRect();
771
+ if (editorWidth > 0 && rect.width > 0) {
772
+ return Math.round(rect.width / editorWidth * 100);
773
+ }
774
+ return 100;
775
+ };
776
+ const applyMediaWidthPercent = (container, percent) => {
777
+ if (!container) return;
778
+ const clamped = Math.max(25, Math.min(100, Math.round(percent)));
779
+ container.dataset.widthPercent = String(clamped);
780
+ container.style.width = `${clamped}%`;
781
+ container.style.maxWidth = "100%";
782
+ container.style.marginLeft = "";
783
+ container.style.marginTop = "";
784
+ if (container.classList.contains("video-container")) {
785
+ container.style.height = "0";
786
+ container.style.paddingBottom = "56.25%";
787
+ return;
788
+ }
789
+ const frame = getImageMediaTarget(container);
790
+ if (!frame) return;
791
+ frame.style.width = "100%";
792
+ frame.style.height = "";
702
793
  const img = frame.querySelector("img");
703
- const isVertical = edge === "top" || edge === "bottom";
704
- frame.style.width = `${Math.round(width)}px`;
705
- frame.style.maxWidth = "100%";
706
- if (isVertical) {
707
- frame.style.height = `${Math.round(height)}px`;
708
- frame.dataset.explicitHeight = "true";
709
- if (img) {
710
- img.style.width = "100%";
711
- img.style.height = "100%";
712
- img.style.objectFit = "contain";
713
- }
794
+ if (img) {
795
+ img.style.height = "auto";
796
+ img.style.objectFit = "";
797
+ }
798
+ };
799
+ const normalizeMediaWidth = container => {
800
+ if (!container) return;
801
+ if (container.classList.contains("image-small")) {
802
+ container.classList.remove("image-small");
803
+ applyMediaWidthPercent(container, 50);
714
804
  return;
715
805
  }
716
- if (!frame.dataset.explicitHeight) {
717
- frame.style.height = "";
806
+ if (container.dataset.widthPercent) {
807
+ applyMediaWidthPercent(container, Number(container.dataset.widthPercent));
808
+ return;
718
809
  }
719
- if (img) {
720
- img.style.width = "100%";
721
- if (frame.dataset.explicitHeight) {
722
- img.style.height = "100%";
723
- img.style.objectFit = "contain";
724
- } else {
725
- img.style.height = "auto";
726
- img.style.objectFit = "";
810
+ const width = container.style.width || "";
811
+ if (width.endsWith("%")) {
812
+ applyMediaWidthPercent(container, parseInt(width, 10) || 100);
813
+ return;
814
+ }
815
+ if (width.endsWith("px")) {
816
+ const editorWidth = getEditorInnerWidth();
817
+ const px = parseFloat(width);
818
+ if (editorWidth > 0 && px > 0) {
819
+ applyMediaWidthPercent(container, Math.round(px / editorWidth * 100));
727
820
  }
821
+ return;
822
+ }
823
+ if (container.classList.contains("video-container")) {
824
+ applyMediaWidthPercent(container, 100);
728
825
  }
729
826
  };
730
- const applyVideoMediaSize = (container, width, height) => {
731
- container.style.paddingBottom = "0";
732
- container.style.width = `${Math.round(width)}px`;
733
- container.style.maxWidth = "100%";
734
- container.style.height = `${Math.round(height)}px`;
827
+ const clearMediaSelection = () => {
828
+ var _editorRef$current;
829
+ (_editorRef$current = editorRef.current) === null || _editorRef$current === void 0 || _editorRef$current.querySelectorAll(".rte-media-selected").forEach(element => {
830
+ element.classList.remove("rte-media-selected");
831
+ });
832
+ setSelectedMedia(null);
833
+ };
834
+ const selectMediaContainer = container => {
835
+ var _editorRef$current2;
836
+ if (!container || !((_editorRef$current2 = editorRef.current) !== null && _editorRef$current2 !== void 0 && _editorRef$current2.contains(container))) return;
837
+ editorRef.current.querySelectorAll(".rte-media-selected").forEach(element => {
838
+ element.classList.remove("rte-media-selected");
839
+ });
840
+ container.classList.add("rte-media-selected");
841
+ setSelectedMedia(container);
735
842
  };
736
843
  const attachMediaResizeHandle = container => {
737
- if (!container || container.querySelector(".media-resize-handles")) return;
738
- const isVideo = container.classList.contains("video-container");
739
- const resizeTarget = isVideo ? container : getImageMediaTarget(container);
844
+ var _resizeTarget$querySe;
845
+ if (!container) return;
846
+ const resizeTarget = container.classList.contains("video-container") ? container : getImageMediaTarget(container);
740
847
  if (!resizeTarget) return;
741
- const handlesWrapper = document.createElement("div");
742
- handlesWrapper.className = "media-resize-handles";
743
- handlesWrapper.setAttribute("contenteditable", "false");
744
- const edges = [{
745
- edge: "left",
746
- title: "Drag to resize width"
747
- }, {
748
- edge: "right",
749
- title: "Drag to resize width"
750
- }, {
751
- edge: "top",
752
- title: "Drag to resize height"
753
- }, {
754
- edge: "bottom",
755
- title: "Drag to resize height"
756
- }];
757
- edges.forEach(({
758
- edge,
759
- title
760
- }) => {
761
- const handle = document.createElement("div");
762
- handle.className = `media-resize-handle media-resize-handle-${edge}`;
763
- handle.title = title;
764
- handle.setAttribute("contenteditable", "false");
765
- handle.dataset.edge = edge;
766
- handle.addEventListener("mousedown", event => {
767
- if (!editable) return;
768
- event.preventDefault();
769
- event.stopPropagation();
770
- const limits = getMediaSizeLimits();
771
- const rect = resizeTarget.getBoundingClientRect();
772
- const startX = event.clientX;
773
- const startY = event.clientY;
774
- const startWidth = rect.width;
775
- const startHeight = rect.height;
776
- const startMarginLeft = Number.parseFloat(resizeTarget.style.marginLeft) || 0;
777
- const startMarginTop = Number.parseFloat(resizeTarget.style.marginTop) || 0;
778
- if (isVideo) {
779
- resizeTarget.style.paddingBottom = "0";
780
- }
781
- const onMouseMove = moveEvent => {
782
- const deltaX = moveEvent.clientX - startX;
783
- const deltaY = moveEvent.clientY - startY;
784
- let nextWidth = startWidth;
785
- let nextHeight = startHeight;
786
- if (edge === "right") {
787
- nextWidth = startWidth + deltaX;
788
- } else if (edge === "left") {
789
- nextWidth = startWidth - deltaX;
790
- } else if (edge === "bottom") {
791
- nextHeight = startHeight + deltaY;
792
- } else if (edge === "top") {
793
- nextHeight = startHeight - deltaY;
794
- }
795
- nextWidth = Math.max(limits.minWidth, Math.min(nextWidth, limits.maxWidth));
796
- nextHeight = Math.max(limits.minHeight, Math.min(nextHeight, limits.maxHeight));
797
- if (edge === "left") {
798
- resizeTarget.style.marginLeft = `${Math.round(startMarginLeft + (startWidth - nextWidth))}px`;
799
- }
800
- if (edge === "top") {
801
- resizeTarget.style.marginTop = `${Math.round(startMarginTop + (startHeight - nextHeight))}px`;
802
- }
803
- if (isVideo) {
804
- applyVideoMediaSize(resizeTarget, nextWidth, nextHeight);
805
- } else {
806
- applyImageMediaSize(resizeTarget, nextWidth, nextHeight, edge);
807
- }
808
- };
809
- const onMouseUp = () => {
810
- document.removeEventListener("mousemove", onMouseMove);
811
- document.removeEventListener("mouseup", onMouseUp);
812
- triggerChange();
813
- };
814
- document.addEventListener("mousemove", onMouseMove);
815
- document.addEventListener("mouseup", onMouseUp);
816
- });
817
- handlesWrapper.appendChild(handle);
848
+ (_resizeTarget$querySe = resizeTarget.querySelector(".media-resize-handle")) === null || _resizeTarget$querySe === void 0 || _resizeTarget$querySe.remove();
849
+ const handle = document.createElement("div");
850
+ handle.className = "media-resize-handle";
851
+ handle.title = "Drag to resize";
852
+ handle.setAttribute("contenteditable", "false");
853
+ handle.addEventListener("mousedown", event => {
854
+ if (!editable) return;
855
+ event.preventDefault();
856
+ event.stopPropagation();
857
+ selectMediaContainer(container);
858
+ const editorWidth = getEditorInnerWidth();
859
+ const startX = event.clientX;
860
+ const startWidth = container.getBoundingClientRect().width;
861
+ const onMouseMove = moveEvent => {
862
+ const nextWidth = Math.max(60, startWidth + (moveEvent.clientX - startX));
863
+ const percent = Math.round(nextWidth / editorWidth * 100);
864
+ applyMediaWidthPercent(container, percent);
865
+ };
866
+ const onMouseUp = () => {
867
+ document.removeEventListener("mousemove", onMouseMove);
868
+ document.removeEventListener("mouseup", onMouseUp);
869
+ triggerChange();
870
+ };
871
+ document.addEventListener("mousemove", onMouseMove);
872
+ document.addEventListener("mouseup", onMouseUp);
818
873
  });
819
- resizeTarget.appendChild(handlesWrapper);
874
+ resizeTarget.appendChild(handle);
820
875
  };
821
876
  const handleEditorFocus = () => {
822
877
  setEditorFocused(true);
823
878
  };
824
879
  const handleEditorBlur = () => {
825
880
  requestAnimationFrame(() => {
826
- var _editorRef$current;
827
- if (!((_editorRef$current = editorRef.current) !== null && _editorRef$current !== void 0 && _editorRef$current.contains(document.activeElement))) {
881
+ var _editorRef$current3;
882
+ if (!((_editorRef$current3 = editorRef.current) !== null && _editorRef$current3 !== void 0 && _editorRef$current3.contains(document.activeElement))) {
828
883
  setEditorFocused(false);
829
884
  }
830
885
  });
831
886
  };
832
887
  const updateMediaControlVisibility = container => {
833
- const handles = container.querySelector(".media-resize-handles");
834
- if (handles instanceof HTMLElement) {
835
- handles.style.display = editable ? "block" : "none";
836
- handles.style.pointerEvents = editable ? "auto" : "none";
888
+ const handle = container.querySelector(".media-resize-handle");
889
+ if (handle instanceof HTMLElement) {
890
+ handle.style.display = editable ? "block" : "none";
891
+ handle.style.pointerEvents = editable ? "auto" : "none";
837
892
  }
838
893
  };
894
+ const BLOCK_TAGS = ["P", "DIV", "H1", "H2", "H3", "BLOCKQUOTE", "LI"];
895
+ const getActiveBlock = node => {
896
+ if (!editorRef.current || !node) return null;
897
+ let current = node.nodeType === 3 ? node.parentNode : node;
898
+ while (current && current !== editorRef.current) {
899
+ if (current.nodeType === 1 && BLOCK_TAGS.includes(current.tagName)) {
900
+ return current;
901
+ }
902
+ current = current.parentNode;
903
+ }
904
+ return editorRef.current;
905
+ };
906
+ const getBlockFormat = node => {
907
+ const block = getActiveBlock(node);
908
+ if (!block || block === editorRef.current) return "div";
909
+ const tag = block.tagName.toLowerCase();
910
+ return tag === "p" || tag === "li" ? "div" : tag;
911
+ };
839
912
  const createMediaDeleteButton = (title, className, onRemove) => {
840
913
  const deleteBtn = document.createElement("button");
841
914
  deleteBtn.type = "button";
@@ -854,10 +927,10 @@ function RichTextEditor({
854
927
  // Listen for selection changes globally to update styles and list type in one pass
855
928
  React.useEffect(() => {
856
929
  const handleGlobalSelectionSync = () => {
857
- var _editorRef$current2;
930
+ var _editorRef$current4;
858
931
  // Only sync if the editor has focus
859
932
  const sel = window.getSelection();
860
- if (!sel || !sel.rangeCount || !((_editorRef$current2 = editorRef.current) !== null && _editorRef$current2 !== void 0 && _editorRef$current2.contains(sel.anchorNode))) {
933
+ if (!sel || !sel.rangeCount || !((_editorRef$current4 = editorRef.current) !== null && _editorRef$current4 !== void 0 && _editorRef$current4.contains(sel.anchorNode))) {
861
934
  return;
862
935
  }
863
936
 
@@ -905,6 +978,7 @@ function RichTextEditor({
905
978
  } else {
906
979
  setCurrentFontSize("16");
907
980
  }
981
+ setCurrentBlockFormat(getBlockFormat(sel.anchorNode));
908
982
  };
909
983
  document.addEventListener("selectionchange", handleGlobalSelectionSync);
910
984
  return () => {
@@ -1128,8 +1202,10 @@ function RichTextEditor({
1128
1202
  }
1129
1203
  setVideoModalOpen(false);
1130
1204
  setVideoUrl("");
1131
- triggerChange && triggerChange();
1132
- requestAnimationFrame(() => processExistingMedia(editorRef.current));
1205
+ requestAnimationFrame(() => {
1206
+ processExistingMedia(editorRef.current);
1207
+ triggerChange();
1208
+ });
1133
1209
  } else {
1134
1210
  console.warn("Invalid Video URL or Platform not supported");
1135
1211
  }
@@ -1140,11 +1216,25 @@ function RichTextEditor({
1140
1216
  if (!videoContainer.querySelector(".video-delete-button")) {
1141
1217
  const deleteBtn = createMediaDeleteButton("Remove video", "video-delete-button image-delete-button", () => {
1142
1218
  videoContainer.remove();
1219
+ clearMediaSelection();
1143
1220
  triggerChange && triggerChange();
1144
1221
  });
1145
1222
  videoContainer.appendChild(deleteBtn);
1146
1223
  }
1224
+ if (!videoContainer.dataset.mediaEnhanced) {
1225
+ videoContainer.dataset.mediaEnhanced = "true";
1226
+ if (!videoContainer.classList.contains("image-align-left") && !videoContainer.classList.contains("image-align-center") && !videoContainer.classList.contains("image-align-right")) {
1227
+ videoContainer.classList.add("image-align-left");
1228
+ }
1229
+ videoContainer.addEventListener("click", event => {
1230
+ if (event.target.closest(".image-delete-button, .media-resize-handle")) return;
1231
+ event.preventDefault();
1232
+ event.stopPropagation();
1233
+ selectMediaContainer(videoContainer);
1234
+ });
1235
+ }
1147
1236
  attachMediaResizeHandle(videoContainer);
1237
+ normalizeMediaWidth(videoContainer);
1148
1238
  updateMediaControlVisibility(videoContainer);
1149
1239
  });
1150
1240
  };
@@ -1164,6 +1254,7 @@ function RichTextEditor({
1164
1254
  }));
1165
1255
  }
1166
1256
  attachMediaResizeHandle(existingWrapper);
1257
+ normalizeMediaWidth(existingWrapper);
1167
1258
  updateMediaControlVisibility(existingWrapper);
1168
1259
  return;
1169
1260
  }
@@ -1173,24 +1264,21 @@ function RichTextEditor({
1173
1264
  wrapper.style.cursor = editable ? "pointer" : "default";
1174
1265
  const frame = document.createElement("div");
1175
1266
  frame.className = "image-media-frame";
1176
- if (img.getAttribute("width") && !frame.style.width) {
1177
- frame.style.width = `${img.getAttribute("width")}px`;
1178
- } else if (img.style.width && img.style.width.endsWith("px")) {
1179
- frame.style.width = img.style.width;
1180
- }
1181
1267
  img.classList.add("rte-image");
1182
1268
  img.setAttribute("data-align", align);
1183
- if (frame.style.width) {
1184
- img.style.width = "100%";
1185
- img.style.height = frame.dataset.explicitHeight ? "100%" : "auto";
1186
- } else {
1187
- img.style.width = "";
1188
- img.style.height = "auto";
1189
- }
1190
- img.addEventListener("click", event => {
1269
+ img.style.height = "auto";
1270
+ img.addEventListener("dblclick", event => {
1191
1271
  if (event.target.closest(".image-delete-button, .media-resize-handle")) return;
1272
+ event.preventDefault();
1273
+ event.stopPropagation();
1192
1274
  openImageModal(img.src);
1193
1275
  });
1276
+ img.addEventListener("click", event => {
1277
+ if (event.target.closest(".image-delete-button, .media-resize-handle")) return;
1278
+ event.preventDefault();
1279
+ event.stopPropagation();
1280
+ selectMediaContainer(wrapper);
1281
+ });
1194
1282
  const deleteBtn = createMediaDeleteButton("Remove image", "image-delete-button", () => {
1195
1283
  wrapper.remove();
1196
1284
  triggerChange && triggerChange();
@@ -1205,6 +1293,7 @@ function RichTextEditor({
1205
1293
  frame.appendChild(deleteBtn);
1206
1294
  wrapper.appendChild(frame);
1207
1295
  attachMediaResizeHandle(wrapper);
1296
+ normalizeMediaWidth(wrapper);
1208
1297
  if (nextSibling) {
1209
1298
  parentNode.insertBefore(wrapper, nextSibling);
1210
1299
  } else {
@@ -1231,9 +1320,19 @@ function RichTextEditor({
1231
1320
  const img = document.createElement('img');
1232
1321
  img.src = dataUrl;
1233
1322
  img.alt = fileName || "image";
1234
- img.addEventListener("click", () => openImageModal(dataUrl));
1323
+ img.addEventListener("dblclick", event => {
1324
+ event.preventDefault();
1325
+ event.stopPropagation();
1326
+ openImageModal(dataUrl);
1327
+ });
1328
+ img.addEventListener("click", event => {
1329
+ event.preventDefault();
1330
+ event.stopPropagation();
1331
+ selectMediaContainer(container);
1332
+ });
1235
1333
  frame.appendChild(img);
1236
1334
  container.appendChild(frame);
1335
+ applyMediaWidthPercent(container, 25);
1237
1336
 
1238
1337
  // Insert at cursor position
1239
1338
  insertNodeAtCursor(container);
@@ -1376,6 +1475,12 @@ function RichTextEditor({
1376
1475
  txt.innerHTML = html;
1377
1476
  return txt.value;
1378
1477
  };
1478
+ const isCursorAtStartOfListItem = (range, listItem) => {
1479
+ const prefixRange = document.createRange();
1480
+ prefixRange.setStart(listItem, 0);
1481
+ prefixRange.setEnd(range.startContainer, range.startOffset);
1482
+ return prefixRange.toString().replace(/[\u200B\u00A0\s]/g, "").length === 0;
1483
+ };
1379
1484
  const isCursorAtEndOfListItem = (range, listItem) => {
1380
1485
  const suffixRange = document.createRange();
1381
1486
  suffixRange.setStart(range.startContainer, range.startOffset);
@@ -1400,6 +1505,8 @@ function RichTextEditor({
1400
1505
  selection.addRange(newRange);
1401
1506
  };
1402
1507
  const handleKeyDown = React.useCallback(e => {
1508
+ if (applyMarkdownShortcut(e)) return;
1509
+
1403
1510
  // Handle Enter key
1404
1511
  if (e.key === 'Enter') {
1405
1512
  e.preventDefault();
@@ -1420,7 +1527,7 @@ function RichTextEditor({
1420
1527
  // If we're at the end of a list item, add a new one
1421
1528
  if (range.collapsed && isCursorAtEndOfListItem(range, listItem)) {
1422
1529
  // If it's empty, create a regular paragraph instead
1423
- if (listItem.textContent.replace(/\u200B/g, '').trim() === '') {
1530
+ if (isListItemEffectivelyEmpty(listItem)) {
1424
1531
  document.execCommand('insertHTML', false, '<div><br></div>');
1425
1532
  // Move the cursor to the new line
1426
1533
  const newRange = document.createRange();
@@ -1452,7 +1559,7 @@ function RichTextEditor({
1452
1559
  } else {
1453
1560
  list.appendChild(newItem);
1454
1561
  }
1455
- if (!newItem.textContent.replace(/\u200B/g, '').trim()) {
1562
+ if (isListItemEffectivelyEmpty(newItem)) {
1456
1563
  newItem.textContent = "";
1457
1564
  prepareListItemForTyping(newItem, selection);
1458
1565
  } else {
@@ -1470,6 +1577,54 @@ function RichTextEditor({
1470
1577
  triggerChange();
1471
1578
  return;
1472
1579
  }
1580
+ if (e.key === "Backspace") {
1581
+ var _node$closest, _node, _editorRef$current5;
1582
+ const selection = window.getSelection();
1583
+ if (!(selection !== null && selection !== void 0 && selection.rangeCount)) return;
1584
+ const range = selection.getRangeAt(0);
1585
+ if (!range.collapsed) return;
1586
+ let node = range.startContainer;
1587
+ if (node.nodeType === 3) {
1588
+ node = node.parentNode;
1589
+ }
1590
+ const listItem = (_node$closest = (_node = node).closest) === null || _node$closest === void 0 ? void 0 : _node$closest.call(_node, "li");
1591
+ if (!listItem || !((_editorRef$current5 = editorRef.current) !== null && _editorRef$current5 !== void 0 && _editorRef$current5.contains(listItem))) return;
1592
+ const list = listItem.parentNode;
1593
+ if (isListItemEffectivelyEmpty(listItem)) {
1594
+ e.preventDefault();
1595
+ const prevLi = listItem.previousElementSibling;
1596
+ const blockMedia = Array.from(listItem.querySelectorAll(LIST_BLOCK_MEDIA_SELECTOR));
1597
+ blockMedia.forEach(media => {
1598
+ var _list$parentNode;
1599
+ (_list$parentNode = list.parentNode) === null || _list$parentNode === void 0 || _list$parentNode.insertBefore(media, list.nextSibling);
1600
+ });
1601
+ listItem.remove();
1602
+ if (list.children.length === 0) {
1603
+ list.remove();
1604
+ }
1605
+ if ((prevLi === null || prevLi === void 0 ? void 0 : prevLi.tagName) === "LI") {
1606
+ const newRange = document.createRange();
1607
+ newRange.selectNodeContents(prevLi);
1608
+ newRange.collapse(false);
1609
+ selection.removeAllRanges();
1610
+ selection.addRange(newRange);
1611
+ }
1612
+ triggerChange();
1613
+ return;
1614
+ }
1615
+ if (isCursorAtStartOfListItem(range, listItem)) {
1616
+ const prevLi = listItem.previousElementSibling;
1617
+ if ((prevLi === null || prevLi === void 0 ? void 0 : prevLi.tagName) === "LI" && isListItemEffectivelyEmpty(prevLi)) {
1618
+ e.preventDefault();
1619
+ prevLi.remove();
1620
+ if (list.children.length === 0) {
1621
+ list.remove();
1622
+ }
1623
+ triggerChange();
1624
+ }
1625
+ }
1626
+ return;
1627
+ }
1473
1628
 
1474
1629
  // Handle Ctrl/Cmd + B/I/U for bold/italic/underline
1475
1630
  if ((e.ctrlKey || e.metaKey) && e.key === "b") {
@@ -1510,6 +1665,65 @@ function RichTextEditor({
1510
1665
  const handleSelect = type => {
1511
1666
  exec(type === "unordered" ? "insertUnorderedList" : "insertOrderedList");
1512
1667
  };
1668
+ const applyBlockFormat = format => {
1669
+ document.execCommand("formatBlock", false, format);
1670
+ setCurrentBlockFormat(format);
1671
+ triggerChange();
1672
+ focus();
1673
+ };
1674
+ const clearFormatting = () => {
1675
+ document.execCommand("removeFormat", false, null);
1676
+ document.execCommand("unlink", false, null);
1677
+ document.execCommand("formatBlock", false, "div");
1678
+ setCurrentBlockFormat("div");
1679
+ setCurrentFontSize("16");
1680
+ setCurrentLineHeight("");
1681
+ setFontColor("#000000");
1682
+ triggerChange();
1683
+ focus();
1684
+ };
1685
+ const deleteTextBeforeCursorInBlock = (block, range, selection) => {
1686
+ const prefixRange = document.createRange();
1687
+ prefixRange.setStart(block, 0);
1688
+ prefixRange.setEnd(range.startContainer, range.startOffset);
1689
+ prefixRange.deleteContents();
1690
+ const nextRange = document.createRange();
1691
+ nextRange.setStart(block, 0);
1692
+ nextRange.collapse(true);
1693
+ selection.removeAllRanges();
1694
+ selection.addRange(nextRange);
1695
+ };
1696
+ const applyMarkdownShortcut = event => {
1697
+ if (event.key !== " " || event.ctrlKey || event.metaKey || event.altKey) {
1698
+ return false;
1699
+ }
1700
+ const selection = window.getSelection();
1701
+ if (!(selection !== null && selection !== void 0 && selection.rangeCount) || !selection.isCollapsed || !editorRef.current) {
1702
+ return false;
1703
+ }
1704
+ const range = selection.getRangeAt(0);
1705
+ const block = getActiveBlock(range.startContainer);
1706
+ if (!block || !editorRef.current.contains(block)) return false;
1707
+ const prefixRange = document.createRange();
1708
+ prefixRange.setStart(block, 0);
1709
+ prefixRange.setEnd(range.startContainer, range.startOffset);
1710
+ const textBeforeCursor = prefixRange.toString().replace(/\u00A0/g, " ").trim();
1711
+ const shortcuts = {
1712
+ "#": () => applyBlockFormat("h1"),
1713
+ "##": () => applyBlockFormat("h2"),
1714
+ "###": () => applyBlockFormat("h3"),
1715
+ ">": () => applyBlockFormat("blockquote"),
1716
+ "-": () => handleSelect("unordered"),
1717
+ "*": () => handleSelect("unordered"),
1718
+ "1.": () => handleSelect("ordered")
1719
+ };
1720
+ const action = shortcuts[textBeforeCursor];
1721
+ if (!action) return false;
1722
+ event.preventDefault();
1723
+ deleteTextBeforeCursorInBlock(block, range, selection);
1724
+ action();
1725
+ return true;
1726
+ };
1513
1727
  const onLineHeightChange = value => {
1514
1728
  if (!value) return;
1515
1729
  const sel = window.getSelection();
@@ -1691,6 +1905,7 @@ function RichTextEditor({
1691
1905
  }
1692
1906
  }, [disabled]);
1693
1907
  const handleEditorClick = React.useCallback(e => {
1908
+ var _editorRef$current6;
1694
1909
  setSelectionVersion(v => v + 1);
1695
1910
  const deleteBtn = e.target.closest('button[title="Remove image"], button[title="Remove video"]');
1696
1911
  if (deleteBtn && editable && editorFocused) {
@@ -1699,10 +1914,15 @@ function RichTextEditor({
1699
1914
  const wrapper = deleteBtn.closest('.image-container, .video-container');
1700
1915
  if (wrapper) {
1701
1916
  wrapper.remove();
1917
+ clearMediaSelection();
1702
1918
  triggerChange();
1703
1919
  }
1704
1920
  return;
1705
1921
  }
1922
+ const clickedMedia = e.target.closest('.image-container, .video-container');
1923
+ if (clickedMedia && (_editorRef$current6 = editorRef.current) !== null && _editorRef$current6 !== void 0 && _editorRef$current6.contains(clickedMedia) && editable) {
1924
+ return;
1925
+ }
1706
1926
 
1707
1927
  // Check if the click is on a link
1708
1928
  const clickedLink = e.target.closest('a');
@@ -1712,13 +1932,8 @@ function RichTextEditor({
1712
1932
  window.open(clickedLink.href, '_blank');
1713
1933
  return;
1714
1934
  }
1715
-
1716
- // NEW: Check if click is on an image for resizing
1717
- const clickedImg = e.target.closest('img');
1718
- if (clickedImg && !clickedImg.closest('.rte-modal')) {
1719
- setSelectedImage(clickedImg);
1720
- } else if (!e.target.closest('.rte-image-toolbar')) {
1721
- setSelectedImage(null);
1935
+ if (!e.target.closest('.rte-media-toolbar')) {
1936
+ clearMediaSelection();
1722
1937
  }
1723
1938
 
1724
1939
  // If disabled is true, prevent editing
@@ -1737,73 +1952,72 @@ function RichTextEditor({
1737
1952
  }, 0);
1738
1953
  }
1739
1954
  }, [editable, disabled, editorFocused, triggerChange]);
1740
- const renderImageToolbar = () => {
1741
- if (!selectedImage || !editorRef.current || !editable) return null;
1955
+ const renderMediaToolbar = () => {
1956
+ if (!selectedMedia || !editorRef.current || !editable) return null;
1742
1957
  const editorRect = editorRef.current.getBoundingClientRect();
1743
- const imgRect = selectedImage.getBoundingClientRect();
1744
- const top = imgRect.top - editorRect.top + editorRef.current.scrollTop;
1745
- const left = imgRect.left - editorRect.left + editorRef.current.scrollLeft;
1746
- const width = imgRect.width;
1958
+ const mediaRect = selectedMedia.getBoundingClientRect();
1959
+ const top = mediaRect.top - editorRect.top + editorRef.current.scrollTop;
1960
+ const left = mediaRect.left - editorRect.left + editorRef.current.scrollLeft;
1961
+ const width = mediaRect.width;
1962
+ const currentPercent = getMediaWidthPercent(selectedMedia);
1963
+ const widthPresets = [25, 50, 75, 100];
1747
1964
  const handleAlignment = align => {
1748
- const wrapper = selectedImage.closest('.image-container');
1749
- if (wrapper) {
1750
- // Remove all alignment classes first
1751
- wrapper.classList.remove('image-align-left', 'image-align-center', 'image-align-right');
1752
- // Add the new alignment class
1753
- wrapper.classList.add(`image-align-${align}`);
1754
- selectedImage.setAttribute('data-align', align);
1755
- triggerChange();
1756
- }
1965
+ selectedMedia.classList.remove("image-align-left", "image-align-center", "image-align-right");
1966
+ selectedMedia.classList.add(`image-align-${align}`);
1967
+ const img = selectedMedia.querySelector("img");
1968
+ if (img) img.setAttribute("data-align", align);
1969
+ triggerChange();
1757
1970
  };
1758
- const removeImage = () => {
1759
- const wrapper = selectedImage.closest('.image-container');
1760
- if (wrapper) {
1761
- wrapper.remove();
1762
- setSelectedImage(null);
1763
- triggerChange();
1764
- }
1971
+ const setWidth = percent => {
1972
+ applyMediaWidthPercent(selectedMedia, percent);
1973
+ triggerChange();
1765
1974
  };
1766
- const toggleSize = () => {
1767
- const wrapper = selectedImage.closest('.image-container');
1768
- if (wrapper) {
1769
- const isSmall = wrapper.classList.contains('image-small');
1770
- if (isSmall) {
1771
- wrapper.classList.remove('image-small');
1772
- } else {
1773
- wrapper.classList.add('image-small');
1774
- }
1775
- triggerChange();
1975
+ const removeMedia = () => {
1976
+ selectedMedia.remove();
1977
+ clearMediaSelection();
1978
+ triggerChange();
1979
+ };
1980
+ const isActivePercent = percent => {
1981
+ if (!selectedMedia.dataset.widthPercent && !(selectedMedia.style.width || "").endsWith("%")) {
1982
+ return false;
1776
1983
  }
1984
+ return Math.abs(currentPercent - percent) <= 3;
1777
1985
  };
1778
1986
  return /*#__PURE__*/React.createElement("div", {
1779
- className: "rte-image-toolbar",
1987
+ className: "rte-media-toolbar",
1780
1988
  style: {
1781
- position: 'absolute',
1782
- top: Math.max(0, top - 45),
1783
- left: Math.max(0, left + width / 2 - 80),
1989
+ position: "absolute",
1990
+ top: Math.max(0, top - 44),
1991
+ left: Math.max(8, left + width / 2 - 156),
1784
1992
  zIndex: 1000
1785
1993
  }
1786
1994
  }, /*#__PURE__*/React.createElement("button", {
1787
1995
  type: "button",
1788
- onClick: () => handleAlignment('left'),
1996
+ onClick: () => handleAlignment("left"),
1789
1997
  title: "Align Left"
1790
1998
  }, "L"), /*#__PURE__*/React.createElement("button", {
1791
1999
  type: "button",
1792
- onClick: () => handleAlignment('center'),
2000
+ onClick: () => handleAlignment("center"),
1793
2001
  title: "Align Center"
1794
2002
  }, "C"), /*#__PURE__*/React.createElement("button", {
1795
2003
  type: "button",
1796
- onClick: () => handleAlignment('right'),
2004
+ onClick: () => handleAlignment("right"),
1797
2005
  title: "Align Right"
1798
- }, "R"), /*#__PURE__*/React.createElement("button", {
2006
+ }, "R"), /*#__PURE__*/React.createElement("span", {
2007
+ className: "rte-media-toolbar-divider"
2008
+ }), widthPresets.map(percent => /*#__PURE__*/React.createElement("button", {
2009
+ key: percent,
1799
2010
  type: "button",
1800
- onClick: toggleSize,
1801
- title: "Toggle 50% Width"
1802
- }, "50%"), /*#__PURE__*/React.createElement("button", {
2011
+ className: isActivePercent(percent) ? "active" : "",
2012
+ onClick: () => setWidth(percent),
2013
+ title: `${percent}% width`
2014
+ }, percent, "%")), /*#__PURE__*/React.createElement("span", {
2015
+ className: "rte-media-toolbar-divider"
2016
+ }), /*#__PURE__*/React.createElement("button", {
1803
2017
  type: "button",
1804
- onClick: removeImage,
2018
+ onClick: removeMedia,
1805
2019
  className: "danger",
1806
- title: "Remove Image"
2020
+ title: "Remove"
1807
2021
  }, "\xD7"));
1808
2022
  };
1809
2023
  if (isLoading) {
@@ -1839,7 +2053,7 @@ function RichTextEditor({
1839
2053
  e.preventDefault();
1840
2054
  e.stopPropagation();
1841
2055
  }
1842
- }, /*#__PURE__*/React.createElement("div", {
2056
+ }, !disabled && /*#__PURE__*/React.createElement("div", {
1843
2057
  className: "rte-toolbar"
1844
2058
  }, /*#__PURE__*/React.createElement("button", {
1845
2059
  type: "button",
@@ -1887,6 +2101,41 @@ function RichTextEditor({
1887
2101
  backgroundColor: '#e5e7eb',
1888
2102
  margin: '0 4px'
1889
2103
  }
2104
+ }), /*#__PURE__*/React.createElement("select", {
2105
+ value: currentBlockFormat,
2106
+ onMouseDown: e => e.stopPropagation(),
2107
+ onChange: e => {
2108
+ e.preventDefault();
2109
+ e.stopPropagation();
2110
+ applyBlockFormat(e.target.value);
2111
+ },
2112
+ className: "rte-toolbar-select rte-heading-select",
2113
+ title: "Text style"
2114
+ }, /*#__PURE__*/React.createElement("option", {
2115
+ value: "div"
2116
+ }, "Paragraph"), /*#__PURE__*/React.createElement("option", {
2117
+ value: "h1"
2118
+ }, "Heading 1"), /*#__PURE__*/React.createElement("option", {
2119
+ value: "h2"
2120
+ }, "Heading 2"), /*#__PURE__*/React.createElement("option", {
2121
+ value: "h3"
2122
+ }, "Heading 3"), /*#__PURE__*/React.createElement("option", {
2123
+ value: "blockquote"
2124
+ }, "Quote")), /*#__PURE__*/React.createElement("button", {
2125
+ type: "button",
2126
+ title: "Clear Formatting",
2127
+ className: "rte-toolbar-button rte-toolbar-button-text",
2128
+ onMouseDown: e => {
2129
+ e.preventDefault();
2130
+ clearFormatting();
2131
+ }
2132
+ }, "Tx"), /*#__PURE__*/React.createElement("div", {
2133
+ style: {
2134
+ width: '1px',
2135
+ height: '20px',
2136
+ backgroundColor: '#e5e7eb',
2137
+ margin: '0 4px'
2138
+ }
1890
2139
  }), /*#__PURE__*/React.createElement("select", {
1891
2140
  value: currentFontSize,
1892
2141
  onMouseDown: e => e.stopPropagation(),
@@ -2053,11 +2302,9 @@ function RichTextEditor({
2053
2302
  e.preventDefault();
2054
2303
  addLink();
2055
2304
  }
2056
- }, /*#__PURE__*/React.createElement("span", {
2057
- style: {
2058
- fontSize: '16px'
2059
- }
2060
- }, "\uD83D\uDD17")), /*#__PURE__*/React.createElement("input", {
2305
+ }, /*#__PURE__*/React.createElement(FaLink, {
2306
+ size: 14
2307
+ })), /*#__PURE__*/React.createElement("input", {
2061
2308
  ref: fileInputRef,
2062
2309
  type: "file",
2063
2310
  accept: "image/*",
@@ -2224,9 +2471,17 @@ function RichTextEditor({
2224
2471
  }
2225
2472
  return null;
2226
2473
  })()), /*#__PURE__*/React.createElement("div", {
2474
+ className: "rte-content-wrapper",
2475
+ style: {
2476
+ position: 'relative'
2477
+ }
2478
+ }, /*#__PURE__*/React.createElement("div", {
2227
2479
  ref: editorRef,
2228
2480
  contentEditable: editable && disabled !== true,
2229
2481
  suppressContentEditableWarning: true,
2482
+ role: "textbox",
2483
+ "aria-multiline": "true",
2484
+ "aria-label": label || "Rich text editor",
2230
2485
  onInput: handleInput,
2231
2486
  onPaste: handlePaste,
2232
2487
  onDrop: handleDrop,
@@ -2242,7 +2497,13 @@ function RichTextEditor({
2242
2497
  paddingLeft: paddingLeft || '12px'
2243
2498
  },
2244
2499
  className: `rte-content${editable ? " rte-is-editable" : ""}${editorFocused ? " rte-is-focused" : ""}`
2245
- }), renderImageToolbar(), /*#__PURE__*/React.createElement("div", {
2500
+ }), isEmpty && editable && disabled !== true && /*#__PURE__*/React.createElement("div", {
2501
+ className: "rte-placeholder",
2502
+ style: {
2503
+ left: paddingLeft || '12px'
2504
+ },
2505
+ "aria-hidden": "true"
2506
+ }, placeholder)), renderMediaToolbar(), /*#__PURE__*/React.createElement("div", {
2246
2507
  className: "rte-footer"
2247
2508
  }, /*#__PURE__*/React.createElement("div", {
2248
2509
  className: "rte-footer-content"
@@ -2258,13 +2519,11 @@ function RichTextEditor({
2258
2519
  }, /*#__PURE__*/React.createElement("div", {
2259
2520
  className: "rte-modal",
2260
2521
  onClick: e => e.stopPropagation()
2522
+ }, /*#__PURE__*/React.createElement("div", {
2523
+ className: "rte-modal-header"
2261
2524
  }, /*#__PURE__*/React.createElement("h3", {
2262
2525
  className: "rte-modal-title"
2263
- }, "Insert Link"), /*#__PURE__*/React.createElement("div", {
2264
- className: "rte-modal-divider"
2265
- }), /*#__PURE__*/React.createElement("div", {
2266
- className: "rte-modal-body"
2267
- }, /*#__PURE__*/React.createElement("div", {
2526
+ }, "Insert Link")), /*#__PURE__*/React.createElement("div", {
2268
2527
  className: "rte-form-group"
2269
2528
  }, /*#__PURE__*/React.createElement("label", {
2270
2529
  className: "rte-label"
@@ -2286,20 +2545,15 @@ function RichTextEditor({
2286
2545
  onChange: e => setLinkUrl(e.target.value),
2287
2546
  onKeyDown: e => e.key === 'Enter' && confirmLink(),
2288
2547
  autoFocus: true
2289
- }))), /*#__PURE__*/React.createElement("div", {
2290
- className: "rte-modal-divider",
2291
- style: {
2292
- margin: '8px 0 20px 0'
2293
- }
2294
- }), /*#__PURE__*/React.createElement("div", {
2295
- className: "rte-modal-footer"
2548
+ })), /*#__PURE__*/React.createElement("div", {
2549
+ className: "rte-modal-actions"
2296
2550
  }, /*#__PURE__*/React.createElement("button", {
2297
2551
  type: "button",
2298
- className: "rte-btn rte-btn-secondary",
2552
+ className: "rte-button rte-button-secondary",
2299
2553
  onClick: cancelLink
2300
2554
  }, "Cancel"), /*#__PURE__*/React.createElement("button", {
2301
2555
  type: "button",
2302
- className: "rte-btn rte-btn-primary",
2556
+ className: "rte-button rte-button-primary",
2303
2557
  onClick: confirmLink,
2304
2558
  disabled: !linkUrl
2305
2559
  }, "Insert")))), tableModalOpen && /*#__PURE__*/React.createElement("div", {