smartrte-react 0.2.7 → 0.2.10

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.
@@ -55,10 +55,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
55
55
  const [showSpecialChars, setShowSpecialChars] = useState(false);
56
56
  const [colorPickerType, setColorPickerType] = useState('text');
57
57
  const savedRangeRef = useRef(null);
58
+ const inlineScriptCaretOverrideRef = useRef(null);
58
59
  const historyRef = useRef({
59
60
  undo: [],
60
61
  redo: [],
61
62
  });
63
+ const inputHistoryGroupRef = useRef(null);
62
64
  const [currentFontSize, setCurrentFontSize] = useState("");
63
65
  const [currentFont, setCurrentFont] = useState("");
64
66
  const [currentBlockType, setCurrentBlockType] = useState("p");
@@ -114,14 +116,28 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
114
116
  const element = node instanceof HTMLElement ? node : null;
115
117
  const block = element?.closest("p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div");
116
118
  const tag = block?.tagName.toLowerCase();
119
+ const scriptOverride = inlineScriptCaretOverrideRef.current;
120
+ const overrideApplies = Boolean(scriptOverride &&
121
+ range.collapsed &&
122
+ range.startContainer === scriptOverride.container &&
123
+ range.startOffset === scriptOverride.offset);
124
+ if (scriptOverride && !overrideApplies) {
125
+ inlineScriptCaretOverrideRef.current = null;
126
+ }
127
+ const subscriptActive = document.queryCommandState("subscript") || Boolean(element?.closest("sub"));
128
+ const superscriptActive = document.queryCommandState("superscript") || Boolean(element?.closest("sup"));
117
129
  setCurrentBlockType(tag === "h1" || tag === "h2" || tag === "h3" ? tag : "p");
118
130
  setActiveState({
119
131
  bold: document.queryCommandState("bold"),
120
132
  italic: document.queryCommandState("italic"),
121
133
  underline: document.queryCommandState("underline"),
122
134
  strikeThrough: document.queryCommandState("strikeThrough"),
123
- subscript: document.queryCommandState("subscript"),
124
- superscript: document.queryCommandState("superscript"),
135
+ subscript: overrideApplies && scriptOverride?.command === "subscript"
136
+ ? false
137
+ : subscriptActive,
138
+ superscript: overrideApplies && scriptOverride?.command === "superscript"
139
+ ? false
140
+ : superscriptActive,
125
141
  unorderedList: Boolean(element?.closest("ul")),
126
142
  orderedList: Boolean(element?.closest("ol")),
127
143
  blockquote: Boolean(element?.closest("blockquote")),
@@ -165,6 +181,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
165
181
  toggleList("ol");
166
182
  return;
167
183
  }
184
+ pushEditorHistory();
168
185
  const beforeHtml = editableRef.current?.innerHTML || "";
169
186
  const ok = document.execCommand(command, false, valueArg);
170
187
  const afterHtml = editableRef.current?.innerHTML || "";
@@ -178,6 +195,74 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
178
195
  }
179
196
  catch { }
180
197
  };
198
+ const getScriptAncestor = (node, tagName) => {
199
+ const editor = editableRef.current;
200
+ let element = node instanceof HTMLElement ? node : node?.parentElement || null;
201
+ while (element && element !== editor) {
202
+ if (element.tagName.toLowerCase() === tagName)
203
+ return element;
204
+ element = element.parentElement;
205
+ }
206
+ return null;
207
+ };
208
+ const toggleInlineScript = (command) => {
209
+ try {
210
+ if (!restoreSavedSelection()) {
211
+ safeSelectRange(getSelectionRangeInEditor());
212
+ }
213
+ const editor = editableRef.current;
214
+ const range = getSelectionRangeInEditor();
215
+ if (!editor || !range)
216
+ return;
217
+ const tagName = command === "subscript" ? "sub" : "sup";
218
+ const script = range.collapsed
219
+ ? getScriptAncestor(range.startContainer, tagName)
220
+ : null;
221
+ if (range.collapsed && document.queryCommandState(command)) {
222
+ document.execCommand(command, false);
223
+ const currentRange = getSelectionRangeInEditor();
224
+ savedRangeRef.current = currentRange?.cloneRange() || null;
225
+ inlineScriptCaretOverrideRef.current = currentRange?.collapsed
226
+ ? {
227
+ command,
228
+ container: currentRange.startContainer,
229
+ offset: currentRange.startOffset,
230
+ }
231
+ : null;
232
+ setActiveState((current) => ({
233
+ ...current,
234
+ subscript: command === "subscript" ? false : current.subscript,
235
+ superscript: command === "superscript" ? false : current.superscript,
236
+ }));
237
+ handleInput();
238
+ requestAnimationFrame(updateActiveState);
239
+ return;
240
+ }
241
+ if (script) {
242
+ const nextRange = document.createRange();
243
+ nextRange.setStartAfter(script);
244
+ nextRange.collapse(true);
245
+ safeSelectRange(nextRange);
246
+ savedRangeRef.current = nextRange.cloneRange();
247
+ inlineScriptCaretOverrideRef.current = {
248
+ command,
249
+ container: nextRange.startContainer,
250
+ offset: nextRange.startOffset,
251
+ };
252
+ setActiveState((current) => ({
253
+ ...current,
254
+ subscript: command === "subscript" ? false : current.subscript,
255
+ superscript: command === "superscript" ? false : current.superscript,
256
+ }));
257
+ requestAnimationFrame(updateActiveState);
258
+ return;
259
+ }
260
+ inlineScriptCaretOverrideRef.current = null;
261
+ exec(command);
262
+ requestAnimationFrame(updateActiveState);
263
+ }
264
+ catch { }
265
+ };
181
266
  const applyFormatBlock = (blockName) => {
182
267
  try {
183
268
  if (!restoreSavedSelection()) {
@@ -206,10 +291,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
206
291
  onChange(html);
207
292
  }
208
293
  };
209
- const pushEditorHistory = () => {
294
+ const pushEditorHistory = (preserveInputGroup = false) => {
210
295
  const editor = editableRef.current;
211
296
  if (!editor)
212
297
  return;
298
+ if (!preserveInputGroup)
299
+ inputHistoryGroupRef.current = null;
213
300
  const selectionCells = selectionRef.current
214
301
  ? getCellsInGridRect(selectionRef.current.tbody, selectionRef.current.sr, selectionRef.current.sc, selectionRef.current.er, selectionRef.current.ec)
215
302
  : [];
@@ -247,22 +334,47 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
247
334
  const history = historyRef.current;
248
335
  const from = dir === "undo" ? history.undo : history.redo;
249
336
  const to = dir === "undo" ? history.redo : history.undo;
250
- const html = from.pop();
337
+ const currentHtml = editor.innerHTML;
338
+ let html;
339
+ while (from.length > 0) {
340
+ const candidate = from.pop();
341
+ if (candidate !== currentHtml) {
342
+ html = candidate;
343
+ break;
344
+ }
345
+ }
251
346
  if (html == null)
252
347
  return false;
253
- to.push(editor.innerHTML);
348
+ if (to[to.length - 1] !== currentHtml)
349
+ to.push(currentHtml);
254
350
  editor.innerHTML = html;
255
351
  fixNegativeMargins(editor);
256
352
  ensureTableWrappers(editor);
257
353
  addTableResizeHandles();
258
354
  clearSelectionDecor();
259
355
  setTableMenu(null);
356
+ inputHistoryGroupRef.current = null;
357
+ focusElementEnd(editor);
358
+ requestAnimationFrame(updateActiveState);
260
359
  if (html !== lastEmittedRef.current) {
261
360
  lastEmittedRef.current = html;
262
361
  onChange?.(html);
263
362
  }
264
363
  return true;
265
364
  };
365
+ const captureInputHistory = (inputType) => {
366
+ const now = Date.now();
367
+ const previous = inputHistoryGroupRef.current;
368
+ const groupable = inputType === "insertText" ||
369
+ inputType === "deleteContentBackward" ||
370
+ inputType === "deleteContentForward";
371
+ const continuesGroup = groupable &&
372
+ previous?.inputType === inputType &&
373
+ now - previous.timestamp < 1000;
374
+ if (!continuesGroup)
375
+ pushEditorHistory(true);
376
+ inputHistoryGroupRef.current = { inputType, timestamp: now };
377
+ };
266
378
  const restoreSavedSelection = () => {
267
379
  const editor = editableRef.current;
268
380
  if (!editor)
@@ -292,6 +404,21 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
292
404
  return;
293
405
  exec("createLink", url);
294
406
  };
407
+ const openEditorLink = (anchor) => {
408
+ const rawHref = anchor.getAttribute("href")?.trim();
409
+ if (!rawHref)
410
+ return;
411
+ try {
412
+ const url = new URL(rawHref, window.location.href);
413
+ if (!["http:", "https:", "mailto:", "tel:"].includes(url.protocol))
414
+ return;
415
+ const target = anchor.getAttribute("target") || "_blank";
416
+ const opened = window.open(url.href, target, target === "_blank" ? "noopener,noreferrer" : undefined);
417
+ if (opened && target === "_blank")
418
+ opened.opener = null;
419
+ }
420
+ catch { }
421
+ };
295
422
  const getSelectionRangeInEditor = () => {
296
423
  const editor = editableRef.current;
297
424
  if (!editor)
@@ -568,6 +695,89 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
568
695
  }
569
696
  return changed;
570
697
  };
698
+ const nestListSelection = () => {
699
+ const editor = editableRef.current;
700
+ if (!editor)
701
+ return;
702
+ if (!restoreSavedSelection())
703
+ safeSelectRange(getSelectionRangeInEditor());
704
+ const range = getSelectionRangeInEditor();
705
+ if (!range || range.collapsed)
706
+ return;
707
+ const items = sortInDocumentOrder(getSelectedListItems(getSelectedBlocks(range)));
708
+ const firstList = items[0]?.parentElement;
709
+ if (items.length === 0 ||
710
+ !firstList ||
711
+ !["ul", "ol"].includes(firstList.tagName.toLowerCase())) {
712
+ return;
713
+ }
714
+ const listTag = firstList.tagName.toLowerCase();
715
+ const directItems = items.filter((item) => item.parentElement === firstList);
716
+ if (directItems.length === 0 || !directItems[0].previousElementSibling)
717
+ return;
718
+ pushEditorHistory();
719
+ if (nestSelectedListItems(directItems, listTag)) {
720
+ handleInput();
721
+ requestAnimationFrame(updateActiveState);
722
+ }
723
+ };
724
+ const applyListStyle = (value) => {
725
+ const listTag = value.startsWith("ordered:") ? "ol" : "ul";
726
+ const styleType = value.replace(/^(ordered|bullet):/, "");
727
+ if (!restoreSavedSelection())
728
+ safeSelectRange(getSelectionRangeInEditor());
729
+ const range = getSelectionRangeInEditor();
730
+ if (!range)
731
+ return;
732
+ const lists = new Set();
733
+ getSelectedListItems(getSelectedBlocks(range)).forEach((item) => {
734
+ const list = item.parentElement;
735
+ if (list &&
736
+ ["ul", "ol"].includes(list.tagName.toLowerCase())) {
737
+ lists.add(list);
738
+ }
739
+ });
740
+ [range.startContainer, range.endContainer].forEach((node) => {
741
+ const element = node instanceof HTMLElement ? node : node.parentElement;
742
+ const list = element?.closest("ul,ol");
743
+ if (list && editableRef.current?.contains(list))
744
+ lists.add(list);
745
+ });
746
+ if (lists.size === 0) {
747
+ const block = getCurrentBlock();
748
+ if (!block?.closest("ul,ol")) {
749
+ toggleList(listTag);
750
+ const currentList = getCurrentBlock()?.closest(listTag);
751
+ if (currentList)
752
+ currentList.style.listStyleType = styleType;
753
+ handleInput();
754
+ return;
755
+ }
756
+ const currentList = block.closest("ul,ol");
757
+ if (currentList)
758
+ lists.add(currentList);
759
+ }
760
+ if (lists.size === 0)
761
+ return;
762
+ pushEditorHistory();
763
+ let lastList = null;
764
+ lists.forEach((list) => {
765
+ const target = list.tagName.toLowerCase() === listTag
766
+ ? list
767
+ : cloneListShell(list, listTag);
768
+ if (target !== list) {
769
+ target.innerHTML = list.innerHTML;
770
+ list.parentElement?.replaceChild(target, list);
771
+ }
772
+ target.style.listStyleType = styleType;
773
+ lastList = target;
774
+ });
775
+ const lastItem = lastList?.lastElementChild;
776
+ if (lastItem)
777
+ focusElementEnd(lastItem);
778
+ handleInput();
779
+ requestAnimationFrame(updateActiveState);
780
+ };
571
781
  const convertSelectedBlocksToList = (blocks, listTag) => {
572
782
  const editor = editableRef.current;
573
783
  if (!editor || blocks.length === 0)
@@ -612,19 +822,99 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
612
822
  focusElementEnd(lastLi);
613
823
  return true;
614
824
  };
825
+ const transformSelectedListItems = (items, listTag) => {
826
+ const editor = editableRef.current;
827
+ if (!editor || items.length === 0)
828
+ return false;
829
+ const selected = new Set(items.filter((item) => {
830
+ const parentItem = item.parentElement?.closest("li");
831
+ return !parentItem || !items.includes(parentItem);
832
+ }));
833
+ const lists = new Set();
834
+ selected.forEach((item) => {
835
+ const list = item.parentElement;
836
+ if (list &&
837
+ (list.tagName.toLowerCase() === "ul" || list.tagName.toLowerCase() === "ol")) {
838
+ lists.add(list);
839
+ }
840
+ });
841
+ let changed = false;
842
+ let lastTarget = null;
843
+ lists.forEach((list) => {
844
+ const parent = list.parentElement;
845
+ if (!parent || !editor.contains(list))
846
+ return;
847
+ const toggleOff = list.tagName.toLowerCase() === listTag;
848
+ let pendingList = null;
849
+ const flushPendingList = () => {
850
+ if (!pendingList?.childNodes.length)
851
+ return;
852
+ parent.insertBefore(pendingList, list);
853
+ pendingList = null;
854
+ };
855
+ Array.from(list.children).forEach((child) => {
856
+ if (!(child instanceof HTMLElement) || child.tagName.toLowerCase() !== "li")
857
+ return;
858
+ const isSelected = selected.has(child);
859
+ if (isSelected && toggleOff) {
860
+ flushPendingList();
861
+ const paragraph = document.createElement("p");
862
+ paragraph.innerHTML = child.innerHTML || "<br>";
863
+ parent.insertBefore(paragraph, list);
864
+ lastTarget = paragraph;
865
+ child.remove();
866
+ changed = true;
867
+ return;
868
+ }
869
+ const outputTag = isSelected ? listTag : list.tagName.toLowerCase();
870
+ if (!pendingList || pendingList.tagName.toLowerCase() !== outputTag) {
871
+ flushPendingList();
872
+ pendingList = cloneListShell(list, outputTag);
873
+ }
874
+ pendingList.appendChild(child);
875
+ if (isSelected) {
876
+ lastTarget = child;
877
+ changed = true;
878
+ }
879
+ });
880
+ flushPendingList();
881
+ list.remove();
882
+ });
883
+ if (changed && lastTarget)
884
+ focusElementEnd(lastTarget);
885
+ return changed;
886
+ };
615
887
  const toggleList = (listTag) => {
616
888
  const editor = editableRef.current;
617
889
  if (!editor)
618
890
  return;
619
891
  if (!restoreSavedSelection())
620
892
  safeSelectRange(getSelectionRangeInEditor());
893
+ const setListActiveState = (active) => {
894
+ setActiveState((current) => ({
895
+ ...current,
896
+ unorderedList: listTag === "ul" ? active : false,
897
+ orderedList: listTag === "ol" ? active : false,
898
+ }));
899
+ };
621
900
  const range = getSelectionRangeInEditor();
622
901
  if (range && !range.collapsed) {
623
902
  const blocks = getSelectedBlocks(range);
624
903
  const selectedListItems = getSelectedListItems(blocks);
904
+ if (selectedListItems.length > 0) {
905
+ pushEditorHistory();
906
+ if (transformSelectedListItems(selectedListItems, listTag)) {
907
+ const active = Boolean(getCurrentBlock()?.closest(listTag));
908
+ setListActiveState(active);
909
+ handleInput();
910
+ requestAnimationFrame(updateActiveState);
911
+ return;
912
+ }
913
+ }
914
+ pushEditorHistory();
625
915
  const convertedBlocks = convertSelectedBlocksToList(blocks, listTag);
626
- const nestedItems = nestSelectedListItems(selectedListItems, listTag);
627
- if (convertedBlocks || nestedItems) {
916
+ if (convertedBlocks) {
917
+ setListActiveState(true);
628
918
  handleInput();
629
919
  requestAnimationFrame(updateActiveState);
630
920
  return;
@@ -632,31 +922,39 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
632
922
  }
633
923
  const block = getCurrentBlock();
634
924
  if (!block) {
925
+ pushEditorHistory();
635
926
  insertEmptyListAtSelection(listTag);
927
+ setListActiveState(true);
636
928
  handleInput();
637
929
  requestAnimationFrame(updateActiveState);
638
930
  return;
639
931
  }
640
932
  const currentList = block.closest("ul,ol");
641
933
  if (currentList && editor.contains(currentList)) {
934
+ pushEditorHistory();
642
935
  if (currentList.tagName.toLowerCase() === listTag) {
643
936
  const li = block.closest("li");
644
- if (li)
937
+ if (li) {
645
938
  unwrapListItem(li, currentList);
939
+ setListActiveState(false);
940
+ }
646
941
  }
647
942
  else {
648
943
  convertListTag(currentList, listTag);
944
+ setListActiveState(true);
649
945
  }
650
946
  handleInput();
651
947
  requestAnimationFrame(updateActiveState);
652
948
  return;
653
949
  }
950
+ pushEditorHistory();
654
951
  const list = document.createElement(listTag);
655
952
  const li = document.createElement("li");
656
953
  li.innerHTML = block.innerHTML || "<br>";
657
954
  list.appendChild(li);
658
955
  block.parentElement?.replaceChild(list, block);
659
956
  focusElementEnd(li);
957
+ setListActiveState(true);
660
958
  handleInput();
661
959
  requestAnimationFrame(updateActiveState);
662
960
  };
@@ -3327,6 +3625,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3327
3625
  return null;
3328
3626
  return getMovableElementFromNode(element);
3329
3627
  };
3628
+ const getMoveTarget = () => {
3629
+ const editor = editableRef.current;
3630
+ if (!editor)
3631
+ return null;
3632
+ const imageTarget = selectedImage?.parentElement?.tagName === "A"
3633
+ ? selectedImage.parentElement
3634
+ : selectedImage;
3635
+ if (imageTarget && editor.contains(imageTarget))
3636
+ return imageTarget;
3637
+ const range = getSelectionRangeInEditor();
3638
+ let node = range?.commonAncestorContainer || null;
3639
+ if (node?.nodeType === Node.TEXT_NODE)
3640
+ node = node.parentNode;
3641
+ const element = node instanceof HTMLElement ? node : null;
3642
+ if (!element)
3643
+ return null;
3644
+ const listItem = element.closest("li");
3645
+ if (listItem && editor.contains(listItem))
3646
+ return listItem;
3647
+ const cell = getClosestCell(element);
3648
+ if (cell) {
3649
+ const block = element.closest("p,h1,h2,h3,h4,h5,h6,blockquote,pre,div");
3650
+ if (block && block !== cell && cell.contains(block))
3651
+ return block;
3652
+ return cell;
3653
+ }
3654
+ return getTopLevelMovableElement();
3655
+ };
3656
+ const outdentListItem = (item) => {
3657
+ const list = item.parentElement;
3658
+ const parentItem = list?.parentElement;
3659
+ const outerList = parentItem?.parentElement;
3660
+ if (!list ||
3661
+ !parentItem ||
3662
+ parentItem.tagName.toLowerCase() !== "li" ||
3663
+ !outerList ||
3664
+ !["ul", "ol"].includes(outerList.tagName.toLowerCase())) {
3665
+ return false;
3666
+ }
3667
+ outerList.insertBefore(item, parentItem.nextSibling);
3668
+ if (!list.querySelector("li"))
3669
+ list.remove();
3670
+ return true;
3671
+ };
3330
3672
  const elementSibling = (element, direction) => {
3331
3673
  let sibling = direction === "previous" ? element.previousSibling : element.nextSibling;
3332
3674
  while (sibling && sibling.nodeType === Node.TEXT_NODE && !sibling.textContent?.trim()) {
@@ -3336,11 +3678,40 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3336
3678
  };
3337
3679
  const moveCurrentElement = (direction) => {
3338
3680
  const editor = editableRef.current;
3339
- const target = getTopLevelMovableElement();
3681
+ let target = getMoveTarget();
3340
3682
  if (!editor || !target)
3341
3683
  return;
3342
3684
  pushEditorHistory();
3343
- if (direction === "up") {
3685
+ if (target.tagName === "TD" || target.tagName === "TH") {
3686
+ const cell = target;
3687
+ const paragraph = document.createElement("p");
3688
+ while (cell.firstChild)
3689
+ paragraph.appendChild(cell.firstChild);
3690
+ cell.appendChild(paragraph);
3691
+ target = paragraph;
3692
+ }
3693
+ if (target.tagName.toLowerCase() === "li") {
3694
+ const list = target.parentElement;
3695
+ if (!list)
3696
+ return;
3697
+ if (direction === "up") {
3698
+ const previous = elementSibling(target, "previous");
3699
+ if (previous?.nodeName === "LI")
3700
+ list.insertBefore(target, previous);
3701
+ }
3702
+ else if (direction === "down") {
3703
+ const next = elementSibling(target, "next");
3704
+ if (next?.nodeName === "LI")
3705
+ list.insertBefore(next, target);
3706
+ }
3707
+ else if (direction === "right") {
3708
+ nestSelectedListItems([target], list.tagName.toLowerCase());
3709
+ }
3710
+ else {
3711
+ outdentListItem(target);
3712
+ }
3713
+ }
3714
+ else if (direction === "up") {
3344
3715
  const previous = elementSibling(target, "previous");
3345
3716
  if (previous)
3346
3717
  target.parentElement?.insertBefore(target, previous);
@@ -3419,7 +3790,24 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3419
3790
  background: "var(--srte-bg)",
3420
3791
  color: "var(--srte-text)",
3421
3792
  boxSizing: "border-box"
3422
- }, children: [_jsxs("div", { onMouseDown: preserveToolbarMouseDown, style: {
3793
+ }, children: [_jsxs("div", { "aria-disabled": readOnly, onMouseDown: (event) => {
3794
+ if (readOnly) {
3795
+ event.preventDefault();
3796
+ event.stopPropagation();
3797
+ return;
3798
+ }
3799
+ preserveToolbarMouseDown(event);
3800
+ }, onClick: (event) => {
3801
+ if (!readOnly)
3802
+ return;
3803
+ event.preventDefault();
3804
+ event.stopPropagation();
3805
+ }, onKeyDown: (event) => {
3806
+ if (!readOnly)
3807
+ return;
3808
+ event.preventDefault();
3809
+ event.stopPropagation();
3810
+ }, style: {
3423
3811
  display: "flex",
3424
3812
  flexWrap: "wrap",
3425
3813
  maxWidth: "100%",
@@ -3430,6 +3818,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3430
3818
  position: "sticky",
3431
3819
  top: 0,
3432
3820
  zIndex: 1,
3821
+ opacity: readOnly ? 0.55 : 1,
3822
+ pointerEvents: readOnly ? "none" : "auto",
3823
+ userSelect: readOnly ? "none" : undefined,
3433
3824
  }, children: [media && (_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: "none" }, onChange: (e) => {
3434
3825
  const list = e.currentTarget.files;
3435
3826
  if (list && list.length) {
@@ -3542,7 +3933,20 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3542
3933
  borderRadius: 6,
3543
3934
  background: "var(--srte-input-bg)",
3544
3935
  color: "var(--srte-input-text)",
3545
- }, 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: {
3936
+ }, children: _jsx("span", { style: { fontWeight: 700, padding: "1px 4px", background: "var(--srte-accent-bg)", borderRadius: 3 }, children: "A" }) }), _jsxs("button", { title: "Subscript", onClick: () => toggleInlineScript("subscript"), "aria-pressed": activeState.subscript, style: activeButtonStyle(activeState.subscript), children: ["X", _jsx("sub", { children: "2" })] }), _jsxs("button", { title: "Superscript", onClick: () => toggleInlineScript("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" }), _jsxs("select", { defaultValue: "", onMouseDown: preserveEditorSelection, onChange: (e) => {
3937
+ const value = e.target.value;
3938
+ if (value)
3939
+ applyListStyle(value);
3940
+ e.currentTarget.value = "";
3941
+ }, title: "List style", "aria-label": "List style", style: {
3942
+ height: 32,
3943
+ maxWidth: 112,
3944
+ padding: "0 6px",
3945
+ border: "1px solid var(--srte-input-border)",
3946
+ borderRadius: 6,
3947
+ background: "var(--srte-input-bg)",
3948
+ color: "var(--srte-input-text)",
3949
+ }, children: [_jsx("option", { value: "", disabled: true, children: "List style" }), _jsxs("optgroup", { label: "Bullets", children: [_jsx("option", { value: "bullet:disc", children: "Disc" }), _jsx("option", { value: "bullet:circle", children: "Circle" }), _jsx("option", { value: "bullet:square", children: "Square" })] }), _jsxs("optgroup", { label: "Numbered", children: [_jsx("option", { value: "ordered:decimal", children: "1, 2, 3" }), _jsx("option", { value: "ordered:lower-alpha", children: "a, b, c" }), _jsx("option", { value: "ordered:upper-alpha", children: "A, B, C" }), _jsx("option", { value: "ordered:lower-roman", children: "i, ii, iii" }), _jsx("option", { value: "ordered:upper-roman", children: "I, II, III" })] })] }), _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: {
3546
3950
  height: 32,
3547
3951
  minWidth: 32,
3548
3952
  padding: "0 8px",
@@ -4064,7 +4468,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4064
4468
  boxSizing: "border-box",
4065
4469
  position: "relative",
4066
4470
  scrollPaddingBottom: 24,
4067
- }, children: [_jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onKeyUp: updateActiveState, onMouseUp: updateActiveState, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
4471
+ }, children: [_jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onBeforeInput: (e) => {
4472
+ const inputType = e.nativeEvent.inputType || "input";
4473
+ if (inputType === "historyUndo" || inputType === "historyRedo") {
4474
+ if (restoreEditorHistory(inputType === "historyUndo" ? "undo" : "redo")) {
4475
+ e.preventDefault();
4476
+ }
4477
+ return;
4478
+ }
4479
+ captureInputHistory(inputType);
4480
+ }, onInput: handleInput, onKeyUp: updateActiveState, onMouseUp: updateActiveState, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
4068
4481
  isComposingRef.current = false;
4069
4482
  handleInput();
4070
4483
  }, onMouseMove: (e) => {
@@ -4084,6 +4497,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4084
4497
  return;
4085
4498
  }
4086
4499
  }
4500
+ pushEditorHistory();
4087
4501
  const html = e.clipboardData?.getData("text/html");
4088
4502
  if (html) {
4089
4503
  e.preventDefault();
@@ -4166,6 +4580,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4166
4580
  }
4167
4581
  }, onClick: (e) => {
4168
4582
  const t = e.target;
4583
+ const anchor = t?.closest("a");
4584
+ if (anchor && editableRef.current?.contains(anchor)) {
4585
+ e.preventDefault();
4586
+ e.stopPropagation();
4587
+ openEditorLink(anchor);
4588
+ return;
4589
+ }
4169
4590
  if (t && t.tagName === "IMG") {
4170
4591
  setSelectedImage(t);
4171
4592
  scheduleImageOverlay();
@@ -4241,13 +4662,29 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4241
4662
  // Keep Tab for indentation in lists; otherwise insert 2 spaces
4242
4663
  if (e.key === "Tab") {
4243
4664
  e.preventDefault();
4244
- if (document.queryCommandState("insertUnorderedList") ||
4245
- document.queryCommandState("insertOrderedList")) {
4665
+ const selection = getSelectionRangeInEditor();
4666
+ const selectedListItems = selection && !selection.collapsed
4667
+ ? getSelectedListItems(getSelectedBlocks(selection))
4668
+ : [];
4669
+ if (selectedListItems.length > 0) {
4670
+ if (!e.shiftKey) {
4671
+ nestListSelection();
4672
+ return;
4673
+ }
4674
+ pushEditorHistory();
4675
+ exec("outdent");
4676
+ return;
4677
+ }
4678
+ const currentBlock = getCurrentBlock();
4679
+ if (currentBlock?.closest("li")) {
4680
+ pushEditorHistory();
4246
4681
  exec(e.shiftKey ? "outdent" : "indent");
4247
4682
  }
4248
4683
  else {
4684
+ captureInputHistory("insertText");
4249
4685
  document.execCommand("insertText", false, " ");
4250
4686
  }
4687
+ return;
4251
4688
  }
4252
4689
  // Table navigation with arrows inside cells
4253
4690
  if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
package/dist/theme.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export type SrteTheme = 'light' | 'dark';
2
- export declare const SRTE_DEFAULT_CSS = "\n.srte-editor {\n --srte-bg: #ffffff;\n --srte-text: #111111;\n --srte-text-muted: #4b5563;\n --srte-border: #dddddd;\n --srte-border-light: #eeeeee;\n --srte-toolbar-bg: #ffffff;\n --srte-input-bg: #ffffff;\n --srte-input-text: #111111;\n --srte-input-border: #e5e7eb;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.35);\n --srte-modal-backdrop-filter: blur(2px);\n --srte-modal-bg: #ffffff;\n --srte-modal-text: #000000;\n --srte-menu-bg: #ffffff;\n --srte-menu-text: #111111;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n --srte-accent: #1e90ff;\n --srte-accent-bg: rgba(30, 144, 255, 0.15);\n --srte-danger: #dc2626;\n --srte-primary: #2563eb;\n --srte-surface-subtle: #f3f4f6;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #f3f4f6;\n}\n.srte-editor.srte-dark {\n --srte-bg: #1e1e1e;\n --srte-text: #e0e0e0;\n --srte-text-muted: #9ca3af;\n --srte-border: #3a3a3a;\n --srte-border-light: #2e2e2e;\n --srte-toolbar-bg: #252525;\n --srte-input-bg: #2a2a2a;\n --srte-input-text: #e0e0e0;\n --srte-input-border: #444444;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.22);\n --srte-modal-backdrop-filter: blur(10px) saturate(0.9);\n --srte-modal-bg: #1e293b;\n --srte-modal-text: #e0e0e0;\n --srte-menu-bg: #1e293b;\n --srte-menu-text: #e0e0e0;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);\n --srte-accent: #3b9eff;\n --srte-accent-bg: rgba(59, 158, 255, 0.2);\n --srte-danger: #ef4444;\n --srte-primary: #3b82f6;\n --srte-surface-subtle: #333333;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #333333;\n}\n.srte-editor [contenteditable] blockquote {\n border-left: 4px solid var(--srte-accent);\n margin: 0.75em 0;\n padding: 0.5em 1em;\n background: var(--srte-surface-subtle);\n color: var(--srte-text);\n}\n.srte-editor [contenteditable] p,\n.srte-editor [contenteditable] h1,\n.srte-editor [contenteditable] h2,\n.srte-editor [contenteditable] h3 {\n color: inherit;\n}\n.srte-editor [contenteditable] p {\n display: block;\n margin: 0 0 0.75em;\n font-size: 1em;\n font-weight: 400;\n line-height: 1.6;\n}\n.srte-editor [contenteditable] h1,\n.srte-editor [contenteditable] h2,\n.srte-editor [contenteditable] h3 {\n display: block;\n margin: 0.75em 0 0.4em;\n font-weight: 700;\n line-height: 1.25;\n}\n.srte-editor [contenteditable] h1 {\n font-size: 2em;\n}\n.srte-editor [contenteditable] h2 {\n font-size: 1.5em;\n}\n.srte-editor [contenteditable] h3 {\n font-size: 1.25em;\n}\n.srte-editor [contenteditable] > :first-child {\n margin-top: 0;\n}\n.srte-editor [contenteditable] ul {\n list-style-type: disc;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] ol {\n list-style-type: decimal;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] li {\n display: list-item;\n margin: 0.25em 0;\n padding-left: 0.25em;\n}\n.srte-editor [contenteditable] li::marker {\n color: currentColor;\n}\n.srte-editor.srte-dark [contenteditable] [style*=\"color\"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *),\n.srte-editor.srte-dark [contenteditable] [style*=\"background\"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *) {\n color: var(--srte-text) !important;\n background: transparent !important;\n background-color: transparent !important;\n}\n.srte-editor [contenteditable] sub,\n.srte-editor [contenteditable] sup {\n line-height: 0;\n}\n";
2
+ export declare const SRTE_DEFAULT_CSS = "\n.srte-editor {\n --srte-bg: #ffffff;\n --srte-text: #111111;\n --srte-text-muted: #4b5563;\n --srte-border: #dddddd;\n --srte-border-light: #eeeeee;\n --srte-toolbar-bg: #ffffff;\n --srte-input-bg: #ffffff;\n --srte-input-text: #111111;\n --srte-input-border: #e5e7eb;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.35);\n --srte-modal-backdrop-filter: blur(2px);\n --srte-modal-bg: #ffffff;\n --srte-modal-text: #000000;\n --srte-menu-bg: #ffffff;\n --srte-menu-text: #111111;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n --srte-accent: #1e90ff;\n --srte-accent-bg: rgba(30, 144, 255, 0.15);\n --srte-danger: #dc2626;\n --srte-primary: #2563eb;\n --srte-surface-subtle: #f3f4f6;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #f3f4f6;\n}\n.srte-editor.srte-dark {\n --srte-bg: #1e1e1e;\n --srte-text: #e0e0e0;\n --srte-text-muted: #9ca3af;\n --srte-border: #3a3a3a;\n --srte-border-light: #2e2e2e;\n --srte-toolbar-bg: #252525;\n --srte-input-bg: #2a2a2a;\n --srte-input-text: #e0e0e0;\n --srte-input-border: #444444;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.22);\n --srte-modal-backdrop-filter: blur(10px) saturate(0.9);\n --srte-modal-bg: #1e293b;\n --srte-modal-text: #e0e0e0;\n --srte-menu-bg: #1e293b;\n --srte-menu-text: #e0e0e0;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);\n --srte-accent: #3b9eff;\n --srte-accent-bg: rgba(59, 158, 255, 0.2);\n --srte-danger: #ef4444;\n --srte-primary: #3b82f6;\n --srte-surface-subtle: #333333;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #333333;\n}\n.srte-editor [contenteditable] blockquote {\n border-left: 4px solid var(--srte-accent);\n margin: 0.75em 0;\n padding: 0.5em 1em;\n background: var(--srte-surface-subtle);\n color: var(--srte-text);\n}\n.srte-editor [contenteditable] p,\n.srte-editor [contenteditable] h1,\n.srte-editor [contenteditable] h2,\n.srte-editor [contenteditable] h3 {\n color: inherit;\n}\n.srte-editor [contenteditable] p {\n display: block;\n margin: 0 0 0.75em;\n font-size: 1em;\n font-weight: 400;\n line-height: 1.6;\n}\n.srte-editor [contenteditable] h1,\n.srte-editor [contenteditable] h2,\n.srte-editor [contenteditable] h3 {\n display: block;\n margin: 0.75em 0 0.4em;\n font-weight: 700;\n line-height: 1.25;\n}\n.srte-editor [contenteditable] h1 {\n font-size: 2em;\n}\n.srte-editor [contenteditable] h2 {\n font-size: 1.5em;\n}\n.srte-editor [contenteditable] h3 {\n font-size: 1.25em;\n}\n.srte-editor [contenteditable] > :first-child {\n margin-top: 0;\n}\n.srte-editor [contenteditable] ul {\n list-style-type: disc;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] ol {\n list-style-type: decimal;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] li {\n display: list-item;\n margin: 0.25em 0;\n padding-left: 0.25em;\n}\n.srte-editor [contenteditable] li::marker {\n color: currentColor;\n}\n.srte-editor [contenteditable] a,\n.srte-editor [contenteditable] a:visited {\n color: var(--srte-primary) !important;\n text-decoration: underline !important;\n text-decoration-thickness: 1px !important;\n text-underline-offset: 2px !important;\n cursor: pointer;\n}\n.srte-editor [contenteditable] a:hover {\n color: var(--srte-accent) !important;\n}\n.srte-editor [contenteditable] a:focus-visible {\n outline: 2px solid var(--srte-accent);\n outline-offset: 2px;\n}\n.srte-editor.srte-dark [contenteditable] [style*=\"color\"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *),\n.srte-editor.srte-dark [contenteditable] [style*=\"background\"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *) {\n color: var(--srte-text) !important;\n background: transparent !important;\n background-color: transparent !important;\n}\n.srte-editor [contenteditable] sub,\n.srte-editor [contenteditable] sup {\n line-height: 0;\n}\n";
3
3
  export declare function ensureStyleSheet(): void;
package/dist/theme.js CHANGED
@@ -109,6 +109,21 @@ export const SRTE_DEFAULT_CSS = `
109
109
  .srte-editor [contenteditable] li::marker {
110
110
  color: currentColor;
111
111
  }
112
+ .srte-editor [contenteditable] a,
113
+ .srte-editor [contenteditable] a:visited {
114
+ color: var(--srte-primary) !important;
115
+ text-decoration: underline !important;
116
+ text-decoration-thickness: 1px !important;
117
+ text-underline-offset: 2px !important;
118
+ cursor: pointer;
119
+ }
120
+ .srte-editor [contenteditable] a:hover {
121
+ color: var(--srte-accent) !important;
122
+ }
123
+ .srte-editor [contenteditable] a:focus-visible {
124
+ outline: 2px solid var(--srte-accent);
125
+ outline-offset: 2px;
126
+ }
112
127
  .srte-editor.srte-dark [contenteditable] [style*="color"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *),
113
128
  .srte-editor.srte-dark [contenteditable] [style*="background"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *) {
114
129
  color: var(--srte-text) !important;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smartrte-react",
3
- "version": "0.2.7",
3
+ "version": "0.2.10",
4
4
  "description": "A powerful, feature-rich Rich Text Editor for React with support for tables, mathematical formulas (LaTeX/KaTeX), and media management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",