smartrte-react 0.2.2 → 0.2.4

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.
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
- import { MediaManager } from "./MediaManager";
3
+ import { MediaManager } from "./MediaManager.js";
4
4
  import * as pdfjsLib from 'pdfjs-dist';
5
5
  import mammoth from 'mammoth';
6
6
  import JSZip from 'jszip';
7
- import { ensureStyleSheet } from '../theme';
7
+ import { ensureStyleSheet } from '../theme.js';
8
8
  // Initialize PDF.js worker
9
9
  if (typeof window !== 'undefined') {
10
10
  pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
@@ -20,6 +20,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
20
20
  ], defaultFont, preserveFontFamily = false, preserveColors = false, preserveDocxStyles = true, theme = "light", className, }) {
21
21
  ensureStyleSheet();
22
22
  const editableRef = useRef(null);
23
+ const editorScrollRef = useRef(null);
23
24
  const lastEmittedRef = useRef("");
24
25
  const isComposingRef = useRef(false);
25
26
  const fileInputRef = useRef(null);
@@ -53,6 +54,18 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
53
54
  const savedRangeRef = useRef(null);
54
55
  const [currentFontSize, setCurrentFontSize] = useState("");
55
56
  const [currentFont, setCurrentFont] = useState("");
57
+ const [activeState, setActiveState] = useState({
58
+ bold: false,
59
+ italic: false,
60
+ underline: false,
61
+ strikeThrough: false,
62
+ subscript: false,
63
+ superscript: false,
64
+ unorderedList: false,
65
+ orderedList: false,
66
+ blockquote: false,
67
+ codeBlock: false,
68
+ });
56
69
  useEffect(() => {
57
70
  const el = editableRef.current;
58
71
  if (!el)
@@ -77,6 +90,35 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
77
90
  el.removeEventListener("contextmenu", onCtx, { capture: true });
78
91
  };
79
92
  }, [value]);
93
+ const updateActiveState = () => {
94
+ const editor = editableRef.current;
95
+ if (!editor)
96
+ return;
97
+ try {
98
+ const sel = window.getSelection();
99
+ const range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
100
+ const inEditor = range && editor.contains(range.commonAncestorContainer);
101
+ if (!inEditor)
102
+ return;
103
+ let node = range.commonAncestorContainer;
104
+ if (node.nodeType === Node.TEXT_NODE)
105
+ node = node.parentNode;
106
+ const element = node instanceof HTMLElement ? node : null;
107
+ setActiveState({
108
+ bold: document.queryCommandState("bold"),
109
+ italic: document.queryCommandState("italic"),
110
+ underline: document.queryCommandState("underline"),
111
+ strikeThrough: document.queryCommandState("strikeThrough"),
112
+ subscript: document.queryCommandState("subscript"),
113
+ superscript: document.queryCommandState("superscript"),
114
+ unorderedList: Boolean(element?.closest("ul")),
115
+ orderedList: Boolean(element?.closest("ol")),
116
+ blockquote: Boolean(element?.closest("blockquote")),
117
+ codeBlock: Boolean(element?.closest("pre")),
118
+ });
119
+ }
120
+ catch { }
121
+ };
80
122
  // Save selection whenever it changes
81
123
  useEffect(() => {
82
124
  const saveSelection = () => {
@@ -86,6 +128,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
86
128
  const editor = editableRef.current;
87
129
  if (editor && editor.contains(range.commonAncestorContainer)) {
88
130
  savedRangeRef.current = range.cloneRange();
131
+ updateActiveState();
89
132
  }
90
133
  }
91
134
  };
@@ -96,8 +139,27 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
96
139
  }, []);
97
140
  const exec = (command, valueArg) => {
98
141
  try {
99
- document.execCommand(command, false, valueArg);
100
- emitChange();
142
+ if (!restoreSavedSelection()) {
143
+ safeSelectRange(getSelectionRangeInEditor());
144
+ }
145
+ if (command === "insertUnorderedList") {
146
+ toggleList("ul");
147
+ return;
148
+ }
149
+ if (command === "insertOrderedList") {
150
+ toggleList("ol");
151
+ return;
152
+ }
153
+ const beforeHtml = editableRef.current?.innerHTML || "";
154
+ const ok = document.execCommand(command, false, valueArg);
155
+ const afterHtml = editableRef.current?.innerHTML || "";
156
+ const needsFallback = !ok ||
157
+ (command === "formatBlock" && beforeHtml === afterHtml);
158
+ if (needsFallback) {
159
+ if (command === "formatBlock" && valueArg)
160
+ applyFormatBlockFallback(valueArg);
161
+ }
162
+ handleInput();
101
163
  }
102
164
  catch { }
103
165
  };
@@ -114,6 +176,29 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
114
176
  onChange(html);
115
177
  }
116
178
  };
179
+ const restoreSavedSelection = () => {
180
+ const editor = editableRef.current;
181
+ if (!editor)
182
+ return false;
183
+ const saved = savedRangeRef.current;
184
+ if (saved && editor.contains(saved.commonAncestorContainer)) {
185
+ editor.focus({ preventScroll: true });
186
+ safeSelectRange(saved.cloneRange());
187
+ return true;
188
+ }
189
+ editor.focus({ preventScroll: true });
190
+ return false;
191
+ };
192
+ const preserveEditorSelection = () => {
193
+ const editor = editableRef.current;
194
+ const sel = window.getSelection();
195
+ if (!editor || !sel || sel.rangeCount === 0)
196
+ return;
197
+ const range = sel.getRangeAt(0);
198
+ if (editor.contains(range.commonAncestorContainer)) {
199
+ savedRangeRef.current = range.cloneRange();
200
+ }
201
+ };
117
202
  const insertLink = () => {
118
203
  const url = window.prompt("Enter URL", "https://");
119
204
  if (!url)
@@ -124,7 +209,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
124
209
  const editor = editableRef.current;
125
210
  if (!editor)
126
211
  return null;
127
- editor.focus();
128
212
  const sel = window.getSelection();
129
213
  if (sel && sel.rangeCount > 0) {
130
214
  const range = sel.getRangeAt(0);
@@ -139,6 +223,307 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
139
223
  range.collapse(false);
140
224
  return range;
141
225
  };
226
+ const getCurrentBlock = () => {
227
+ const editor = editableRef.current;
228
+ const range = getSelectionRangeInEditor();
229
+ if (!editor || !range)
230
+ return null;
231
+ let node = range.commonAncestorContainer;
232
+ if (node.nodeType === Node.TEXT_NODE)
233
+ node = node.parentNode;
234
+ const element = node instanceof HTMLElement ? node : null;
235
+ const block = element?.closest("p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div");
236
+ if (!block || block === editor || !editor.contains(block))
237
+ return null;
238
+ return block;
239
+ };
240
+ const blockSelector = "p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div";
241
+ const getSelectedBlocks = (range) => {
242
+ const editor = editableRef.current;
243
+ if (!editor)
244
+ return [];
245
+ const blocks = Array.from(editor.querySelectorAll(blockSelector))
246
+ .filter((block) => {
247
+ if (block === editor)
248
+ return false;
249
+ const parentBlock = block.parentElement?.closest(blockSelector);
250
+ if (parentBlock && parentBlock !== editor && editor.contains(parentBlock))
251
+ return false;
252
+ try {
253
+ return range.intersectsNode(block);
254
+ }
255
+ catch {
256
+ return false;
257
+ }
258
+ });
259
+ if (blocks.length > 0)
260
+ return blocks;
261
+ const current = getCurrentBlock();
262
+ return current ? [current] : [];
263
+ };
264
+ const getSelectedListItems = (blocks) => {
265
+ const seen = new Set();
266
+ const items = [];
267
+ blocks.forEach((block) => {
268
+ const li = block.tagName.toLowerCase() === "li"
269
+ ? block
270
+ : block.closest("li");
271
+ if (li && editableRef.current?.contains(li) && !seen.has(li)) {
272
+ seen.add(li);
273
+ items.push(li);
274
+ }
275
+ });
276
+ return items;
277
+ };
278
+ const copyCellOrBlockStyles = (from, to) => {
279
+ to.innerHTML = from.innerHTML || "<br>";
280
+ const style = from.getAttribute("style");
281
+ if (style)
282
+ to.setAttribute("style", style);
283
+ };
284
+ const applyHeaderCellStyle = (cell) => {
285
+ cell.style.fontWeight = "700";
286
+ cell.style.background = "#f3f4f6";
287
+ cell.style.textAlign = cell.style.textAlign || "left";
288
+ };
289
+ const clearHeaderCellStyle = (cell) => {
290
+ if (cell.style.fontWeight === "700" || cell.style.fontWeight === "bold")
291
+ cell.style.fontWeight = "";
292
+ if (cell.style.background === "var(--srte-surface-subtle)" || cell.style.background === "rgb(243, 244, 246)" || cell.style.background === "#f3f4f6")
293
+ cell.style.background = "";
294
+ };
295
+ const replaceTableCellTag = (cell, tag) => {
296
+ const replacement = document.createElement(tag);
297
+ replacement.innerHTML = cell.innerHTML || "&nbsp;";
298
+ replacement.colSpan = cell.colSpan;
299
+ replacement.rowSpan = cell.rowSpan;
300
+ const style = cell.getAttribute("style");
301
+ if (style)
302
+ replacement.setAttribute("style", style);
303
+ if (cell.__rtePrevBg != null) {
304
+ replacement.style.background = cell.__rtePrevBg || "";
305
+ delete cell.__rtePrevBg;
306
+ }
307
+ if (replacement.style.background === "var(--srte-accent-bg)" ||
308
+ replacement.style.background.includes("59, 130, 246") ||
309
+ replacement.style.background.includes("59, 158, 255")) {
310
+ replacement.style.background = "";
311
+ }
312
+ replacement.style.outline = "";
313
+ replacement.style.outlineOffset = "";
314
+ replacement.style.border = replacement.style.border || "1px solid #d1d5db";
315
+ replacement.style.padding = replacement.style.padding || "6px";
316
+ replacement.style.minWidth = replacement.style.minWidth || "60px";
317
+ if (tag === "th")
318
+ applyHeaderCellStyle(replacement);
319
+ else
320
+ clearHeaderCellStyle(replacement);
321
+ cell.parentElement?.replaceChild(replacement, cell);
322
+ return replacement;
323
+ };
324
+ const applyFormatBlockFallback = (blockName) => {
325
+ const editor = editableRef.current;
326
+ const block = getCurrentBlock();
327
+ const tag = blockName.replace(/[<>]/g, "").toLowerCase() || "p";
328
+ if (!editor || !block || block === editor || !/^(p|h1|h2|h3|h4|h5|h6|pre|blockquote)$/.test(tag))
329
+ return;
330
+ const replacement = document.createElement(tag);
331
+ copyCellOrBlockStyles(block, replacement);
332
+ block.parentElement?.replaceChild(replacement, block);
333
+ const range = document.createRange();
334
+ range.selectNodeContents(replacement);
335
+ range.collapse(false);
336
+ safeSelectRange(range);
337
+ };
338
+ const cloneListShell = (list, tagName) => {
339
+ const clone = document.createElement(tagName || list.tagName.toLowerCase());
340
+ Array.from(list.attributes).forEach((attr) => clone.setAttribute(attr.name, attr.value));
341
+ return clone;
342
+ };
343
+ const focusElementEnd = (element) => {
344
+ const range = document.createRange();
345
+ range.selectNodeContents(element);
346
+ range.collapse(false);
347
+ safeSelectRange(range);
348
+ savedRangeRef.current = range.cloneRange();
349
+ };
350
+ const insertEmptyListAtSelection = (listTag) => {
351
+ const editor = editableRef.current;
352
+ const range = getSelectionRangeInEditor();
353
+ if (!editor || !range)
354
+ return;
355
+ const list = document.createElement(listTag);
356
+ const li = document.createElement("li");
357
+ li.innerHTML = "<br>";
358
+ list.appendChild(li);
359
+ range.deleteContents();
360
+ range.insertNode(list);
361
+ focusElementEnd(li);
362
+ };
363
+ const unwrapListItem = (li, list) => {
364
+ const parent = list.parentElement;
365
+ if (!parent)
366
+ return;
367
+ const paragraph = document.createElement("p");
368
+ paragraph.innerHTML = li.innerHTML || "<br>";
369
+ const beforeList = cloneListShell(list);
370
+ const afterList = cloneListShell(list);
371
+ while (list.firstChild && list.firstChild !== li) {
372
+ beforeList.appendChild(list.firstChild);
373
+ }
374
+ while (li.nextSibling) {
375
+ afterList.appendChild(li.nextSibling);
376
+ }
377
+ if (beforeList.childNodes.length)
378
+ parent.insertBefore(beforeList, list);
379
+ parent.insertBefore(paragraph, list);
380
+ if (afterList.childNodes.length)
381
+ parent.insertBefore(afterList, list);
382
+ li.remove();
383
+ if (!list.querySelector("li"))
384
+ list.remove();
385
+ focusElementEnd(paragraph);
386
+ };
387
+ const convertListTag = (list, listTag) => {
388
+ const replacement = cloneListShell(list, listTag);
389
+ replacement.innerHTML = list.innerHTML;
390
+ list.parentElement?.replaceChild(replacement, list);
391
+ const li = replacement.querySelector("li");
392
+ focusElementEnd(li || replacement);
393
+ };
394
+ const getOrCreateNestedList = (li, listTag) => {
395
+ let nested = Array.from(li.children).find((child) => {
396
+ const tag = child.tagName.toLowerCase();
397
+ return tag === "ul" || tag === "ol";
398
+ });
399
+ if (nested && nested.tagName.toLowerCase() !== listTag) {
400
+ const replacement = cloneListShell(nested, listTag);
401
+ replacement.innerHTML = nested.innerHTML;
402
+ nested.parentElement?.replaceChild(replacement, nested);
403
+ nested = replacement;
404
+ }
405
+ if (!nested) {
406
+ nested = document.createElement(listTag);
407
+ li.appendChild(nested);
408
+ }
409
+ return nested;
410
+ };
411
+ const nestSelectedListItems = (items, listTag) => {
412
+ if (items.length === 0)
413
+ return false;
414
+ let changed = false;
415
+ let lastTargetList = null;
416
+ items.forEach((li) => {
417
+ const previous = li.previousElementSibling;
418
+ if (!previous || previous.tagName.toLowerCase() !== "li")
419
+ return;
420
+ const targetList = getOrCreateNestedList(previous, listTag);
421
+ targetList.appendChild(li);
422
+ lastTargetList = targetList;
423
+ changed = true;
424
+ });
425
+ if (changed) {
426
+ const lastItem = lastTargetList?.lastElementChild;
427
+ if (lastItem)
428
+ focusElementEnd(lastItem);
429
+ }
430
+ return changed;
431
+ };
432
+ const convertSelectedBlocksToList = (blocks, listTag) => {
433
+ const editor = editableRef.current;
434
+ if (!editor || blocks.length === 0)
435
+ return false;
436
+ const selected = blocks.filter((block) => {
437
+ if (!editor.contains(block))
438
+ return false;
439
+ if (block.tagName.toLowerCase() === "li")
440
+ return false;
441
+ if (block.closest("ul,ol"))
442
+ return false;
443
+ return block.parentElement;
444
+ });
445
+ if (selected.length === 0)
446
+ return false;
447
+ const groups = new Map();
448
+ selected.forEach((block) => {
449
+ const parent = block.parentElement;
450
+ if (!parent)
451
+ return;
452
+ const group = groups.get(parent) || [];
453
+ group.push(block);
454
+ groups.set(parent, group);
455
+ });
456
+ let lastLi = null;
457
+ groups.forEach((group, parent) => {
458
+ group.sort((a, b) => {
459
+ const position = a.compareDocumentPosition(b);
460
+ return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
461
+ });
462
+ const list = document.createElement(listTag);
463
+ parent.insertBefore(list, group[0]);
464
+ group.forEach((block) => {
465
+ const li = document.createElement("li");
466
+ li.innerHTML = block.innerHTML || "<br>";
467
+ list.appendChild(li);
468
+ block.remove();
469
+ lastLi = li;
470
+ });
471
+ });
472
+ if (lastLi)
473
+ focusElementEnd(lastLi);
474
+ return true;
475
+ };
476
+ const toggleList = (listTag) => {
477
+ const editor = editableRef.current;
478
+ if (!editor)
479
+ return;
480
+ if (!restoreSavedSelection())
481
+ safeSelectRange(getSelectionRangeInEditor());
482
+ const range = getSelectionRangeInEditor();
483
+ if (range && !range.collapsed) {
484
+ const blocks = getSelectedBlocks(range);
485
+ const selectedListItems = getSelectedListItems(blocks);
486
+ const convertedBlocks = convertSelectedBlocksToList(blocks, listTag);
487
+ const nestedItems = nestSelectedListItems(selectedListItems, listTag);
488
+ if (convertedBlocks || nestedItems) {
489
+ handleInput();
490
+ requestAnimationFrame(updateActiveState);
491
+ return;
492
+ }
493
+ }
494
+ const block = getCurrentBlock();
495
+ if (!block) {
496
+ insertEmptyListAtSelection(listTag);
497
+ handleInput();
498
+ requestAnimationFrame(updateActiveState);
499
+ return;
500
+ }
501
+ const currentList = block.closest("ul,ol");
502
+ if (currentList && editor.contains(currentList)) {
503
+ if (currentList.tagName.toLowerCase() === listTag) {
504
+ const li = block.closest("li");
505
+ if (li)
506
+ unwrapListItem(li, currentList);
507
+ }
508
+ else {
509
+ convertListTag(currentList, listTag);
510
+ }
511
+ handleInput();
512
+ requestAnimationFrame(updateActiveState);
513
+ return;
514
+ }
515
+ const list = document.createElement(listTag);
516
+ const li = document.createElement("li");
517
+ li.innerHTML = block.innerHTML || "<br>";
518
+ list.appendChild(li);
519
+ block.parentElement?.replaceChild(list, block);
520
+ focusElementEnd(li);
521
+ handleInput();
522
+ requestAnimationFrame(updateActiveState);
523
+ };
524
+ const toggleListFallback = (listTag) => {
525
+ toggleList(listTag);
526
+ };
142
527
  const insertTextAtSelection = (text) => {
143
528
  if (!text)
144
529
  return;
@@ -310,15 +695,17 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
310
695
  };
311
696
  const scheduleImageOverlay = () => {
312
697
  const img = selectedImage;
698
+ const scroller = editorScrollRef.current;
313
699
  if (!img) {
314
700
  setImageOverlay(null);
315
701
  return;
316
702
  }
317
703
  try {
318
704
  const rect = img.getBoundingClientRect();
705
+ const hostRect = scroller?.getBoundingClientRect();
319
706
  setImageOverlay({
320
- left: rect.left,
321
- top: rect.top,
707
+ left: hostRect && scroller ? rect.left - hostRect.left + scroller.scrollLeft : rect.left,
708
+ top: hostRect && scroller ? rect.top - hostRect.top + scroller.scrollTop : rect.top,
322
709
  width: rect.width,
323
710
  height: rect.height,
324
711
  });
@@ -617,6 +1004,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
617
1004
  if (!media)
618
1005
  return;
619
1006
  const list = Array.from(files).filter((f) => f.type.startsWith("image/"));
1007
+ if (mediaManager) {
1008
+ try {
1009
+ const uploaded = await mediaManager.upload(list);
1010
+ uploaded.forEach((item) => insertImageAtSelection(item));
1011
+ return;
1012
+ }
1013
+ catch (error) {
1014
+ console.error("Image upload failed, inserting local image data instead:", error);
1015
+ }
1016
+ }
620
1017
  for (const f of list) {
621
1018
  await new Promise((resolve) => {
622
1019
  const reader = new FileReader();
@@ -669,6 +1066,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
669
1066
  let fullHtml = '';
670
1067
  for (let i = 1; i <= pdf.numPages; i++) {
671
1068
  const page = await pdf.getPage(i);
1069
+ const viewport = page.getViewport({ scale: 1 });
672
1070
  const textContent = await page.getTextContent();
673
1071
  const styles = textContent.styles;
674
1072
  // 1. Group items into lines
@@ -740,8 +1138,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
740
1138
  const width = item.width;
741
1139
  const fontName = item.fontName;
742
1140
  const fontObj = styles[fontName];
743
- const isBold = fontObj?.fontFamily?.toLowerCase().includes('bold') || false;
744
- // const isItalic = fontObj?.fontFamily?.toLowerCase().includes('italic') || false;
1141
+ const fontFamily = fontObj?.fontFamily?.toLowerCase() || '';
1142
+ const isBold = fontFamily.includes('bold') || false;
1143
+ const isItalic = fontFamily.includes('italic') || fontFamily.includes('oblique');
1144
+ const fontSize = Math.max(8, Math.round(Math.abs(item.transform[3])));
745
1145
  if (lastX > 0) {
746
1146
  const gap = x - lastX;
747
1147
  if (gap > 2) { // Minimal space threshold
@@ -760,10 +1160,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
760
1160
  itemXs.push(x);
761
1161
  }
762
1162
  // Append text style
763
- let chunk = item.str;
764
- if (isBold)
765
- chunk = `<strong>${chunk}</strong>`;
766
- // if (isItalic) chunk = `<em>${chunk}</em>`;
1163
+ const chunkStyle = cssRules([
1164
+ ['font-size', `${fontSize}px`],
1165
+ ['font-weight', isBold ? '700' : ''],
1166
+ ['font-style', isItalic ? 'italic' : ''],
1167
+ ]);
1168
+ let chunk = `<span${styleAttr(chunkStyle)}>${escapeHtml(item.str)}</span>`;
767
1169
  lineText += item.str;
768
1170
  lineHtmlContent += chunk;
769
1171
  lastX = x + width;
@@ -834,8 +1236,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
834
1236
  const w = item.width;
835
1237
  const txt = item.str;
836
1238
  const fontObj = styles[item.fontName];
837
- const isBold = fontObj?.fontFamily?.toLowerCase().includes('bold');
838
- const styledTxt = isBold ? `<strong>${txt}</strong>` : txt;
1239
+ const fontFamily = fontObj?.fontFamily?.toLowerCase() || '';
1240
+ const isBold = fontFamily.includes('bold');
1241
+ const isItalic = fontFamily.includes('italic') || fontFamily.includes('oblique');
1242
+ const fontSize = Math.max(8, Math.round(Math.abs(item.transform[3])));
1243
+ const styledTxt = `<span${styleAttr(cssRules([
1244
+ ['font-size', `${fontSize}px`],
1245
+ ['font-weight', isBold ? '700' : ''],
1246
+ ['font-style', isItalic ? 'italic' : ''],
1247
+ ]))}>${escapeHtml(txt)}</span>`;
839
1248
  // Decide which column this belongs to
840
1249
  // Find closest column to the left (or close enough)
841
1250
  let colIdx = 0;
@@ -854,7 +1263,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
854
1263
  cellContents[colIdx] += styledTxt;
855
1264
  }
856
1265
  cellContents.forEach(content => {
857
- rowHtml += `<td style="border:1px solid var(--srte-border);padding:8px;vertical-align:top;">${content || '&nbsp;'}</td>`;
1266
+ rowHtml += `<td style="border:1px solid #d1d5db;padding:8px;vertical-align:top;">${content || '&nbsp;'}</td>`;
858
1267
  });
859
1268
  rowHtml += '</tr>';
860
1269
  tableHtml += rowHtml;
@@ -877,10 +1286,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
877
1286
  closeList();
878
1287
  if (isHeader) {
879
1288
  const tag = maxH > medianHeight * 1.5 ? 'h2' : 'h3';
880
- html += `<${tag}>${lineHtmlContent}</${tag}>`;
1289
+ const firstX = line.items[0]?.transform?.[4] || 0;
1290
+ const lastItem = line.items[line.items.length - 1];
1291
+ const lastRight = lastItem ? lastItem.transform[4] + lastItem.width : firstX;
1292
+ const center = (firstX + lastRight) / 2;
1293
+ const align = Math.abs(center - viewport.width / 2) < viewport.width * 0.12 ? 'center' : firstX > viewport.width * 0.55 ? 'right' : '';
1294
+ html += `<${tag}${styleAttr(cssRules([['text-align', align]]))}>${lineHtmlContent}</${tag}>`;
881
1295
  }
882
1296
  else {
883
- html += `<p>${lineHtmlContent}</p>`;
1297
+ const firstX = line.items[0]?.transform?.[4] || 0;
1298
+ const lastItem = line.items[line.items.length - 1];
1299
+ const lastRight = lastItem ? lastItem.transform[4] + lastItem.width : firstX;
1300
+ const center = (firstX + lastRight) / 2;
1301
+ const align = Math.abs(center - viewport.width / 2) < viewport.width * 0.12 ? 'center' : firstX > viewport.width * 0.55 ? 'right' : '';
1302
+ html += `<p${styleAttr(cssRules([
1303
+ ['text-align', align],
1304
+ ['margin-left', firstX > 40 && !align ? `${Math.round(firstX)}px` : ''],
1305
+ ]))}>${lineHtmlContent}</p>`;
884
1306
  }
885
1307
  }
886
1308
  }
@@ -889,7 +1311,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
889
1311
  closeTable();
890
1312
  fullHtml += html;
891
1313
  }
892
- insertImportedHtml(fullHtml, mode);
1314
+ insertImportedHtml(fullHtml, mode, { preserveColors: true, preserveDocumentLayout: true });
893
1315
  }
894
1316
  catch (error) {
895
1317
  console.error('Error reading PDF:', error);
@@ -1146,63 +1568,163 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1146
1568
  .replace(/&/g, "&amp;")
1147
1569
  .replace(/</g, "&lt;")
1148
1570
  .replace(/>/g, "&gt;");
1571
+ const escapeHtmlAttribute = (value) => escapeHtml(value).replace(/"/g, "&quot;");
1149
1572
  const markdownToHtml = (markdown) => {
1150
1573
  const lines = markdown.replace(/\r\n/g, "\n").split("\n");
1151
1574
  let html = "";
1152
1575
  let listType = null;
1576
+ let paragraph = [];
1577
+ let codeFence = null;
1153
1578
  const closeList = () => {
1154
1579
  if (listType) {
1155
1580
  html += `</${listType}>`;
1156
1581
  listType = null;
1157
1582
  }
1158
1583
  };
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) => {
1584
+ const inline = (text) => {
1585
+ const codeTokens = [];
1586
+ let value = text.replace(/`([^`]+)`/g, (_match, code) => {
1587
+ const token = `@@SRTE_CODE_${codeTokens.length}@@`;
1588
+ codeTokens.push(`<code>${escapeHtml(code)}</code>`);
1589
+ return token;
1590
+ });
1591
+ value = escapeHtml(value)
1592
+ .replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_match, alt, src, title) => {
1593
+ const titleAttr = title ? ` title="${escapeHtmlAttribute(title)}"` : "";
1594
+ return `<img src="${escapeHtmlAttribute(src)}" alt="${escapeHtmlAttribute(alt)}"${titleAttr}>`;
1595
+ })
1596
+ .replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_match, label, href, title) => {
1597
+ const titleAttr = title ? ` title="${escapeHtmlAttribute(title)}"` : "";
1598
+ return `<a href="${escapeHtmlAttribute(href)}"${titleAttr}>${label}</a>`;
1599
+ })
1600
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
1601
+ .replace(/__([^_]+)__/g, "<strong>$1</strong>")
1602
+ .replace(/~~([^~]+)~~/g, "<s>$1</s>")
1603
+ .replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>")
1604
+ .replace(/(^|[^_])_([^_\n]+)_/g, "$1<em>$2</em>");
1605
+ codeTokens.forEach((replacement, index) => {
1606
+ value = value.replace(`@@SRTE_CODE_${index}@@`, replacement);
1607
+ });
1608
+ return value;
1609
+ };
1610
+ const closeParagraph = () => {
1611
+ if (!paragraph.length)
1612
+ return;
1613
+ html += `<p>${inline(paragraph.join(" "))}</p>`;
1614
+ paragraph = [];
1615
+ };
1616
+ const isTableSeparator = (line) => /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
1617
+ const parseTableRow = (line) => {
1618
+ let value = line.trim();
1619
+ if (value.startsWith("|"))
1620
+ value = value.slice(1);
1621
+ if (value.endsWith("|"))
1622
+ value = value.slice(0, -1);
1623
+ return value.split("|").map((cell) => cell.trim());
1624
+ };
1625
+ const renderTable = (startIndex) => {
1626
+ const header = parseTableRow(lines[startIndex]);
1627
+ let index = startIndex + 2;
1628
+ const rows = [];
1629
+ while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
1630
+ rows.push(parseTableRow(lines[index]));
1631
+ index += 1;
1632
+ }
1633
+ const headHtml = `<thead><tr>${header.map((cell) => `<th>${inline(cell)}</th>`).join("")}</tr></thead>`;
1634
+ const bodyHtml = rows.length
1635
+ ? `<tbody>${rows.map((row) => `<tr>${header.map((_cell, cellIndex) => `<td>${inline(row[cellIndex] || "")}</td>`).join("")}</tr>`).join("")}</tbody>`
1636
+ : "";
1637
+ html += `<table style="border-collapse: collapse; width: 100%; margin: 12px 0;">${headHtml}${bodyHtml}</table>`;
1638
+ return index;
1639
+ };
1640
+ for (let i = 0; i < lines.length; i += 1) {
1641
+ const line = lines[i];
1164
1642
  const trimmed = line.trim();
1643
+ const fence = /^```([A-Za-z0-9_-]+)?\s*$/.exec(trimmed);
1644
+ if (fence) {
1645
+ closeParagraph();
1646
+ closeList();
1647
+ if (codeFence) {
1648
+ const langClass = codeFence.lang ? ` class="language-${escapeHtmlAttribute(codeFence.lang)}"` : "";
1649
+ html += `<pre><code${langClass}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`;
1650
+ codeFence = null;
1651
+ }
1652
+ else {
1653
+ codeFence = { lang: fence[1] || "", lines: [] };
1654
+ }
1655
+ continue;
1656
+ }
1657
+ if (codeFence) {
1658
+ codeFence.lines.push(line);
1659
+ continue;
1660
+ }
1165
1661
  if (!trimmed) {
1662
+ closeParagraph();
1166
1663
  closeList();
1167
- return;
1664
+ continue;
1665
+ }
1666
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
1667
+ closeParagraph();
1668
+ closeList();
1669
+ html += "<hr>";
1670
+ continue;
1671
+ }
1672
+ if (i + 1 < lines.length && trimmed.includes("|") && isTableSeparator(lines[i + 1])) {
1673
+ closeParagraph();
1674
+ closeList();
1675
+ i = renderTable(i) - 1;
1676
+ continue;
1168
1677
  }
1169
1678
  const heading = /^(#{1,6})\s+(.+)$/.exec(trimmed);
1170
1679
  if (heading) {
1680
+ closeParagraph();
1171
1681
  closeList();
1172
1682
  const level = heading[1].length;
1173
1683
  html += `<h${level}>${inline(heading[2])}</h${level}>`;
1174
- return;
1684
+ continue;
1175
1685
  }
1176
- const bullet = /^[-*]\s+(.+)$/.exec(trimmed);
1686
+ const bullet = /^[-*+]\s+(.+)$/.exec(trimmed);
1177
1687
  if (bullet) {
1688
+ closeParagraph();
1178
1689
  if (listType !== "ul") {
1179
1690
  closeList();
1180
1691
  html += "<ul>";
1181
1692
  listType = "ul";
1182
1693
  }
1183
1694
  html += `<li>${inline(bullet[1])}</li>`;
1184
- return;
1695
+ continue;
1185
1696
  }
1186
1697
  const numbered = /^\d+[.)]\s+(.+)$/.exec(trimmed);
1187
1698
  if (numbered) {
1699
+ closeParagraph();
1188
1700
  if (listType !== "ol") {
1189
1701
  closeList();
1190
1702
  html += "<ol>";
1191
1703
  listType = "ol";
1192
1704
  }
1193
1705
  html += `<li>${inline(numbered[1])}</li>`;
1194
- return;
1706
+ continue;
1195
1707
  }
1196
- if (trimmed.startsWith("> ")) {
1708
+ const quote = /^>\s?(.*)$/.exec(trimmed);
1709
+ if (quote) {
1710
+ closeParagraph();
1197
1711
  closeList();
1198
- html += `<blockquote>${inline(trimmed.slice(2))}</blockquote>`;
1199
- return;
1712
+ html += `<blockquote>${inline(quote[1]) || "<br>"}</blockquote>`;
1713
+ continue;
1200
1714
  }
1201
1715
  closeList();
1202
- html += `<p>${inline(trimmed)}</p>`;
1203
- });
1716
+ paragraph.push(trimmed);
1717
+ }
1718
+ if (codeFence) {
1719
+ const langClass = codeFence.lang ? ` class="language-${escapeHtmlAttribute(codeFence.lang)}"` : "";
1720
+ html += `<pre><code${langClass}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`;
1721
+ }
1722
+ closeParagraph();
1204
1723
  closeList();
1205
- return html;
1724
+ const root = document.createElement("div");
1725
+ root.innerHTML = html;
1726
+ enhanceImportedTables(root);
1727
+ return root.innerHTML;
1206
1728
  };
1207
1729
  const htmlToMarkdown = (html) => {
1208
1730
  const root = document.createElement("div");
@@ -1250,7 +1772,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1250
1772
  const html = type === "html" ? text : markdownToHtml(text);
1251
1773
  const el = editableRef.current;
1252
1774
  const hasContent = el && el.textContent && el.textContent.trim().length > 0;
1253
- insertImportedHtml(html, hasContent ? "append" : "replace");
1775
+ insertImportedHtml(html, hasContent ? "append" : "replace", {
1776
+ preserveColors: true,
1777
+ preserveDocumentLayout: true,
1778
+ });
1254
1779
  };
1255
1780
  const downloadText = (filename, content, mimeType) => {
1256
1781
  const blob = new Blob([content], { type: mimeType });
@@ -1271,6 +1796,139 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1271
1796
  const html = editableRef.current?.innerHTML || "";
1272
1797
  downloadText("smart-rte-export.md", htmlToMarkdown(html), "text/markdown");
1273
1798
  };
1799
+ const htmlToDocxXml = (html) => {
1800
+ const root = document.createElement("div");
1801
+ root.innerHTML = html;
1802
+ const xmlEscape = (value) => value
1803
+ .replace(/&/g, "&amp;")
1804
+ .replace(/</g, "&lt;")
1805
+ .replace(/>/g, "&gt;")
1806
+ .replace(/"/g, "&quot;");
1807
+ const colorValue = (value) => {
1808
+ const hex = /^#([0-9a-f]{6})$/i.exec(value.trim());
1809
+ if (hex)
1810
+ return hex[1].toUpperCase();
1811
+ const rgb = /^rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)$/i.exec(value.trim());
1812
+ if (!rgb)
1813
+ return "";
1814
+ return [rgb[1], rgb[2], rgb[3]]
1815
+ .map((part) => Math.max(0, Math.min(255, Number(part))).toString(16).padStart(2, "0"))
1816
+ .join("")
1817
+ .toUpperCase();
1818
+ };
1819
+ const sizeToHalfPoints = (value) => {
1820
+ const trimmed = value.trim();
1821
+ const match = /^([\d.]+)(px|pt)$/i.exec(trimmed);
1822
+ if (!match)
1823
+ return "";
1824
+ const raw = Number(match[1]);
1825
+ const pt = match[2].toLowerCase() === "px" ? raw * 0.75 : raw;
1826
+ return String(Math.max(2, Math.round(pt * 2)));
1827
+ };
1828
+ const runProperties = (el) => {
1829
+ const style = el.style;
1830
+ const color = colorValue(style.color);
1831
+ const size = sizeToHalfPoints(style.fontSize);
1832
+ const isBold = el.tagName === "B" || el.tagName === "STRONG" || /bold|700|800|900/.test(style.fontWeight);
1833
+ const isItalic = el.tagName === "I" || el.tagName === "EM" || style.fontStyle === "italic";
1834
+ const isUnderline = el.tagName === "U" || style.textDecoration.includes("underline");
1835
+ return [
1836
+ isBold ? "<w:b/>" : "",
1837
+ isItalic ? "<w:i/>" : "",
1838
+ isUnderline ? '<w:u w:val="single"/>' : "",
1839
+ color ? `<w:color w:val="${color}"/>` : "",
1840
+ size ? `<w:sz w:val="${size}"/>` : "",
1841
+ ].join("");
1842
+ };
1843
+ const runs = (node, inheritedProps = "") => {
1844
+ if (node.nodeType === Node.TEXT_NODE) {
1845
+ const text = node.textContent || "";
1846
+ return text ? `<w:r>${inheritedProps ? `<w:rPr>${inheritedProps}</w:rPr>` : ""}<w:t xml:space="preserve">${xmlEscape(text)}</w:t></w:r>` : "";
1847
+ }
1848
+ if (!(node instanceof HTMLElement))
1849
+ return "";
1850
+ if (node.tagName === "BR")
1851
+ return "<w:r><w:br/></w:r>";
1852
+ if (node.tagName === "IMG") {
1853
+ const alt = node.getAttribute("alt") || node.getAttribute("title") || "Image";
1854
+ return `<w:r><w:t>[Image: ${xmlEscape(alt)}]</w:t></w:r>`;
1855
+ }
1856
+ const props = `${inheritedProps}${runProperties(node)}`;
1857
+ return Array.from(node.childNodes).map((child) => runs(child, props)).join("");
1858
+ };
1859
+ const paragraph = (el, fallbackTag = "p") => {
1860
+ const tag = el.tagName.toLowerCase();
1861
+ const headingMatch = /^h([1-6])$/.exec(tag);
1862
+ const style = el.style;
1863
+ const align = style.textAlign ? `<w:jc w:val="${xmlEscape(style.textAlign)}"/>` : "";
1864
+ const headingSize = headingMatch ? `<w:rPr><w:b/><w:sz w:val="${Math.max(24, 40 - Number(headingMatch[1]) * 4)}"/></w:rPr>` : "";
1865
+ const body = runs(el);
1866
+ return `<w:p><w:pPr>${align}${headingSize}</w:pPr>${body || "<w:r><w:t></w:t></w:r>"}</w:p>`;
1867
+ };
1868
+ const tableCell = (cell) => {
1869
+ const fill = colorValue(cell.style.backgroundColor);
1870
+ const shading = fill ? `<w:shd w:val="clear" w:color="auto" w:fill="${fill}"/>` : "";
1871
+ const cellContent = Array.from(cell.childNodes)
1872
+ .map((child) => child instanceof HTMLElement && ["P", "DIV", "H1", "H2", "H3", "H4", "H5", "H6"].includes(child.tagName)
1873
+ ? paragraph(child)
1874
+ : `<w:p>${runs(child)}</w:p>`)
1875
+ .join("");
1876
+ 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>`;
1877
+ };
1878
+ const tableXml = (table) => {
1879
+ const rows = Array.from(table.querySelectorAll("tr"));
1880
+ 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>`;
1881
+ };
1882
+ const blockXml = (node) => {
1883
+ if (node.nodeType === Node.TEXT_NODE) {
1884
+ const text = node.textContent?.trim();
1885
+ return text ? `<w:p>${runs(node)}</w:p>` : "";
1886
+ }
1887
+ if (!(node instanceof HTMLElement))
1888
+ return "";
1889
+ if (node.tagName === "TABLE")
1890
+ return tableXml(node);
1891
+ if (node.tagName === "UL" || node.tagName === "OL") {
1892
+ return Array.from(node.children).map((li) => `<w:p><w:r><w:t>• </w:t></w:r>${runs(li)}</w:p>`).join("");
1893
+ }
1894
+ if (node.tagName === "BLOCKQUOTE") {
1895
+ return `<w:p><w:pPr><w:ind w:left="720"/></w:pPr>${runs(node)}</w:p>`;
1896
+ }
1897
+ if (node.tagName === "HR")
1898
+ return '<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:color="D1D5DB"/></w:pBdr></w:pPr></w:p>';
1899
+ if (["P", "DIV", "PRE", "H1", "H2", "H3", "H4", "H5", "H6"].includes(node.tagName))
1900
+ return paragraph(node);
1901
+ return Array.from(node.childNodes).map(blockXml).join("");
1902
+ };
1903
+ const body = Array.from(root.childNodes).map(blockXml).join("");
1904
+ 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>`;
1905
+ };
1906
+ const exportDocx = async () => {
1907
+ const html = editableRef.current?.innerHTML || "";
1908
+ const zip = new JSZip();
1909
+ 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>`);
1910
+ 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>`);
1911
+ zip.folder("word")?.file("document.xml", htmlToDocxXml(html));
1912
+ 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>`);
1913
+ const blob = await zip.generateAsync({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" });
1914
+ const url = URL.createObjectURL(blob);
1915
+ const link = document.createElement("a");
1916
+ link.href = url;
1917
+ link.download = "smart-rte-export.docx";
1918
+ document.body.appendChild(link);
1919
+ link.click();
1920
+ link.remove();
1921
+ URL.revokeObjectURL(url);
1922
+ };
1923
+ const exportPdf = () => {
1924
+ const html = editableRef.current?.innerHTML || "";
1925
+ const printWindow = window.open("", "_blank", "width=900,height=700");
1926
+ if (!printWindow)
1927
+ return;
1928
+ printWindow.document.open();
1929
+ printWindow.document.write(`<!doctype html><html><head><title>Export PDF</title><style>@page{margin:18mm}html,body{background:#fff}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;break-inside:auto}tr,img,blockquote,pre{break-inside:avoid}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 || "<p></p>"}<script>window.addEventListener("load",function(){setTimeout(function(){window.focus();window.print();},150);});</script></body></html>`);
1930
+ printWindow.document.close();
1931
+ };
1274
1932
  const fixNegativeMargins = (root) => {
1275
1933
  try {
1276
1934
  const nodes = root.querySelectorAll('*');
@@ -1438,10 +2096,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1438
2096
  const wrapper = document.createElement('div');
1439
2097
  wrapper.setAttribute('data-table-wrapper', 'true');
1440
2098
  wrapper.style.overflowX = 'auto';
2099
+ wrapper.style.overflowY = 'visible';
1441
2100
  wrapper.style.webkitOverflowScrolling = 'touch';
1442
2101
  wrapper.style.width = '100%';
1443
2102
  wrapper.style.maxWidth = '100%';
1444
2103
  wrapper.style.display = 'block';
2104
+ wrapper.style.paddingBottom = '8px';
1445
2105
  // Use insertBefore + appendChild to move element without losing too much state
1446
2106
  // simpler than replaceChild for wrapping
1447
2107
  parent.insertBefore(wrapper, table);
@@ -1489,7 +2149,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1489
2149
  html += "<tr>";
1490
2150
  for (let c = 0; c < safeCols; c++) {
1491
2151
  html +=
1492
- '<td style="border:1px solid var(--srte-border);padding:6px;min-width:60px;">&nbsp;</td>';
2152
+ '<td style="border:1px solid #d1d5db;padding:6px;min-width:60px;">&nbsp;</td>';
1493
2153
  }
1494
2154
  html += "</tr>";
1495
2155
  }
@@ -1572,16 +2232,56 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1572
2232
  }
1573
2233
  catch { }
1574
2234
  };
2235
+ const getTableGrid = (tbody) => {
2236
+ const rows = Array.from(tbody.querySelectorAll("tr"));
2237
+ const grid = [];
2238
+ rows.forEach((row, rIdx) => {
2239
+ grid[rIdx] = grid[rIdx] || [];
2240
+ let cIdx = 0;
2241
+ const cells = cellsOfRow(row);
2242
+ cells.forEach((cell) => {
2243
+ while (grid[rIdx][cIdx])
2244
+ cIdx += 1;
2245
+ const rowSpan = Math.max(1, cell.rowSpan || 1);
2246
+ const colSpan = Math.max(1, cell.colSpan || 1);
2247
+ for (let r = rIdx; r < rIdx + rowSpan; r += 1) {
2248
+ grid[r] = grid[r] || [];
2249
+ for (let c = cIdx; c < cIdx + colSpan; c += 1) {
2250
+ grid[r][c] = cell;
2251
+ }
2252
+ }
2253
+ cIdx += colSpan;
2254
+ });
2255
+ });
2256
+ return { rows, grid };
2257
+ };
2258
+ const getCellsInGridRect = (tbody, sr, sc, er, ec) => {
2259
+ const { grid } = getTableGrid(tbody);
2260
+ const seen = new Set();
2261
+ const cells = [];
2262
+ for (let r = sr; r <= er; r += 1) {
2263
+ for (let c = sc; c <= ec; c += 1) {
2264
+ const cell = grid[r]?.[c];
2265
+ if (cell && !seen.has(cell)) {
2266
+ seen.add(cell);
2267
+ cells.push(cell);
2268
+ }
2269
+ }
2270
+ }
2271
+ return cells;
2272
+ };
1575
2273
  const getCellPosition = (cell) => {
1576
2274
  const row = cell.parentElement;
1577
2275
  const tbody = row?.parentElement;
1578
2276
  const table = tbody?.parentElement;
1579
2277
  if (!row || !tbody || !table)
1580
2278
  return null;
1581
- const rows = Array.from(tbody.querySelectorAll("tr"));
2279
+ const { rows, grid } = getTableGrid(tbody);
1582
2280
  const rIdx = rows.indexOf(row);
1583
- const cells = Array.from(row.children).filter((c) => ["TD", "TH"].includes(c.tagName));
1584
- const cIdx = cells.indexOf(cell);
2281
+ let cIdx = -1;
2282
+ if (rIdx >= 0) {
2283
+ cIdx = (grid[rIdx] || []).findIndex((candidate) => candidate === cell);
2284
+ }
1585
2285
  return { row, tbody, table, rIdx, cIdx };
1586
2286
  };
1587
2287
  const cellsOfRow = (row) => Array.from(row.children).filter((c) => ["TD", "TH"].includes(c.tagName));
@@ -1590,42 +2290,27 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1590
2290
  if (!sel)
1591
2291
  return;
1592
2292
  const { tbody, sr, sc, er, ec } = sel;
1593
- const rows = Array.from(tbody.querySelectorAll("tr"));
1594
- for (let r = sr; r <= er; r++) {
1595
- const row = rows[r];
1596
- const cells = cellsOfRow(row);
1597
- for (let c = sc; c <= ec; c++) {
1598
- const cell = cells[c];
1599
- if (!cell)
1600
- continue;
1601
- if (cell.__rtePrevBg != null) {
1602
- cell.style.background = cell.__rtePrevBg;
1603
- delete cell.__rtePrevBg;
1604
- }
1605
- cell.style.outline = "";
1606
- cell.style.outlineOffset = "";
2293
+ const cells = getCellsInGridRect(tbody, sr, sc, er, ec);
2294
+ cells.forEach((cell) => {
2295
+ if (cell.__rtePrevBg != null) {
2296
+ cell.style.background = cell.__rtePrevBg;
2297
+ delete cell.__rtePrevBg;
1607
2298
  }
1608
- }
2299
+ cell.style.outline = "";
2300
+ cell.style.outlineOffset = "";
2301
+ });
1609
2302
  selectionRef.current = null;
1610
2303
  };
1611
2304
  const updateSelectionDecor = (tbody, sr, sc, er, ec) => {
1612
2305
  clearSelectionDecor();
1613
2306
  selectionRef.current = { tbody, sr, sc, er, ec };
1614
- const rows = Array.from(tbody.querySelectorAll("tr"));
1615
- for (let r = sr; r <= er; r++) {
1616
- const row = rows[r];
1617
- const cells = cellsOfRow(row);
1618
- for (let c = sc; c <= ec; c++) {
1619
- const cell = cells[c];
1620
- if (!cell)
1621
- continue;
1622
- cell.__rtePrevBg =
1623
- cell.style.background || "";
1624
- cell.style.background = "var(--srte-accent-bg)";
1625
- cell.style.outline = "2px solid var(--srte-accent)";
1626
- cell.style.outlineOffset = "-2px";
1627
- }
1628
- }
2307
+ const cells = getCellsInGridRect(tbody, sr, sc, er, ec);
2308
+ cells.forEach((cell) => {
2309
+ cell.__rtePrevBg = cell.style.background || "";
2310
+ cell.style.background = "var(--srte-accent-bg)";
2311
+ cell.style.outline = "2px solid var(--srte-accent)";
2312
+ cell.style.outlineOffset = "-2px";
2313
+ });
1629
2314
  };
1630
2315
  const canMergeSelection = () => {
1631
2316
  const sel = selectionRef.current;
@@ -1633,32 +2318,37 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1633
2318
  return false;
1634
2319
  return sel.sr !== sel.er || sel.sc !== sel.ec;
1635
2320
  };
2321
+ const canMergeFromCell = (cell) => Boolean(cell && canMergeSelection() && isCellInsideSelection(cell));
2322
+ const canSplitCell = (cell) => Boolean(cell && (Math.max(1, cell.rowSpan || 1) > 1 || Math.max(1, cell.colSpan || 1) > 1));
2323
+ const isCellInsideSelection = (cell) => {
2324
+ const sel = selectionRef.current;
2325
+ if (!sel)
2326
+ return false;
2327
+ const pos = getCellPosition(cell);
2328
+ if (!pos || pos.tbody !== sel.tbody)
2329
+ return false;
2330
+ return pos.rIdx >= sel.sr && pos.rIdx <= sel.er && pos.cIdx >= sel.sc && pos.cIdx <= sel.ec;
2331
+ };
2332
+ const shouldUseTableSelection = (fallbackCell) => Boolean(selectionRef.current && fallbackCell && isCellInsideSelection(fallbackCell));
1636
2333
  const mergeSelection = () => {
1637
2334
  const sel = selectionRef.current;
1638
2335
  if (!sel)
1639
2336
  return;
1640
2337
  const { tbody, sr, sc, er, ec } = sel;
1641
- const rows = Array.from(tbody.querySelectorAll("tr"));
1642
- const anchorRow = rows[sr];
1643
- const anchor = cellsOfRow(anchorRow)[sc];
2338
+ const { grid } = getTableGrid(tbody);
2339
+ const anchor = grid[sr]?.[sc];
1644
2340
  if (!anchor)
1645
2341
  return;
1646
2342
  // Collect content and remove other cells
1647
2343
  const contents = [];
1648
- for (let r = sr; r <= er; r++) {
1649
- const row = rows[r];
1650
- const cells = cellsOfRow(row);
1651
- for (let c = sc; c <= ec; c++) {
1652
- const cell = cells[c];
1653
- if (!cell)
1654
- continue;
1655
- if (r === sr && c === sc)
1656
- continue;
1657
- const html = cell.innerHTML.trim();
1658
- if (html)
1659
- contents.push(html);
1660
- }
1661
- }
2344
+ const cellsToMerge = getCellsInGridRect(tbody, sr, sc, er, ec);
2345
+ cellsToMerge.forEach((cell) => {
2346
+ if (cell === anchor)
2347
+ return;
2348
+ const html = cell.innerHTML.trim();
2349
+ if (html)
2350
+ contents.push(html);
2351
+ });
1662
2352
  if (contents.length) {
1663
2353
  anchor.innerHTML = (anchor.innerHTML || "") + " " + contents.join(" ");
1664
2354
  }
@@ -1666,18 +2356,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1666
2356
  anchor.colSpan = ec - sc + 1;
1667
2357
  anchor.rowSpan = er - sr + 1;
1668
2358
  // Remove other cells
1669
- for (let r = sr; r <= er; r++) {
1670
- const row = rows[r];
1671
- const cells = cellsOfRow(row);
1672
- for (let c = ec; c >= sc; c--) {
1673
- const cell = cells[c];
1674
- if (!cell)
1675
- continue;
1676
- if (r === sr && c === sc)
1677
- continue;
2359
+ cellsToMerge.forEach((cell) => {
2360
+ if (cell !== anchor)
1678
2361
  cell.remove();
1679
- }
1680
- }
2362
+ });
1681
2363
  moveCaretToCell(anchor, false);
1682
2364
  clearSelectionDecor();
1683
2365
  handleInput();
@@ -1691,7 +2373,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1691
2373
  const numCols = Array.from(row.children).filter((c) => ["TD", "TH"].includes(c.tagName)).length;
1692
2374
  for (let i = 0; i < numCols; i++) {
1693
2375
  const td = document.createElement("td");
1694
- td.style.border = "1px solid var(--srte-border)";
2376
+ td.style.border = "1px solid #d1d5db";
1695
2377
  td.style.padding = "6px";
1696
2378
  td.style.minWidth = "60px";
1697
2379
  td.innerHTML = "&nbsp;";
@@ -1721,7 +2403,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1721
2403
  for (const r of rows) {
1722
2404
  const cells = Array.from(r.children).filter((c) => ["TD", "TH"].includes(c.tagName));
1723
2405
  const td = document.createElement("td");
1724
- td.style.border = "1px solid var(--srte-border)";
2406
+ td.style.border = "1px solid #d1d5db";
1725
2407
  td.style.padding = "6px";
1726
2408
  td.style.minWidth = "60px";
1727
2409
  td.innerHTML = "&nbsp;";
@@ -1750,14 +2432,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1750
2432
  table.parentElement?.removeChild(table);
1751
2433
  };
1752
2434
  const toggleHeaderCell = (cell) => {
2435
+ clearSelectionDecor();
1753
2436
  const isTh = cell.tagName === "TH";
1754
- const replacement = document.createElement(isTh ? "td" : "th");
1755
- replacement.innerHTML = cell.innerHTML || "&nbsp;";
1756
- replacement.style.border =
1757
- cell.style.border || "1px solid var(--srte-border)";
1758
- replacement.style.padding = cell.style.padding || "6px";
1759
- replacement.style.minWidth = cell.style.minWidth || "60px";
1760
- cell.parentElement?.replaceChild(replacement, cell);
2437
+ replaceTableCellTag(cell, isTh ? "td" : "th");
2438
+ handleInput();
1761
2439
  };
1762
2440
  const deleteTable = (cell) => {
1763
2441
  const pos = getCellPosition(cell);
@@ -1782,7 +2460,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1782
2460
  const currentRow = Array.from(tbody.querySelectorAll("tr"))[rIdx];
1783
2461
  for (let j = 1; j < cs; j++) {
1784
2462
  const td = document.createElement("td");
1785
- td.style.border = "1px solid var(--srte-border)";
2463
+ td.style.border = "1px solid #d1d5db";
1786
2464
  td.style.padding = "6px";
1787
2465
  td.style.minWidth = "60px";
1788
2466
  td.innerHTML = "&nbsp;";
@@ -1795,7 +2473,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1795
2473
  const row = Array.from(tbody.querySelectorAll("tr"))[rIdx + i];
1796
2474
  for (let j = 0; j < cs; j++) {
1797
2475
  const td = document.createElement("td");
1798
- td.style.border = "1px solid var(--srte-border)";
2476
+ td.style.border = "1px solid #d1d5db";
1799
2477
  td.style.padding = "6px";
1800
2478
  td.style.minWidth = "60px";
1801
2479
  td.innerHTML = "&nbsp;";
@@ -1810,29 +2488,17 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1810
2488
  const pos = getCellPosition(cell);
1811
2489
  if (!pos)
1812
2490
  return;
1813
- const { tbody } = pos;
1814
- const firstRow = tbody.querySelector("tr");
1815
- if (!firstRow)
1816
- return;
1817
- const cells = cellsOfRow(firstRow);
2491
+ clearSelectionDecor();
2492
+ const { row } = pos;
2493
+ const cells = cellsOfRow(row);
1818
2494
  const shouldMakeHeader = cells.some((c) => c.tagName !== "TH");
1819
2495
  for (const c of cells) {
1820
2496
  const isTh = c.tagName === "TH";
1821
2497
  if (shouldMakeHeader && !isTh) {
1822
- const th = document.createElement("th");
1823
- th.innerHTML = c.innerHTML || "&nbsp;";
1824
- th.style.border = c.style.border || "1px solid var(--srte-border)";
1825
- th.style.padding = c.style.padding || "6px";
1826
- th.style.minWidth = c.style.minWidth || "60px";
1827
- firstRow.replaceChild(th, c);
2498
+ replaceTableCellTag(c, "th");
1828
2499
  }
1829
2500
  else if (!shouldMakeHeader && isTh) {
1830
- const td = document.createElement("td");
1831
- td.innerHTML = c.innerHTML || "&nbsp;";
1832
- td.style.border = c.style.border || "1px solid var(--srte-border)";
1833
- td.style.padding = c.style.padding || "6px";
1834
- td.style.minWidth = c.style.minWidth || "60px";
1835
- firstRow.replaceChild(td, c);
2501
+ replaceTableCellTag(c, "td");
1836
2502
  }
1837
2503
  }
1838
2504
  handleInput();
@@ -1841,35 +2507,30 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1841
2507
  const pos = getCellPosition(cell);
1842
2508
  if (!pos)
1843
2509
  return;
2510
+ clearSelectionDecor();
1844
2511
  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);
2512
+ const { grid } = getTableGrid(tbody);
2513
+ const seen = new Set();
2514
+ const columnCells = grid
2515
+ .map((row) => row?.[cIdx])
2516
+ .filter((candidate) => {
2517
+ if (!candidate || seen.has(candidate))
2518
+ return false;
2519
+ seen.add(candidate);
2520
+ return true;
2521
+ });
1849
2522
  const shouldMakeHeader = columnCells.some((c) => c.tagName !== "TH");
1850
2523
  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);
2524
+ replaceTableCellTag(c, shouldMakeHeader ? "th" : "td");
1857
2525
  }
1858
2526
  handleInput();
1859
2527
  };
1860
2528
  const applyBgToSelection = (hex, fallbackCell) => {
1861
- const sel = selectionRef.current;
2529
+ const sel = shouldUseTableSelection(fallbackCell) ? selectionRef.current : null;
1862
2530
  if (sel) {
1863
- const rows = Array.from(sel.tbody.querySelectorAll("tr"));
1864
- for (let r = sel.sr; r <= sel.er; r++) {
1865
- const row = rows[r];
1866
- const cells = cellsOfRow(row);
1867
- for (let c = sel.sc; c <= sel.ec; c++) {
1868
- const cell = cells[c];
1869
- if (cell)
1870
- cell.style.background = hex;
1871
- }
1872
- }
2531
+ getCellsInGridRect(sel.tbody, sel.sr, sel.sc, sel.er, sel.ec).forEach((cell) => {
2532
+ cell.style.background = hex;
2533
+ });
1873
2534
  }
1874
2535
  else if (fallbackCell) {
1875
2536
  fallbackCell.style.background = hex;
@@ -1879,25 +2540,21 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1879
2540
  const applyToggle = (cell) => {
1880
2541
  const cur = cell.style.border;
1881
2542
  cell.style.border =
1882
- cur && cur !== "none" ? "none" : "1px solid #000";
2543
+ cur && cur !== "none" ? "none" : "1px solid #d1d5db";
1883
2544
  };
1884
- const sel = selectionRef.current;
2545
+ const sel = shouldUseTableSelection(fallbackCell) ? selectionRef.current : null;
1885
2546
  if (sel) {
1886
- const rows = Array.from(sel.tbody.querySelectorAll("tr"));
1887
- for (let r = sel.sr; r <= sel.er; r++) {
1888
- const row = rows[r];
1889
- const cells = cellsOfRow(row);
1890
- for (let c = sel.sc; c <= sel.ec; c++) {
1891
- const cell = cells[c];
1892
- if (cell)
1893
- applyToggle(cell);
1894
- }
1895
- }
2547
+ getCellsInGridRect(sel.tbody, sel.sr, sel.sc, sel.er, sel.ec).forEach(applyToggle);
1896
2548
  }
1897
2549
  else if (fallbackCell) {
1898
2550
  applyToggle(fallbackCell);
1899
2551
  }
1900
2552
  };
2553
+ const runTableCellAction = (cell, action) => {
2554
+ action(cell);
2555
+ handleInput();
2556
+ setTableMenu(null);
2557
+ };
1901
2558
  // Table column and row resizing functions
1902
2559
  const getColumnCells = (table, colIndex) => {
1903
2560
  const tbody = table.querySelector('tbody');
@@ -2018,19 +2675,40 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2018
2675
  });
2019
2676
  });
2020
2677
  };
2678
+ const activeButtonStyle = (active = false, extra = {}) => ({
2679
+ height: 32,
2680
+ minWidth: 32,
2681
+ padding: "0 8px",
2682
+ border: active
2683
+ ? "2px solid var(--srte-accent)"
2684
+ : "1px solid var(--srte-input-border)",
2685
+ borderRadius: 6,
2686
+ background: active ? "var(--srte-accent-bg)" : "var(--srte-input-bg)",
2687
+ color: "var(--srte-input-text)",
2688
+ boxShadow: active ? "inset 0 0 0 1px var(--srte-accent)" : "none",
2689
+ ...extra,
2690
+ });
2691
+ const preserveToolbarMouseDown = (event) => {
2692
+ const target = event.target;
2693
+ const button = target?.closest("button");
2694
+ preserveEditorSelection();
2695
+ if (button && !button.hasAttribute("disabled")) {
2696
+ event.preventDefault();
2697
+ }
2698
+ };
2021
2699
  const editorClass = `srte-editor${theme === 'dark' ? ' srte-dark' : ''}${className ? ' ' + className : ''}`;
2022
2700
  return (_jsxs("div", { className: editorClass, style: {
2023
2701
  border: "1px solid var(--srte-border)",
2024
2702
  borderRadius: 6,
2025
2703
  width: "100%",
2026
2704
  maxWidth: "100vw",
2027
- overflow: "hidden",
2705
+ overflow: "visible",
2028
2706
  display: "flex",
2029
2707
  flexDirection: "column",
2030
2708
  background: "var(--srte-bg)",
2031
2709
  color: "var(--srte-text)",
2032
2710
  boxSizing: "border-box"
2033
- }, children: [_jsxs("div", { style: {
2711
+ }, children: [_jsxs("div", { onMouseDown: preserveToolbarMouseDown, style: {
2034
2712
  display: "flex",
2035
2713
  flexWrap: "wrap",
2036
2714
  maxWidth: "100%",
@@ -2079,7 +2757,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2079
2757
  } }), _jsx("input", { ref: mdInputRef, type: "file", accept: ".md,.markdown,text/markdown,text/plain", style: { display: "none" }, onChange: (e) => {
2080
2758
  importTextFile(e.currentTarget.files, "md");
2081
2759
  e.currentTarget.value = "";
2082
- } }), _jsxs("select", { defaultValue: "p", onChange: (e) => {
2760
+ } }), _jsxs("select", { defaultValue: "p", onMouseDown: preserveEditorSelection, onChange: (e) => {
2083
2761
  const val = e.target.value;
2084
2762
  if (val === "p")
2085
2763
  applyFormatBlock("<p>");
@@ -2096,42 +2774,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2096
2774
  borderRadius: 6,
2097
2775
  background: "var(--srte-input-bg)",
2098
2776
  color: "var(--srte-input-text)",
2099
- }, children: [_jsx("option", { value: "p", children: "Paragraph" }), _jsx("option", { value: "h1", children: "Heading 1" }), _jsx("option", { value: "h2", children: "Heading 2" }), _jsx("option", { value: "h3", children: "Heading 3" })] }), _jsx("button", { title: "Bold", onClick: () => exec("bold"), style: {
2100
- height: 32,
2101
- minWidth: 32,
2102
- padding: "0 8px",
2103
- border: "1px solid var(--srte-input-border)",
2104
- borderRadius: 6,
2105
- background: "var(--srte-input-bg)",
2106
- color: "var(--srte-input-text)",
2107
- }, children: _jsx("span", { style: { fontWeight: 700 }, children: "B" }) }), _jsx("button", { title: "Italic", onClick: () => exec("italic"), style: {
2108
- height: 32,
2109
- minWidth: 32,
2110
- padding: "0 8px",
2111
- border: "1px solid var(--srte-input-border)",
2112
- borderRadius: 6,
2113
- background: "var(--srte-input-bg)",
2114
- fontStyle: "italic",
2115
- color: "var(--srte-input-text)",
2116
- }, children: "I" }), _jsx("button", { title: "Underline", onClick: () => exec("underline"), style: {
2117
- height: 32,
2118
- minWidth: 32,
2119
- padding: "0 8px",
2120
- border: "1px solid var(--srte-input-border)",
2121
- borderRadius: 6,
2122
- background: "var(--srte-input-bg)",
2123
- textDecoration: "underline",
2124
- color: "var(--srte-input-text)",
2125
- }, children: "U" }), _jsx("button", { title: "Strikethrough", onClick: () => exec("strikeThrough"), style: {
2126
- height: 32,
2127
- minWidth: 32,
2128
- padding: "0 8px",
2129
- border: "1px solid var(--srte-input-border)",
2130
- borderRadius: 6,
2131
- background: "var(--srte-input-bg)",
2132
- textDecoration: "line-through",
2133
- color: "var(--srte-input-text)",
2134
- }, children: "S" }), _jsxs("select", { value: currentFontSize, onMouseDown: () => {
2777
+ }, children: [_jsx("option", { value: "p", children: "Paragraph" }), _jsx("option", { value: "h1", children: "Heading 1" }), _jsx("option", { value: "h2", children: "Heading 2" }), _jsx("option", { value: "h3", children: "Heading 3" })] }), _jsx("button", { title: "Bold", onClick: () => exec("bold"), "aria-pressed": activeState.bold, style: activeButtonStyle(activeState.bold), children: _jsx("span", { style: { fontWeight: 700 }, children: "B" }) }), _jsx("button", { title: "Italic", onClick: () => exec("italic"), "aria-pressed": activeState.italic, style: activeButtonStyle(activeState.italic, { fontStyle: "italic" }), children: "I" }), _jsx("button", { title: "Underline", onClick: () => exec("underline"), "aria-pressed": activeState.underline, style: activeButtonStyle(activeState.underline, { textDecoration: "underline" }), children: "U" }), _jsx("button", { title: "Strikethrough", onClick: () => exec("strikeThrough"), "aria-pressed": activeState.strikeThrough, style: activeButtonStyle(activeState.strikeThrough, { textDecoration: "line-through" }), children: "S" }), _jsxs("select", { value: currentFontSize, onMouseDown: () => {
2135
2778
  // Save selection before dropdown interaction
2136
2779
  const sel = window.getSelection();
2137
2780
  if (sel && sel.rangeCount > 0) {
@@ -2188,7 +2831,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2188
2831
  borderRadius: 6,
2189
2832
  background: "var(--srte-input-bg)",
2190
2833
  color: "var(--srte-input-text)",
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: {
2834
+ }, 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"), "aria-pressed": activeState.subscript, style: activeButtonStyle(activeState.subscript), children: ["X", _jsx("sub", { children: "2" })] }), _jsxs("button", { title: "Superscript", onClick: () => exec("superscript"), "aria-pressed": activeState.superscript, style: activeButtonStyle(activeState.superscript), children: ["X", _jsx("sup", { children: "2" })] }), _jsx("button", { title: "Bulleted list", onClick: () => exec("insertUnorderedList"), "aria-pressed": activeState.unorderedList, style: activeButtonStyle(activeState.unorderedList, { padding: "0 10px" }), children: "\u2022 List" }), _jsx("button", { title: "Numbered list", onClick: () => exec("insertOrderedList"), "aria-pressed": activeState.orderedList, style: activeButtonStyle(activeState.orderedList, { padding: "0 10px" }), children: "1. List" }), _jsx("button", { title: "Blockquote", onClick: toggleBlockquote, "aria-pressed": activeState.blockquote, style: activeButtonStyle(activeState.blockquote), children: "\u275D" }), _jsx("button", { title: "Special characters", onClick: () => setShowSpecialChars(true), style: {
2192
2835
  height: 32,
2193
2836
  minWidth: 32,
2194
2837
  padding: "0 8px",
@@ -2196,54 +2839,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2196
2839
  borderRadius: 6,
2197
2840
  background: "var(--srte-input-bg)",
2198
2841
  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: {
2208
- height: 32,
2209
- padding: "0 10px",
2210
- border: "1px solid var(--srte-input-border)",
2211
- borderRadius: 6,
2212
- background: "var(--srte-input-bg)",
2213
- color: "var(--srte-input-text)",
2214
- }, children: "\u2022 List" }), _jsx("button", { title: "Numbered list", onClick: () => exec("insertOrderedList"), style: {
2215
- height: 32,
2216
- padding: "0 10px",
2217
- border: "1px solid var(--srte-input-border)",
2218
- borderRadius: 6,
2219
- background: "var(--srte-input-bg)",
2220
- color: "var(--srte-input-text)",
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: {
2230
- height: 32,
2231
- minWidth: 32,
2232
- padding: "0 8px",
2233
- border: "1px solid var(--srte-input-border)",
2234
- borderRadius: 6,
2235
- background: "var(--srte-input-bg)",
2236
- color: "var(--srte-input-text)",
2237
- }, children: "\u03A9" }), _jsx("button", { title: "Code block", onClick: () => exec("formatBlock", "<pre>"), style: {
2238
- height: 32,
2842
+ }, children: "\u03A9" }), _jsx("button", { title: "Code block", onClick: () => exec("formatBlock", "<pre>"), "aria-pressed": activeState.codeBlock, style: activeButtonStyle(activeState.codeBlock, {
2239
2843
  minWidth: 36,
2240
- padding: "0 8px",
2241
- border: "1px solid var(--srte-input-border)",
2242
- borderRadius: 6,
2243
- background: "var(--srte-input-bg)",
2244
2844
  fontFamily: "ui-monospace, SFMono-Regular, Menlo",
2245
- color: "var(--srte-input-text)",
2246
- }, children: "< />" }), formula && (_jsx("button", { title: "Insert formula", onClick: () => setShowFormulaDialog(true), style: {
2845
+ }), children: "< />" }), formula && (_jsx("button", { title: "Insert formula", onClick: () => setShowFormulaDialog(true), style: {
2247
2846
  height: 32,
2248
2847
  minWidth: 32,
2249
2848
  padding: "0 8px",
@@ -2323,7 +2922,21 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2323
2922
  borderRadius: 6,
2324
2923
  background: "var(--srte-input-bg)",
2325
2924
  color: "var(--srte-input-text)",
2326
- }, children: "Export MD" }), _jsxs("div", { style: {
2925
+ }, children: "Export MD" }), _jsx("button", { title: "Export DOCX", onClick: exportDocx, style: {
2926
+ height: 32,
2927
+ padding: "0 10px",
2928
+ border: "1px solid var(--srte-input-border)",
2929
+ borderRadius: 6,
2930
+ background: "var(--srte-input-bg)",
2931
+ color: "var(--srte-input-text)",
2932
+ }, children: "Export DOCX" }), _jsx("button", { title: "Export PDF", onClick: exportPdf, style: {
2933
+ height: 32,
2934
+ padding: "0 10px",
2935
+ border: "1px solid var(--srte-input-border)",
2936
+ borderRadius: 6,
2937
+ background: "var(--srte-input-bg)",
2938
+ color: "var(--srte-input-text)",
2939
+ }, children: "Export PDF" }), _jsxs("div", { style: {
2327
2940
  display: "inline-flex",
2328
2941
  gap: 4,
2329
2942
  alignItems: "center",
@@ -2407,17 +3020,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2407
3020
  position: "fixed",
2408
3021
  inset: 0,
2409
3022
  background: "var(--srte-modal-backdrop)",
3023
+ backdropFilter: "var(--srte-modal-backdrop-filter)",
3024
+ WebkitBackdropFilter: "var(--srte-modal-backdrop-filter)",
2410
3025
  display: "flex",
2411
3026
  alignItems: "center",
2412
3027
  justifyContent: "center",
2413
- zIndex: 50,
3028
+ zIndex: 90,
2414
3029
  }, onClick: () => setShowTableDialog(false), children: _jsxs("div", { style: {
2415
3030
  background: "var(--srte-modal-bg)",
2416
3031
  color: "var(--srte-modal-text)",
2417
3032
  padding: 16,
2418
3033
  borderRadius: 8,
2419
3034
  minWidth: 280,
2420
- }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 8 }, children: "Insert table" }), _jsxs("div", { style: { display: "flex", gap: 12, alignItems: "center" }, children: [_jsx("div", { style: {
3035
+ }, onMouseDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 8 }, children: "Insert table" }), _jsxs("div", { style: { display: "flex", gap: 12, alignItems: "center" }, children: [_jsx("div", { style: {
2421
3036
  display: "grid",
2422
3037
  gridTemplateColumns: "repeat(10, 18px)",
2423
3038
  gap: 2,
@@ -2451,6 +3066,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2451
3066
  position: "fixed",
2452
3067
  inset: 0,
2453
3068
  background: "var(--srte-modal-backdrop)",
3069
+ backdropFilter: "var(--srte-modal-backdrop-filter)",
3070
+ WebkitBackdropFilter: "var(--srte-modal-backdrop-filter)",
2454
3071
  display: "flex",
2455
3072
  alignItems: "center",
2456
3073
  justifyContent: "center",
@@ -2505,6 +3122,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2505
3122
  position: "fixed",
2506
3123
  inset: 0,
2507
3124
  background: "var(--srte-modal-backdrop)",
3125
+ backdropFilter: "var(--srte-modal-backdrop-filter)",
3126
+ WebkitBackdropFilter: "var(--srte-modal-backdrop-filter)",
2508
3127
  display: "flex",
2509
3128
  alignItems: "center",
2510
3129
  justifyContent: "center",
@@ -2571,6 +3190,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2571
3190
  position: "fixed",
2572
3191
  inset: 0,
2573
3192
  background: "var(--srte-modal-backdrop)",
3193
+ backdropFilter: "var(--srte-modal-backdrop-filter)",
3194
+ WebkitBackdropFilter: "var(--srte-modal-backdrop-filter)",
2574
3195
  display: "flex",
2575
3196
  alignItems: "center",
2576
3197
  justifyContent: "center",
@@ -2615,6 +3236,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2615
3236
  position: "fixed",
2616
3237
  inset: 0,
2617
3238
  background: "var(--srte-modal-backdrop)",
3239
+ backdropFilter: "var(--srte-modal-backdrop-filter)",
3240
+ WebkitBackdropFilter: "var(--srte-modal-backdrop-filter)",
2618
3241
  display: "flex",
2619
3242
  alignItems: "center",
2620
3243
  justifyContent: "center",
@@ -2712,383 +3335,389 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2712
3335
  borderRadius: 4,
2713
3336
  background: "var(--srte-input-bg)",
2714
3337
  color: "var(--srte-modal-text)",
2715
- }, title: sym, children: sym }, i)))] })] }) })), _jsx("div", { style: {
3338
+ }, title: sym, children: sym }, i)))] })] }) })), _jsxs("div", { ref: editorScrollRef, style: {
2716
3339
  width: "100%",
2717
3340
  maxWidth: "100%",
2718
3341
  flex: "1 1 auto",
3342
+ minWidth: 0,
2719
3343
  minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
2720
3344
  maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
2721
3345
  overflowY: "auto",
2722
- overflowX: "hidden",
3346
+ overflowX: "auto",
3347
+ overscrollBehavior: "contain",
2723
3348
  boxSizing: "border-box",
2724
3349
  position: "relative",
2725
- }, children: _jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
2726
- isComposingRef.current = false;
2727
- handleInput();
2728
- }, onPaste: (e) => {
2729
- const items = e.clipboardData?.files;
2730
- if (media && items && items.length) {
2731
- const hasImage = Array.from(items).some((f) => f.type.startsWith("image/"));
2732
- if (hasImage) {
3350
+ scrollPaddingBottom: 24,
3351
+ }, children: [_jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onKeyUp: updateActiveState, onMouseUp: updateActiveState, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
3352
+ isComposingRef.current = false;
3353
+ handleInput();
3354
+ }, onPaste: (e) => {
3355
+ const items = e.clipboardData?.files;
3356
+ if (media && items && items.length) {
3357
+ const hasImage = Array.from(items).some((f) => f.type.startsWith("image/"));
3358
+ if (hasImage) {
3359
+ e.preventDefault();
3360
+ handleLocalImageFiles(items);
3361
+ return;
3362
+ }
3363
+ }
3364
+ const html = e.clipboardData?.getData("text/html");
3365
+ if (html) {
2733
3366
  e.preventDefault();
2734
- handleLocalImageFiles(items);
2735
- return;
3367
+ insertCleanHtml(cleanPastedHtml(html));
2736
3368
  }
2737
- }
2738
- const html = e.clipboardData?.getData("text/html");
2739
- if (html) {
2740
- e.preventDefault();
2741
- insertCleanHtml(cleanPastedHtml(html));
2742
- }
2743
- }, onDragOver: (e) => {
2744
- // Allow dragging images within editor and file drops
2745
- if (draggedImageRef.current ||
2746
- e.dataTransfer?.types?.includes("Files")) {
2747
- e.preventDefault();
2748
- }
2749
- }, onDrop: (e) => {
2750
- // Move existing dragged image inside editor
2751
- if (draggedImageRef.current) {
2752
- e.preventDefault();
2753
- const x = e.clientX;
2754
- const y = e.clientY;
2755
- let range = null;
2756
- // @ts-ignore
2757
- if (document.caretRangeFromPoint) {
2758
- // @ts-ignore
2759
- range = document.caretRangeFromPoint(x, y);
3369
+ }, onDragOver: (e) => {
3370
+ // Allow dragging images within editor and file drops
3371
+ if (draggedImageRef.current ||
3372
+ e.dataTransfer?.types?.includes("Files")) {
3373
+ e.preventDefault();
2760
3374
  }
2761
- else if (document.caretPositionFromPoint) {
2762
- const pos = document.caretPositionFromPoint(x, y);
2763
- if (pos) {
2764
- range = document.createRange();
2765
- range.setStart(pos.offsetNode, pos.offset);
3375
+ }, onDrop: (e) => {
3376
+ // Move existing dragged image inside editor
3377
+ if (draggedImageRef.current) {
3378
+ e.preventDefault();
3379
+ const x = e.clientX;
3380
+ const y = e.clientY;
3381
+ let range = null;
3382
+ // @ts-ignore
3383
+ if (document.caretRangeFromPoint) {
3384
+ // @ts-ignore
3385
+ range = document.caretRangeFromPoint(x, y);
2766
3386
  }
2767
- }
2768
- const img = draggedImageRef.current;
2769
- draggedImageRef.current = null;
2770
- if (range &&
2771
- img &&
2772
- editableRef.current?.contains(range.commonAncestorContainer)) {
2773
- // Avoid inserting inside the image itself
2774
- if (range.startContainer === img || range.endContainer === img)
2775
- return;
2776
- // If dropping inside a link, insert right after the link element
2777
- let container = range.commonAncestorContainer;
2778
- let linkAncestor = null;
2779
- let el = container;
2780
- while (el && el !== editableRef.current) {
2781
- if (el.tagName === "A") {
2782
- linkAncestor = el;
2783
- break;
3387
+ else if (document.caretPositionFromPoint) {
3388
+ const pos = document.caretPositionFromPoint(x, y);
3389
+ if (pos) {
3390
+ range = document.createRange();
3391
+ range.setStart(pos.offsetNode, pos.offset);
2784
3392
  }
2785
- el = el.parentElement;
2786
- }
2787
- if (linkAncestor) {
2788
- linkAncestor.parentElement?.insertBefore(img, linkAncestor.nextSibling);
2789
3393
  }
2790
- else {
2791
- range.insertNode(img);
3394
+ const img = draggedImageRef.current;
3395
+ draggedImageRef.current = null;
3396
+ if (range &&
3397
+ img &&
3398
+ editableRef.current?.contains(range.commonAncestorContainer)) {
3399
+ // Avoid inserting inside the image itself
3400
+ if (range.startContainer === img || range.endContainer === img)
3401
+ return;
3402
+ // If dropping inside a link, insert right after the link element
3403
+ let container = range.commonAncestorContainer;
3404
+ let linkAncestor = null;
3405
+ let el = container;
3406
+ while (el && el !== editableRef.current) {
3407
+ if (el.tagName === "A") {
3408
+ linkAncestor = el;
3409
+ break;
3410
+ }
3411
+ el = el.parentElement;
3412
+ }
3413
+ if (linkAncestor) {
3414
+ linkAncestor.parentElement?.insertBefore(img, linkAncestor.nextSibling);
3415
+ }
3416
+ else {
3417
+ range.insertNode(img);
3418
+ }
3419
+ const r = document.createRange();
3420
+ r.setStartAfter(img);
3421
+ r.collapse(true);
3422
+ safeSelectRange(r);
3423
+ setSelectedImage(img);
3424
+ scheduleImageOverlay();
3425
+ handleInput();
2792
3426
  }
2793
- const r = document.createRange();
2794
- r.setStartAfter(img);
2795
- r.collapse(true);
2796
- safeSelectRange(r);
2797
- setSelectedImage(img);
2798
- scheduleImageOverlay();
2799
- handleInput();
3427
+ return;
2800
3428
  }
2801
- return;
2802
- }
2803
- if (media && e.dataTransfer?.files?.length) {
2804
- e.preventDefault();
2805
- // Try to move caret to drop point
2806
- const x = e.clientX;
2807
- const y = e.clientY;
2808
- let range = null;
2809
- // @ts-ignore
2810
- if (document.caretRangeFromPoint) {
3429
+ if (media && e.dataTransfer?.files?.length) {
3430
+ e.preventDefault();
3431
+ // Try to move caret to drop point
3432
+ const x = e.clientX;
3433
+ const y = e.clientY;
3434
+ let range = null;
2811
3435
  // @ts-ignore
2812
- range = document.caretRangeFromPoint(x, y);
2813
- }
2814
- else if (document.caretPositionFromPoint) {
2815
- const pos = document.caretPositionFromPoint(x, y);
2816
- if (pos) {
2817
- range = document.createRange();
2818
- range.setStart(pos.offsetNode, pos.offset);
3436
+ if (document.caretRangeFromPoint) {
3437
+ // @ts-ignore
3438
+ range = document.caretRangeFromPoint(x, y);
3439
+ }
3440
+ else if (document.caretPositionFromPoint) {
3441
+ const pos = document.caretPositionFromPoint(x, y);
3442
+ if (pos) {
3443
+ range = document.createRange();
3444
+ range.setStart(pos.offsetNode, pos.offset);
3445
+ }
3446
+ }
3447
+ if (range) {
3448
+ const sel = window.getSelection();
3449
+ sel?.removeAllRanges();
3450
+ sel?.addRange(range);
2819
3451
  }
3452
+ handleLocalImageFiles(e.dataTransfer.files);
2820
3453
  }
2821
- if (range) {
2822
- const sel = window.getSelection();
2823
- sel?.removeAllRanges();
2824
- sel?.addRange(range);
3454
+ }, onClick: (e) => {
3455
+ const t = e.target;
3456
+ if (t && t.tagName === "IMG") {
3457
+ setSelectedImage(t);
3458
+ scheduleImageOverlay();
2825
3459
  }
2826
- handleLocalImageFiles(e.dataTransfer.files);
2827
- }
2828
- }, onClick: (e) => {
2829
- const t = e.target;
2830
- if (t && t.tagName === "IMG") {
2831
- setSelectedImage(t);
2832
- scheduleImageOverlay();
2833
- }
2834
- else {
2835
- setSelectedImage(null);
2836
- setImageOverlay(null);
2837
- }
2838
- }, onDragStart: (e) => {
2839
- const t = e.target;
2840
- if (t && t.tagName === "IMG") {
2841
- draggedImageRef.current = t;
2842
- try {
2843
- e.dataTransfer?.setData("text/plain", "moving-image");
2844
- e.dataTransfer.effectAllowed = "move";
2845
- // Provide a subtle drag image
2846
- const dt = e.dataTransfer;
2847
- if (dt && typeof dt.setDragImage === "function") {
2848
- const ghost = new Image();
2849
- ghost.src = t.src;
2850
- ghost.width = Math.min(120, t.width);
2851
- ghost.height = Math.min(120, t.height);
2852
- dt.setDragImage(ghost, 10, 10);
3460
+ else {
3461
+ setSelectedImage(null);
3462
+ setImageOverlay(null);
3463
+ }
3464
+ }, onDragStart: (e) => {
3465
+ const t = e.target;
3466
+ if (t && t.tagName === "IMG") {
3467
+ draggedImageRef.current = t;
3468
+ try {
3469
+ e.dataTransfer?.setData("text/plain", "moving-image");
3470
+ e.dataTransfer.effectAllowed = "move";
3471
+ // Provide a subtle drag image
3472
+ const dt = e.dataTransfer;
3473
+ if (dt && typeof dt.setDragImage === "function") {
3474
+ const ghost = new Image();
3475
+ ghost.src = t.src;
3476
+ ghost.width = Math.min(120, t.width);
3477
+ ghost.height = Math.min(120, t.height);
3478
+ dt.setDragImage(ghost, 10, 10);
3479
+ }
2853
3480
  }
3481
+ catch { }
2854
3482
  }
2855
- catch { }
2856
- }
2857
- else {
3483
+ else {
3484
+ draggedImageRef.current = null;
3485
+ }
3486
+ }, onDragEnd: () => {
2858
3487
  draggedImageRef.current = null;
2859
- }
2860
- }, onDragEnd: () => {
2861
- draggedImageRef.current = null;
2862
- }, style: {
2863
- minHeight: "100%",
2864
- maxWidth: "100%",
2865
- overflowX: "hidden",
2866
- padding: "16px",
2867
- outline: "none",
2868
- lineHeight: 1.6,
2869
- boxSizing: "border-box",
2870
- fontFamily: defaultFont || "inherit",
2871
- }, "data-placeholder": placeholder, onFocus: (e) => {
2872
- // Ensure the editor has at least one paragraph to type into
2873
- const el = e.currentTarget;
2874
- if (!el.innerHTML || el.innerHTML === "<br>") {
2875
- el.innerHTML = "<p><br></p>";
2876
- }
2877
- }, onKeyDown: (e) => {
2878
- if (formula &&
2879
- (e.metaKey || e.ctrlKey) &&
2880
- String(e.key).toLowerCase() === "m") {
2881
- e.preventDefault();
2882
- setShowFormulaDialog(true);
2883
- return;
2884
- }
2885
- // Keep Tab for indentation in lists; otherwise insert 2 spaces
2886
- if (e.key === "Tab") {
2887
- e.preventDefault();
2888
- if (document.queryCommandState("insertUnorderedList") ||
2889
- document.queryCommandState("insertOrderedList")) {
2890
- exec(e.shiftKey ? "outdent" : "indent");
3488
+ }, style: {
3489
+ minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
3490
+ maxWidth: "100%",
3491
+ overflowX: "visible",
3492
+ padding: "16px",
3493
+ paddingBottom: "32px",
3494
+ outline: "none",
3495
+ lineHeight: 1.6,
3496
+ boxSizing: "border-box",
3497
+ fontFamily: defaultFont || "inherit",
3498
+ }, "data-placeholder": placeholder, onFocus: (e) => {
3499
+ // Ensure the editor has at least one paragraph to type into
3500
+ const el = e.currentTarget;
3501
+ if (!el.innerHTML || el.innerHTML === "<br>") {
3502
+ el.innerHTML = "<p><br></p>";
2891
3503
  }
2892
- else {
2893
- document.execCommand("insertText", false, " ");
3504
+ updateActiveState();
3505
+ }, onKeyDown: (e) => {
3506
+ if (formula &&
3507
+ (e.metaKey || e.ctrlKey) &&
3508
+ String(e.key).toLowerCase() === "m") {
3509
+ e.preventDefault();
3510
+ setShowFormulaDialog(true);
3511
+ return;
2894
3512
  }
2895
- }
2896
- // Table navigation with arrows inside cells
2897
- if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
2898
- const sel = window.getSelection();
2899
- const cell = getClosestCell(sel?.anchorNode || null);
2900
- if (table &&
2901
- cell &&
2902
- cell.parentElement &&
2903
- cell.parentElement.parentElement) {
2904
- const row = cell.parentElement;
2905
- const tbody = row.parentElement;
2906
- const cells = Array.from(row.children).filter((c) => c.tagName === "TD" ||
2907
- c.tagName === "TH");
2908
- const rows = Array.from(tbody.children);
2909
- const rIdx = rows.indexOf(row);
2910
- const cIdx = cells.indexOf(cell);
2911
- const atStart = (sel?.anchorOffset || 0) === 0;
2912
- const cellTextLen = (cell.textContent || "").length;
2913
- const atEnd = (sel?.anchorOffset || 0) >= cellTextLen;
2914
- let target = null;
2915
- if (e.key === "ArrowLeft" && atStart && cIdx > 0) {
2916
- target = row.children[cIdx - 1];
2917
- }
2918
- else if (e.key === "ArrowRight" &&
2919
- atEnd &&
2920
- cIdx < row.children.length - 1) {
2921
- target = row.children[cIdx + 1];
2922
- }
2923
- else if (e.key === "ArrowUp" && rIdx > 0 && atStart) {
2924
- target = rows[rIdx - 1].children[cIdx];
3513
+ // Keep Tab for indentation in lists; otherwise insert 2 spaces
3514
+ if (e.key === "Tab") {
3515
+ e.preventDefault();
3516
+ if (document.queryCommandState("insertUnorderedList") ||
3517
+ document.queryCommandState("insertOrderedList")) {
3518
+ exec(e.shiftKey ? "outdent" : "indent");
2925
3519
  }
2926
- else if (e.key === "ArrowDown" &&
2927
- rIdx < rows.length - 1 &&
2928
- atEnd) {
2929
- target = rows[rIdx + 1].children[cIdx];
3520
+ else {
3521
+ document.execCommand("insertText", false, " ");
2930
3522
  }
2931
- if (target) {
2932
- e.preventDefault();
2933
- moveCaretToCell(target, e.key === "ArrowRight" || e.key === "ArrowDown");
3523
+ }
3524
+ // Table navigation with arrows inside cells
3525
+ if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
3526
+ const sel = window.getSelection();
3527
+ const cell = getClosestCell(sel?.anchorNode || null);
3528
+ if (table &&
3529
+ cell &&
3530
+ cell.parentElement &&
3531
+ cell.parentElement.parentElement) {
3532
+ const row = cell.parentElement;
3533
+ const tbody = row.parentElement;
3534
+ const cells = Array.from(row.children).filter((c) => c.tagName === "TD" ||
3535
+ c.tagName === "TH");
3536
+ const rows = Array.from(tbody.children);
3537
+ const rIdx = rows.indexOf(row);
3538
+ const cIdx = cells.indexOf(cell);
3539
+ const atStart = (sel?.anchorOffset || 0) === 0;
3540
+ const cellTextLen = (cell.textContent || "").length;
3541
+ const atEnd = (sel?.anchorOffset || 0) >= cellTextLen;
3542
+ let target = null;
3543
+ if (e.key === "ArrowLeft" && atStart && cIdx > 0) {
3544
+ target = row.children[cIdx - 1];
3545
+ }
3546
+ else if (e.key === "ArrowRight" &&
3547
+ atEnd &&
3548
+ cIdx < row.children.length - 1) {
3549
+ target = row.children[cIdx + 1];
3550
+ }
3551
+ else if (e.key === "ArrowUp" && rIdx > 0 && atStart) {
3552
+ target = rows[rIdx - 1].children[cIdx];
3553
+ }
3554
+ else if (e.key === "ArrowDown" &&
3555
+ rIdx < rows.length - 1 &&
3556
+ atEnd) {
3557
+ target = rows[rIdx + 1].children[cIdx];
3558
+ }
3559
+ if (target) {
3560
+ e.preventDefault();
3561
+ moveCaretToCell(target, e.key === "ArrowRight" || e.key === "ArrowDown");
3562
+ }
2934
3563
  }
2935
3564
  }
2936
- }
2937
- }, onMouseDown: (e) => {
2938
- const cell = getClosestCell(e.target);
2939
- if (!cell) {
2940
- clearSelectionDecor();
2941
- return;
2942
- }
2943
- const pos = getCellPosition(cell);
2944
- if (!pos)
2945
- return;
2946
- selectingRef.current = { tbody: pos.tbody, start: cell };
2947
- const onMove = (ev) => {
2948
- const under = document.elementFromPoint(ev.clientX, ev.clientY);
2949
- const overCell = getClosestCell(under);
2950
- const startInfo = selectingRef.current;
2951
- if (!overCell || !startInfo)
2952
- return;
2953
- const a = getCellPosition(startInfo.start);
2954
- const b = getCellPosition(overCell);
2955
- if (!a || !b || a.tbody !== b.tbody)
3565
+ }, onMouseDown: (e) => {
3566
+ const cell = getClosestCell(e.target);
3567
+ if (!cell) {
3568
+ clearSelectionDecor();
2956
3569
  return;
2957
- const sr = Math.min(a.rIdx, b.rIdx);
2958
- const sc = Math.min(a.cIdx, b.cIdx);
2959
- const er = Math.max(a.rIdx, b.rIdx);
2960
- const ec = Math.max(a.cIdx, b.cIdx);
2961
- updateSelectionDecor(a.tbody, sr, sc, er, ec);
2962
- };
2963
- const onUp = () => {
2964
- window.removeEventListener("mousemove", onMove);
2965
- window.removeEventListener("mouseup", onUp);
2966
- selectingRef.current = null;
2967
- };
2968
- window.addEventListener("mousemove", onMove);
2969
- window.addEventListener("mouseup", onUp);
2970
- }, onContextMenu: (e) => {
2971
- const target = e.target;
2972
- if (target && target.tagName === "IMG") {
2973
- e.preventDefault();
2974
- const vw = window.innerWidth;
2975
- const vh = window.innerHeight;
2976
- const menuW = 220;
2977
- const menuH = 200;
2978
- const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
2979
- const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
2980
- setImageMenu({ x, y, img: target });
2981
- setTableMenu(null);
2982
- return;
2983
- }
2984
- const cell = getClosestCell(e.target);
2985
- if (cell) {
2986
- e.preventDefault();
2987
- const vw = window.innerWidth;
2988
- const vh = window.innerHeight;
2989
- const menuW = 220;
2990
- const menuH = 300;
2991
- const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
2992
- const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
2993
- setTableMenu({ x, y, cell });
2994
- }
2995
- else {
2996
- setTableMenu(null);
2997
- setImageMenu(null);
2998
- }
2999
- } }) }), selectedImage && imageOverlay && (_jsxs("div", { style: {
3000
- position: "fixed",
3001
- left: imageOverlay.left,
3002
- top: imageOverlay.top,
3003
- width: imageOverlay.width,
3004
- height: imageOverlay.height,
3005
- pointerEvents: "none",
3006
- zIndex: 49,
3007
- }, children: [_jsx("div", { style: {
3008
- position: "absolute",
3009
- inset: 0,
3010
- outline: "2px solid var(--srte-accent)",
3011
- outlineOffset: -2,
3012
- } }), _jsx("div", { title: "Resize", onMouseDown: (e) => {
3013
- e.preventDefault();
3014
- if (!selectedImage)
3570
+ }
3571
+ const pos = getCellPosition(cell);
3572
+ if (!pos)
3015
3573
  return;
3016
- resizingRef.current = {
3017
- side: "left",
3018
- startX: e.clientX,
3019
- startWidth: selectedImage.getBoundingClientRect().width,
3020
- };
3574
+ selectingRef.current = { tbody: pos.tbody, start: cell };
3021
3575
  const onMove = (ev) => {
3022
- const info = resizingRef.current;
3023
- if (!info || !selectedImage)
3576
+ const under = document.elementFromPoint(ev.clientX, ev.clientY);
3577
+ const overCell = getClosestCell(under);
3578
+ const startInfo = selectingRef.current;
3579
+ if (!overCell || !startInfo)
3024
3580
  return;
3025
- const delta = info.startX - ev.clientX;
3026
- const next = Math.max(80, Math.round(info.startWidth + delta));
3027
- selectedImage.style.width = next + "px";
3028
- selectedImage.style.height = "auto";
3029
- scheduleImageOverlay();
3030
- };
3031
- const onUp = () => {
3032
- window.removeEventListener("mousemove", onMove);
3033
- window.removeEventListener("mouseup", onUp);
3034
- resizingRef.current = null;
3035
- handleInput();
3036
- };
3037
- window.addEventListener("mousemove", onMove);
3038
- window.addEventListener("mouseup", onUp);
3039
- }, style: {
3040
- position: "absolute",
3041
- left: -6,
3042
- top: "50%",
3043
- transform: "translateY(-50%)",
3044
- width: 8,
3045
- height: 24,
3046
- background: "var(--srte-accent)",
3047
- borderRadius: 2,
3048
- cursor: "ew-resize",
3049
- pointerEvents: "auto",
3050
- } }), _jsx("div", { title: "Resize", onMouseDown: (e) => {
3051
- e.preventDefault();
3052
- if (!selectedImage)
3053
- return;
3054
- resizingRef.current = {
3055
- side: "right",
3056
- startX: e.clientX,
3057
- startWidth: selectedImage.getBoundingClientRect().width,
3058
- };
3059
- const onMove = (ev) => {
3060
- const info = resizingRef.current;
3061
- if (!info || !selectedImage)
3581
+ const a = getCellPosition(startInfo.start);
3582
+ const b = getCellPosition(overCell);
3583
+ if (!a || !b || a.tbody !== b.tbody)
3062
3584
  return;
3063
- const delta = ev.clientX - info.startX;
3064
- const next = Math.max(80, Math.round(info.startWidth + delta));
3065
- selectedImage.style.width = next + "px";
3066
- selectedImage.style.height = "auto";
3067
- scheduleImageOverlay();
3585
+ const sr = Math.min(a.rIdx, b.rIdx);
3586
+ const sc = Math.min(a.cIdx, b.cIdx);
3587
+ const er = Math.max(a.rIdx, b.rIdx);
3588
+ const ec = Math.max(a.cIdx, b.cIdx);
3589
+ updateSelectionDecor(a.tbody, sr, sc, er, ec);
3068
3590
  };
3069
3591
  const onUp = () => {
3070
3592
  window.removeEventListener("mousemove", onMove);
3071
3593
  window.removeEventListener("mouseup", onUp);
3072
- resizingRef.current = null;
3073
- handleInput();
3594
+ selectingRef.current = null;
3074
3595
  };
3075
3596
  window.addEventListener("mousemove", onMove);
3076
3597
  window.addEventListener("mouseup", onUp);
3077
- }, style: {
3598
+ }, onContextMenu: (e) => {
3599
+ const target = e.target;
3600
+ if (target && target.tagName === "IMG") {
3601
+ e.preventDefault();
3602
+ const vw = window.innerWidth;
3603
+ const vh = window.innerHeight;
3604
+ const menuW = 220;
3605
+ const menuH = 200;
3606
+ const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
3607
+ const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
3608
+ setImageMenu({ x, y, img: target });
3609
+ setTableMenu(null);
3610
+ return;
3611
+ }
3612
+ const cell = getClosestCell(e.target);
3613
+ if (cell) {
3614
+ e.preventDefault();
3615
+ const vw = window.innerWidth;
3616
+ const vh = window.innerHeight;
3617
+ const menuW = 220;
3618
+ const menuH = 300;
3619
+ const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
3620
+ const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
3621
+ setTableMenu({ x, y, cell });
3622
+ }
3623
+ else {
3624
+ setTableMenu(null);
3625
+ setImageMenu(null);
3626
+ }
3627
+ } }), selectedImage && imageOverlay && (_jsxs("div", { style: {
3078
3628
  position: "absolute",
3079
- right: -6,
3080
- top: "50%",
3081
- transform: "translateY(-50%)",
3082
- width: 8,
3083
- height: 24,
3084
- background: "var(--srte-accent)",
3085
- borderRadius: 2,
3086
- cursor: "ew-resize",
3087
- pointerEvents: "auto",
3088
- } })] })), tableMenu && (_jsx("div", { style: {
3629
+ left: imageOverlay.left,
3630
+ top: imageOverlay.top,
3631
+ width: imageOverlay.width,
3632
+ height: imageOverlay.height,
3633
+ pointerEvents: "none",
3634
+ zIndex: 5,
3635
+ }, children: [_jsx("div", { style: {
3636
+ position: "absolute",
3637
+ inset: 0,
3638
+ outline: "2px solid var(--srte-accent)",
3639
+ outlineOffset: -2,
3640
+ } }), _jsx("div", { title: "Resize", onMouseDown: (e) => {
3641
+ e.preventDefault();
3642
+ if (!selectedImage)
3643
+ return;
3644
+ resizingRef.current = {
3645
+ side: "left",
3646
+ startX: e.clientX,
3647
+ startWidth: selectedImage.getBoundingClientRect().width,
3648
+ };
3649
+ const onMove = (ev) => {
3650
+ const info = resizingRef.current;
3651
+ if (!info || !selectedImage)
3652
+ return;
3653
+ const delta = info.startX - ev.clientX;
3654
+ const next = Math.max(80, Math.round(info.startWidth + delta));
3655
+ selectedImage.style.width = next + "px";
3656
+ selectedImage.style.height = "auto";
3657
+ scheduleImageOverlay();
3658
+ };
3659
+ const onUp = () => {
3660
+ window.removeEventListener("mousemove", onMove);
3661
+ window.removeEventListener("mouseup", onUp);
3662
+ resizingRef.current = null;
3663
+ handleInput();
3664
+ };
3665
+ window.addEventListener("mousemove", onMove);
3666
+ window.addEventListener("mouseup", onUp);
3667
+ }, style: {
3668
+ position: "absolute",
3669
+ left: -4,
3670
+ top: "50%",
3671
+ transform: "translateY(-50%)",
3672
+ width: 8,
3673
+ height: 24,
3674
+ background: "var(--srte-accent)",
3675
+ borderRadius: 2,
3676
+ cursor: "ew-resize",
3677
+ pointerEvents: "auto",
3678
+ } }), _jsx("div", { title: "Resize", onMouseDown: (e) => {
3679
+ e.preventDefault();
3680
+ if (!selectedImage)
3681
+ return;
3682
+ resizingRef.current = {
3683
+ side: "right",
3684
+ startX: e.clientX,
3685
+ startWidth: selectedImage.getBoundingClientRect().width,
3686
+ };
3687
+ const onMove = (ev) => {
3688
+ const info = resizingRef.current;
3689
+ if (!info || !selectedImage)
3690
+ return;
3691
+ const delta = ev.clientX - info.startX;
3692
+ const next = Math.max(80, Math.round(info.startWidth + delta));
3693
+ selectedImage.style.width = next + "px";
3694
+ selectedImage.style.height = "auto";
3695
+ scheduleImageOverlay();
3696
+ };
3697
+ const onUp = () => {
3698
+ window.removeEventListener("mousemove", onMove);
3699
+ window.removeEventListener("mouseup", onUp);
3700
+ resizingRef.current = null;
3701
+ handleInput();
3702
+ };
3703
+ window.addEventListener("mousemove", onMove);
3704
+ window.addEventListener("mouseup", onUp);
3705
+ }, style: {
3706
+ position: "absolute",
3707
+ right: -4,
3708
+ top: "50%",
3709
+ transform: "translateY(-50%)",
3710
+ width: 8,
3711
+ height: 24,
3712
+ background: "var(--srte-accent)",
3713
+ borderRadius: 2,
3714
+ cursor: "ew-resize",
3715
+ pointerEvents: "auto",
3716
+ } })] }))] }), tableMenu && (_jsx("div", { style: {
3089
3717
  position: "fixed",
3090
3718
  inset: 0,
3091
3719
  zIndex: 60,
3720
+ background: "transparent",
3092
3721
  }, onClick: () => setTableMenu(null), onContextMenu: (e) => {
3093
3722
  // Prevent native menu while overlay is shown and reposition our menu
3094
3723
  e.preventDefault();
@@ -3120,56 +3749,53 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3120
3749
  maxHeight: 260,
3121
3750
  overflowY: "auto",
3122
3751
  color: "var(--srte-menu-text)",
3123
- }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, fontSize: 11, margin: "2px 6px 6px" }, children: "Table" }), _jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsxs("button", { style: {
3124
- display: "flex",
3125
- alignItems: "center",
3126
- gap: 8,
3127
- padding: "6px 8px",
3128
- fontSize: 12,
3129
- }, onClick: () => setShowTableDialog(true), children: [_jsx("span", { children: "\u2795" }), _jsx("span", { children: "Insert table\u2026" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("div", { style: {
3752
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, fontSize: 11, margin: "2px 6px 6px" }, children: "Table" }), _jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsxs("div", { style: {
3130
3753
  display: "flex",
3131
3754
  gap: 8,
3132
3755
  alignItems: "center",
3133
3756
  padding: "4px 6px",
3134
3757
  fontSize: 12,
3135
3758
  }, children: [_jsx("span", { children: "Fill:" }), _jsx("input", { type: "color", defaultValue: "#ffffff", onChange: (e) => {
3136
- applyBgToSelection(e.target.value, tableMenu.cell);
3137
- setTableMenu(null);
3759
+ runTableCellAction(tableMenu.cell, (cell) => applyBgToSelection(e.target.value, cell));
3138
3760
  }, style: {
3139
3761
  width: 28,
3140
3762
  height: 18,
3141
3763
  padding: 0,
3142
3764
  border: "none",
3143
3765
  background: "transparent",
3144
- } })] }), _jsxs("button", { style: {
3766
+ } })] }), _jsxs("button", { title: "Show or hide border for this cell, or for the selected cell range.", style: {
3145
3767
  display: "flex",
3146
3768
  alignItems: "center",
3147
3769
  gap: 8,
3148
3770
  padding: "6px 8px",
3149
3771
  fontSize: 12,
3150
3772
  }, onClick: () => {
3151
- toggleBorderSelection(tableMenu.cell);
3152
- setTableMenu(null);
3153
- }, children: [_jsx("span", { children: "\u25A6" }), _jsx("span", { children: "Toggle border" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("button", { disabled: !canMergeSelection(), style: {
3773
+ runTableCellAction(tableMenu.cell, toggleBorderSelection);
3774
+ }, children: [_jsx("span", { children: "\u25A6" }), _jsx("span", { children: "Show/hide cell border" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("button", { disabled: !canMergeFromCell(tableMenu.cell), style: {
3154
3775
  display: "flex",
3155
3776
  alignItems: "center",
3156
3777
  gap: 8,
3157
3778
  padding: "6px 8px",
3158
3779
  fontSize: 12,
3159
- opacity: canMergeSelection() ? 1 : 0.5,
3160
- cursor: canMergeSelection() ? "pointer" : "default",
3780
+ opacity: canMergeFromCell(tableMenu.cell) ? 1 : 0.45,
3781
+ cursor: canMergeFromCell(tableMenu.cell) ? "pointer" : "not-allowed",
3161
3782
  }, onClick: () => {
3783
+ if (!canMergeFromCell(tableMenu.cell))
3784
+ return;
3162
3785
  mergeSelection();
3163
3786
  setTableMenu(null);
3164
- }, children: [_jsx("span", { children: "\u21C4" }), _jsx("span", { children: "Merge cells" })] }), _jsxs("button", { style: {
3787
+ }, children: [_jsx("span", { children: "\u21C4" }), _jsx("span", { children: "Merge cells" })] }), _jsxs("button", { disabled: !canSplitCell(tableMenu.cell), title: canSplitCell(tableMenu.cell) ? "Split this merged cell back into individual cells." : "Only merged cells can be split.", style: {
3165
3788
  display: "flex",
3166
3789
  alignItems: "center",
3167
3790
  gap: 8,
3168
3791
  padding: "6px 8px",
3169
3792
  fontSize: 12,
3793
+ opacity: canSplitCell(tableMenu.cell) ? 1 : 0.45,
3794
+ cursor: canSplitCell(tableMenu.cell) ? "pointer" : "not-allowed",
3170
3795
  }, onClick: () => {
3171
- splitCell(tableMenu.cell);
3172
- setTableMenu(null);
3796
+ if (!canSplitCell(tableMenu.cell))
3797
+ return;
3798
+ runTableCellAction(tableMenu.cell, splitCell);
3173
3799
  }, children: [_jsx("span", { children: "\u2922" }), _jsx("span", { children: "Split cell" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("button", { style: {
3174
3800
  display: "flex",
3175
3801
  alignItems: "center",
@@ -3177,8 +3803,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3177
3803
  padding: "6px 8px",
3178
3804
  fontSize: 12,
3179
3805
  }, onClick: () => {
3180
- addRow(tableMenu.cell, "above");
3181
- setTableMenu(null);
3806
+ runTableCellAction(tableMenu.cell, (cell) => addRow(cell, "above"));
3182
3807
  }, children: [_jsx("span", { children: "\u21A5" }), _jsx("span", { children: "Row above" })] }), _jsxs("button", { style: {
3183
3808
  display: "flex",
3184
3809
  alignItems: "center",
@@ -3186,8 +3811,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3186
3811
  padding: "6px 8px",
3187
3812
  fontSize: 12,
3188
3813
  }, onClick: () => {
3189
- addRow(tableMenu.cell, "below");
3190
- setTableMenu(null);
3814
+ runTableCellAction(tableMenu.cell, (cell) => addRow(cell, "below"));
3191
3815
  }, children: [_jsx("span", { children: "\u21A7" }), _jsx("span", { children: "Row below" })] }), _jsxs("button", { style: {
3192
3816
  display: "flex",
3193
3817
  alignItems: "center",
@@ -3195,8 +3819,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3195
3819
  padding: "6px 8px",
3196
3820
  fontSize: 12,
3197
3821
  }, onClick: () => {
3198
- addCol(tableMenu.cell, "left");
3199
- setTableMenu(null);
3822
+ runTableCellAction(tableMenu.cell, (cell) => addCol(cell, "left"));
3200
3823
  }, children: [_jsx("span", { children: "\u2190" }), _jsx("span", { children: "Column left" })] }), _jsxs("button", { style: {
3201
3824
  display: "flex",
3202
3825
  alignItems: "center",
@@ -3204,8 +3827,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3204
3827
  padding: "6px 8px",
3205
3828
  fontSize: 12,
3206
3829
  }, onClick: () => {
3207
- addCol(tableMenu.cell, "right");
3208
- setTableMenu(null);
3830
+ runTableCellAction(tableMenu.cell, (cell) => addCol(cell, "right"));
3209
3831
  }, children: [_jsx("span", { children: "\u2192" }), _jsx("span", { children: "Column right" })] }), _jsxs("button", { style: {
3210
3832
  display: "flex",
3211
3833
  alignItems: "center",
@@ -3213,8 +3835,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3213
3835
  padding: "6px 8px",
3214
3836
  fontSize: 12,
3215
3837
  }, onClick: () => {
3216
- deleteRow(tableMenu.cell);
3217
- setTableMenu(null);
3838
+ runTableCellAction(tableMenu.cell, deleteRow);
3218
3839
  }, children: [_jsx("span", { children: "\u2716" }), _jsx("span", { children: "Delete row" })] }), _jsxs("button", { style: {
3219
3840
  display: "flex",
3220
3841
  alignItems: "center",
@@ -3222,8 +3843,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3222
3843
  padding: "6px 8px",
3223
3844
  fontSize: 12,
3224
3845
  }, onClick: () => {
3225
- deleteCol(tableMenu.cell);
3226
- setTableMenu(null);
3846
+ runTableCellAction(tableMenu.cell, deleteCol);
3227
3847
  }, children: [_jsx("span", { children: "\u2716" }), _jsx("span", { children: "Delete column" })] }), _jsxs("button", { style: {
3228
3848
  display: "flex",
3229
3849
  alignItems: "center",
@@ -3231,36 +3851,37 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3231
3851
  padding: "6px 8px",
3232
3852
  fontSize: 12,
3233
3853
  }, onClick: () => {
3234
- toggleHeaderCell(tableMenu.cell);
3235
- setTableMenu(null);
3236
- }, children: [_jsx("span", { children: "H" }), _jsx("span", { children: "Toggle header" })] }), _jsxs("button", { style: {
3854
+ runTableCellAction(tableMenu.cell, toggleHeaderCell);
3855
+ }, children: [_jsx("span", { children: "H" }), _jsx("span", { children: tableMenu.cell.tagName === "TH" ? "Remove cell header" : "Make cell header" })] }), _jsxs("button", { style: {
3237
3856
  display: "flex",
3238
3857
  alignItems: "center",
3239
3858
  gap: 8,
3240
3859
  padding: "6px 8px",
3241
3860
  fontSize: 12,
3242
3861
  }, onClick: () => {
3243
- toggleHeaderRow(tableMenu.cell);
3244
- setTableMenu(null);
3245
- }, children: [_jsx("span", { children: "H\u2081" }), _jsx("span", { children: "Toggle header row" })] }), _jsxs("button", { style: {
3862
+ runTableCellAction(tableMenu.cell, toggleHeaderRow);
3863
+ }, children: [_jsx("span", { children: "H\u2081" }), _jsx("span", { children: "Make this row header" })] }), _jsxs("button", { style: {
3246
3864
  display: "flex",
3247
3865
  alignItems: "center",
3248
3866
  gap: 8,
3249
3867
  padding: "6px 8px",
3250
3868
  fontSize: 12,
3251
3869
  }, 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: {
3870
+ runTableCellAction(tableMenu.cell, toggleHeaderColumn);
3871
+ }, children: [_jsx("span", { children: "H\u2195" }), _jsx("span", { children: "Make this column header" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("button", { style: {
3255
3872
  display: "flex",
3256
3873
  alignItems: "center",
3257
3874
  gap: 8,
3258
3875
  padding: "6px 8px",
3259
3876
  fontSize: 12,
3260
3877
  }, onClick: () => {
3261
- deleteTable(tableMenu.cell);
3262
- setTableMenu(null);
3263
- }, children: [_jsx("span", { children: "\uD83D\uDDD1" }), _jsx("span", { children: "Delete table" })] })] })] }) })), imageMenu && (_jsx("div", { style: { position: "fixed", inset: 0, zIndex: 60 }, onClick: () => setImageMenu(null), onContextMenu: (e) => {
3878
+ runTableCellAction(tableMenu.cell, deleteTable);
3879
+ }, children: [_jsx("span", { children: "\uD83D\uDDD1" }), _jsx("span", { children: "Delete table" })] })] })] }) })), imageMenu && (_jsx("div", { style: {
3880
+ position: "fixed",
3881
+ inset: 0,
3882
+ zIndex: 60,
3883
+ background: "transparent",
3884
+ }, onClick: () => setImageMenu(null), onContextMenu: (e) => {
3264
3885
  e.preventDefault();
3265
3886
  const vw = window.innerWidth;
3266
3887
  const vh = window.innerHeight;