testomatio-editor-blocks 0.4.49 → 0.4.51

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.
@@ -18,6 +18,7 @@ const markdownParser = OverType.MarkdownParser;
18
18
  function ImageUploadIcon() {
19
19
  return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M12.667 2C13.0335 2.00008 13.3474 2.13057 13.6084 2.3916C13.8694 2.65264 13.9999 2.96648 14 3.33301V12.667C13.9999 13.0335 13.8694 13.3474 13.6084 13.6084C13.3474 13.8694 13.0335 13.9999 12.667 14H3.33301C2.96648 13.9999 2.65264 13.8694 2.3916 13.6084C2.13057 13.3474 2.00008 13.0335 2 12.667V3.33301C2.00008 2.96648 2.13057 2.65264 2.3916 2.3916C2.65264 2.13057 2.96648 2.00008 3.33301 2H12.667ZM3.33301 12.667H12.667V3.33301H3.33301V12.667ZM12 11.333H4L6 8.66699L7.5 10.667L9.5 8L12 11.333ZM5.66699 4.66699C5.94455 4.66707 6.18066 4.76375 6.375 4.95801C6.56944 5.15245 6.66699 5.38921 6.66699 5.66699C6.66692 5.94463 6.56937 6.18063 6.375 6.375C6.18063 6.56937 5.94463 6.66692 5.66699 6.66699C5.38921 6.66699 5.15245 6.56944 4.95801 6.375C4.76375 6.18066 4.66707 5.94455 4.66699 5.66699C4.66699 5.38921 4.76356 5.15245 4.95801 4.95801C5.15245 4.76356 5.38921 4.66699 5.66699 4.66699Z", fill: "currentColor" }) }));
20
20
  }
21
+ const UNDO_STACK_LIMIT = 100;
21
22
  function getActiveFormats(formatting, selStart, selEnd) {
22
23
  const active = new Set();
23
24
  if (selStart === selEnd)
@@ -170,7 +171,7 @@ function stripInlineMarkdown(markdown) {
170
171
  }
171
172
  return { plainText, links, formatting };
172
173
  }
173
- function buildFullMarkdown(plainText, links, formatting) {
174
+ export function buildFullMarkdown(plainText, links, formatting) {
174
175
  if (links.length === 0 && formatting.length === 0)
175
176
  return plainText;
176
177
  const markers = [];
@@ -184,7 +185,7 @@ function buildFullMarkdown(plainText, links, formatting) {
184
185
  closeMarker = isMultiline ? "\n```" : "`";
185
186
  }
186
187
  else {
187
- openMarker = fmt.type === "bold" ? "**" : "*";
188
+ openMarker = fmt.type === "bold" ? "**" : "_";
188
189
  closeMarker = openMarker;
189
190
  }
190
191
  // Opening: outer markers (bold) before inner (italic) → bold order=0, italic order=1
@@ -211,17 +212,24 @@ function buildFullMarkdown(plainText, links, formatting) {
211
212
  function adjustFormattingForEdit(formatting, editPos, delta) {
212
213
  return formatting
213
214
  .map((fmt) => {
214
- if (editPos <= fmt.start) {
215
- return { ...fmt, start: fmt.start + delta, end: fmt.end + delta };
216
- }
217
- if (editPos >= fmt.end) {
218
- return fmt;
215
+ if (delta >= 0) {
216
+ if (editPos <= fmt.start) {
217
+ return { ...fmt, start: fmt.start + delta, end: fmt.end + delta };
218
+ }
219
+ if (editPos >= fmt.end) {
220
+ return fmt;
221
+ }
222
+ return { ...fmt, end: fmt.end + delta };
219
223
  }
220
- return { ...fmt, end: fmt.end + delta };
224
+ const delEnd = editPos + Math.abs(delta);
225
+ const newStart = fmt.start < editPos ? fmt.start : fmt.start >= delEnd ? fmt.start + delta : editPos;
226
+ const newEnd = fmt.end <= editPos ? fmt.end : fmt.end >= delEnd ? fmt.end + delta : editPos;
227
+ return { ...fmt, start: newStart, end: newEnd };
221
228
  })
222
229
  .filter((fmt) => fmt.end > fmt.start);
223
230
  }
224
231
  function getCaretRectInPreview(preview, offset, textareaValue) {
232
+ var _a;
225
233
  // Convert textarea-space offset to preview-space (strip newlines)
226
234
  let nlCount = 0;
227
235
  if (textareaValue) {
@@ -242,7 +250,16 @@ function getCaretRectInPreview(preview, offset, textareaValue) {
242
250
  const range = document.createRange();
243
251
  range.setStart(textNode, localOffset);
244
252
  range.collapse(true);
245
- const rect = range.getBoundingClientRect();
253
+ let rect = range.getBoundingClientRect();
254
+ // Collapsed ranges at position 0 can return an empty rect in some browsers
255
+ if (rect.height === 0 && rect.top === 0 && rect.left === 0) {
256
+ const span = document.createElement("span");
257
+ span.textContent = "\u200B";
258
+ range.insertNode(span);
259
+ rect = span.getBoundingClientRect();
260
+ (_a = span.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(span);
261
+ preview.normalize();
262
+ }
246
263
  const previewRect = preview.getBoundingClientRect();
247
264
  return {
248
265
  top: rect.top - previewRect.top + preview.scrollTop,
@@ -403,13 +420,19 @@ function applyFormattingHighlights(preview, formatting, textareaValue) {
403
420
  function adjustLinksForEdit(links, editPos, delta) {
404
421
  return links
405
422
  .map((link) => {
406
- if (editPos <= link.start) {
407
- return { ...link, start: link.start + delta, end: link.end + delta };
408
- }
409
- if (editPos >= link.end) {
410
- return link;
423
+ if (delta >= 0) {
424
+ if (editPos <= link.start) {
425
+ return { ...link, start: link.start + delta, end: link.end + delta };
426
+ }
427
+ if (editPos >= link.end) {
428
+ return link;
429
+ }
430
+ return { ...link, end: link.end + delta };
411
431
  }
412
- return { ...link, end: link.end + delta };
432
+ const delEnd = editPos + Math.abs(delta);
433
+ const newStart = link.start < editPos ? link.start : link.start >= delEnd ? link.start + delta : editPos;
434
+ const newEnd = link.end <= editPos ? link.end : link.end >= delEnd ? link.end + delta : editPos;
435
+ return { ...link, start: newStart, end: newEnd };
413
436
  })
414
437
  .filter((link) => link.end > link.start);
415
438
  }
@@ -511,8 +534,8 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
511
534
  const linkSelectionRef = useRef(null);
512
535
  const linksRef = useRef([]);
513
536
  const formattingRef = useRef([]);
514
- const formattingUndoRef = useRef([]);
515
- const formattingRedoRef = useRef([]);
537
+ const undoStackRef = useRef([]);
538
+ const redoStackRef = useRef([]);
516
539
  const caretRef = useRef(null);
517
540
  const prevTextRef = useRef("");
518
541
  const isSyncingRef = useRef(false);
@@ -524,10 +547,26 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
524
547
  useEffect(() => {
525
548
  onChangeRef.current = onChange;
526
549
  }, [onChange]);
550
+ const pushUndoSnapshot = useCallback((text, formatting, links, cursorStart, cursorEnd) => {
551
+ const lastSnapshot = undoStackRef.current[undoStackRef.current.length - 1];
552
+ if (lastSnapshot &&
553
+ lastSnapshot.text === text &&
554
+ JSON.stringify(lastSnapshot.formatting) === JSON.stringify(formatting) &&
555
+ JSON.stringify(lastSnapshot.links) === JSON.stringify(links)) {
556
+ return;
557
+ }
558
+ undoStackRef.current = [
559
+ ...undoStackRef.current.slice(-(UNDO_STACK_LIMIT - 1)),
560
+ { text, formatting: [...formatting], links: [...links], cursorStart, cursorEnd },
561
+ ];
562
+ redoStackRef.current = [];
563
+ }, []);
527
564
  const handleEditorChange = useCallback((nextValue) => {
528
565
  var _a;
529
566
  if (isSyncingRef.current)
530
567
  return;
568
+ if (nextValue === prevTextRef.current)
569
+ return;
531
570
  const prevText = prevTextRef.current;
532
571
  const delta = nextValue.length - prevText.length;
533
572
  // Find where the edit happened by comparing old and new text
@@ -536,10 +575,13 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
536
575
  while (editPos < minLen && prevText[editPos] === nextValue[editPos]) {
537
576
  editPos++;
538
577
  }
578
+ // Capture pre-edit state for undo BEFORE mutating
579
+ const prevFormatting = [...formattingRef.current];
580
+ const prevLinks = [...linksRef.current];
539
581
  linksRef.current = adjustLinksForEdit(linksRef.current, editPos, delta);
540
582
  formattingRef.current = adjustFormattingForEdit(formattingRef.current, editPos, delta);
541
- formattingUndoRef.current = [];
542
- formattingRedoRef.current = [];
583
+ // Push pre-edit state to undo stack
584
+ pushUndoSnapshot(prevText, prevFormatting, prevLinks, editPos, editPos);
543
585
  prevTextRef.current = nextValue;
544
586
  const markdown = buildFullMarkdown(nextValue, linksRef.current, formattingRef.current);
545
587
  setPlainTextValue((prev) => {
@@ -547,9 +589,8 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
547
589
  return prev === normalized ? prev : normalized;
548
590
  });
549
591
  (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, markdown);
550
- }, []);
592
+ }, [pushUndoSnapshot]);
551
593
  useEffect(() => {
552
- var _a;
553
594
  const container = editorContainerRef.current;
554
595
  if (!container) {
555
596
  return;
@@ -558,6 +599,11 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
558
599
  linksRef.current = links;
559
600
  formattingRef.current = formatting;
560
601
  prevTextRef.current = plainText;
602
+ // Push initial state as the baseline undo snapshot
603
+ undoStackRef.current = [
604
+ { text: plainText, formatting: [...formatting], links: [...links], cursorStart: 0, cursorEnd: 0 },
605
+ ];
606
+ redoStackRef.current = [];
561
607
  const [instance] = OverType.init(container, {
562
608
  value: plainText,
563
609
  placeholder: resolvedPlaceholder,
@@ -575,9 +621,25 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
575
621
  applyFormattingHighlights(this.preview, formattingRef.current, (_a = this.textarea) === null || _a === void 0 ? void 0 : _a.value);
576
622
  applyLinkHighlights(this.preview, linksRef.current);
577
623
  };
578
- // Apply initial highlights
579
- applyFormattingHighlights(instance.preview, formattingRef.current, (_a = instance.textarea) === null || _a === void 0 ? void 0 : _a.value);
580
- applyLinkHighlights(instance.preview, linksRef.current);
624
+ // Force a full update through the monkey-patched pipeline
625
+ instance.updatePreview();
626
+ // Safety net: re-apply formatting if the preview gets reset externally
627
+ // (e.g. by the original updatePreview being called outside our monkey-patch)
628
+ let isApplyingFormatting = false;
629
+ const formattingObserver = new MutationObserver(() => {
630
+ var _a;
631
+ if (isApplyingFormatting)
632
+ return;
633
+ const hasFormatting = formattingRef.current.length > 0;
634
+ const hasAnyFormattingElement = instance.preview.querySelector("strong.step-preview-bold, em.step-preview-italic, code.step-preview-code") !== null;
635
+ if (hasFormatting && !hasAnyFormattingElement) {
636
+ isApplyingFormatting = true;
637
+ applyFormattingHighlights(instance.preview, formattingRef.current, (_a = instance.textarea) === null || _a === void 0 ? void 0 : _a.value);
638
+ applyLinkHighlights(instance.preview, linksRef.current);
639
+ isApplyingFormatting = false;
640
+ }
641
+ });
642
+ formattingObserver.observe(instance.preview, { childList: true, subtree: true });
581
643
  // Create custom caret element inside the wrapper
582
644
  const caretEl = document.createElement("div");
583
645
  caretEl.className = "bn-step-custom-caret";
@@ -586,6 +648,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
586
648
  editorInstanceRef.current = instance;
587
649
  setTextareaNode(instance.textarea);
588
650
  return () => {
651
+ formattingObserver.disconnect();
589
652
  caretRef.current = null;
590
653
  instance.destroy();
591
654
  editorInstanceRef.current = null;
@@ -709,6 +772,21 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
709
772
  delete textareaNode.dataset.stepField;
710
773
  }
711
774
  }, [fieldName, textareaNode]);
775
+ // Block native undo/redo at the beforeinput level so the browser never
776
+ // applies its own history on the textarea — our custom stack handles it.
777
+ useEffect(() => {
778
+ if (!textareaNode)
779
+ return;
780
+ const blockNativeUndoRedo = (e) => {
781
+ if (e.inputType === "historyUndo" || e.inputType === "historyRedo") {
782
+ e.preventDefault();
783
+ }
784
+ };
785
+ textareaNode.addEventListener("beforeinput", blockNativeUndoRedo);
786
+ return () => {
787
+ textareaNode.removeEventListener("beforeinput", blockNativeUndoRedo);
788
+ };
789
+ }, [textareaNode]);
712
790
  useEffect(() => {
713
791
  if (!textareaNode) {
714
792
  return;
@@ -735,6 +813,16 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
735
813
  const handleBlur = () => {
736
814
  setIsFocused(false);
737
815
  setShowAllSuggestions(false);
816
+ // Re-apply formatting highlights after blur because OverType may
817
+ // re-render the preview (via debounced selectionchange) and strip them.
818
+ const instance = editorInstanceRef.current;
819
+ if (instance) {
820
+ requestAnimationFrame(() => {
821
+ var _a;
822
+ applyFormattingHighlights(instance.preview, formattingRef.current, (_a = instance.textarea) === null || _a === void 0 ? void 0 : _a.value);
823
+ applyLinkHighlights(instance.preview, linksRef.current);
824
+ });
825
+ }
738
826
  };
739
827
  textareaNode.addEventListener("focus", handleFocus);
740
828
  textareaNode.addEventListener("blur", handleBlur);
@@ -870,24 +958,22 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
870
958
  // Check if selection is already formatted
871
959
  const existingIdx = formattingRef.current.findIndex((f) => f.type === fmtType && f.start <= start && f.end >= end);
872
960
  // Save current state for undo before modifying
873
- formattingUndoRef.current = [
874
- ...formattingUndoRef.current,
875
- { formatting: [...formattingRef.current], links: [...linksRef.current] },
876
- ];
877
- formattingRedoRef.current = [];
961
+ const currentText = instance.getValue();
962
+ pushUndoSnapshot(currentText, formattingRef.current, linksRef.current, start, end);
878
963
  if (existingIdx !== -1) {
879
964
  // Remove formatting
880
965
  formattingRef.current = formattingRef.current.filter((_, i) => i !== existingIdx);
881
966
  }
882
967
  else if (start !== end) {
883
- // Remove overlapping formatting of other types before applying new format
884
- formattingRef.current = formattingRef.current.filter((f) => f.type === fmtType || f.start >= end || f.end <= start);
968
+ // Remove overlapping formatting:
969
+ // - Code: remove ALL overlapping formatting (code replaces bold/italic)
970
+ // - Bold/Italic: remove only overlapping formatting of the SAME type
971
+ formattingRef.current = formattingRef.current.filter((f) => f.start >= end || f.end <= start || (fmtType !== "code" && f.type !== fmtType));
885
972
  // Add formatting for selection
886
973
  formattingRef.current = [...formattingRef.current, { start, end, type: fmtType }];
887
974
  }
888
975
  else {
889
976
  // No selection — nothing to format
890
- formattingUndoRef.current = formattingUndoRef.current.slice(0, -1);
891
977
  return;
892
978
  }
893
979
  const currentValue = instance.getValue();
@@ -898,7 +984,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
898
984
  // Re-apply highlights
899
985
  applyFormattingHighlights(instance.preview, formattingRef.current, textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.value);
900
986
  applyLinkHighlights(instance.preview, linksRef.current);
901
- }, [textareaNode]);
987
+ }, [textareaNode, pushUndoSnapshot]);
902
988
  const updateActiveFormats = useCallback(() => {
903
989
  var _a, _b;
904
990
  if (!textareaNode)
@@ -973,6 +1059,8 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
973
1059
  return;
974
1060
  }
975
1061
  const currentValue = instance.getValue();
1062
+ // Push undo snapshot before link edit
1063
+ pushUndoSnapshot(currentValue, formattingRef.current, linksRef.current, sel.start, sel.end);
976
1064
  const linkText = text || sel.text || url;
977
1065
  // Replace selected text with link display text (no markdown syntax in textarea)
978
1066
  const before = currentValue.slice(0, sel.start);
@@ -995,20 +1083,26 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
995
1083
  linkSelectionRef.current = null;
996
1084
  setCursorLink(null);
997
1085
  requestAnimationFrame(() => textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.focus());
998
- }, [textareaNode]);
1086
+ }, [textareaNode, pushUndoSnapshot]);
999
1087
  const handleRemoveLink = useCallback(() => {
1000
- var _a, _b;
1088
+ var _a, _b, _c, _d;
1089
+ const instance = editorInstanceRef.current;
1090
+ // Push undo snapshot before link removal
1091
+ if (instance) {
1092
+ const currentText = instance.getValue();
1093
+ const cursorPos = (_b = (_a = instance.textarea) === null || _a === void 0 ? void 0 : _a.selectionStart) !== null && _b !== void 0 ? _b : 0;
1094
+ pushUndoSnapshot(currentText, formattingRef.current, linksRef.current, cursorPos, cursorPos);
1095
+ }
1001
1096
  linksRef.current = linksRef.current.filter((l) => l !== cursorLink);
1002
1097
  setCursorLink(null);
1003
- const instance = editorInstanceRef.current;
1004
1098
  if (instance) {
1005
1099
  const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
1006
- (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, markdown);
1100
+ (_c = onChangeRef.current) === null || _c === void 0 ? void 0 : _c.call(onChangeRef, markdown);
1007
1101
  // Re-apply highlights since links changed
1008
- applyFormattingHighlights(instance.preview, formattingRef.current, (_b = instance.textarea) === null || _b === void 0 ? void 0 : _b.value);
1102
+ applyFormattingHighlights(instance.preview, formattingRef.current, (_d = instance.textarea) === null || _d === void 0 ? void 0 : _d.value);
1009
1103
  applyLinkHighlights(instance.preview, linksRef.current);
1010
1104
  }
1011
- }, [cursorLink]);
1105
+ }, [cursorLink, pushUndoSnapshot]);
1012
1106
  const suggestionPool = useMemo(() => {
1013
1107
  if (!suggestionFilter) {
1014
1108
  return suggestions;
@@ -1062,6 +1156,11 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
1062
1156
  }, [value]);
1063
1157
  const handleRemoveImage = useCallback((image) => {
1064
1158
  var _a;
1159
+ // Push undo snapshot before image removal
1160
+ if (editorInstanceRef.current) {
1161
+ const currentText = editorInstanceRef.current.getValue();
1162
+ pushUndoSnapshot(currentText, formattingRef.current, linksRef.current, image.start, image.end);
1163
+ }
1065
1164
  const before = value.slice(0, image.start);
1066
1165
  const after = value.slice(image.end);
1067
1166
  const nextValue = `${before}${after}`.replace(/\n{3,}/g, "\n\n");
@@ -1071,7 +1170,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
1071
1170
  (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, nextValue);
1072
1171
  setPlainTextValue(markdownToPlainText(nextValue));
1073
1172
  setPreviewImageUrl((prev) => (prev === image.url ? null : prev));
1074
- }, [value]);
1173
+ }, [value, pushUndoSnapshot]);
1075
1174
  const handleImageClick = useCallback((url) => {
1076
1175
  setPreviewImageUrl(url);
1077
1176
  }, []);
@@ -1096,14 +1195,18 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
1096
1195
  return true;
1097
1196
  }, [textareaNode]);
1098
1197
  const applySuggestion = useCallback((suggestion) => {
1099
- var _a;
1198
+ var _a, _b;
1100
1199
  const escaped = escapeMarkdownText(suggestion.title);
1101
1200
  const instance = editorInstanceRef.current;
1102
1201
  if (instance) {
1202
+ // Push undo snapshot before applying suggestion
1203
+ const currentText = instance.getValue();
1204
+ const cursorPos = (_a = textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.selectionStart) !== null && _a !== void 0 ? _a : 0;
1205
+ pushUndoSnapshot(currentText, formattingRef.current, linksRef.current, cursorPos, cursorPos);
1103
1206
  instance.setValue(escaped);
1104
1207
  }
1105
1208
  setPlainTextValue(suggestion.title);
1106
- (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, escaped);
1209
+ (_b = onChangeRef.current) === null || _b === void 0 ? void 0 : _b.call(onChangeRef, escaped);
1107
1210
  onSuggestionSelect === null || onSuggestionSelect === void 0 ? void 0 : onSuggestionSelect(suggestion);
1108
1211
  setActiveSuggestionIndex(0);
1109
1212
  setShowAllSuggestions(false);
@@ -1114,11 +1217,11 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
1114
1217
  textareaNode.selectionEnd = escaped.length;
1115
1218
  }
1116
1219
  });
1117
- }, [onSuggestionSelect, textareaNode]);
1220
+ }, [onSuggestionSelect, textareaNode, pushUndoSnapshot]);
1118
1221
  const keydownHandlerRef = useRef(null);
1119
1222
  useEffect(() => {
1120
1223
  keydownHandlerRef.current = (event) => {
1121
- var _a, _b, _c, _d, _e, _f;
1224
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1122
1225
  if (readOnly) {
1123
1226
  const openKeys = enableAutocomplete && (event.metaKey || event.ctrlKey) && AUTOCOMPLETE_TRIGGER_KEYS.has(event.code);
1124
1227
  if (!READ_ONLY_ALLOWED_KEYS.has(event.key) && !openKeys) {
@@ -1147,54 +1250,94 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
1147
1250
  handleToolbarAction("toggleCode");
1148
1251
  return;
1149
1252
  }
1253
+ if (event.key === "a" || event.key === "A") {
1254
+ event.stopPropagation();
1255
+ return;
1256
+ }
1150
1257
  if (event.key === "z" || event.key === "Z") {
1151
- const undoStack = formattingUndoRef.current;
1152
- if (undoStack.length > 0) {
1153
- event.preventDefault();
1154
- event.stopImmediatePropagation();
1155
- formattingRedoRef.current = [
1156
- ...formattingRedoRef.current,
1157
- { formatting: [...formattingRef.current], links: [...linksRef.current] },
1158
- ];
1159
- const prev = undoStack[undoStack.length - 1];
1160
- formattingUndoRef.current = undoStack.slice(0, -1);
1161
- formattingRef.current = prev.formatting;
1162
- linksRef.current = prev.links;
1163
- const instance = editorInstanceRef.current;
1164
- if (instance) {
1165
- const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
1166
- (_b = onChangeRef.current) === null || _b === void 0 ? void 0 : _b.call(onChangeRef, markdown);
1167
- setPlainTextValue(markdownToPlainText(markdown));
1168
- applyFormattingHighlights(instance.preview, formattingRef.current, (_c = instance.textarea) === null || _c === void 0 ? void 0 : _c.value);
1169
- applyLinkHighlights(instance.preview, linksRef.current);
1170
- }
1258
+ event.preventDefault();
1259
+ event.stopImmediatePropagation();
1260
+ const stack = undoStackRef.current;
1261
+ if (stack.length === 0)
1262
+ return;
1263
+ const instance = editorInstanceRef.current;
1264
+ if (!instance)
1171
1265
  return;
1266
+ // Push current state to redo stack
1267
+ const currentText = instance.getValue();
1268
+ redoStackRef.current = [
1269
+ ...redoStackRef.current,
1270
+ {
1271
+ text: currentText,
1272
+ formatting: [...formattingRef.current],
1273
+ links: [...linksRef.current],
1274
+ cursorStart: (_b = textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.selectionStart) !== null && _b !== void 0 ? _b : 0,
1275
+ cursorEnd: (_c = textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.selectionEnd) !== null && _c !== void 0 ? _c : 0,
1276
+ },
1277
+ ];
1278
+ // Pop from undo stack
1279
+ const prev = stack[stack.length - 1];
1280
+ undoStackRef.current = stack.slice(0, -1);
1281
+ // Restore state
1282
+ formattingRef.current = prev.formatting;
1283
+ linksRef.current = prev.links;
1284
+ prevTextRef.current = prev.text;
1285
+ isSyncingRef.current = true;
1286
+ instance.setValue(prev.text);
1287
+ isSyncingRef.current = false;
1288
+ if (textareaNode) {
1289
+ textareaNode.selectionStart = prev.cursorStart;
1290
+ textareaNode.selectionEnd = prev.cursorEnd;
1172
1291
  }
1292
+ const markdown = buildFullMarkdown(prev.text, prev.links, prev.formatting);
1293
+ (_d = onChangeRef.current) === null || _d === void 0 ? void 0 : _d.call(onChangeRef, markdown);
1294
+ setPlainTextValue(markdownToPlainText(markdown));
1295
+ applyFormattingHighlights(instance.preview, formattingRef.current, (_e = instance.textarea) === null || _e === void 0 ? void 0 : _e.value);
1296
+ applyLinkHighlights(instance.preview, linksRef.current);
1297
+ return;
1173
1298
  }
1174
1299
  }
1175
1300
  if (modKey && event.shiftKey && (event.key === "z" || event.key === "Z")) {
1176
- const redoStack = formattingRedoRef.current;
1177
- if (redoStack.length > 0) {
1178
- event.preventDefault();
1179
- event.stopImmediatePropagation();
1180
- formattingUndoRef.current = [
1181
- ...formattingUndoRef.current,
1182
- { formatting: [...formattingRef.current], links: [...linksRef.current] },
1183
- ];
1184
- const next = redoStack[redoStack.length - 1];
1185
- formattingRedoRef.current = redoStack.slice(0, -1);
1186
- formattingRef.current = next.formatting;
1187
- linksRef.current = next.links;
1188
- const instance = editorInstanceRef.current;
1189
- if (instance) {
1190
- const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
1191
- (_d = onChangeRef.current) === null || _d === void 0 ? void 0 : _d.call(onChangeRef, markdown);
1192
- setPlainTextValue(markdownToPlainText(markdown));
1193
- applyFormattingHighlights(instance.preview, formattingRef.current, (_e = instance.textarea) === null || _e === void 0 ? void 0 : _e.value);
1194
- applyLinkHighlights(instance.preview, linksRef.current);
1195
- }
1301
+ event.preventDefault();
1302
+ event.stopImmediatePropagation();
1303
+ const stack = redoStackRef.current;
1304
+ if (stack.length === 0)
1305
+ return;
1306
+ const instance = editorInstanceRef.current;
1307
+ if (!instance)
1196
1308
  return;
1309
+ // Push current state to undo stack
1310
+ const currentText = instance.getValue();
1311
+ undoStackRef.current = [
1312
+ ...undoStackRef.current,
1313
+ {
1314
+ text: currentText,
1315
+ formatting: [...formattingRef.current],
1316
+ links: [...linksRef.current],
1317
+ cursorStart: (_f = textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.selectionStart) !== null && _f !== void 0 ? _f : 0,
1318
+ cursorEnd: (_g = textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.selectionEnd) !== null && _g !== void 0 ? _g : 0,
1319
+ },
1320
+ ];
1321
+ // Pop from redo stack
1322
+ const next = stack[stack.length - 1];
1323
+ redoStackRef.current = stack.slice(0, -1);
1324
+ // Restore state
1325
+ formattingRef.current = next.formatting;
1326
+ linksRef.current = next.links;
1327
+ prevTextRef.current = next.text;
1328
+ isSyncingRef.current = true;
1329
+ instance.setValue(next.text);
1330
+ isSyncingRef.current = false;
1331
+ if (textareaNode) {
1332
+ textareaNode.selectionStart = next.cursorStart;
1333
+ textareaNode.selectionEnd = next.cursorEnd;
1197
1334
  }
1335
+ const markdown = buildFullMarkdown(next.text, next.links, next.formatting);
1336
+ (_h = onChangeRef.current) === null || _h === void 0 ? void 0 : _h.call(onChangeRef, markdown);
1337
+ setPlainTextValue(markdownToPlainText(markdown));
1338
+ applyFormattingHighlights(instance.preview, next.formatting, (_j = instance.textarea) === null || _j === void 0 ? void 0 : _j.value);
1339
+ applyLinkHighlights(instance.preview, next.links);
1340
+ return;
1198
1341
  }
1199
1342
  if (enableAutocomplete && shouldShowAutocomplete) {
1200
1343
  if (event.key === "ArrowDown") {
@@ -1209,7 +1352,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
1209
1352
  }
1210
1353
  if (event.key === "Enter" || event.key === "Tab") {
1211
1354
  event.preventDefault();
1212
- const suggestion = (_f = filteredSuggestions[activeSuggestionIndex]) !== null && _f !== void 0 ? _f : filteredSuggestions[0];
1355
+ const suggestion = (_k = filteredSuggestions[activeSuggestionIndex]) !== null && _k !== void 0 ? _k : filteredSuggestions[0];
1213
1356
  if (suggestion) {
1214
1357
  applySuggestion(suggestion);
1215
1358
  }
@@ -1,3 +1,18 @@
1
+ const BLOCK_MARKDOWN_PREFIX = /^(\s*)(#{1,6}\s|[-*+]\s|\d+[.)]\s|>\s|```|~~~|\||!\[)/;
2
+ function isInlineOnlyPaste(plainText, parsedBlocks) {
3
+ if (parsedBlocks.length !== 1)
4
+ return false;
5
+ const [block] = parsedBlocks;
6
+ if (block.type !== "paragraph")
7
+ return false;
8
+ if (block.children && block.children.length > 0)
9
+ return false;
10
+ if (/\r?\n/.test(plainText))
11
+ return false;
12
+ if (BLOCK_MARKDOWN_PREFIX.test(plainText))
13
+ return false;
14
+ return true;
15
+ }
1
16
  export function createMarkdownPasteHandler(converter) {
2
17
  return ({ event, editor, defaultPasteHandler }) => {
3
18
  var _a, _b, _c, _d, _e, _f, _g, _h;
@@ -21,6 +36,9 @@ export function createMarkdownPasteHandler(converter) {
21
36
  const parsedBlocks = converter(plainText);
22
37
  if (parsedBlocks.length === 0)
23
38
  return defaultPasteHandler();
39
+ if (isInlineOnlyPaste(plainText, parsedBlocks)) {
40
+ return defaultPasteHandler({ plainTextAsMarkdown: false });
41
+ }
24
42
  const selection = editor.getSelection();
25
43
  const selectedIds = (_h = (_g = selection === null || selection === void 0 ? void 0 : selection.blocks) === null || _g === void 0 ? void 0 : _g.map((block) => block.id).filter((id) => Boolean(id))) !== null && _h !== void 0 ? _h : [];
26
44
  if (selectedIds.length > 0) {
@@ -100,7 +100,7 @@ function applyTextStyles(text, styles) {
100
100
  wrappers.push({ prefix: "**", suffix: "**" });
101
101
  }
102
102
  if (styles.italic) {
103
- wrappers.push({ prefix: "*", suffix: "*" });
103
+ wrappers.push({ prefix: "_", suffix: "_" });
104
104
  }
105
105
  if (styles.strike) {
106
106
  wrappers.push({ prefix: "~~", suffix: "~~" });
@@ -511,10 +511,7 @@ function serializeBlocks(blocks, ctx) {
511
511
  }
512
512
  export function blocksToMarkdown(blocks) {
513
513
  const lines = serializeBlocks(blocks, { listDepth: 0, insideQuote: false });
514
- const cleaned = lines
515
- .join("\n")
516
- .replace(/\n{3,}/g, "\n\n")
517
- .trimEnd();
514
+ const cleaned = lines.join("\n").trimEnd();
518
515
  return cleaned;
519
516
  }
520
517
  function parseInlineMarkdown(text) {
@@ -592,8 +589,9 @@ function parseInlineMarkdown(text) {
592
589
  continue;
593
590
  }
594
591
  }
595
- if (cleaned.startsWith("*", i)) {
596
- const end = cleaned.indexOf("*", i + 1);
592
+ if (cleaned[i] === "*" || cleaned[i] === "_") {
593
+ const marker = cleaned[i];
594
+ const end = cleaned.indexOf(marker, i + 1);
597
595
  if (end !== -1) {
598
596
  pushPlain();
599
597
  const inner = cleaned.slice(i + 1, end);
@@ -947,6 +947,10 @@ html.dark .bn-step-image-preview__content {
947
947
  color: var(--step-input-border-focus);
948
948
  }
949
949
 
950
+ .bn-container {
951
+ overflow-x: clip;
952
+ }
953
+
950
954
  [data-tooltip] {
951
955
  position: relative;
952
956
  }
@@ -1079,7 +1083,8 @@ html.dark .bn-step-image-preview__content {
1079
1083
  }
1080
1084
 
1081
1085
  .bn-step-editor .overtype-wrapper .overtype-preview strong.step-preview-bold {
1082
- font-weight: 600 !important;
1086
+ -webkit-text-stroke: 0.5px currentColor;
1087
+ font-weight: inherit !important;
1083
1088
  color: inherit !important;
1084
1089
  }
1085
1090
 
@@ -1090,8 +1095,8 @@ html.dark .bn-step-image-preview__content {
1090
1095
 
1091
1096
  .bn-step-editor .overtype-wrapper .overtype-preview code.step-preview-code {
1092
1097
  background-color: transparent !important;
1093
- border-radius: 3px !important;
1094
- font-family: "Fira Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
1098
+ font-family: inherit !important;
1099
+ font-size: inherit !important;
1095
1100
  color: rgb(146, 64, 14) !important;
1096
1101
  }
1097
1102
 
@@ -1356,3 +1361,11 @@ html.dark .bn-step-image-preview__content {
1356
1361
  .bn-testcase--draft {
1357
1362
  --status-color: var(--status-default);
1358
1363
  }
1364
+
1365
+ /* Prevent unnecessary horizontal scrollbar on BlockNote tables.
1366
+ ProseMirror sets overflow-x: auto on .tableWrapper, but the
1367
+ table-widgets-container extends slightly past the wrapper edge,
1368
+ triggering a scrollbar even when table content fits. */
1369
+ .bn-editor [data-content-type="table"] .tableWrapper {
1370
+ overflow-x: hidden;
1371
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.49",
3
+ "version": "0.4.51",
4
4
  "description": "Custom BlockNote schema, markdown conversion helpers, and UI for Testomatio-style test cases and steps.",
5
5
  "type": "module",
6
6
  "main": "./package/index.js",
@@ -140,7 +140,7 @@ function fallbackHtmlToMarkdown(html: string): string {
140
140
  .replace(/<br\s*\/?>/gi, "\n")
141
141
  .replace(/<\/?(div|p)>/gi, "\n")
142
142
  .replace(/<strong>(.*?)<\/strong>/gis, (_m, content) => `**${content}**`)
143
- .replace(/<(em|i)>(.*?)<\/(em|i)>/gis, (_m, _tag, content) => `*${content}*`)
143
+ .replace(/<(em|i)>(.*?)<\/(em|i)>/gis, (_m, _tag, content) => `_${content}_`)
144
144
  .replace(/<span[^>]*>/gi, "")
145
145
  .replace(/<\/span>/gi, "")
146
146
  .replace(/<u>(.*?)<\/u>/gis, (_m, content) => `<u>${content}</u>`);