smartrte-react 0.2.1 → 0.2.3

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.
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react";
3
3
  import { MediaManager } from "./MediaManager";
4
4
  import * as pdfjsLib from 'pdfjs-dist';
5
5
  import mammoth from 'mammoth';
6
+ import JSZip from 'jszip';
6
7
  import { ensureStyleSheet } from '../theme';
7
8
  // Initialize PDF.js worker
8
9
  if (typeof window !== 'undefined') {
@@ -16,7 +17,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
16
17
  { name: "Times New Roman", value: "'Times New Roman', Times, serif" },
17
18
  { name: "Verdana", value: "Verdana, Geneva, sans-serif" },
18
19
  { name: "Courier New", value: "'Courier New', Courier, monospace" },
19
- ], defaultFont, theme = "light", className, }) {
20
+ ], defaultFont, preserveFontFamily = false, preserveColors = false, preserveDocxStyles = true, theme = "light", className, }) {
20
21
  ensureStyleSheet();
21
22
  const editableRef = useRef(null);
22
23
  const lastEmittedRef = useRef("");
@@ -24,6 +25,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
24
25
  const fileInputRef = useRef(null);
25
26
  const pdfInputRef = useRef(null);
26
27
  const docxInputRef = useRef(null);
28
+ const htmlInputRef = useRef(null);
29
+ const mdInputRef = useRef(null);
27
30
  const [loadingPdf, setLoadingPdf] = useState(false);
28
31
  const [loadingDocx, setLoadingDocx] = useState(false);
29
32
  // State for import confirmation
@@ -45,6 +48,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
45
48
  const [imageMenu, setImageMenu] = useState(null);
46
49
  const [showMediaManager, setShowMediaManager] = useState(false);
47
50
  const [showColorPicker, setShowColorPicker] = useState(false);
51
+ const [showSpecialChars, setShowSpecialChars] = useState(false);
48
52
  const [colorPickerType, setColorPickerType] = useState('text');
49
53
  const savedRangeRef = useRef(null);
50
54
  const [currentFontSize, setCurrentFontSize] = useState("");
@@ -93,27 +97,91 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
93
97
  const exec = (command, valueArg) => {
94
98
  try {
95
99
  document.execCommand(command, false, valueArg);
96
- // Emit after command
97
- const el = editableRef.current;
98
- if (el && onChange) {
99
- const html = el.innerHTML;
100
- if (html !== lastEmittedRef.current) {
101
- lastEmittedRef.current = html;
102
- onChange(html);
103
- }
104
- }
100
+ emitChange();
105
101
  }
106
102
  catch { }
107
103
  };
108
104
  const applyFormatBlock = (blockName) => {
109
105
  exec("formatBlock", blockName);
110
106
  };
107
+ const emitChange = () => {
108
+ const el = editableRef.current;
109
+ if (!el || !onChange)
110
+ return;
111
+ const html = el.innerHTML;
112
+ if (html !== lastEmittedRef.current) {
113
+ lastEmittedRef.current = html;
114
+ onChange(html);
115
+ }
116
+ };
111
117
  const insertLink = () => {
112
118
  const url = window.prompt("Enter URL", "https://");
113
119
  if (!url)
114
120
  return;
115
121
  exec("createLink", url);
116
122
  };
123
+ const getSelectionRangeInEditor = () => {
124
+ const editor = editableRef.current;
125
+ if (!editor)
126
+ return null;
127
+ editor.focus();
128
+ const sel = window.getSelection();
129
+ if (sel && sel.rangeCount > 0) {
130
+ const range = sel.getRangeAt(0);
131
+ if (editor.contains(range.commonAncestorContainer))
132
+ return range;
133
+ }
134
+ if (savedRangeRef.current && editor.contains(savedRangeRef.current.commonAncestorContainer)) {
135
+ return savedRangeRef.current.cloneRange();
136
+ }
137
+ const range = document.createRange();
138
+ range.selectNodeContents(editor);
139
+ range.collapse(false);
140
+ return range;
141
+ };
142
+ const insertTextAtSelection = (text) => {
143
+ if (!text)
144
+ return;
145
+ try {
146
+ const range = getSelectionRangeInEditor();
147
+ if (!range)
148
+ return;
149
+ range.deleteContents();
150
+ const node = document.createTextNode(text);
151
+ range.insertNode(node);
152
+ const nextRange = document.createRange();
153
+ nextRange.setStartAfter(node);
154
+ nextRange.collapse(true);
155
+ safeSelectRange(nextRange);
156
+ handleInput();
157
+ }
158
+ catch { }
159
+ };
160
+ const toggleBlockquote = () => {
161
+ try {
162
+ const range = getSelectionRangeInEditor();
163
+ if (!range)
164
+ return;
165
+ let node = range.commonAncestorContainer;
166
+ if (node.nodeType === Node.TEXT_NODE)
167
+ node = node.parentElement;
168
+ const element = node;
169
+ const quote = element?.closest?.('blockquote');
170
+ if (quote && editableRef.current?.contains(quote)) {
171
+ const replacement = document.createElement('p');
172
+ replacement.innerHTML = quote.innerHTML || '<br>';
173
+ quote.parentElement?.replaceChild(replacement, quote);
174
+ const nextRange = document.createRange();
175
+ nextRange.selectNodeContents(replacement);
176
+ nextRange.collapse(false);
177
+ safeSelectRange(nextRange);
178
+ handleInput();
179
+ return;
180
+ }
181
+ exec("formatBlock", "<blockquote>");
182
+ }
183
+ catch { }
184
+ };
117
185
  const applyFontSize = (size) => {
118
186
  try {
119
187
  // Update current font size state
@@ -395,11 +463,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
395
463
  window.removeEventListener('touchend', onTouchEnd);
396
464
  };
397
465
  }, [table]);
398
- const insertImageAtSelection = (src) => {
466
+ const insertImageAtSelection = (srcOrItem) => {
399
467
  try {
400
468
  const host = editableRef.current;
401
469
  if (!host)
402
470
  return;
471
+ const src = typeof srcOrItem === "string" ? srcOrItem : srcOrItem.url;
403
472
  host.focus();
404
473
  let sel = window.getSelection();
405
474
  let range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
@@ -415,7 +484,21 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
415
484
  img.style.maxWidth = "100%";
416
485
  img.style.height = "auto";
417
486
  img.style.display = "inline-block";
418
- img.alt = "image";
487
+ img.alt = typeof srcOrItem === "string" ? "image" : (srcOrItem.alt || srcOrItem.title || "image");
488
+ if (typeof srcOrItem !== "string") {
489
+ if (srcOrItem.title)
490
+ img.title = srcOrItem.title;
491
+ if (srcOrItem.license?.author)
492
+ img.dataset.licenseAuthor = srcOrItem.license.author;
493
+ if (srcOrItem.license?.licenseType)
494
+ img.dataset.licenseType = srcOrItem.license.licenseType;
495
+ if (srcOrItem.license?.licenseText)
496
+ img.dataset.licenseText = srcOrItem.license.licenseText;
497
+ if (srcOrItem.license?.sourceUrl)
498
+ img.dataset.licenseUrl = srcOrItem.license.sourceUrl;
499
+ if (srcOrItem.license?.workName)
500
+ img.dataset.workName = srcOrItem.license.workName;
501
+ }
419
502
  if (range) {
420
503
  range.insertNode(img);
421
504
  }
@@ -586,6 +669,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
586
669
  let fullHtml = '';
587
670
  for (let i = 1; i <= pdf.numPages; i++) {
588
671
  const page = await pdf.getPage(i);
672
+ const viewport = page.getViewport({ scale: 1 });
589
673
  const textContent = await page.getTextContent();
590
674
  const styles = textContent.styles;
591
675
  // 1. Group items into lines
@@ -657,8 +741,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
657
741
  const width = item.width;
658
742
  const fontName = item.fontName;
659
743
  const fontObj = styles[fontName];
660
- const isBold = fontObj?.fontFamily?.toLowerCase().includes('bold') || false;
661
- // const isItalic = fontObj?.fontFamily?.toLowerCase().includes('italic') || false;
744
+ const fontFamily = fontObj?.fontFamily?.toLowerCase() || '';
745
+ const isBold = fontFamily.includes('bold') || false;
746
+ const isItalic = fontFamily.includes('italic') || fontFamily.includes('oblique');
747
+ const fontSize = Math.max(8, Math.round(Math.abs(item.transform[3])));
662
748
  if (lastX > 0) {
663
749
  const gap = x - lastX;
664
750
  if (gap > 2) { // Minimal space threshold
@@ -677,10 +763,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
677
763
  itemXs.push(x);
678
764
  }
679
765
  // Append text style
680
- let chunk = item.str;
681
- if (isBold)
682
- chunk = `<strong>${chunk}</strong>`;
683
- // if (isItalic) chunk = `<em>${chunk}</em>`;
766
+ const chunkStyle = cssRules([
767
+ ['font-size', `${fontSize}px`],
768
+ ['font-weight', isBold ? '700' : ''],
769
+ ['font-style', isItalic ? 'italic' : ''],
770
+ ]);
771
+ let chunk = `<span${styleAttr(chunkStyle)}>${escapeHtml(item.str)}</span>`;
684
772
  lineText += item.str;
685
773
  lineHtmlContent += chunk;
686
774
  lastX = x + width;
@@ -751,8 +839,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
751
839
  const w = item.width;
752
840
  const txt = item.str;
753
841
  const fontObj = styles[item.fontName];
754
- const isBold = fontObj?.fontFamily?.toLowerCase().includes('bold');
755
- const styledTxt = isBold ? `<strong>${txt}</strong>` : txt;
842
+ const fontFamily = fontObj?.fontFamily?.toLowerCase() || '';
843
+ const isBold = fontFamily.includes('bold');
844
+ const isItalic = fontFamily.includes('italic') || fontFamily.includes('oblique');
845
+ const fontSize = Math.max(8, Math.round(Math.abs(item.transform[3])));
846
+ const styledTxt = `<span${styleAttr(cssRules([
847
+ ['font-size', `${fontSize}px`],
848
+ ['font-weight', isBold ? '700' : ''],
849
+ ['font-style', isItalic ? 'italic' : ''],
850
+ ]))}>${escapeHtml(txt)}</span>`;
756
851
  // Decide which column this belongs to
757
852
  // Find closest column to the left (or close enough)
758
853
  let colIdx = 0;
@@ -794,10 +889,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
794
889
  closeList();
795
890
  if (isHeader) {
796
891
  const tag = maxH > medianHeight * 1.5 ? 'h2' : 'h3';
797
- html += `<${tag}>${lineHtmlContent}</${tag}>`;
892
+ const firstX = line.items[0]?.transform?.[4] || 0;
893
+ const lastItem = line.items[line.items.length - 1];
894
+ const lastRight = lastItem ? lastItem.transform[4] + lastItem.width : firstX;
895
+ const center = (firstX + lastRight) / 2;
896
+ const align = Math.abs(center - viewport.width / 2) < viewport.width * 0.12 ? 'center' : firstX > viewport.width * 0.55 ? 'right' : '';
897
+ html += `<${tag}${styleAttr(cssRules([['text-align', align]]))}>${lineHtmlContent}</${tag}>`;
798
898
  }
799
899
  else {
800
- html += `<p>${lineHtmlContent}</p>`;
900
+ const firstX = line.items[0]?.transform?.[4] || 0;
901
+ const lastItem = line.items[line.items.length - 1];
902
+ const lastRight = lastItem ? lastItem.transform[4] + lastItem.width : firstX;
903
+ const center = (firstX + lastRight) / 2;
904
+ const align = Math.abs(center - viewport.width / 2) < viewport.width * 0.12 ? 'center' : firstX > viewport.width * 0.55 ? 'right' : '';
905
+ html += `<p${styleAttr(cssRules([
906
+ ['text-align', align],
907
+ ['margin-left', firstX > 40 && !align ? `${Math.round(firstX)}px` : ''],
908
+ ]))}>${lineHtmlContent}</p>`;
801
909
  }
802
910
  }
803
911
  }
@@ -806,30 +914,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
806
914
  closeTable();
807
915
  fullHtml += html;
808
916
  }
809
- const el = editableRef.current;
810
- if (el) {
811
- el.focus();
812
- if (mode === 'replace') {
813
- // Select all and replace
814
- const range = document.createRange();
815
- range.selectNodeContents(el);
816
- const sel = window.getSelection();
817
- sel?.removeAllRanges();
818
- sel?.addRange(range);
819
- exec("delete"); // Clear content safely
820
- exec("insertHTML", fullHtml);
821
- }
822
- else {
823
- // Append
824
- const range = document.createRange();
825
- range.selectNodeContents(el);
826
- range.collapse(false); // End
827
- const sel = window.getSelection();
828
- sel?.removeAllRanges();
829
- sel?.addRange(range);
830
- exec("insertHTML", "<br>" + fullHtml);
831
- }
832
- }
917
+ insertImportedHtml(fullHtml, mode, { preserveColors: true, preserveDocumentLayout: true });
833
918
  }
834
919
  catch (error) {
835
920
  console.error('Error reading PDF:', error);
@@ -859,55 +944,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
859
944
  try {
860
945
  setLoadingDocx(true);
861
946
  const arrayBuffer = await file.arrayBuffer();
862
- const result = await mammoth.convertToHtml({ arrayBuffer });
863
- let html = result.value;
947
+ const html = preserveDocxStyles
948
+ ? await convertDocxToStyledHtml(arrayBuffer)
949
+ : await convertDocxWithMammoth(arrayBuffer);
864
950
  if (html) {
865
- // Process HTML to ensure tables have borders and structure
866
- const temp = document.createElement('div');
867
- temp.innerHTML = html;
868
- const tables = temp.querySelectorAll('table');
869
- tables.forEach(tbl => {
870
- tbl.style.borderCollapse = 'collapse';
871
- tbl.style.minWidth = '100%';
872
- // Browser parser auto-adds tbody, but we verify styles
873
- const cells = tbl.querySelectorAll('td, th');
874
- cells.forEach(cell => {
875
- cell.style.border = '1px solid #000';
876
- cell.style.padding = '8px';
877
- cell.style.verticalAlign = 'top';
878
- });
951
+ insertImportedHtml(`<div class="srte-preserve-colors">${html}</div>`, mode, {
952
+ preserveColors: preserveDocxStyles,
953
+ preserveDocumentLayout: preserveDocxStyles,
879
954
  });
880
- html = temp.innerHTML;
881
- const el = editableRef.current;
882
- if (el) {
883
- el.focus();
884
- if (mode === 'replace') {
885
- const range = document.createRange();
886
- range.selectNodeContents(el);
887
- const sel = window.getSelection();
888
- sel?.removeAllRanges();
889
- sel?.addRange(range);
890
- exec("delete");
891
- exec("insertHTML", html);
892
- }
893
- else {
894
- const range = document.createRange();
895
- range.selectNodeContents(el);
896
- range.collapse(false);
897
- const sel = window.getSelection();
898
- sel?.removeAllRanges();
899
- sel?.addRange(range);
900
- exec("insertHTML", "<br>" + html);
901
- }
902
- // Initialize handlers for the new content
903
- // We use setTimeout to let the DOM settle after execCommand
904
- setTimeout(() => {
905
- ensureTableWrappers(el);
906
- addTableResizeHandles();
907
- fixNegativeMargins(el);
908
- handleInput();
909
- }, 10);
910
- }
911
955
  }
912
956
  }
913
957
  catch (error) {
@@ -917,6 +961,578 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
917
961
  setLoadingDocx(false);
918
962
  }
919
963
  };
964
+ const convertDocxWithMammoth = async (arrayBuffer) => {
965
+ const result = await mammoth.convertToHtml({ arrayBuffer });
966
+ const temp = document.createElement('div');
967
+ temp.innerHTML = result.value;
968
+ enhanceImportedTables(temp);
969
+ return temp.innerHTML;
970
+ };
971
+ const convertDocxToStyledHtml = async (arrayBuffer) => {
972
+ try {
973
+ const zip = await JSZip.loadAsync(arrayBuffer);
974
+ const documentXml = await zip.file('word/document.xml')?.async('text');
975
+ if (!documentXml)
976
+ return convertDocxWithMammoth(arrayBuffer);
977
+ const parser = new DOMParser();
978
+ const doc = parser.parseFromString(documentXml, 'application/xml');
979
+ if (doc.querySelector('parsererror'))
980
+ return convertDocxWithMammoth(arrayBuffer);
981
+ const body = Array.from(doc.getElementsByTagName('*')).find((node) => node.localName === 'body');
982
+ if (!body)
983
+ return convertDocxWithMammoth(arrayBuffer);
984
+ const html = directChildren(body)
985
+ .filter((node) => node.localName !== 'sectPr')
986
+ .map((node) => {
987
+ if (node.localName === 'p')
988
+ return convertDocxParagraph(node);
989
+ if (node.localName === 'tbl')
990
+ return convertDocxTable(node);
991
+ return '';
992
+ })
993
+ .join('');
994
+ if (!html.trim())
995
+ return convertDocxWithMammoth(arrayBuffer);
996
+ const temp = document.createElement('div');
997
+ temp.innerHTML = html;
998
+ enhanceImportedTables(temp);
999
+ return temp.innerHTML;
1000
+ }
1001
+ catch (error) {
1002
+ console.warn('Falling back to Mammoth DOCX import:', error);
1003
+ return convertDocxWithMammoth(arrayBuffer);
1004
+ }
1005
+ };
1006
+ const directChildren = (node) => Array.from(node.children);
1007
+ const firstChildByName = (node, localName) => node
1008
+ ? directChildren(node).find((child) => child.localName === localName)
1009
+ : undefined;
1010
+ const childrenByName = (node, localName) => node
1011
+ ? directChildren(node).filter((child) => child.localName === localName)
1012
+ : [];
1013
+ const docxAttr = (node, name) => {
1014
+ if (!node)
1015
+ return '';
1016
+ return (node.getAttribute(`w:${name}`) ||
1017
+ node.getAttribute(name) ||
1018
+ node.getAttributeNS('http://schemas.openxmlformats.org/wordprocessingml/2006/main', name) ||
1019
+ '');
1020
+ };
1021
+ const docxHexColor = (value) => {
1022
+ if (!value || value.toLowerCase() === 'auto')
1023
+ return '';
1024
+ const normalized = value.replace(/[^0-9a-f]/gi, '');
1025
+ return normalized.length === 6 ? `#${normalized}` : '';
1026
+ };
1027
+ const twipsToPt = (value) => {
1028
+ const n = Number(value);
1029
+ return Number.isFinite(n) ? `${Math.max(n / 20, 0)}pt` : '';
1030
+ };
1031
+ const halfPointsToPt = (value) => {
1032
+ const n = Number(value);
1033
+ return Number.isFinite(n) ? `${Math.max(n / 2, 1)}pt` : '';
1034
+ };
1035
+ const cssRules = (rules) => rules
1036
+ .filter(([, value]) => Boolean(value))
1037
+ .map(([name, value]) => `${name}: ${value}`)
1038
+ .join('; ');
1039
+ const styleAttr = (style) => (style ? ` style="${escapeHtml(style)}"` : '');
1040
+ const convertDocxParagraphStyle = (paragraph) => {
1041
+ const pPr = firstChildByName(paragraph, 'pPr');
1042
+ if (!pPr)
1043
+ return '';
1044
+ const spacing = firstChildByName(pPr, 'spacing');
1045
+ const jc = firstChildByName(pPr, 'jc');
1046
+ const indent = firstChildByName(pPr, 'ind');
1047
+ const borderBottom = firstChildByName(firstChildByName(pPr, 'pBdr'), 'bottom');
1048
+ const line = docxAttr(spacing, 'line');
1049
+ const lineRule = docxAttr(spacing, 'lineRule');
1050
+ return cssRules([
1051
+ ['text-align', docxAttr(jc, 'val')],
1052
+ ['margin-top', twipsToPt(docxAttr(spacing, 'before'))],
1053
+ ['margin-bottom', twipsToPt(docxAttr(spacing, 'after'))],
1054
+ ['margin-left', twipsToPt(docxAttr(indent, 'left'))],
1055
+ ['text-indent', twipsToPt(docxAttr(indent, 'firstLine'))],
1056
+ ['line-height', line && lineRule === 'auto' ? `${Number(line) / 240}` : ''],
1057
+ ['border-bottom', docxBorderCss(borderBottom)],
1058
+ ]);
1059
+ };
1060
+ const convertDocxRunStyle = (run) => {
1061
+ const rPr = firstChildByName(run, 'rPr');
1062
+ if (!rPr)
1063
+ return '';
1064
+ const color = docxHexColor(docxAttr(firstChildByName(rPr, 'color'), 'val'));
1065
+ const highlight = docxHexColor(docxAttr(firstChildByName(rPr, 'highlight'), 'val'));
1066
+ const shade = docxHexColor(docxAttr(firstChildByName(rPr, 'shd'), 'fill'));
1067
+ const size = halfPointsToPt(docxAttr(firstChildByName(rPr, 'sz'), 'val'));
1068
+ const underline = firstChildByName(rPr, 'u');
1069
+ return cssRules([
1070
+ ['font-weight', firstChildByName(rPr, 'b') ? '700' : ''],
1071
+ ['font-style', firstChildByName(rPr, 'i') ? 'italic' : ''],
1072
+ ['text-decoration', underline ? 'underline' : ''],
1073
+ ['color', color],
1074
+ ['background-color', highlight || shade],
1075
+ ['font-size', size],
1076
+ ]);
1077
+ };
1078
+ const convertDocxRun = (run) => {
1079
+ const rPr = firstChildByName(run, 'rPr');
1080
+ const vertAlign = docxAttr(firstChildByName(rPr, 'vertAlign'), 'val');
1081
+ const style = convertDocxRunStyle(run);
1082
+ const content = directChildren(run)
1083
+ .map((child) => {
1084
+ if (child.localName === 't')
1085
+ return escapeHtml(child.textContent || '');
1086
+ if (child.localName === 'tab')
1087
+ return '&emsp;';
1088
+ if (child.localName === 'br') {
1089
+ return docxAttr(child, 'type') === 'page'
1090
+ ? '<hr class="srte-docx-page-break">'
1091
+ : '<br>';
1092
+ }
1093
+ return '';
1094
+ })
1095
+ .join('');
1096
+ if (!content)
1097
+ return '';
1098
+ const tag = vertAlign === 'superscript' ? 'sup' : vertAlign === 'subscript' ? 'sub' : 'span';
1099
+ return `<${tag}${styleAttr(style)}>${content}</${tag}>`;
1100
+ };
1101
+ const convertDocxParagraph = (paragraph) => {
1102
+ const style = convertDocxParagraphStyle(paragraph);
1103
+ const content = childrenByName(paragraph, 'r').map(convertDocxRun).join('');
1104
+ return `<p${styleAttr(style)}>${content || '<br>'}</p>`;
1105
+ };
1106
+ const docxBorderCss = (border) => {
1107
+ if (!border)
1108
+ return '';
1109
+ const val = docxAttr(border, 'val');
1110
+ if (!val || val === 'nil' || val === 'none')
1111
+ return '';
1112
+ const size = Number(docxAttr(border, 'sz')) || 4;
1113
+ const width = Math.max(size / 8, 0.5);
1114
+ const color = docxHexColor(docxAttr(border, 'color')) || '#d1d5db';
1115
+ return `${width}px solid ${color}`;
1116
+ };
1117
+ const convertDocxCellStyle = (cell) => {
1118
+ const tcPr = firstChildByName(cell, 'tcPr');
1119
+ const width = twipsToPt(docxAttr(firstChildByName(tcPr, 'tcW'), 'w'));
1120
+ const shade = docxHexColor(docxAttr(firstChildByName(tcPr, 'shd'), 'fill'));
1121
+ const borders = firstChildByName(tcPr, 'tcBorders');
1122
+ const top = docxBorderCss(firstChildByName(borders, 'top'));
1123
+ const right = docxBorderCss(firstChildByName(borders, 'right'));
1124
+ const bottom = docxBorderCss(firstChildByName(borders, 'bottom'));
1125
+ const left = docxBorderCss(firstChildByName(borders, 'left'));
1126
+ return cssRules([
1127
+ ['width', width],
1128
+ ['background-color', shade],
1129
+ ['border-top', top],
1130
+ ['border-right', right],
1131
+ ['border-bottom', bottom],
1132
+ ['border-left', left],
1133
+ ['padding', '8px'],
1134
+ ['vertical-align', 'top'],
1135
+ ]);
1136
+ };
1137
+ const convertDocxTable = (table) => {
1138
+ const rows = childrenByName(table, 'tr')
1139
+ .map((row) => {
1140
+ const cells = childrenByName(row, 'tc')
1141
+ .map((cell) => {
1142
+ const content = directChildren(cell)
1143
+ .filter((child) => child.localName === 'p' || child.localName === 'tbl')
1144
+ .map((child) => child.localName === 'p' ? convertDocxParagraph(child) : convertDocxTable(child))
1145
+ .join('');
1146
+ return `<td${styleAttr(convertDocxCellStyle(cell))}>${content || '<p><br></p>'}</td>`;
1147
+ })
1148
+ .join('');
1149
+ return `<tr>${cells}</tr>`;
1150
+ })
1151
+ .join('');
1152
+ return `<table style="border-collapse: collapse; width: 100%; margin: 12px 0;"><tbody>${rows}</tbody></table>`;
1153
+ };
1154
+ const enhanceImportedTables = (root) => {
1155
+ const tables = root.querySelectorAll('table');
1156
+ tables.forEach(tbl => {
1157
+ tbl.style.borderCollapse = tbl.style.borderCollapse || 'collapse';
1158
+ tbl.style.width = tbl.style.width || '100%';
1159
+ const cells = tbl.querySelectorAll('td, th');
1160
+ cells.forEach(cell => {
1161
+ const el = cell;
1162
+ if (!el.style.border && !el.style.borderTop && !el.style.borderRight && !el.style.borderBottom && !el.style.borderLeft) {
1163
+ el.style.border = '1px solid #d1d5db';
1164
+ }
1165
+ el.style.padding = el.style.padding || '8px';
1166
+ el.style.verticalAlign = el.style.verticalAlign || 'top';
1167
+ });
1168
+ });
1169
+ };
1170
+ const escapeHtml = (value) => value
1171
+ .replace(/&/g, "&amp;")
1172
+ .replace(/</g, "&lt;")
1173
+ .replace(/>/g, "&gt;");
1174
+ const escapeHtmlAttribute = (value) => escapeHtml(value).replace(/"/g, "&quot;");
1175
+ const markdownToHtml = (markdown) => {
1176
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
1177
+ let html = "";
1178
+ let listType = null;
1179
+ let paragraph = [];
1180
+ let codeFence = null;
1181
+ const closeList = () => {
1182
+ if (listType) {
1183
+ html += `</${listType}>`;
1184
+ listType = null;
1185
+ }
1186
+ };
1187
+ const inline = (text) => {
1188
+ const codeTokens = [];
1189
+ let value = text.replace(/`([^`]+)`/g, (_match, code) => {
1190
+ const token = `@@SRTE_CODE_${codeTokens.length}@@`;
1191
+ codeTokens.push(`<code>${escapeHtml(code)}</code>`);
1192
+ return token;
1193
+ });
1194
+ value = escapeHtml(value)
1195
+ .replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_match, alt, src, title) => {
1196
+ const titleAttr = title ? ` title="${escapeHtmlAttribute(title)}"` : "";
1197
+ return `<img src="${escapeHtmlAttribute(src)}" alt="${escapeHtmlAttribute(alt)}"${titleAttr}>`;
1198
+ })
1199
+ .replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_match, label, href, title) => {
1200
+ const titleAttr = title ? ` title="${escapeHtmlAttribute(title)}"` : "";
1201
+ return `<a href="${escapeHtmlAttribute(href)}"${titleAttr}>${label}</a>`;
1202
+ })
1203
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
1204
+ .replace(/__([^_]+)__/g, "<strong>$1</strong>")
1205
+ .replace(/~~([^~]+)~~/g, "<s>$1</s>")
1206
+ .replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>")
1207
+ .replace(/(^|[^_])_([^_\n]+)_/g, "$1<em>$2</em>");
1208
+ codeTokens.forEach((replacement, index) => {
1209
+ value = value.replace(`@@SRTE_CODE_${index}@@`, replacement);
1210
+ });
1211
+ return value;
1212
+ };
1213
+ const closeParagraph = () => {
1214
+ if (!paragraph.length)
1215
+ return;
1216
+ html += `<p>${inline(paragraph.join(" "))}</p>`;
1217
+ paragraph = [];
1218
+ };
1219
+ const isTableSeparator = (line) => /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
1220
+ const parseTableRow = (line) => {
1221
+ let value = line.trim();
1222
+ if (value.startsWith("|"))
1223
+ value = value.slice(1);
1224
+ if (value.endsWith("|"))
1225
+ value = value.slice(0, -1);
1226
+ return value.split("|").map((cell) => cell.trim());
1227
+ };
1228
+ const renderTable = (startIndex) => {
1229
+ const header = parseTableRow(lines[startIndex]);
1230
+ let index = startIndex + 2;
1231
+ const rows = [];
1232
+ while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
1233
+ rows.push(parseTableRow(lines[index]));
1234
+ index += 1;
1235
+ }
1236
+ const headHtml = `<thead><tr>${header.map((cell) => `<th>${inline(cell)}</th>`).join("")}</tr></thead>`;
1237
+ const bodyHtml = rows.length
1238
+ ? `<tbody>${rows.map((row) => `<tr>${header.map((_cell, cellIndex) => `<td>${inline(row[cellIndex] || "")}</td>`).join("")}</tr>`).join("")}</tbody>`
1239
+ : "";
1240
+ html += `<table style="border-collapse: collapse; width: 100%; margin: 12px 0;">${headHtml}${bodyHtml}</table>`;
1241
+ return index;
1242
+ };
1243
+ for (let i = 0; i < lines.length; i += 1) {
1244
+ const line = lines[i];
1245
+ const trimmed = line.trim();
1246
+ const fence = /^```([A-Za-z0-9_-]+)?\s*$/.exec(trimmed);
1247
+ if (fence) {
1248
+ closeParagraph();
1249
+ closeList();
1250
+ if (codeFence) {
1251
+ const langClass = codeFence.lang ? ` class="language-${escapeHtmlAttribute(codeFence.lang)}"` : "";
1252
+ html += `<pre><code${langClass}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`;
1253
+ codeFence = null;
1254
+ }
1255
+ else {
1256
+ codeFence = { lang: fence[1] || "", lines: [] };
1257
+ }
1258
+ continue;
1259
+ }
1260
+ if (codeFence) {
1261
+ codeFence.lines.push(line);
1262
+ continue;
1263
+ }
1264
+ if (!trimmed) {
1265
+ closeParagraph();
1266
+ closeList();
1267
+ continue;
1268
+ }
1269
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
1270
+ closeParagraph();
1271
+ closeList();
1272
+ html += "<hr>";
1273
+ continue;
1274
+ }
1275
+ if (i + 1 < lines.length && trimmed.includes("|") && isTableSeparator(lines[i + 1])) {
1276
+ closeParagraph();
1277
+ closeList();
1278
+ i = renderTable(i) - 1;
1279
+ continue;
1280
+ }
1281
+ const heading = /^(#{1,6})\s+(.+)$/.exec(trimmed);
1282
+ if (heading) {
1283
+ closeParagraph();
1284
+ closeList();
1285
+ const level = heading[1].length;
1286
+ html += `<h${level}>${inline(heading[2])}</h${level}>`;
1287
+ continue;
1288
+ }
1289
+ const bullet = /^[-*+]\s+(.+)$/.exec(trimmed);
1290
+ if (bullet) {
1291
+ closeParagraph();
1292
+ if (listType !== "ul") {
1293
+ closeList();
1294
+ html += "<ul>";
1295
+ listType = "ul";
1296
+ }
1297
+ html += `<li>${inline(bullet[1])}</li>`;
1298
+ continue;
1299
+ }
1300
+ const numbered = /^\d+[.)]\s+(.+)$/.exec(trimmed);
1301
+ if (numbered) {
1302
+ closeParagraph();
1303
+ if (listType !== "ol") {
1304
+ closeList();
1305
+ html += "<ol>";
1306
+ listType = "ol";
1307
+ }
1308
+ html += `<li>${inline(numbered[1])}</li>`;
1309
+ continue;
1310
+ }
1311
+ const quote = /^>\s?(.*)$/.exec(trimmed);
1312
+ if (quote) {
1313
+ closeParagraph();
1314
+ closeList();
1315
+ html += `<blockquote>${inline(quote[1]) || "<br>"}</blockquote>`;
1316
+ continue;
1317
+ }
1318
+ closeList();
1319
+ paragraph.push(trimmed);
1320
+ }
1321
+ if (codeFence) {
1322
+ const langClass = codeFence.lang ? ` class="language-${escapeHtmlAttribute(codeFence.lang)}"` : "";
1323
+ html += `<pre><code${langClass}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`;
1324
+ }
1325
+ closeParagraph();
1326
+ closeList();
1327
+ const root = document.createElement("div");
1328
+ root.innerHTML = html;
1329
+ enhanceImportedTables(root);
1330
+ return root.innerHTML;
1331
+ };
1332
+ const htmlToMarkdown = (html) => {
1333
+ const root = document.createElement("div");
1334
+ root.innerHTML = html;
1335
+ const walk = (node) => {
1336
+ if (node.nodeType === Node.TEXT_NODE)
1337
+ return node.textContent || "";
1338
+ if (!(node instanceof HTMLElement))
1339
+ return "";
1340
+ const content = Array.from(node.childNodes).map(walk).join("");
1341
+ const tag = node.tagName.toLowerCase();
1342
+ if (tag === "strong" || tag === "b")
1343
+ return `**${content}**`;
1344
+ if (tag === "em" || tag === "i")
1345
+ return `*${content}*`;
1346
+ if (tag === "code")
1347
+ return `\`${content}\``;
1348
+ if (tag === "br")
1349
+ return "\n";
1350
+ if (/h[1-6]/.test(tag))
1351
+ return `${"#".repeat(Number(tag[1]))} ${content.trim()}\n\n`;
1352
+ if (tag === "p")
1353
+ return `${content.trim()}\n\n`;
1354
+ if (tag === "li")
1355
+ return `- ${content.trim()}\n`;
1356
+ if (tag === "ul" || tag === "ol")
1357
+ return `${content}\n`;
1358
+ if (tag === "blockquote")
1359
+ return `> ${content.trim()}\n\n`;
1360
+ if (tag === "table")
1361
+ return `${node.outerHTML}\n\n`;
1362
+ if (tag === "img")
1363
+ return `![${node.getAttribute("alt") || ""}](${node.getAttribute("src") || ""})`;
1364
+ if (tag === "a")
1365
+ return `[${content}](${node.getAttribute("href") || ""})`;
1366
+ return content;
1367
+ };
1368
+ return Array.from(root.childNodes).map(walk).join("").replace(/\n{3,}/g, "\n\n").trim();
1369
+ };
1370
+ const importTextFile = async (files, type) => {
1371
+ if (!files || files.length === 0)
1372
+ return;
1373
+ const file = files[0];
1374
+ const text = await file.text();
1375
+ const html = type === "html" ? text : markdownToHtml(text);
1376
+ const el = editableRef.current;
1377
+ const hasContent = el && el.textContent && el.textContent.trim().length > 0;
1378
+ insertImportedHtml(html, hasContent ? "append" : "replace", {
1379
+ preserveColors: true,
1380
+ preserveDocumentLayout: true,
1381
+ });
1382
+ };
1383
+ const downloadText = (filename, content, mimeType) => {
1384
+ const blob = new Blob([content], { type: mimeType });
1385
+ const url = URL.createObjectURL(blob);
1386
+ const link = document.createElement("a");
1387
+ link.href = url;
1388
+ link.download = filename;
1389
+ document.body.appendChild(link);
1390
+ link.click();
1391
+ link.remove();
1392
+ URL.revokeObjectURL(url);
1393
+ };
1394
+ const exportHtml = () => {
1395
+ const html = editableRef.current?.innerHTML || "";
1396
+ downloadText("smart-rte-export.html", html, "text/html");
1397
+ };
1398
+ const exportMarkdown = () => {
1399
+ const html = editableRef.current?.innerHTML || "";
1400
+ downloadText("smart-rte-export.md", htmlToMarkdown(html), "text/markdown");
1401
+ };
1402
+ const htmlToDocxXml = (html) => {
1403
+ const root = document.createElement("div");
1404
+ root.innerHTML = html;
1405
+ const xmlEscape = (value) => value
1406
+ .replace(/&/g, "&amp;")
1407
+ .replace(/</g, "&lt;")
1408
+ .replace(/>/g, "&gt;")
1409
+ .replace(/"/g, "&quot;");
1410
+ const colorValue = (value) => {
1411
+ const hex = /^#([0-9a-f]{6})$/i.exec(value.trim());
1412
+ if (hex)
1413
+ return hex[1].toUpperCase();
1414
+ const rgb = /^rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)$/i.exec(value.trim());
1415
+ if (!rgb)
1416
+ return "";
1417
+ return [rgb[1], rgb[2], rgb[3]]
1418
+ .map((part) => Math.max(0, Math.min(255, Number(part))).toString(16).padStart(2, "0"))
1419
+ .join("")
1420
+ .toUpperCase();
1421
+ };
1422
+ const sizeToHalfPoints = (value) => {
1423
+ const trimmed = value.trim();
1424
+ const match = /^([\d.]+)(px|pt)$/i.exec(trimmed);
1425
+ if (!match)
1426
+ return "";
1427
+ const raw = Number(match[1]);
1428
+ const pt = match[2].toLowerCase() === "px" ? raw * 0.75 : raw;
1429
+ return String(Math.max(2, Math.round(pt * 2)));
1430
+ };
1431
+ const runProperties = (el) => {
1432
+ const style = el.style;
1433
+ const color = colorValue(style.color);
1434
+ const size = sizeToHalfPoints(style.fontSize);
1435
+ const isBold = el.tagName === "B" || el.tagName === "STRONG" || /bold|700|800|900/.test(style.fontWeight);
1436
+ const isItalic = el.tagName === "I" || el.tagName === "EM" || style.fontStyle === "italic";
1437
+ const isUnderline = el.tagName === "U" || style.textDecoration.includes("underline");
1438
+ return [
1439
+ isBold ? "<w:b/>" : "",
1440
+ isItalic ? "<w:i/>" : "",
1441
+ isUnderline ? '<w:u w:val="single"/>' : "",
1442
+ color ? `<w:color w:val="${color}"/>` : "",
1443
+ size ? `<w:sz w:val="${size}"/>` : "",
1444
+ ].join("");
1445
+ };
1446
+ const runs = (node, inheritedProps = "") => {
1447
+ if (node.nodeType === Node.TEXT_NODE) {
1448
+ const text = node.textContent || "";
1449
+ return text ? `<w:r>${inheritedProps ? `<w:rPr>${inheritedProps}</w:rPr>` : ""}<w:t xml:space="preserve">${xmlEscape(text)}</w:t></w:r>` : "";
1450
+ }
1451
+ if (!(node instanceof HTMLElement))
1452
+ return "";
1453
+ if (node.tagName === "BR")
1454
+ return "<w:r><w:br/></w:r>";
1455
+ if (node.tagName === "IMG") {
1456
+ const alt = node.getAttribute("alt") || node.getAttribute("title") || "Image";
1457
+ return `<w:r><w:t>[Image: ${xmlEscape(alt)}]</w:t></w:r>`;
1458
+ }
1459
+ const props = `${inheritedProps}${runProperties(node)}`;
1460
+ return Array.from(node.childNodes).map((child) => runs(child, props)).join("");
1461
+ };
1462
+ const paragraph = (el, fallbackTag = "p") => {
1463
+ const tag = el.tagName.toLowerCase();
1464
+ const headingMatch = /^h([1-6])$/.exec(tag);
1465
+ const style = el.style;
1466
+ const align = style.textAlign ? `<w:jc w:val="${xmlEscape(style.textAlign)}"/>` : "";
1467
+ const headingSize = headingMatch ? `<w:rPr><w:b/><w:sz w:val="${Math.max(24, 40 - Number(headingMatch[1]) * 4)}"/></w:rPr>` : "";
1468
+ const body = runs(el);
1469
+ return `<w:p><w:pPr>${align}${headingSize}</w:pPr>${body || "<w:r><w:t></w:t></w:r>"}</w:p>`;
1470
+ };
1471
+ const tableCell = (cell) => {
1472
+ const fill = colorValue(cell.style.backgroundColor);
1473
+ const shading = fill ? `<w:shd w:val="clear" w:color="auto" w:fill="${fill}"/>` : "";
1474
+ const cellContent = Array.from(cell.childNodes)
1475
+ .map((child) => child instanceof HTMLElement && ["P", "DIV", "H1", "H2", "H3", "H4", "H5", "H6"].includes(child.tagName)
1476
+ ? paragraph(child)
1477
+ : `<w:p>${runs(child)}</w:p>`)
1478
+ .join("");
1479
+ return `<w:tc><w:tcPr>${shading}<w:tcBorders><w:top w:val="single" w:sz="4" w:color="D1D5DB"/><w:left w:val="single" w:sz="4" w:color="D1D5DB"/><w:bottom w:val="single" w:sz="4" w:color="D1D5DB"/><w:right w:val="single" w:sz="4" w:color="D1D5DB"/></w:tcBorders></w:tcPr>${cellContent || "<w:p/>"}</w:tc>`;
1480
+ };
1481
+ const tableXml = (table) => {
1482
+ const rows = Array.from(table.querySelectorAll("tr"));
1483
+ return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/><w:tblBorders><w:top w:val="single" w:sz="4" w:color="D1D5DB"/><w:left w:val="single" w:sz="4" w:color="D1D5DB"/><w:bottom w:val="single" w:sz="4" w:color="D1D5DB"/><w:right w:val="single" w:sz="4" w:color="D1D5DB"/><w:insideH w:val="single" w:sz="4" w:color="D1D5DB"/><w:insideV w:val="single" w:sz="4" w:color="D1D5DB"/></w:tblBorders></w:tblPr>${rows.map((row) => `<w:tr>${Array.from(row.children).map((cell) => tableCell(cell)).join("")}</w:tr>`).join("")}</w:tbl>`;
1484
+ };
1485
+ const blockXml = (node) => {
1486
+ if (node.nodeType === Node.TEXT_NODE) {
1487
+ const text = node.textContent?.trim();
1488
+ return text ? `<w:p>${runs(node)}</w:p>` : "";
1489
+ }
1490
+ if (!(node instanceof HTMLElement))
1491
+ return "";
1492
+ if (node.tagName === "TABLE")
1493
+ return tableXml(node);
1494
+ if (node.tagName === "UL" || node.tagName === "OL") {
1495
+ return Array.from(node.children).map((li) => `<w:p><w:r><w:t>• </w:t></w:r>${runs(li)}</w:p>`).join("");
1496
+ }
1497
+ if (node.tagName === "BLOCKQUOTE") {
1498
+ return `<w:p><w:pPr><w:ind w:left="720"/></w:pPr>${runs(node)}</w:p>`;
1499
+ }
1500
+ if (node.tagName === "HR")
1501
+ return '<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:color="D1D5DB"/></w:pBdr></w:pPr></w:p>';
1502
+ if (["P", "DIV", "PRE", "H1", "H2", "H3", "H4", "H5", "H6"].includes(node.tagName))
1503
+ return paragraph(node);
1504
+ return Array.from(node.childNodes).map(blockXml).join("");
1505
+ };
1506
+ const body = Array.from(root.childNodes).map(blockXml).join("");
1507
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>${body}<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="720" w:right="720" w:bottom="720" w:left="720"/></w:sectPr></w:body></w:document>`;
1508
+ };
1509
+ const exportDocx = async () => {
1510
+ const html = editableRef.current?.innerHTML || "";
1511
+ const zip = new JSZip();
1512
+ zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`);
1513
+ zip.folder("_rels")?.file(".rels", `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`);
1514
+ zip.folder("word")?.file("document.xml", htmlToDocxXml(html));
1515
+ zip.folder("word")?.folder("_rels")?.file("document.xml.rels", `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>`);
1516
+ const blob = await zip.generateAsync({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" });
1517
+ const url = URL.createObjectURL(blob);
1518
+ const link = document.createElement("a");
1519
+ link.href = url;
1520
+ link.download = "smart-rte-export.docx";
1521
+ document.body.appendChild(link);
1522
+ link.click();
1523
+ link.remove();
1524
+ URL.revokeObjectURL(url);
1525
+ };
1526
+ const exportPdf = () => {
1527
+ const html = editableRef.current?.innerHTML || "";
1528
+ const printWindow = window.open("", "_blank", "noopener,noreferrer,width=900,height=700");
1529
+ if (!printWindow)
1530
+ return;
1531
+ printWindow.document.write(`<!doctype html><html><head><title>Export PDF</title><style>body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.6;padding:32px;color:#111}table{border-collapse:collapse;width:100%;margin:12px 0}td,th{border:1px solid #d1d5db;padding:8px;vertical-align:top}img{max-width:100%;height:auto}blockquote{border-left:4px solid #d1d5db;padding-left:12px;color:#374151}pre,code{background:#f3f4f6}pre{padding:12px;white-space:pre-wrap}@media print{body{padding:0}}</style></head><body>${html}</body></html>`);
1532
+ printWindow.document.close();
1533
+ printWindow.focus();
1534
+ setTimeout(() => printWindow.print(), 250);
1535
+ };
920
1536
  const fixNegativeMargins = (root) => {
921
1537
  try {
922
1538
  const nodes = root.querySelectorAll('*');
@@ -929,6 +1545,152 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
929
1545
  }
930
1546
  catch { }
931
1547
  };
1548
+ const cleanPastedHtml = (html, options = {}) => {
1549
+ const shouldPreserveColors = options.preserveColors ?? preserveColors;
1550
+ const shouldPreserveDocumentLayout = options.preserveDocumentLayout ?? false;
1551
+ const template = document.createElement('template');
1552
+ template.innerHTML = html
1553
+ .replace(/&nbsp;/gi, ' ')
1554
+ .replace(/\u00a0/g, ' ')
1555
+ .replace(/[\u200b\u200c\u200d]/g, '');
1556
+ template.content.querySelectorAll('meta, link, style, script').forEach((node) => node.remove());
1557
+ const allowedStyleNames = new Set([
1558
+ 'font-weight',
1559
+ 'font-style',
1560
+ 'text-decoration',
1561
+ 'text-align',
1562
+ 'vertical-align',
1563
+ 'border',
1564
+ 'border-top',
1565
+ 'border-right',
1566
+ 'border-bottom',
1567
+ 'border-left',
1568
+ 'border-collapse',
1569
+ 'padding',
1570
+ 'padding-top',
1571
+ 'padding-right',
1572
+ 'padding-bottom',
1573
+ 'padding-left',
1574
+ 'list-style-type',
1575
+ 'white-space',
1576
+ ]);
1577
+ if (preserveFontFamily)
1578
+ allowedStyleNames.add('font-family');
1579
+ if (shouldPreserveColors) {
1580
+ allowedStyleNames.add('color');
1581
+ allowedStyleNames.add('background');
1582
+ allowedStyleNames.add('background-color');
1583
+ }
1584
+ if (shouldPreserveDocumentLayout) {
1585
+ [
1586
+ 'font-size',
1587
+ 'line-height',
1588
+ 'margin',
1589
+ 'margin-top',
1590
+ 'margin-right',
1591
+ 'margin-bottom',
1592
+ 'margin-left',
1593
+ 'text-indent',
1594
+ 'width',
1595
+ 'min-width',
1596
+ ].forEach((name) => allowedStyleNames.add(name));
1597
+ }
1598
+ template.content.querySelectorAll('*').forEach((node) => {
1599
+ const className = node.getAttribute('class');
1600
+ if (className !== 'srte-preserve-colors')
1601
+ node.removeAttribute('class');
1602
+ node.removeAttribute('id');
1603
+ if (!shouldPreserveDocumentLayout) {
1604
+ node.removeAttribute('width');
1605
+ node.removeAttribute('height');
1606
+ }
1607
+ const style = node.getAttribute('style');
1608
+ if (!style)
1609
+ return;
1610
+ const safeRules = style
1611
+ .split(';')
1612
+ .map((rule) => rule.trim())
1613
+ .filter(Boolean)
1614
+ .filter((rule) => {
1615
+ const separator = rule.indexOf(':');
1616
+ if (separator === -1)
1617
+ return false;
1618
+ const name = rule.slice(0, separator).trim().toLowerCase();
1619
+ const value = rule.slice(separator + 1).trim().toLowerCase();
1620
+ if (!allowedStyleNames.has(name))
1621
+ return false;
1622
+ if (value.includes('position') || value.includes('expression') || value.includes('javascript:'))
1623
+ return false;
1624
+ if (name === 'white-space' && value !== 'pre-wrap')
1625
+ return false;
1626
+ if ((name === 'width' || name === 'min-width') && !/^[\d.]+(px|pt|em|rem|%)$/.test(value))
1627
+ return false;
1628
+ return true;
1629
+ });
1630
+ if (safeRules.length)
1631
+ node.setAttribute('style', safeRules.join('; '));
1632
+ else
1633
+ node.removeAttribute('style');
1634
+ });
1635
+ return template.innerHTML;
1636
+ };
1637
+ const normalizeEditorContent = () => {
1638
+ const el = editableRef.current;
1639
+ if (!el)
1640
+ return;
1641
+ fixNegativeMargins(el);
1642
+ ensureTableWrappers(el);
1643
+ addTableResizeHandles();
1644
+ };
1645
+ const insertHtmlAtEnd = (html) => {
1646
+ const el = editableRef.current;
1647
+ if (!el)
1648
+ return;
1649
+ el.focus();
1650
+ const range = document.createRange();
1651
+ range.selectNodeContents(el);
1652
+ range.collapse(false);
1653
+ const sel = window.getSelection();
1654
+ sel?.removeAllRanges();
1655
+ sel?.addRange(range);
1656
+ const separator = el.textContent?.trim() ? '<p><br></p>' : '';
1657
+ document.execCommand('insertHTML', false, `${separator}${html}`);
1658
+ };
1659
+ const replaceEditorHtml = (html) => {
1660
+ const el = editableRef.current;
1661
+ if (!el)
1662
+ return;
1663
+ el.innerHTML = html;
1664
+ el.focus();
1665
+ const range = document.createRange();
1666
+ range.selectNodeContents(el);
1667
+ range.collapse(false);
1668
+ const sel = window.getSelection();
1669
+ sel?.removeAllRanges();
1670
+ sel?.addRange(range);
1671
+ };
1672
+ const insertImportedHtml = (html, mode, cleanOptions) => {
1673
+ try {
1674
+ const cleanHtml = cleanPastedHtml(html, cleanOptions);
1675
+ if (mode === 'replace')
1676
+ replaceEditorHtml(cleanHtml);
1677
+ else
1678
+ insertHtmlAtEnd(cleanHtml);
1679
+ normalizeEditorContent();
1680
+ emitChange();
1681
+ }
1682
+ catch (error) {
1683
+ console.error('Error inserting imported content:', error);
1684
+ }
1685
+ };
1686
+ const insertCleanHtml = (html) => {
1687
+ try {
1688
+ document.execCommand("insertHTML", false, cleanPastedHtml(html));
1689
+ normalizeEditorContent();
1690
+ emitChange();
1691
+ }
1692
+ catch { }
1693
+ };
932
1694
  const ensureTableWrappers = (root) => {
933
1695
  try {
934
1696
  const tables = root.querySelectorAll('table');
@@ -938,10 +1700,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
938
1700
  const wrapper = document.createElement('div');
939
1701
  wrapper.setAttribute('data-table-wrapper', 'true');
940
1702
  wrapper.style.overflowX = 'auto';
1703
+ wrapper.style.overflowY = 'visible';
941
1704
  wrapper.style.webkitOverflowScrolling = 'touch';
942
1705
  wrapper.style.width = '100%';
943
1706
  wrapper.style.maxWidth = '100%';
944
1707
  wrapper.style.display = 'block';
1708
+ wrapper.style.paddingBottom = '8px';
945
1709
  // Use insertBefore + appendChild to move element without losing too much state
946
1710
  // simpler than replaceChild for wrapping
947
1711
  parent.insertBefore(wrapper, table);
@@ -1335,6 +2099,27 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1335
2099
  firstRow.replaceChild(td, c);
1336
2100
  }
1337
2101
  }
2102
+ handleInput();
2103
+ };
2104
+ const toggleHeaderColumn = (cell) => {
2105
+ const pos = getCellPosition(cell);
2106
+ if (!pos)
2107
+ return;
2108
+ const { tbody, cIdx } = pos;
2109
+ const rows = Array.from(tbody.querySelectorAll("tr"));
2110
+ const columnCells = rows
2111
+ .map((row) => cellsOfRow(row)[cIdx])
2112
+ .filter(Boolean);
2113
+ const shouldMakeHeader = columnCells.some((c) => c.tagName !== "TH");
2114
+ for (const c of columnCells) {
2115
+ const replacement = document.createElement(shouldMakeHeader ? "th" : "td");
2116
+ replacement.innerHTML = c.innerHTML || "&nbsp;";
2117
+ replacement.style.border = c.style.border || "1px solid var(--srte-border)";
2118
+ replacement.style.padding = c.style.padding || "6px";
2119
+ replacement.style.minWidth = c.style.minWidth || "60px";
2120
+ c.parentElement?.replaceChild(replacement, c);
2121
+ }
2122
+ handleInput();
1338
2123
  };
1339
2124
  const applyBgToSelection = (hex, fallbackCell) => {
1340
2125
  const sel = selectionRef.current;
@@ -1503,7 +2288,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1503
2288
  borderRadius: 6,
1504
2289
  width: "100%",
1505
2290
  maxWidth: "100vw",
1506
- overflow: "hidden",
2291
+ overflow: "visible",
1507
2292
  display: "flex",
1508
2293
  flexDirection: "column",
1509
2294
  background: "var(--srte-bg)",
@@ -1552,6 +2337,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1552
2337
  } }), _jsx("input", { ref: docxInputRef, type: "file", accept: ".docx", style: { display: "none" }, onChange: (e) => {
1553
2338
  handleDocxFiles(e.currentTarget.files);
1554
2339
  e.currentTarget.value = "";
2340
+ } }), _jsx("input", { ref: htmlInputRef, type: "file", accept: ".html,.htm,text/html", style: { display: "none" }, onChange: (e) => {
2341
+ importTextFile(e.currentTarget.files, "html");
2342
+ e.currentTarget.value = "";
2343
+ } }), _jsx("input", { ref: mdInputRef, type: "file", accept: ".md,.markdown,text/markdown,text/plain", style: { display: "none" }, onChange: (e) => {
2344
+ importTextFile(e.currentTarget.files, "md");
2345
+ e.currentTarget.value = "";
1555
2346
  } }), _jsxs("select", { defaultValue: "p", onChange: (e) => {
1556
2347
  const val = e.target.value;
1557
2348
  if (val === "p")
@@ -1621,7 +2412,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1621
2412
  borderRadius: 6,
1622
2413
  background: "var(--srte-input-bg)",
1623
2414
  color: "var(--srte-input-text)",
1624
- }, children: [_jsx("option", { value: "", disabled: true, children: "Size" }), _jsx("option", { value: "8", children: "8" }), _jsx("option", { value: "9", children: "9" }), _jsx("option", { value: "10", children: "10" }), _jsx("option", { value: "11", children: "11" }), _jsx("option", { value: "12", children: "12" }), _jsx("option", { value: "14", children: "14" }), _jsx("option", { value: "18", children: "18" }), _jsx("option", { value: "24", children: "24" }), _jsx("option", { value: "30", children: "30" }), _jsx("option", { value: "36", children: "36" }), _jsx("option", { value: "48", children: "48" }), _jsx("option", { value: "60", children: "60" }), _jsx("option", { value: "72", children: "72" }), _jsx("option", { value: "96", children: "96" })] }), _jsxs("select", { value: currentFont, onMouseDown: () => {
2415
+ }, children: [_jsx("option", { value: "", disabled: true, children: "Size" }), _jsx("option", { value: "8", children: "8" }), _jsx("option", { value: "9", children: "9" }), _jsx("option", { value: "10", children: "10" }), _jsx("option", { value: "11", children: "11" }), _jsx("option", { value: "12", children: "12" }), _jsx("option", { value: "14", children: "14" }), _jsx("option", { value: "18", children: "18" }), _jsx("option", { value: "24", children: "24" }), _jsx("option", { value: "30", children: "30" }), _jsx("option", { value: "36", children: "36" }), _jsx("option", { value: "48", children: "48" }), _jsx("option", { value: "60", children: "60" }), _jsx("option", { value: "72", children: "72" }), _jsx("option", { value: "96", children: "96" })] }), preserveFontFamily && (_jsxs("select", { value: currentFont, onMouseDown: () => {
1625
2416
  const sel = window.getSelection();
1626
2417
  if (sel && sel.rangeCount > 0) {
1627
2418
  const range = sel.getRangeAt(0);
@@ -1638,7 +2429,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1638
2429
  background: "var(--srte-input-bg)",
1639
2430
  color: "var(--srte-input-text)",
1640
2431
  maxWidth: 100,
1641
- }, children: [_jsx("option", { value: "", disabled: true, children: "Font" }), fonts.map((f) => (_jsx("option", { value: f.value, children: f.name }, f.value)))] }), _jsx("button", { title: "Text Color", onClick: () => {
2432
+ }, children: [_jsx("option", { value: "", disabled: true, children: "Font" }), fonts.map((f) => (_jsx("option", { value: f.value, children: f.name }, f.value)))] })), _jsx("button", { title: "Text Color", onClick: () => {
1642
2433
  setColorPickerType('text');
1643
2434
  setShowColorPicker(true);
1644
2435
  }, style: {
@@ -1650,7 +2441,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1650
2441
  background: "var(--srte-input-bg)",
1651
2442
  color: "var(--srte-input-text)",
1652
2443
  position: "relative",
1653
- }, children: _jsx("span", { style: { fontWeight: 700 }, children: "A" }) }), _jsx("button", { title: "Background Color", onClick: () => {
2444
+ }, children: _jsx("span", { style: { fontWeight: 700, borderBottom: "3px solid currentColor", lineHeight: 1 }, children: "A" }) }), _jsx("button", { title: "Background Color", onClick: () => {
1654
2445
  setColorPickerType('background');
1655
2446
  setShowColorPicker(true);
1656
2447
  }, style: {
@@ -1661,7 +2452,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1661
2452
  borderRadius: 6,
1662
2453
  background: "var(--srte-input-bg)",
1663
2454
  color: "var(--srte-input-text)",
1664
- }, children: _jsx("span", { style: { fontWeight: 700, padding: "2px 4px" }, children: "A" }) }), _jsx("button", { title: "Bulleted list", onClick: () => exec("insertUnorderedList"), style: {
2455
+ }, children: _jsx("span", { style: { fontWeight: 700, padding: "1px 4px", background: "var(--srte-accent-bg)", borderRadius: 3 }, children: "A" }) }), _jsxs("button", { title: "Subscript", onClick: () => exec("subscript"), style: {
2456
+ height: 32,
2457
+ minWidth: 32,
2458
+ padding: "0 8px",
2459
+ border: "1px solid var(--srte-input-border)",
2460
+ borderRadius: 6,
2461
+ background: "var(--srte-input-bg)",
2462
+ color: "var(--srte-input-text)",
2463
+ }, children: ["X", _jsx("sub", { children: "2" })] }), _jsxs("button", { title: "Superscript", onClick: () => exec("superscript"), style: {
2464
+ height: 32,
2465
+ minWidth: 32,
2466
+ padding: "0 8px",
2467
+ border: "1px solid var(--srte-input-border)",
2468
+ borderRadius: 6,
2469
+ background: "var(--srte-input-bg)",
2470
+ color: "var(--srte-input-text)",
2471
+ }, children: ["X", _jsx("sup", { children: "2" })] }), _jsx("button", { title: "Bulleted list", onClick: () => exec("insertUnorderedList"), style: {
1665
2472
  height: 32,
1666
2473
  padding: "0 10px",
1667
2474
  border: "1px solid var(--srte-input-border)",
@@ -1675,7 +2482,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1675
2482
  borderRadius: 6,
1676
2483
  background: "var(--srte-input-bg)",
1677
2484
  color: "var(--srte-input-text)",
1678
- }, children: "1. List" }), _jsx("button", { title: "Blockquote", onClick: () => exec("formatBlock", "<blockquote>"), style: {
2485
+ }, children: "1. List" }), _jsx("button", { title: "Blockquote", onClick: toggleBlockquote, style: {
1679
2486
  height: 32,
1680
2487
  minWidth: 32,
1681
2488
  padding: "0 8px",
@@ -1683,7 +2490,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1683
2490
  borderRadius: 6,
1684
2491
  background: "var(--srte-input-bg)",
1685
2492
  color: "var(--srte-input-text)",
1686
- }, children: "\u275D" }), _jsx("button", { title: "Code block", onClick: () => exec("formatBlock", "<pre>"), style: {
2493
+ }, children: "\u275D" }), _jsx("button", { title: "Special characters", onClick: () => setShowSpecialChars(true), style: {
2494
+ height: 32,
2495
+ minWidth: 32,
2496
+ padding: "0 8px",
2497
+ border: "1px solid var(--srte-input-border)",
2498
+ borderRadius: 6,
2499
+ background: "var(--srte-input-bg)",
2500
+ color: "var(--srte-input-text)",
2501
+ }, children: "\u03A9" }), _jsx("button", { title: "Code block", onClick: () => exec("formatBlock", "<pre>"), style: {
1687
2502
  height: 32,
1688
2503
  minWidth: 36,
1689
2504
  padding: "0 8px",
@@ -1744,7 +2559,49 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1744
2559
  background: "var(--srte-input-bg)",
1745
2560
  color: "var(--srte-input-text)",
1746
2561
  opacity: loadingDocx ? 0.5 : 1,
1747
- }, children: loadingDocx ? '⌛ Importing...' : '📝 DOCX' }), _jsxs("div", { style: {
2562
+ }, children: loadingDocx ? '⌛ Importing...' : '📝 DOCX' }), _jsx("button", { title: "Import HTML", onClick: () => htmlInputRef.current?.click(), style: {
2563
+ height: 32,
2564
+ padding: "0 10px",
2565
+ border: "1px solid var(--srte-input-border)",
2566
+ borderRadius: 6,
2567
+ background: "var(--srte-input-bg)",
2568
+ color: "var(--srte-input-text)",
2569
+ }, children: "HTML" }), _jsx("button", { title: "Import Markdown", onClick: () => mdInputRef.current?.click(), style: {
2570
+ height: 32,
2571
+ padding: "0 10px",
2572
+ border: "1px solid var(--srte-input-border)",
2573
+ borderRadius: 6,
2574
+ background: "var(--srte-input-bg)",
2575
+ color: "var(--srte-input-text)",
2576
+ }, children: "MD" }), _jsx("button", { title: "Export HTML", onClick: exportHtml, style: {
2577
+ height: 32,
2578
+ padding: "0 10px",
2579
+ border: "1px solid var(--srte-input-border)",
2580
+ borderRadius: 6,
2581
+ background: "var(--srte-input-bg)",
2582
+ color: "var(--srte-input-text)",
2583
+ }, children: "Export HTML" }), _jsx("button", { title: "Export Markdown", onClick: exportMarkdown, style: {
2584
+ height: 32,
2585
+ padding: "0 10px",
2586
+ border: "1px solid var(--srte-input-border)",
2587
+ borderRadius: 6,
2588
+ background: "var(--srte-input-bg)",
2589
+ color: "var(--srte-input-text)",
2590
+ }, children: "Export MD" }), _jsx("button", { title: "Export DOCX", onClick: exportDocx, style: {
2591
+ height: 32,
2592
+ padding: "0 10px",
2593
+ border: "1px solid var(--srte-input-border)",
2594
+ borderRadius: 6,
2595
+ background: "var(--srte-input-bg)",
2596
+ color: "var(--srte-input-text)",
2597
+ }, children: "Export DOCX" }), _jsx("button", { title: "Export PDF", onClick: exportPdf, style: {
2598
+ height: 32,
2599
+ padding: "0 10px",
2600
+ border: "1px solid var(--srte-input-border)",
2601
+ borderRadius: 6,
2602
+ background: "var(--srte-input-bg)",
2603
+ color: "var(--srte-input-text)",
2604
+ }, children: "Export PDF" }), _jsxs("div", { style: {
1748
2605
  display: "inline-flex",
1749
2606
  gap: 4,
1750
2607
  alignItems: "center",
@@ -1823,7 +2680,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1823
2680
  color: "var(--srte-input-text)",
1824
2681
  }, children: "\u293E Redo" })] }), media && mediaManager && (_jsx(MediaManager, { open: showMediaManager, onClose: () => setShowMediaManager(false), adapter: mediaManager, onSelect: (item) => {
1825
2682
  if (item?.url)
1826
- insertImageAtSelection(item.url);
2683
+ insertImageAtSelection(item);
1827
2684
  } })), table && showTableDialog && (_jsx("div", { style: {
1828
2685
  position: "fixed",
1829
2686
  inset: 0,
@@ -1988,6 +2845,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1988
2845
  background: 'var(--srte-input-bg)',
1989
2846
  color: 'var(--srte-input-text)',
1990
2847
  cursor: 'pointer',
2848
+ }, children: "Close" }) })] }) })), showSpecialChars && (_jsx("div", { style: {
2849
+ position: "fixed",
2850
+ inset: 0,
2851
+ background: "var(--srte-modal-backdrop)",
2852
+ display: "flex",
2853
+ alignItems: "center",
2854
+ justifyContent: "center",
2855
+ zIndex: 50,
2856
+ }, onClick: () => setShowSpecialChars(false), children: _jsxs("div", { style: {
2857
+ background: "var(--srte-modal-bg)",
2858
+ color: "var(--srte-modal-text)",
2859
+ padding: 16,
2860
+ borderRadius: 8,
2861
+ width: 420,
2862
+ maxWidth: "90vw",
2863
+ boxShadow: "var(--srte-menu-shadow)",
2864
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 12 }, children: "Special characters" }), [
2865
+ {
2866
+ label: "Greek",
2867
+ chars: ["α", "β", "γ", "δ", "ε", "ζ", "η", "θ", "ι", "κ", "λ", "μ", "ν", "ξ", "π", "ρ", "σ", "τ", "φ", "χ", "ψ", "ω", "Δ", "Σ", "Ω"],
2868
+ },
2869
+ {
2870
+ label: "Medical / Math",
2871
+ chars: ["±", "≤", "≥", "≠", "≈", "∞", "°", "µ", "×", "÷", "→", "←", "↑", "↓", "∴", "∵", "√", "∑", "∫", "₂", "₃", "²", "³"],
2872
+ },
2873
+ ].map((group) => (_jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("div", { style: { fontSize: 12, color: "var(--srte-text-muted)", marginBottom: 6 }, children: group.label }), _jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 }, children: group.chars.map((char) => (_jsx("button", { type: "button", onClick: () => {
2874
+ insertTextAtSelection(char);
2875
+ setShowSpecialChars(false);
2876
+ }, style: {
2877
+ height: 32,
2878
+ minWidth: 32,
2879
+ padding: "0 8px",
2880
+ border: "1px solid var(--srte-input-border)",
2881
+ borderRadius: 6,
2882
+ background: "var(--srte-input-bg)",
2883
+ color: "var(--srte-input-text)",
2884
+ fontSize: 16,
2885
+ }, children: char }, char))) })] }, group.label))), _jsx("div", { style: { display: "flex", justifyContent: "flex-end" }, children: _jsx("button", { type: "button", onClick: () => setShowSpecialChars(false), style: {
2886
+ padding: "6px 16px",
2887
+ border: "1px solid var(--srte-input-border)",
2888
+ borderRadius: 6,
2889
+ background: "var(--srte-input-bg)",
2890
+ color: "var(--srte-input-text)",
2891
+ cursor: "pointer",
1991
2892
  }, children: "Close" }) })] }) })), formula && showFormulaDialog && (_jsx("div", { style: {
1992
2893
  position: "fixed",
1993
2894
  inset: 0,
@@ -2093,12 +2994,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2093
2994
  width: "100%",
2094
2995
  maxWidth: "100%",
2095
2996
  flex: "1 1 auto",
2997
+ minWidth: 0,
2096
2998
  minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
2097
2999
  maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
2098
3000
  overflowY: "auto",
2099
- overflowX: "hidden",
3001
+ overflowX: "auto",
3002
+ overscrollBehavior: "contain",
2100
3003
  boxSizing: "border-box",
2101
3004
  position: "relative",
3005
+ scrollPaddingBottom: 24,
2102
3006
  }, children: _jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
2103
3007
  isComposingRef.current = false;
2104
3008
  handleInput();
@@ -2109,8 +3013,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2109
3013
  if (hasImage) {
2110
3014
  e.preventDefault();
2111
3015
  handleLocalImageFiles(items);
3016
+ return;
2112
3017
  }
2113
3018
  }
3019
+ const html = e.clipboardData?.getData("text/html");
3020
+ if (html) {
3021
+ e.preventDefault();
3022
+ insertCleanHtml(cleanPastedHtml(html));
3023
+ }
2114
3024
  }, onDragOver: (e) => {
2115
3025
  // Allow dragging images within editor and file drops
2116
3026
  if (draggedImageRef.current ||
@@ -2231,10 +3141,11 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2231
3141
  }, onDragEnd: () => {
2232
3142
  draggedImageRef.current = null;
2233
3143
  }, style: {
2234
- minHeight: "100%",
3144
+ minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
2235
3145
  maxWidth: "100%",
2236
- overflowX: "hidden",
3146
+ overflowX: "visible",
2237
3147
  padding: "16px",
3148
+ paddingBottom: "32px",
2238
3149
  outline: "none",
2239
3150
  lineHeight: 1.6,
2240
3151
  boxSizing: "border-box",
@@ -2613,7 +3524,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2613
3524
  }, onClick: () => {
2614
3525
  toggleHeaderRow(tableMenu.cell);
2615
3526
  setTableMenu(null);
2616
- }, children: [_jsx("span", { children: "H\u2081" }), _jsx("span", { children: "Toggle header row" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("button", { style: {
3527
+ }, children: [_jsx("span", { children: "H\u2081" }), _jsx("span", { children: "Toggle header row" })] }), _jsxs("button", { style: {
3528
+ display: "flex",
3529
+ alignItems: "center",
3530
+ gap: 8,
3531
+ padding: "6px 8px",
3532
+ fontSize: 12,
3533
+ }, onClick: () => {
3534
+ toggleHeaderColumn(tableMenu.cell);
3535
+ setTableMenu(null);
3536
+ }, children: [_jsx("span", { children: "H\u2195" }), _jsx("span", { children: "Toggle header column" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("button", { style: {
2617
3537
  display: "flex",
2618
3538
  alignItems: "center",
2619
3539
  gap: 8,
@@ -2640,7 +3560,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2640
3560
  borderRadius: 8,
2641
3561
  boxShadow: "var(--srte-menu-shadow)",
2642
3562
  padding: 8,
2643
- width: 220,
3563
+ width: 280,
3564
+ maxHeight: "80vh",
3565
+ overflowY: "auto",
2644
3566
  color: "var(--srte-menu-text)",
2645
3567
  }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, fontSize: 11, margin: "2px 6px 6px" }, children: "Image" }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [_jsx("span", { style: { width: 48, fontSize: 12 }, children: "Link" }), _jsx("input", { defaultValue: imageMenu.img.parentElement?.tagName === "A"
2646
3568
  ? imageMenu.img.parentElement.href
@@ -2742,7 +3664,58 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2742
3664
  img.style.margin = "0 0 8px 8px";
2743
3665
  scheduleImageOverlay();
2744
3666
  handleInput();
2745
- }, children: "\u27F9" })] }), _jsxs("div", { style: { display: "flex", gap: 6 }, children: [_jsx("button", { onClick: () => {
3667
+ }, children: "\u27F9" })] }), _jsxs("div", { style: {
3668
+ borderTop: "1px solid var(--srte-border-light)",
3669
+ paddingTop: 6,
3670
+ display: "grid",
3671
+ gap: 6,
3672
+ }, children: [_jsx("div", { style: { fontWeight: 600, fontSize: 11 }, children: "License" }), _jsx("input", { placeholder: "Work name", defaultValue: imageMenu.img.dataset.workName || "", onChange: (e) => {
3673
+ imageMenu.img.dataset.workName = e.target.value;
3674
+ handleInput();
3675
+ }, style: {
3676
+ padding: "4px 6px",
3677
+ border: "1px solid var(--srte-border-light)",
3678
+ borderRadius: 4,
3679
+ color: "var(--srte-input-text)",
3680
+ background: "var(--srte-input-bg)",
3681
+ } }), _jsx("input", { placeholder: "Author", defaultValue: imageMenu.img.dataset.licenseAuthor || "", onChange: (e) => {
3682
+ imageMenu.img.dataset.licenseAuthor = e.target.value;
3683
+ handleInput();
3684
+ }, style: {
3685
+ padding: "4px 6px",
3686
+ border: "1px solid var(--srte-border-light)",
3687
+ borderRadius: 4,
3688
+ color: "var(--srte-input-text)",
3689
+ background: "var(--srte-input-bg)",
3690
+ } }), _jsxs("select", { defaultValue: imageMenu.img.dataset.licenseType || "", onChange: (e) => {
3691
+ imageMenu.img.dataset.licenseType = e.target.value;
3692
+ handleInput();
3693
+ }, style: {
3694
+ height: 28,
3695
+ padding: "0 6px",
3696
+ border: "1px solid var(--srte-border-light)",
3697
+ borderRadius: 4,
3698
+ color: "var(--srte-input-text)",
3699
+ background: "var(--srte-input-bg)",
3700
+ }, children: [_jsx("option", { value: "", children: "License type" }), _jsx("option", { value: "public-domain", children: "Public domain" }), _jsx("option", { value: "cc0", children: "CC0" }), _jsx("option", { value: "cc-by", children: "CC BY" }), _jsx("option", { value: "cc-by-sa", children: "CC BY-SA" }), _jsx("option", { value: "cc-by-nc", children: "CC BY-NC" }), _jsx("option", { value: "rights-managed", children: "Rights managed" }), _jsx("option", { value: "custom", children: "Custom" })] }), _jsx("input", { placeholder: "License notes", defaultValue: imageMenu.img.dataset.licenseText || "", onChange: (e) => {
3701
+ imageMenu.img.dataset.licenseText = e.target.value;
3702
+ handleInput();
3703
+ }, style: {
3704
+ padding: "4px 6px",
3705
+ border: "1px solid var(--srte-border-light)",
3706
+ borderRadius: 4,
3707
+ color: "var(--srte-input-text)",
3708
+ background: "var(--srte-input-bg)",
3709
+ } }), _jsx("input", { placeholder: "Source URL", defaultValue: imageMenu.img.dataset.licenseUrl || "", onChange: (e) => {
3710
+ imageMenu.img.dataset.licenseUrl = e.target.value;
3711
+ handleInput();
3712
+ }, style: {
3713
+ padding: "4px 6px",
3714
+ border: "1px solid var(--srte-border-light)",
3715
+ borderRadius: 4,
3716
+ color: "var(--srte-input-text)",
3717
+ background: "var(--srte-input-bg)",
3718
+ } })] }), _jsxs("div", { style: { display: "flex", gap: 6 }, children: [_jsx("button", { onClick: () => {
2746
3719
  replaceTargetRef.current = imageMenu.img;
2747
3720
  fileInputRef.current?.click();
2748
3721
  }, children: "Replace\u2026" }), _jsx("button", { onClick: () => {