smartrte-react 0.2.1 → 0.2.2

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.
@@ -26,6 +26,24 @@ type ClassicEditorProps = {
26
26
  * Example: "Arial, sans-serif"
27
27
  */
28
28
  defaultFont?: string;
29
+ /**
30
+ * Preserve font-family styles from pasted/imported content.
31
+ * Defaults to false so host applications can keep a single app font while
32
+ * still preserving bold, italic, headings, lists, tables, and colors.
33
+ */
34
+ preserveFontFamily?: boolean;
35
+ /**
36
+ * Preserve foreground/background colors from pasted/imported content.
37
+ * Defaults to false so dark-mode editors remain readable when content is
38
+ * copied from sources such as Google Docs with hardcoded black-on-white styles.
39
+ */
40
+ preserveColors?: boolean;
41
+ /**
42
+ * Preserve visual styling from imported DOCX files except font-family.
43
+ * This keeps Word-authored colors, spacing, borders, and table fills while
44
+ * still allowing the host app to control the editor font.
45
+ */
46
+ preserveDocxStyles?: boolean;
29
47
  /**
30
48
  * Theme mode for the editor.
31
49
  * - "light" (default): Uses the built-in light theme.
@@ -39,5 +57,5 @@ type ClassicEditorProps = {
39
57
  */
40
58
  className?: string;
41
59
  };
42
- export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table, media, formula, mediaManager, fonts, defaultFont, theme, className, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
60
+ export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table, media, formula, mediaManager, fonts, defaultFont, preserveFontFamily, preserveColors, preserveDocxStyles, theme, className, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
43
61
  export {};
@@ -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
  }
@@ -806,30 +889,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
806
889
  closeTable();
807
890
  fullHtml += html;
808
891
  }
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
- }
892
+ insertImportedHtml(fullHtml, mode);
833
893
  }
834
894
  catch (error) {
835
895
  console.error('Error reading PDF:', error);
@@ -859,55 +919,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
859
919
  try {
860
920
  setLoadingDocx(true);
861
921
  const arrayBuffer = await file.arrayBuffer();
862
- const result = await mammoth.convertToHtml({ arrayBuffer });
863
- let html = result.value;
922
+ const html = preserveDocxStyles
923
+ ? await convertDocxToStyledHtml(arrayBuffer)
924
+ : await convertDocxWithMammoth(arrayBuffer);
864
925
  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
- });
926
+ insertImportedHtml(`<div class="srte-preserve-colors">${html}</div>`, mode, {
927
+ preserveColors: preserveDocxStyles,
928
+ preserveDocumentLayout: preserveDocxStyles,
879
929
  });
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
930
  }
912
931
  }
913
932
  catch (error) {
@@ -917,6 +936,341 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
917
936
  setLoadingDocx(false);
918
937
  }
919
938
  };
939
+ const convertDocxWithMammoth = async (arrayBuffer) => {
940
+ const result = await mammoth.convertToHtml({ arrayBuffer });
941
+ const temp = document.createElement('div');
942
+ temp.innerHTML = result.value;
943
+ enhanceImportedTables(temp);
944
+ return temp.innerHTML;
945
+ };
946
+ const convertDocxToStyledHtml = async (arrayBuffer) => {
947
+ try {
948
+ const zip = await JSZip.loadAsync(arrayBuffer);
949
+ const documentXml = await zip.file('word/document.xml')?.async('text');
950
+ if (!documentXml)
951
+ return convertDocxWithMammoth(arrayBuffer);
952
+ const parser = new DOMParser();
953
+ const doc = parser.parseFromString(documentXml, 'application/xml');
954
+ if (doc.querySelector('parsererror'))
955
+ return convertDocxWithMammoth(arrayBuffer);
956
+ const body = Array.from(doc.getElementsByTagName('*')).find((node) => node.localName === 'body');
957
+ if (!body)
958
+ return convertDocxWithMammoth(arrayBuffer);
959
+ const html = directChildren(body)
960
+ .filter((node) => node.localName !== 'sectPr')
961
+ .map((node) => {
962
+ if (node.localName === 'p')
963
+ return convertDocxParagraph(node);
964
+ if (node.localName === 'tbl')
965
+ return convertDocxTable(node);
966
+ return '';
967
+ })
968
+ .join('');
969
+ if (!html.trim())
970
+ return convertDocxWithMammoth(arrayBuffer);
971
+ const temp = document.createElement('div');
972
+ temp.innerHTML = html;
973
+ enhanceImportedTables(temp);
974
+ return temp.innerHTML;
975
+ }
976
+ catch (error) {
977
+ console.warn('Falling back to Mammoth DOCX import:', error);
978
+ return convertDocxWithMammoth(arrayBuffer);
979
+ }
980
+ };
981
+ const directChildren = (node) => Array.from(node.children);
982
+ const firstChildByName = (node, localName) => node
983
+ ? directChildren(node).find((child) => child.localName === localName)
984
+ : undefined;
985
+ const childrenByName = (node, localName) => node
986
+ ? directChildren(node).filter((child) => child.localName === localName)
987
+ : [];
988
+ const docxAttr = (node, name) => {
989
+ if (!node)
990
+ return '';
991
+ return (node.getAttribute(`w:${name}`) ||
992
+ node.getAttribute(name) ||
993
+ node.getAttributeNS('http://schemas.openxmlformats.org/wordprocessingml/2006/main', name) ||
994
+ '');
995
+ };
996
+ const docxHexColor = (value) => {
997
+ if (!value || value.toLowerCase() === 'auto')
998
+ return '';
999
+ const normalized = value.replace(/[^0-9a-f]/gi, '');
1000
+ return normalized.length === 6 ? `#${normalized}` : '';
1001
+ };
1002
+ const twipsToPt = (value) => {
1003
+ const n = Number(value);
1004
+ return Number.isFinite(n) ? `${Math.max(n / 20, 0)}pt` : '';
1005
+ };
1006
+ const halfPointsToPt = (value) => {
1007
+ const n = Number(value);
1008
+ return Number.isFinite(n) ? `${Math.max(n / 2, 1)}pt` : '';
1009
+ };
1010
+ const cssRules = (rules) => rules
1011
+ .filter(([, value]) => Boolean(value))
1012
+ .map(([name, value]) => `${name}: ${value}`)
1013
+ .join('; ');
1014
+ const styleAttr = (style) => (style ? ` style="${escapeHtml(style)}"` : '');
1015
+ const convertDocxParagraphStyle = (paragraph) => {
1016
+ const pPr = firstChildByName(paragraph, 'pPr');
1017
+ if (!pPr)
1018
+ return '';
1019
+ const spacing = firstChildByName(pPr, 'spacing');
1020
+ const jc = firstChildByName(pPr, 'jc');
1021
+ const indent = firstChildByName(pPr, 'ind');
1022
+ const borderBottom = firstChildByName(firstChildByName(pPr, 'pBdr'), 'bottom');
1023
+ const line = docxAttr(spacing, 'line');
1024
+ const lineRule = docxAttr(spacing, 'lineRule');
1025
+ return cssRules([
1026
+ ['text-align', docxAttr(jc, 'val')],
1027
+ ['margin-top', twipsToPt(docxAttr(spacing, 'before'))],
1028
+ ['margin-bottom', twipsToPt(docxAttr(spacing, 'after'))],
1029
+ ['margin-left', twipsToPt(docxAttr(indent, 'left'))],
1030
+ ['text-indent', twipsToPt(docxAttr(indent, 'firstLine'))],
1031
+ ['line-height', line && lineRule === 'auto' ? `${Number(line) / 240}` : ''],
1032
+ ['border-bottom', docxBorderCss(borderBottom)],
1033
+ ]);
1034
+ };
1035
+ const convertDocxRunStyle = (run) => {
1036
+ const rPr = firstChildByName(run, 'rPr');
1037
+ if (!rPr)
1038
+ return '';
1039
+ const color = docxHexColor(docxAttr(firstChildByName(rPr, 'color'), 'val'));
1040
+ const highlight = docxHexColor(docxAttr(firstChildByName(rPr, 'highlight'), 'val'));
1041
+ const shade = docxHexColor(docxAttr(firstChildByName(rPr, 'shd'), 'fill'));
1042
+ const size = halfPointsToPt(docxAttr(firstChildByName(rPr, 'sz'), 'val'));
1043
+ const underline = firstChildByName(rPr, 'u');
1044
+ return cssRules([
1045
+ ['font-weight', firstChildByName(rPr, 'b') ? '700' : ''],
1046
+ ['font-style', firstChildByName(rPr, 'i') ? 'italic' : ''],
1047
+ ['text-decoration', underline ? 'underline' : ''],
1048
+ ['color', color],
1049
+ ['background-color', highlight || shade],
1050
+ ['font-size', size],
1051
+ ]);
1052
+ };
1053
+ const convertDocxRun = (run) => {
1054
+ const rPr = firstChildByName(run, 'rPr');
1055
+ const vertAlign = docxAttr(firstChildByName(rPr, 'vertAlign'), 'val');
1056
+ const style = convertDocxRunStyle(run);
1057
+ const content = directChildren(run)
1058
+ .map((child) => {
1059
+ if (child.localName === 't')
1060
+ return escapeHtml(child.textContent || '');
1061
+ if (child.localName === 'tab')
1062
+ return '&emsp;';
1063
+ if (child.localName === 'br') {
1064
+ return docxAttr(child, 'type') === 'page'
1065
+ ? '<hr class="srte-docx-page-break">'
1066
+ : '<br>';
1067
+ }
1068
+ return '';
1069
+ })
1070
+ .join('');
1071
+ if (!content)
1072
+ return '';
1073
+ const tag = vertAlign === 'superscript' ? 'sup' : vertAlign === 'subscript' ? 'sub' : 'span';
1074
+ return `<${tag}${styleAttr(style)}>${content}</${tag}>`;
1075
+ };
1076
+ const convertDocxParagraph = (paragraph) => {
1077
+ const style = convertDocxParagraphStyle(paragraph);
1078
+ const content = childrenByName(paragraph, 'r').map(convertDocxRun).join('');
1079
+ return `<p${styleAttr(style)}>${content || '<br>'}</p>`;
1080
+ };
1081
+ const docxBorderCss = (border) => {
1082
+ if (!border)
1083
+ return '';
1084
+ const val = docxAttr(border, 'val');
1085
+ if (!val || val === 'nil' || val === 'none')
1086
+ return '';
1087
+ const size = Number(docxAttr(border, 'sz')) || 4;
1088
+ const width = Math.max(size / 8, 0.5);
1089
+ const color = docxHexColor(docxAttr(border, 'color')) || '#d1d5db';
1090
+ return `${width}px solid ${color}`;
1091
+ };
1092
+ const convertDocxCellStyle = (cell) => {
1093
+ const tcPr = firstChildByName(cell, 'tcPr');
1094
+ const width = twipsToPt(docxAttr(firstChildByName(tcPr, 'tcW'), 'w'));
1095
+ const shade = docxHexColor(docxAttr(firstChildByName(tcPr, 'shd'), 'fill'));
1096
+ const borders = firstChildByName(tcPr, 'tcBorders');
1097
+ const top = docxBorderCss(firstChildByName(borders, 'top'));
1098
+ const right = docxBorderCss(firstChildByName(borders, 'right'));
1099
+ const bottom = docxBorderCss(firstChildByName(borders, 'bottom'));
1100
+ const left = docxBorderCss(firstChildByName(borders, 'left'));
1101
+ return cssRules([
1102
+ ['width', width],
1103
+ ['background-color', shade],
1104
+ ['border-top', top],
1105
+ ['border-right', right],
1106
+ ['border-bottom', bottom],
1107
+ ['border-left', left],
1108
+ ['padding', '8px'],
1109
+ ['vertical-align', 'top'],
1110
+ ]);
1111
+ };
1112
+ const convertDocxTable = (table) => {
1113
+ const rows = childrenByName(table, 'tr')
1114
+ .map((row) => {
1115
+ const cells = childrenByName(row, 'tc')
1116
+ .map((cell) => {
1117
+ const content = directChildren(cell)
1118
+ .filter((child) => child.localName === 'p' || child.localName === 'tbl')
1119
+ .map((child) => child.localName === 'p' ? convertDocxParagraph(child) : convertDocxTable(child))
1120
+ .join('');
1121
+ return `<td${styleAttr(convertDocxCellStyle(cell))}>${content || '<p><br></p>'}</td>`;
1122
+ })
1123
+ .join('');
1124
+ return `<tr>${cells}</tr>`;
1125
+ })
1126
+ .join('');
1127
+ return `<table style="border-collapse: collapse; width: 100%; margin: 12px 0;"><tbody>${rows}</tbody></table>`;
1128
+ };
1129
+ const enhanceImportedTables = (root) => {
1130
+ const tables = root.querySelectorAll('table');
1131
+ tables.forEach(tbl => {
1132
+ tbl.style.borderCollapse = tbl.style.borderCollapse || 'collapse';
1133
+ tbl.style.width = tbl.style.width || '100%';
1134
+ const cells = tbl.querySelectorAll('td, th');
1135
+ cells.forEach(cell => {
1136
+ const el = cell;
1137
+ if (!el.style.border && !el.style.borderTop && !el.style.borderRight && !el.style.borderBottom && !el.style.borderLeft) {
1138
+ el.style.border = '1px solid #d1d5db';
1139
+ }
1140
+ el.style.padding = el.style.padding || '8px';
1141
+ el.style.verticalAlign = el.style.verticalAlign || 'top';
1142
+ });
1143
+ });
1144
+ };
1145
+ const escapeHtml = (value) => value
1146
+ .replace(/&/g, "&amp;")
1147
+ .replace(/</g, "&lt;")
1148
+ .replace(/>/g, "&gt;");
1149
+ const markdownToHtml = (markdown) => {
1150
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
1151
+ let html = "";
1152
+ let listType = null;
1153
+ const closeList = () => {
1154
+ if (listType) {
1155
+ html += `</${listType}>`;
1156
+ listType = null;
1157
+ }
1158
+ };
1159
+ const inline = (text) => escapeHtml(text)
1160
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
1161
+ .replace(/\*([^*]+)\*/g, "<em>$1</em>")
1162
+ .replace(/`([^`]+)`/g, "<code>$1</code>");
1163
+ lines.forEach((line) => {
1164
+ const trimmed = line.trim();
1165
+ if (!trimmed) {
1166
+ closeList();
1167
+ return;
1168
+ }
1169
+ const heading = /^(#{1,6})\s+(.+)$/.exec(trimmed);
1170
+ if (heading) {
1171
+ closeList();
1172
+ const level = heading[1].length;
1173
+ html += `<h${level}>${inline(heading[2])}</h${level}>`;
1174
+ return;
1175
+ }
1176
+ const bullet = /^[-*]\s+(.+)$/.exec(trimmed);
1177
+ if (bullet) {
1178
+ if (listType !== "ul") {
1179
+ closeList();
1180
+ html += "<ul>";
1181
+ listType = "ul";
1182
+ }
1183
+ html += `<li>${inline(bullet[1])}</li>`;
1184
+ return;
1185
+ }
1186
+ const numbered = /^\d+[.)]\s+(.+)$/.exec(trimmed);
1187
+ if (numbered) {
1188
+ if (listType !== "ol") {
1189
+ closeList();
1190
+ html += "<ol>";
1191
+ listType = "ol";
1192
+ }
1193
+ html += `<li>${inline(numbered[1])}</li>`;
1194
+ return;
1195
+ }
1196
+ if (trimmed.startsWith("> ")) {
1197
+ closeList();
1198
+ html += `<blockquote>${inline(trimmed.slice(2))}</blockquote>`;
1199
+ return;
1200
+ }
1201
+ closeList();
1202
+ html += `<p>${inline(trimmed)}</p>`;
1203
+ });
1204
+ closeList();
1205
+ return html;
1206
+ };
1207
+ const htmlToMarkdown = (html) => {
1208
+ const root = document.createElement("div");
1209
+ root.innerHTML = html;
1210
+ const walk = (node) => {
1211
+ if (node.nodeType === Node.TEXT_NODE)
1212
+ return node.textContent || "";
1213
+ if (!(node instanceof HTMLElement))
1214
+ return "";
1215
+ const content = Array.from(node.childNodes).map(walk).join("");
1216
+ const tag = node.tagName.toLowerCase();
1217
+ if (tag === "strong" || tag === "b")
1218
+ return `**${content}**`;
1219
+ if (tag === "em" || tag === "i")
1220
+ return `*${content}*`;
1221
+ if (tag === "code")
1222
+ return `\`${content}\``;
1223
+ if (tag === "br")
1224
+ return "\n";
1225
+ if (/h[1-6]/.test(tag))
1226
+ return `${"#".repeat(Number(tag[1]))} ${content.trim()}\n\n`;
1227
+ if (tag === "p")
1228
+ return `${content.trim()}\n\n`;
1229
+ if (tag === "li")
1230
+ return `- ${content.trim()}\n`;
1231
+ if (tag === "ul" || tag === "ol")
1232
+ return `${content}\n`;
1233
+ if (tag === "blockquote")
1234
+ return `> ${content.trim()}\n\n`;
1235
+ if (tag === "table")
1236
+ return `${node.outerHTML}\n\n`;
1237
+ if (tag === "img")
1238
+ return `![${node.getAttribute("alt") || ""}](${node.getAttribute("src") || ""})`;
1239
+ if (tag === "a")
1240
+ return `[${content}](${node.getAttribute("href") || ""})`;
1241
+ return content;
1242
+ };
1243
+ return Array.from(root.childNodes).map(walk).join("").replace(/\n{3,}/g, "\n\n").trim();
1244
+ };
1245
+ const importTextFile = async (files, type) => {
1246
+ if (!files || files.length === 0)
1247
+ return;
1248
+ const file = files[0];
1249
+ const text = await file.text();
1250
+ const html = type === "html" ? text : markdownToHtml(text);
1251
+ const el = editableRef.current;
1252
+ const hasContent = el && el.textContent && el.textContent.trim().length > 0;
1253
+ insertImportedHtml(html, hasContent ? "append" : "replace");
1254
+ };
1255
+ const downloadText = (filename, content, mimeType) => {
1256
+ const blob = new Blob([content], { type: mimeType });
1257
+ const url = URL.createObjectURL(blob);
1258
+ const link = document.createElement("a");
1259
+ link.href = url;
1260
+ link.download = filename;
1261
+ document.body.appendChild(link);
1262
+ link.click();
1263
+ link.remove();
1264
+ URL.revokeObjectURL(url);
1265
+ };
1266
+ const exportHtml = () => {
1267
+ const html = editableRef.current?.innerHTML || "";
1268
+ downloadText("smart-rte-export.html", html, "text/html");
1269
+ };
1270
+ const exportMarkdown = () => {
1271
+ const html = editableRef.current?.innerHTML || "";
1272
+ downloadText("smart-rte-export.md", htmlToMarkdown(html), "text/markdown");
1273
+ };
920
1274
  const fixNegativeMargins = (root) => {
921
1275
  try {
922
1276
  const nodes = root.querySelectorAll('*');
@@ -929,6 +1283,152 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
929
1283
  }
930
1284
  catch { }
931
1285
  };
1286
+ const cleanPastedHtml = (html, options = {}) => {
1287
+ const shouldPreserveColors = options.preserveColors ?? preserveColors;
1288
+ const shouldPreserveDocumentLayout = options.preserveDocumentLayout ?? false;
1289
+ const template = document.createElement('template');
1290
+ template.innerHTML = html
1291
+ .replace(/&nbsp;/gi, ' ')
1292
+ .replace(/\u00a0/g, ' ')
1293
+ .replace(/[\u200b\u200c\u200d]/g, '');
1294
+ template.content.querySelectorAll('meta, link, style, script').forEach((node) => node.remove());
1295
+ const allowedStyleNames = new Set([
1296
+ 'font-weight',
1297
+ 'font-style',
1298
+ 'text-decoration',
1299
+ 'text-align',
1300
+ 'vertical-align',
1301
+ 'border',
1302
+ 'border-top',
1303
+ 'border-right',
1304
+ 'border-bottom',
1305
+ 'border-left',
1306
+ 'border-collapse',
1307
+ 'padding',
1308
+ 'padding-top',
1309
+ 'padding-right',
1310
+ 'padding-bottom',
1311
+ 'padding-left',
1312
+ 'list-style-type',
1313
+ 'white-space',
1314
+ ]);
1315
+ if (preserveFontFamily)
1316
+ allowedStyleNames.add('font-family');
1317
+ if (shouldPreserveColors) {
1318
+ allowedStyleNames.add('color');
1319
+ allowedStyleNames.add('background');
1320
+ allowedStyleNames.add('background-color');
1321
+ }
1322
+ if (shouldPreserveDocumentLayout) {
1323
+ [
1324
+ 'font-size',
1325
+ 'line-height',
1326
+ 'margin',
1327
+ 'margin-top',
1328
+ 'margin-right',
1329
+ 'margin-bottom',
1330
+ 'margin-left',
1331
+ 'text-indent',
1332
+ 'width',
1333
+ 'min-width',
1334
+ ].forEach((name) => allowedStyleNames.add(name));
1335
+ }
1336
+ template.content.querySelectorAll('*').forEach((node) => {
1337
+ const className = node.getAttribute('class');
1338
+ if (className !== 'srte-preserve-colors')
1339
+ node.removeAttribute('class');
1340
+ node.removeAttribute('id');
1341
+ if (!shouldPreserveDocumentLayout) {
1342
+ node.removeAttribute('width');
1343
+ node.removeAttribute('height');
1344
+ }
1345
+ const style = node.getAttribute('style');
1346
+ if (!style)
1347
+ return;
1348
+ const safeRules = style
1349
+ .split(';')
1350
+ .map((rule) => rule.trim())
1351
+ .filter(Boolean)
1352
+ .filter((rule) => {
1353
+ const separator = rule.indexOf(':');
1354
+ if (separator === -1)
1355
+ return false;
1356
+ const name = rule.slice(0, separator).trim().toLowerCase();
1357
+ const value = rule.slice(separator + 1).trim().toLowerCase();
1358
+ if (!allowedStyleNames.has(name))
1359
+ return false;
1360
+ if (value.includes('position') || value.includes('expression') || value.includes('javascript:'))
1361
+ return false;
1362
+ if (name === 'white-space' && value !== 'pre-wrap')
1363
+ return false;
1364
+ if ((name === 'width' || name === 'min-width') && !/^[\d.]+(px|pt|em|rem|%)$/.test(value))
1365
+ return false;
1366
+ return true;
1367
+ });
1368
+ if (safeRules.length)
1369
+ node.setAttribute('style', safeRules.join('; '));
1370
+ else
1371
+ node.removeAttribute('style');
1372
+ });
1373
+ return template.innerHTML;
1374
+ };
1375
+ const normalizeEditorContent = () => {
1376
+ const el = editableRef.current;
1377
+ if (!el)
1378
+ return;
1379
+ fixNegativeMargins(el);
1380
+ ensureTableWrappers(el);
1381
+ addTableResizeHandles();
1382
+ };
1383
+ const insertHtmlAtEnd = (html) => {
1384
+ const el = editableRef.current;
1385
+ if (!el)
1386
+ return;
1387
+ el.focus();
1388
+ const range = document.createRange();
1389
+ range.selectNodeContents(el);
1390
+ range.collapse(false);
1391
+ const sel = window.getSelection();
1392
+ sel?.removeAllRanges();
1393
+ sel?.addRange(range);
1394
+ const separator = el.textContent?.trim() ? '<p><br></p>' : '';
1395
+ document.execCommand('insertHTML', false, `${separator}${html}`);
1396
+ };
1397
+ const replaceEditorHtml = (html) => {
1398
+ const el = editableRef.current;
1399
+ if (!el)
1400
+ return;
1401
+ el.innerHTML = html;
1402
+ el.focus();
1403
+ const range = document.createRange();
1404
+ range.selectNodeContents(el);
1405
+ range.collapse(false);
1406
+ const sel = window.getSelection();
1407
+ sel?.removeAllRanges();
1408
+ sel?.addRange(range);
1409
+ };
1410
+ const insertImportedHtml = (html, mode, cleanOptions) => {
1411
+ try {
1412
+ const cleanHtml = cleanPastedHtml(html, cleanOptions);
1413
+ if (mode === 'replace')
1414
+ replaceEditorHtml(cleanHtml);
1415
+ else
1416
+ insertHtmlAtEnd(cleanHtml);
1417
+ normalizeEditorContent();
1418
+ emitChange();
1419
+ }
1420
+ catch (error) {
1421
+ console.error('Error inserting imported content:', error);
1422
+ }
1423
+ };
1424
+ const insertCleanHtml = (html) => {
1425
+ try {
1426
+ document.execCommand("insertHTML", false, cleanPastedHtml(html));
1427
+ normalizeEditorContent();
1428
+ emitChange();
1429
+ }
1430
+ catch { }
1431
+ };
932
1432
  const ensureTableWrappers = (root) => {
933
1433
  try {
934
1434
  const tables = root.querySelectorAll('table');
@@ -1335,6 +1835,27 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1335
1835
  firstRow.replaceChild(td, c);
1336
1836
  }
1337
1837
  }
1838
+ handleInput();
1839
+ };
1840
+ const toggleHeaderColumn = (cell) => {
1841
+ const pos = getCellPosition(cell);
1842
+ if (!pos)
1843
+ return;
1844
+ const { tbody, cIdx } = pos;
1845
+ const rows = Array.from(tbody.querySelectorAll("tr"));
1846
+ const columnCells = rows
1847
+ .map((row) => cellsOfRow(row)[cIdx])
1848
+ .filter(Boolean);
1849
+ const shouldMakeHeader = columnCells.some((c) => c.tagName !== "TH");
1850
+ for (const c of columnCells) {
1851
+ const replacement = document.createElement(shouldMakeHeader ? "th" : "td");
1852
+ replacement.innerHTML = c.innerHTML || "&nbsp;";
1853
+ replacement.style.border = c.style.border || "1px solid var(--srte-border)";
1854
+ replacement.style.padding = c.style.padding || "6px";
1855
+ replacement.style.minWidth = c.style.minWidth || "60px";
1856
+ c.parentElement?.replaceChild(replacement, c);
1857
+ }
1858
+ handleInput();
1338
1859
  };
1339
1860
  const applyBgToSelection = (hex, fallbackCell) => {
1340
1861
  const sel = selectionRef.current;
@@ -1552,6 +2073,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1552
2073
  } }), _jsx("input", { ref: docxInputRef, type: "file", accept: ".docx", style: { display: "none" }, onChange: (e) => {
1553
2074
  handleDocxFiles(e.currentTarget.files);
1554
2075
  e.currentTarget.value = "";
2076
+ } }), _jsx("input", { ref: htmlInputRef, type: "file", accept: ".html,.htm,text/html", style: { display: "none" }, onChange: (e) => {
2077
+ importTextFile(e.currentTarget.files, "html");
2078
+ e.currentTarget.value = "";
2079
+ } }), _jsx("input", { ref: mdInputRef, type: "file", accept: ".md,.markdown,text/markdown,text/plain", style: { display: "none" }, onChange: (e) => {
2080
+ importTextFile(e.currentTarget.files, "md");
2081
+ e.currentTarget.value = "";
1555
2082
  } }), _jsxs("select", { defaultValue: "p", onChange: (e) => {
1556
2083
  const val = e.target.value;
1557
2084
  if (val === "p")
@@ -1621,7 +2148,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1621
2148
  borderRadius: 6,
1622
2149
  background: "var(--srte-input-bg)",
1623
2150
  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: () => {
2151
+ }, 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
2152
  const sel = window.getSelection();
1626
2153
  if (sel && sel.rangeCount > 0) {
1627
2154
  const range = sel.getRangeAt(0);
@@ -1638,7 +2165,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1638
2165
  background: "var(--srte-input-bg)",
1639
2166
  color: "var(--srte-input-text)",
1640
2167
  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: () => {
2168
+ }, 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
2169
  setColorPickerType('text');
1643
2170
  setShowColorPicker(true);
1644
2171
  }, style: {
@@ -1650,7 +2177,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1650
2177
  background: "var(--srte-input-bg)",
1651
2178
  color: "var(--srte-input-text)",
1652
2179
  position: "relative",
1653
- }, children: _jsx("span", { style: { fontWeight: 700 }, children: "A" }) }), _jsx("button", { title: "Background Color", onClick: () => {
2180
+ }, children: _jsx("span", { style: { fontWeight: 700, borderBottom: "3px solid currentColor", lineHeight: 1 }, children: "A" }) }), _jsx("button", { title: "Background Color", onClick: () => {
1654
2181
  setColorPickerType('background');
1655
2182
  setShowColorPicker(true);
1656
2183
  }, style: {
@@ -1661,7 +2188,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1661
2188
  borderRadius: 6,
1662
2189
  background: "var(--srte-input-bg)",
1663
2190
  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: {
2191
+ }, 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: {
2192
+ height: 32,
2193
+ minWidth: 32,
2194
+ padding: "0 8px",
2195
+ border: "1px solid var(--srte-input-border)",
2196
+ borderRadius: 6,
2197
+ background: "var(--srte-input-bg)",
2198
+ color: "var(--srte-input-text)",
2199
+ }, children: ["X", _jsx("sub", { children: "2" })] }), _jsxs("button", { title: "Superscript", onClick: () => exec("superscript"), style: {
2200
+ height: 32,
2201
+ minWidth: 32,
2202
+ padding: "0 8px",
2203
+ border: "1px solid var(--srte-input-border)",
2204
+ borderRadius: 6,
2205
+ background: "var(--srte-input-bg)",
2206
+ color: "var(--srte-input-text)",
2207
+ }, children: ["X", _jsx("sup", { children: "2" })] }), _jsx("button", { title: "Bulleted list", onClick: () => exec("insertUnorderedList"), style: {
1665
2208
  height: 32,
1666
2209
  padding: "0 10px",
1667
2210
  border: "1px solid var(--srte-input-border)",
@@ -1675,7 +2218,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1675
2218
  borderRadius: 6,
1676
2219
  background: "var(--srte-input-bg)",
1677
2220
  color: "var(--srte-input-text)",
1678
- }, children: "1. List" }), _jsx("button", { title: "Blockquote", onClick: () => exec("formatBlock", "<blockquote>"), style: {
2221
+ }, children: "1. List" }), _jsx("button", { title: "Blockquote", onClick: toggleBlockquote, style: {
2222
+ height: 32,
2223
+ minWidth: 32,
2224
+ padding: "0 8px",
2225
+ border: "1px solid var(--srte-input-border)",
2226
+ borderRadius: 6,
2227
+ background: "var(--srte-input-bg)",
2228
+ color: "var(--srte-input-text)",
2229
+ }, children: "\u275D" }), _jsx("button", { title: "Special characters", onClick: () => setShowSpecialChars(true), style: {
1679
2230
  height: 32,
1680
2231
  minWidth: 32,
1681
2232
  padding: "0 8px",
@@ -1683,7 +2234,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1683
2234
  borderRadius: 6,
1684
2235
  background: "var(--srte-input-bg)",
1685
2236
  color: "var(--srte-input-text)",
1686
- }, children: "\u275D" }), _jsx("button", { title: "Code block", onClick: () => exec("formatBlock", "<pre>"), style: {
2237
+ }, children: "\u03A9" }), _jsx("button", { title: "Code block", onClick: () => exec("formatBlock", "<pre>"), style: {
1687
2238
  height: 32,
1688
2239
  minWidth: 36,
1689
2240
  padding: "0 8px",
@@ -1744,7 +2295,35 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1744
2295
  background: "var(--srte-input-bg)",
1745
2296
  color: "var(--srte-input-text)",
1746
2297
  opacity: loadingDocx ? 0.5 : 1,
1747
- }, children: loadingDocx ? '⌛ Importing...' : '📝 DOCX' }), _jsxs("div", { style: {
2298
+ }, children: loadingDocx ? '⌛ Importing...' : '📝 DOCX' }), _jsx("button", { title: "Import HTML", onClick: () => htmlInputRef.current?.click(), style: {
2299
+ height: 32,
2300
+ padding: "0 10px",
2301
+ border: "1px solid var(--srte-input-border)",
2302
+ borderRadius: 6,
2303
+ background: "var(--srte-input-bg)",
2304
+ color: "var(--srte-input-text)",
2305
+ }, children: "HTML" }), _jsx("button", { title: "Import Markdown", onClick: () => mdInputRef.current?.click(), style: {
2306
+ height: 32,
2307
+ padding: "0 10px",
2308
+ border: "1px solid var(--srte-input-border)",
2309
+ borderRadius: 6,
2310
+ background: "var(--srte-input-bg)",
2311
+ color: "var(--srte-input-text)",
2312
+ }, children: "MD" }), _jsx("button", { title: "Export HTML", onClick: exportHtml, style: {
2313
+ height: 32,
2314
+ padding: "0 10px",
2315
+ border: "1px solid var(--srte-input-border)",
2316
+ borderRadius: 6,
2317
+ background: "var(--srte-input-bg)",
2318
+ color: "var(--srte-input-text)",
2319
+ }, children: "Export HTML" }), _jsx("button", { title: "Export Markdown", onClick: exportMarkdown, style: {
2320
+ height: 32,
2321
+ padding: "0 10px",
2322
+ border: "1px solid var(--srte-input-border)",
2323
+ borderRadius: 6,
2324
+ background: "var(--srte-input-bg)",
2325
+ color: "var(--srte-input-text)",
2326
+ }, children: "Export MD" }), _jsxs("div", { style: {
1748
2327
  display: "inline-flex",
1749
2328
  gap: 4,
1750
2329
  alignItems: "center",
@@ -1823,7 +2402,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1823
2402
  color: "var(--srte-input-text)",
1824
2403
  }, children: "\u293E Redo" })] }), media && mediaManager && (_jsx(MediaManager, { open: showMediaManager, onClose: () => setShowMediaManager(false), adapter: mediaManager, onSelect: (item) => {
1825
2404
  if (item?.url)
1826
- insertImageAtSelection(item.url);
2405
+ insertImageAtSelection(item);
1827
2406
  } })), table && showTableDialog && (_jsx("div", { style: {
1828
2407
  position: "fixed",
1829
2408
  inset: 0,
@@ -1988,6 +2567,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1988
2567
  background: 'var(--srte-input-bg)',
1989
2568
  color: 'var(--srte-input-text)',
1990
2569
  cursor: 'pointer',
2570
+ }, children: "Close" }) })] }) })), showSpecialChars && (_jsx("div", { style: {
2571
+ position: "fixed",
2572
+ inset: 0,
2573
+ background: "var(--srte-modal-backdrop)",
2574
+ display: "flex",
2575
+ alignItems: "center",
2576
+ justifyContent: "center",
2577
+ zIndex: 50,
2578
+ }, onClick: () => setShowSpecialChars(false), children: _jsxs("div", { style: {
2579
+ background: "var(--srte-modal-bg)",
2580
+ color: "var(--srte-modal-text)",
2581
+ padding: 16,
2582
+ borderRadius: 8,
2583
+ width: 420,
2584
+ maxWidth: "90vw",
2585
+ boxShadow: "var(--srte-menu-shadow)",
2586
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 12 }, children: "Special characters" }), [
2587
+ {
2588
+ label: "Greek",
2589
+ chars: ["α", "β", "γ", "δ", "ε", "ζ", "η", "θ", "ι", "κ", "λ", "μ", "ν", "ξ", "π", "ρ", "σ", "τ", "φ", "χ", "ψ", "ω", "Δ", "Σ", "Ω"],
2590
+ },
2591
+ {
2592
+ label: "Medical / Math",
2593
+ chars: ["±", "≤", "≥", "≠", "≈", "∞", "°", "µ", "×", "÷", "→", "←", "↑", "↓", "∴", "∵", "√", "∑", "∫", "₂", "₃", "²", "³"],
2594
+ },
2595
+ ].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: () => {
2596
+ insertTextAtSelection(char);
2597
+ setShowSpecialChars(false);
2598
+ }, style: {
2599
+ height: 32,
2600
+ minWidth: 32,
2601
+ padding: "0 8px",
2602
+ border: "1px solid var(--srte-input-border)",
2603
+ borderRadius: 6,
2604
+ background: "var(--srte-input-bg)",
2605
+ color: "var(--srte-input-text)",
2606
+ fontSize: 16,
2607
+ }, children: char }, char))) })] }, group.label))), _jsx("div", { style: { display: "flex", justifyContent: "flex-end" }, children: _jsx("button", { type: "button", onClick: () => setShowSpecialChars(false), style: {
2608
+ padding: "6px 16px",
2609
+ border: "1px solid var(--srte-input-border)",
2610
+ borderRadius: 6,
2611
+ background: "var(--srte-input-bg)",
2612
+ color: "var(--srte-input-text)",
2613
+ cursor: "pointer",
1991
2614
  }, children: "Close" }) })] }) })), formula && showFormulaDialog && (_jsx("div", { style: {
1992
2615
  position: "fixed",
1993
2616
  inset: 0,
@@ -2109,8 +2732,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2109
2732
  if (hasImage) {
2110
2733
  e.preventDefault();
2111
2734
  handleLocalImageFiles(items);
2735
+ return;
2112
2736
  }
2113
2737
  }
2738
+ const html = e.clipboardData?.getData("text/html");
2739
+ if (html) {
2740
+ e.preventDefault();
2741
+ insertCleanHtml(cleanPastedHtml(html));
2742
+ }
2114
2743
  }, onDragOver: (e) => {
2115
2744
  // Allow dragging images within editor and file drops
2116
2745
  if (draggedImageRef.current ||
@@ -2613,7 +3242,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2613
3242
  }, onClick: () => {
2614
3243
  toggleHeaderRow(tableMenu.cell);
2615
3244
  setTableMenu(null);
2616
- }, children: [_jsx("span", { children: "H\u2081" }), _jsx("span", { children: "Toggle header row" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("button", { style: {
3245
+ }, children: [_jsx("span", { children: "H\u2081" }), _jsx("span", { children: "Toggle header row" })] }), _jsxs("button", { style: {
3246
+ display: "flex",
3247
+ alignItems: "center",
3248
+ gap: 8,
3249
+ padding: "6px 8px",
3250
+ fontSize: 12,
3251
+ }, onClick: () => {
3252
+ toggleHeaderColumn(tableMenu.cell);
3253
+ setTableMenu(null);
3254
+ }, children: [_jsx("span", { children: "H\u2195" }), _jsx("span", { children: "Toggle header column" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("button", { style: {
2617
3255
  display: "flex",
2618
3256
  alignItems: "center",
2619
3257
  gap: 8,
@@ -2640,7 +3278,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2640
3278
  borderRadius: 8,
2641
3279
  boxShadow: "var(--srte-menu-shadow)",
2642
3280
  padding: 8,
2643
- width: 220,
3281
+ width: 280,
3282
+ maxHeight: "80vh",
3283
+ overflowY: "auto",
2644
3284
  color: "var(--srte-menu-text)",
2645
3285
  }, 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
3286
  ? imageMenu.img.parentElement.href
@@ -2742,7 +3382,58 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2742
3382
  img.style.margin = "0 0 8px 8px";
2743
3383
  scheduleImageOverlay();
2744
3384
  handleInput();
2745
- }, children: "\u27F9" })] }), _jsxs("div", { style: { display: "flex", gap: 6 }, children: [_jsx("button", { onClick: () => {
3385
+ }, children: "\u27F9" })] }), _jsxs("div", { style: {
3386
+ borderTop: "1px solid var(--srte-border-light)",
3387
+ paddingTop: 6,
3388
+ display: "grid",
3389
+ gap: 6,
3390
+ }, children: [_jsx("div", { style: { fontWeight: 600, fontSize: 11 }, children: "License" }), _jsx("input", { placeholder: "Work name", defaultValue: imageMenu.img.dataset.workName || "", onChange: (e) => {
3391
+ imageMenu.img.dataset.workName = e.target.value;
3392
+ handleInput();
3393
+ }, style: {
3394
+ padding: "4px 6px",
3395
+ border: "1px solid var(--srte-border-light)",
3396
+ borderRadius: 4,
3397
+ color: "var(--srte-input-text)",
3398
+ background: "var(--srte-input-bg)",
3399
+ } }), _jsx("input", { placeholder: "Author", defaultValue: imageMenu.img.dataset.licenseAuthor || "", onChange: (e) => {
3400
+ imageMenu.img.dataset.licenseAuthor = e.target.value;
3401
+ handleInput();
3402
+ }, style: {
3403
+ padding: "4px 6px",
3404
+ border: "1px solid var(--srte-border-light)",
3405
+ borderRadius: 4,
3406
+ color: "var(--srte-input-text)",
3407
+ background: "var(--srte-input-bg)",
3408
+ } }), _jsxs("select", { defaultValue: imageMenu.img.dataset.licenseType || "", onChange: (e) => {
3409
+ imageMenu.img.dataset.licenseType = e.target.value;
3410
+ handleInput();
3411
+ }, style: {
3412
+ height: 28,
3413
+ padding: "0 6px",
3414
+ border: "1px solid var(--srte-border-light)",
3415
+ borderRadius: 4,
3416
+ color: "var(--srte-input-text)",
3417
+ background: "var(--srte-input-bg)",
3418
+ }, 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) => {
3419
+ imageMenu.img.dataset.licenseText = e.target.value;
3420
+ handleInput();
3421
+ }, style: {
3422
+ padding: "4px 6px",
3423
+ border: "1px solid var(--srte-border-light)",
3424
+ borderRadius: 4,
3425
+ color: "var(--srte-input-text)",
3426
+ background: "var(--srte-input-bg)",
3427
+ } }), _jsx("input", { placeholder: "Source URL", defaultValue: imageMenu.img.dataset.licenseUrl || "", onChange: (e) => {
3428
+ imageMenu.img.dataset.licenseUrl = e.target.value;
3429
+ handleInput();
3430
+ }, style: {
3431
+ padding: "4px 6px",
3432
+ border: "1px solid var(--srte-border-light)",
3433
+ borderRadius: 4,
3434
+ color: "var(--srte-input-text)",
3435
+ background: "var(--srte-input-bg)",
3436
+ } })] }), _jsxs("div", { style: { display: "flex", gap: 6 }, children: [_jsx("button", { onClick: () => {
2746
3437
  replaceTargetRef.current = imageMenu.img;
2747
3438
  fileInputRef.current?.click();
2748
3439
  }, children: "Replace\u2026" }), _jsx("button", { onClick: () => {
@@ -10,6 +10,13 @@ export type MediaItem = {
10
10
  title?: string;
11
11
  alt?: string;
12
12
  tags?: string[];
13
+ license?: {
14
+ author?: string;
15
+ licenseType?: string;
16
+ licenseText?: string;
17
+ sourceUrl?: string;
18
+ workName?: string;
19
+ };
13
20
  };
14
21
  export type MediaSearchQuery = {
15
22
  q?: string;
@@ -7,6 +7,7 @@ export function MediaManager(props) {
7
7
  const [error, setError] = useState(null);
8
8
  const [query, setQuery] = useState("");
9
9
  const [results, setResults] = useState([]);
10
+ const [infoItem, setInfoItem] = useState(null);
10
11
  const fileInputRef = useRef(null);
11
12
  useEffect(() => {
12
13
  if (!open)
@@ -151,22 +152,72 @@ export function MediaManager(props) {
151
152
  gap: 12,
152
153
  overflowY: "auto",
153
154
  paddingBottom: 16,
154
- }, children: results.map((it) => (_jsxs("button", { onClick: () => {
155
- onSelect(it);
156
- onClose();
157
- }, title: it.title || it.url, style: {
158
- display: "block",
155
+ }, children: results.map((it) => (_jsxs("div", { title: it.title || it.url, style: {
156
+ display: "flex",
157
+ flexDirection: "column",
158
+ gap: 6,
159
159
  border: "1px solid var(--srte-border-light)",
160
160
  borderRadius: 8,
161
161
  padding: 6,
162
162
  background: "var(--srte-input-bg)",
163
- textAlign: "center",
164
- }, children: [_jsx("img", { src: it.url, alt: it.alt || "", style: {
165
- maxWidth: "100%",
166
- maxHeight: 100,
167
- display: "block",
168
- margin: "0 auto",
169
- objectFit: "cover",
170
- borderRadius: 6,
171
- } }), _jsx("div", { style: { fontSize: 11, marginTop: 6, color: "var(--srte-text-muted)" }, children: it.width && it.height ? `${it.width}×${it.height}` : "" })] }, it.id || it.url))) })] }))] }) }));
163
+ color: "var(--srte-input-text)",
164
+ }, children: [_jsx("button", { type: "button", onClick: () => {
165
+ onSelect(it);
166
+ onClose();
167
+ }, style: {
168
+ border: "none",
169
+ padding: 0,
170
+ background: "transparent",
171
+ cursor: "pointer",
172
+ }, children: _jsx("img", { src: it.url, alt: it.alt || "", style: {
173
+ maxWidth: "100%",
174
+ maxHeight: 100,
175
+ display: "block",
176
+ margin: "0 auto",
177
+ objectFit: "cover",
178
+ borderRadius: 6,
179
+ } }) }), _jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 6 }, children: [_jsx("div", { style: { fontSize: 11, color: "var(--srte-text-muted)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: it.title || it.alt || (it.width && it.height ? `${it.width}×${it.height}` : "Image") }), _jsx("button", { type: "button", onClick: () => setInfoItem(it), title: "Image info", style: {
180
+ width: 24,
181
+ height: 24,
182
+ border: "1px solid var(--srte-border)",
183
+ borderRadius: 999,
184
+ background: "var(--srte-surface-subtle)",
185
+ color: "var(--srte-input-text)",
186
+ cursor: "pointer",
187
+ flex: "0 0 auto",
188
+ }, children: "i" })] }), it.tags && it.tags.length > 0 && (_jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4 }, children: it.tags.slice(0, 3).map((tag) => (_jsx("span", { style: {
189
+ fontSize: 10,
190
+ padding: "1px 5px",
191
+ borderRadius: 999,
192
+ background: "var(--srte-surface-subtle)",
193
+ color: "var(--srte-text-muted)",
194
+ }, children: tag }, tag))) }))] }, it.id || it.url))) })] })), infoItem && (_jsx("div", { style: {
195
+ position: "fixed",
196
+ inset: 0,
197
+ background: "var(--srte-modal-backdrop)",
198
+ display: "flex",
199
+ alignItems: "center",
200
+ justifyContent: "center",
201
+ zIndex: 90,
202
+ }, onClick: () => setInfoItem(null), children: _jsxs("div", { style: {
203
+ width: 420,
204
+ maxWidth: "90vw",
205
+ background: "var(--srte-modal-bg)",
206
+ color: "var(--srte-modal-text)",
207
+ borderRadius: 10,
208
+ boxShadow: "var(--srte-menu-shadow)",
209
+ padding: 16,
210
+ }, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { style: { display: "flex", justifyContent: "space-between", gap: 12, marginBottom: 12 }, children: [_jsx("div", { style: { fontWeight: 600 }, children: "Image info" }), _jsx("button", { type: "button", onClick: () => setInfoItem(null), children: "\u2715" })] }), _jsx("img", { src: infoItem.url, alt: infoItem.alt || "", style: { maxWidth: "100%", maxHeight: 180, display: "block", margin: "0 auto 12px", borderRadius: 8 } }), [
211
+ ["Title", infoItem.title],
212
+ ["Alt text", infoItem.alt],
213
+ ["Dimensions", infoItem.width && infoItem.height ? `${infoItem.width}×${infoItem.height}` : undefined],
214
+ ["MIME type", infoItem.mimeType],
215
+ ["Size", infoItem.sizeBytes ? `${Math.round(infoItem.sizeBytes / 1024)} KB` : undefined],
216
+ ["Created", infoItem.createdAt],
217
+ ["Tags", infoItem.tags?.join(", ")],
218
+ ["Work", infoItem.license?.workName],
219
+ ["Author", infoItem.license?.author],
220
+ ["License", [infoItem.license?.licenseType, infoItem.license?.licenseText].filter(Boolean).join(" - ")],
221
+ ["Source", infoItem.license?.sourceUrl],
222
+ ].filter(([, value]) => value).map(([label, value]) => (_jsxs("div", { style: { display: "grid", gridTemplateColumns: "92px 1fr", gap: 8, fontSize: 12, marginBottom: 6 }, children: [_jsx("div", { style: { color: "var(--srte-text-muted)" }, children: label }), _jsx("div", { style: { overflowWrap: "anywhere" }, children: value })] }, label)))] }) }))] }) }));
172
223
  }
package/dist/theme.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export type SrteTheme = 'light' | 'dark';
2
- export declare const SRTE_DEFAULT_CSS = "\n.srte-editor {\n --srte-bg: #ffffff;\n --srte-text: #111111;\n --srte-text-muted: #4b5563;\n --srte-border: #dddddd;\n --srte-border-light: #eeeeee;\n --srte-toolbar-bg: #ffffff;\n --srte-input-bg: #ffffff;\n --srte-input-text: #111111;\n --srte-input-border: #e5e7eb;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.35);\n --srte-modal-bg: #ffffff;\n --srte-modal-text: #000000;\n --srte-menu-bg: #ffffff;\n --srte-menu-text: #111111;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n --srte-accent: #1e90ff;\n --srte-accent-bg: rgba(30, 144, 255, 0.15);\n --srte-danger: #dc2626;\n --srte-primary: #2563eb;\n --srte-surface-subtle: #f3f4f6;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #f3f4f6;\n}\n.srte-editor.srte-dark {\n --srte-bg: #1e1e1e;\n --srte-text: #e0e0e0;\n --srte-text-muted: #9ca3af;\n --srte-border: #3a3a3a;\n --srte-border-light: #2e2e2e;\n --srte-toolbar-bg: #252525;\n --srte-input-bg: #2a2a2a;\n --srte-input-text: #e0e0e0;\n --srte-input-border: #444444;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.6);\n --srte-modal-bg: #252525;\n --srte-modal-text: #e0e0e0;\n --srte-menu-bg: #2a2a2a;\n --srte-menu-text: #e0e0e0;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);\n --srte-accent: #3b9eff;\n --srte-accent-bg: rgba(59, 158, 255, 0.2);\n --srte-danger: #ef4444;\n --srte-primary: #3b82f6;\n --srte-surface-subtle: #333333;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #333333;\n}\n";
2
+ export declare const SRTE_DEFAULT_CSS = "\n.srte-editor {\n --srte-bg: #ffffff;\n --srte-text: #111111;\n --srte-text-muted: #4b5563;\n --srte-border: #dddddd;\n --srte-border-light: #eeeeee;\n --srte-toolbar-bg: #ffffff;\n --srte-input-bg: #ffffff;\n --srte-input-text: #111111;\n --srte-input-border: #e5e7eb;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.35);\n --srte-modal-bg: #ffffff;\n --srte-modal-text: #000000;\n --srte-menu-bg: #ffffff;\n --srte-menu-text: #111111;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n --srte-accent: #1e90ff;\n --srte-accent-bg: rgba(30, 144, 255, 0.15);\n --srte-danger: #dc2626;\n --srte-primary: #2563eb;\n --srte-surface-subtle: #f3f4f6;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #f3f4f6;\n}\n.srte-editor.srte-dark {\n --srte-bg: #1e1e1e;\n --srte-text: #e0e0e0;\n --srte-text-muted: #9ca3af;\n --srte-border: #3a3a3a;\n --srte-border-light: #2e2e2e;\n --srte-toolbar-bg: #252525;\n --srte-input-bg: #2a2a2a;\n --srte-input-text: #e0e0e0;\n --srte-input-border: #444444;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.6);\n --srte-modal-bg: #252525;\n --srte-modal-text: #e0e0e0;\n --srte-menu-bg: #2a2a2a;\n --srte-menu-text: #e0e0e0;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);\n --srte-accent: #3b9eff;\n --srte-accent-bg: rgba(59, 158, 255, 0.2);\n --srte-danger: #ef4444;\n --srte-primary: #3b82f6;\n --srte-surface-subtle: #333333;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #333333;\n}\n.srte-editor [contenteditable] blockquote {\n border-left: 4px solid var(--srte-accent);\n margin: 0.75em 0;\n padding: 0.5em 1em;\n background: var(--srte-surface-subtle);\n color: var(--srte-text);\n}\n.srte-editor.srte-dark [contenteditable] [style*=\"color\"]:not(.srte-preserve-colors):not(.srte-preserve-colors *),\n.srte-editor.srte-dark [contenteditable] [style*=\"background\"]:not(.srte-preserve-colors):not(.srte-preserve-colors *) {\n color: var(--srte-text) !important;\n background: transparent !important;\n background-color: transparent !important;\n}\n.srte-editor [contenteditable] sub,\n.srte-editor [contenteditable] sup {\n line-height: 0;\n}\n";
3
3
  export declare function ensureStyleSheet(): void;
package/dist/theme.js CHANGED
@@ -47,6 +47,23 @@ export const SRTE_DEFAULT_CSS = `
47
47
  --srte-on-primary: #ffffff;
48
48
  --srte-cancel-bg: #333333;
49
49
  }
50
+ .srte-editor [contenteditable] blockquote {
51
+ border-left: 4px solid var(--srte-accent);
52
+ margin: 0.75em 0;
53
+ padding: 0.5em 1em;
54
+ background: var(--srte-surface-subtle);
55
+ color: var(--srte-text);
56
+ }
57
+ .srte-editor.srte-dark [contenteditable] [style*="color"]:not(.srte-preserve-colors):not(.srte-preserve-colors *),
58
+ .srte-editor.srte-dark [contenteditable] [style*="background"]:not(.srte-preserve-colors):not(.srte-preserve-colors *) {
59
+ color: var(--srte-text) !important;
60
+ background: transparent !important;
61
+ background-color: transparent !important;
62
+ }
63
+ .srte-editor [contenteditable] sub,
64
+ .srte-editor [contenteditable] sup {
65
+ line-height: 0;
66
+ }
50
67
  `;
51
68
  const SRTE_STYLE_ID = 'srte-theme-defaults';
52
69
  export function ensureStyleSheet() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smartrte-react",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "A powerful, feature-rich Rich Text Editor for React with support for tables, mathematical formulas (LaTeX/KaTeX), and media management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -38,6 +38,16 @@
38
38
  },
39
39
  "author": "Smart RTE Contributors",
40
40
  "license": "MIT",
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.json",
43
+ "prepublishOnly": "pnpm run build",
44
+ "dev": "pnpm build",
45
+ "lint": "eslint . || true",
46
+ "test": "vitest run || true",
47
+ "storybook": "storybook dev -p 6006",
48
+ "build-storybook": "storybook build",
49
+ "e2e": "playwright test || true"
50
+ },
41
51
  "publishConfig": {
42
52
  "access": "public"
43
53
  },
@@ -62,16 +72,8 @@
62
72
  "vitest": "^2.1.4"
63
73
  },
64
74
  "dependencies": {
75
+ "jszip": "^3.10.1",
65
76
  "mammoth": "^1.11.0",
66
77
  "pdfjs-dist": "^5.4.530"
67
- },
68
- "scripts": {
69
- "build": "tsc -p tsconfig.json",
70
- "dev": "pnpm build",
71
- "lint": "eslint . || true",
72
- "test": "vitest run || true",
73
- "storybook": "storybook dev -p 6006",
74
- "build-storybook": "storybook build",
75
- "e2e": "playwright test || true"
76
78
  }
77
- }
79
+ }