testomatio-editor-blocks 0.4.46 → 0.4.47

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,17 @@ 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
+ function getActiveFormats(formatting, selStart, selEnd) {
22
+ const active = new Set();
23
+ if (selStart === selEnd)
24
+ return active;
25
+ for (const f of formatting) {
26
+ if (selStart < f.end && selEnd > f.start) {
27
+ active.add(f.type);
28
+ }
29
+ }
30
+ return active;
31
+ }
21
32
  function stripInlineMarkdown(markdown) {
22
33
  const links = [];
23
34
  const formatting = [];
@@ -248,8 +259,6 @@ function getCaretRectInPreview(preview, offset, textareaValue) {
248
259
  return null;
249
260
  }
250
261
  function applyFormattingHighlights(preview, formatting, textareaValue) {
251
- if (formatting.length === 0)
252
- return;
253
262
  // Remove previous formatting highlights
254
263
  const existingBold = preview.querySelectorAll("strong.step-preview-bold");
255
264
  for (let i = 0; i < existingBold.length; i++) {
@@ -287,6 +296,8 @@ function applyFormattingHighlights(preview, formatting, textareaValue) {
287
296
  // After unwrapping formatting elements, merge adjacent/empty text nodes
288
297
  // so the tree walker sees clean text nodes matching the original structure.
289
298
  preview.normalize();
299
+ if (formatting.length === 0)
300
+ return;
290
301
  // OverType splits textarea lines into <div> elements, discarding the \n
291
302
  // characters. Convert textarea-space positions (with \n) to preview-space
292
303
  // positions (without \n) so we can find the correct text nodes.
@@ -500,10 +511,14 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
500
511
  const linkSelectionRef = useRef(null);
501
512
  const linksRef = useRef([]);
502
513
  const formattingRef = useRef([]);
514
+ const formattingUndoRef = useRef([]);
515
+ const formattingRedoRef = useRef([]);
503
516
  const caretRef = useRef(null);
504
517
  const prevTextRef = useRef("");
505
518
  const isSyncingRef = useRef(false);
506
519
  const [cursorLink, setCursorLink] = useState(null);
520
+ const [activeFormats, setActiveFormats] = useState(new Set());
521
+ const [linkActive, setLinkActive] = useState(false);
507
522
  const Components = useComponentsContext();
508
523
  const resolvedPlaceholder = placeholder !== null && placeholder !== void 0 ? placeholder : "";
509
524
  useEffect(() => {
@@ -523,6 +538,8 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
523
538
  }
524
539
  linksRef.current = adjustLinksForEdit(linksRef.current, editPos, delta);
525
540
  formattingRef.current = adjustFormattingForEdit(formattingRef.current, editPos, delta);
541
+ formattingUndoRef.current = [];
542
+ formattingRedoRef.current = [];
526
543
  prevTextRef.current = nextValue;
527
544
  const markdown = buildFullMarkdown(nextValue, linksRef.current, formattingRef.current);
528
545
  setPlainTextValue((prev) => {
@@ -831,17 +848,33 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
831
848
  };
832
849
  }, [enableImageUpload, insertImageMarkdown, onImageFile, textareaNode, uploadImage]);
833
850
  const handleToolbarAction = useCallback((action) => {
834
- var _a, _b, _c;
851
+ var _a, _b, _c, _d, _e, _f, _g;
835
852
  const instance = editorInstanceRef.current;
836
853
  if (!textareaNode || !instance) {
837
854
  return;
838
855
  }
839
856
  textareaNode.focus();
840
857
  const fmtType = action === "toggleBold" ? "bold" : action === "toggleCode" ? "code" : "italic";
841
- const start = (_a = textareaNode.selectionStart) !== null && _a !== void 0 ? _a : 0;
842
- const end = (_b = textareaNode.selectionEnd) !== null && _b !== void 0 ? _b : 0;
858
+ const rawStart = (_a = textareaNode.selectionStart) !== null && _a !== void 0 ? _a : 0;
859
+ const rawEnd = (_b = textareaNode.selectionEnd) !== null && _b !== void 0 ? _b : 0;
860
+ // Trim leading/trailing whitespace from the selection so that
861
+ // formatting markers wrap only the meaningful content.
862
+ const selectedText = textareaNode.value.slice(rawStart, rawEnd);
863
+ const leadingWs = (_d = (_c = selectedText.match(/^(\s*)/)) === null || _c === void 0 ? void 0 : _c[1].length) !== null && _d !== void 0 ? _d : 0;
864
+ const trailingWs = (_f = (_e = selectedText.match(/(\s*)$/)) === null || _e === void 0 ? void 0 : _e[1].length) !== null && _f !== void 0 ? _f : 0;
865
+ const start = rawStart + leadingWs;
866
+ const end = rawEnd - trailingWs;
867
+ // If selection is all whitespace, nothing to format
868
+ if (start >= end)
869
+ return;
843
870
  // Check if selection is already formatted
844
871
  const existingIdx = formattingRef.current.findIndex((f) => f.type === fmtType && f.start <= start && f.end >= end);
872
+ // Save current state for undo before modifying
873
+ formattingUndoRef.current = [
874
+ ...formattingUndoRef.current,
875
+ { formatting: [...formattingRef.current], links: [...linksRef.current] },
876
+ ];
877
+ formattingRedoRef.current = [];
845
878
  if (existingIdx !== -1) {
846
879
  // Remove formatting
847
880
  formattingRef.current = formattingRef.current.filter((_, i) => i !== existingIdx);
@@ -852,17 +885,56 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
852
885
  }
853
886
  else {
854
887
  // No selection — nothing to format
888
+ formattingUndoRef.current = formattingUndoRef.current.slice(0, -1);
855
889
  return;
856
890
  }
857
891
  const currentValue = instance.getValue();
858
892
  prevTextRef.current = currentValue;
859
893
  const markdown = buildFullMarkdown(currentValue, linksRef.current, formattingRef.current);
860
- (_c = onChangeRef.current) === null || _c === void 0 ? void 0 : _c.call(onChangeRef, markdown);
894
+ (_g = onChangeRef.current) === null || _g === void 0 ? void 0 : _g.call(onChangeRef, markdown);
861
895
  setPlainTextValue(markdownToPlainText(markdown));
862
896
  // Re-apply highlights
863
897
  applyFormattingHighlights(instance.preview, formattingRef.current, textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.value);
864
898
  applyLinkHighlights(instance.preview, linksRef.current);
865
899
  }, [textareaNode]);
900
+ const updateActiveFormats = useCallback(() => {
901
+ var _a, _b;
902
+ if (!textareaNode)
903
+ return;
904
+ const selStart = (_a = textareaNode.selectionStart) !== null && _a !== void 0 ? _a : 0;
905
+ const selEnd = (_b = textareaNode.selectionEnd) !== null && _b !== void 0 ? _b : 0;
906
+ const next = getActiveFormats(formattingRef.current, selStart, selEnd);
907
+ setActiveFormats((prev) => {
908
+ if (prev.size === next.size && [...prev].every((t) => next.has(t)))
909
+ return prev;
910
+ return next;
911
+ });
912
+ const hasLink = selStart !== selEnd && linksRef.current.some((l) => selStart < l.end && selEnd > l.start);
913
+ setLinkActive(hasLink);
914
+ }, [textareaNode]);
915
+ useEffect(() => {
916
+ if (!textareaNode)
917
+ return;
918
+ const onSelectionChange = () => {
919
+ if (document.activeElement === textareaNode) {
920
+ updateActiveFormats();
921
+ }
922
+ };
923
+ const onBlur = () => {
924
+ setActiveFormats(new Set());
925
+ setLinkActive(false);
926
+ };
927
+ document.addEventListener("selectionchange", onSelectionChange);
928
+ textareaNode.addEventListener("keyup", updateActiveFormats);
929
+ textareaNode.addEventListener("mouseup", updateActiveFormats);
930
+ textareaNode.addEventListener("blur", onBlur);
931
+ return () => {
932
+ document.removeEventListener("selectionchange", onSelectionChange);
933
+ textareaNode.removeEventListener("keyup", updateActiveFormats);
934
+ textareaNode.removeEventListener("mouseup", updateActiveFormats);
935
+ textareaNode.removeEventListener("blur", onBlur);
936
+ };
937
+ }, [textareaNode, updateActiveFormats]);
866
938
  const linkPopoverRef = useRef(null);
867
939
  // Close link popover on outside click
868
940
  useEffect(() => {
@@ -1044,7 +1116,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
1044
1116
  const keydownHandlerRef = useRef(null);
1045
1117
  useEffect(() => {
1046
1118
  keydownHandlerRef.current = (event) => {
1047
- var _a, _b;
1119
+ var _a, _b, _c, _d, _e, _f;
1048
1120
  if (readOnly) {
1049
1121
  const openKeys = enableAutocomplete && (event.metaKey || event.ctrlKey) && AUTOCOMPLETE_TRIGGER_KEYS.has(event.code);
1050
1122
  if (!READ_ONLY_ALLOWED_KEYS.has(event.key) && !openKeys) {
@@ -1073,6 +1145,54 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
1073
1145
  handleToolbarAction("toggleCode");
1074
1146
  return;
1075
1147
  }
1148
+ if (event.key === "z" || event.key === "Z") {
1149
+ const undoStack = formattingUndoRef.current;
1150
+ if (undoStack.length > 0) {
1151
+ event.preventDefault();
1152
+ event.stopImmediatePropagation();
1153
+ formattingRedoRef.current = [
1154
+ ...formattingRedoRef.current,
1155
+ { formatting: [...formattingRef.current], links: [...linksRef.current] },
1156
+ ];
1157
+ const prev = undoStack[undoStack.length - 1];
1158
+ formattingUndoRef.current = undoStack.slice(0, -1);
1159
+ formattingRef.current = prev.formatting;
1160
+ linksRef.current = prev.links;
1161
+ const instance = editorInstanceRef.current;
1162
+ if (instance) {
1163
+ const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
1164
+ (_b = onChangeRef.current) === null || _b === void 0 ? void 0 : _b.call(onChangeRef, markdown);
1165
+ setPlainTextValue(markdownToPlainText(markdown));
1166
+ applyFormattingHighlights(instance.preview, formattingRef.current, (_c = instance.textarea) === null || _c === void 0 ? void 0 : _c.value);
1167
+ applyLinkHighlights(instance.preview, linksRef.current);
1168
+ }
1169
+ return;
1170
+ }
1171
+ }
1172
+ }
1173
+ if (modKey && event.shiftKey && (event.key === "z" || event.key === "Z")) {
1174
+ const redoStack = formattingRedoRef.current;
1175
+ if (redoStack.length > 0) {
1176
+ event.preventDefault();
1177
+ event.stopImmediatePropagation();
1178
+ formattingUndoRef.current = [
1179
+ ...formattingUndoRef.current,
1180
+ { formatting: [...formattingRef.current], links: [...linksRef.current] },
1181
+ ];
1182
+ const next = redoStack[redoStack.length - 1];
1183
+ formattingRedoRef.current = redoStack.slice(0, -1);
1184
+ formattingRef.current = next.formatting;
1185
+ linksRef.current = next.links;
1186
+ const instance = editorInstanceRef.current;
1187
+ if (instance) {
1188
+ const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
1189
+ (_d = onChangeRef.current) === null || _d === void 0 ? void 0 : _d.call(onChangeRef, markdown);
1190
+ setPlainTextValue(markdownToPlainText(markdown));
1191
+ applyFormattingHighlights(instance.preview, formattingRef.current, (_e = instance.textarea) === null || _e === void 0 ? void 0 : _e.value);
1192
+ applyLinkHighlights(instance.preview, linksRef.current);
1193
+ }
1194
+ return;
1195
+ }
1076
1196
  }
1077
1197
  if (enableAutocomplete && shouldShowAutocomplete) {
1078
1198
  if (event.key === "ArrowDown") {
@@ -1087,7 +1207,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
1087
1207
  }
1088
1208
  if (event.key === "Enter" || event.key === "Tab") {
1089
1209
  event.preventDefault();
1090
- const suggestion = (_b = filteredSuggestions[activeSuggestionIndex]) !== null && _b !== void 0 ? _b : filteredSuggestions[0];
1210
+ const suggestion = (_f = filteredSuggestions[activeSuggestionIndex]) !== null && _f !== void 0 ? _f : filteredSuggestions[0];
1091
1211
  if (suggestion) {
1092
1212
  applySuggestion(suggestion);
1093
1213
  }
@@ -1162,20 +1282,20 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
1162
1282
  }, tabIndex: -1, children: "Edit link" }), _jsx("a", { className: "bn-step-link-tooltip__btn", href: cursorLink.url, target: "_blank", rel: "noopener noreferrer", onMouseDown: (event) => event.stopPropagation(), tabIndex: -1, children: _jsxs("svg", { width: "12", height: "12", viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", children: [_jsx("path", { d: "M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" }), _jsx("path", { d: "M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" })] }) }), _jsx("button", { type: "button", className: "bn-step-link-tooltip__btn bn-step-link-tooltip__btn--danger", onMouseDown: (event) => {
1163
1283
  event.preventDefault();
1164
1284
  handleRemoveLink();
1165
- }, tabIndex: -1, children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M7 3h2a1 1 0 0 0-2 0ZM6 3a2 2 0 1 1 4 0h4a.5.5 0 0 1 0 1h-.564l-1.205 8.838A2.5 2.5 0 0 1 9.754 15H6.246a2.5 2.5 0 0 1-2.477-2.162L2.564 4H2a.5.5 0 0 1 0-1h4Zm1 3.5a.5.5 0 0 0-1 0v5a.5.5 0 0 0 1 0v-5ZM9.5 6a.5.5 0 0 0-.5.5v5a.5.5 0 0 0 1 0v-5a.5.5 0 0 0-.5-.5Z" }) }) })] })), showToolbar && (_jsxs("div", { className: "bn-step-toolbar", "aria-label": `${label} controls`, children: [showFormattingButtons && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "bn-step-toolbar__button", "data-tooltip": "Bold", onMouseDown: (event) => {
1285
+ }, tabIndex: -1, children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M7 3h2a1 1 0 0 0-2 0ZM6 3a2 2 0 1 1 4 0h4a.5.5 0 0 1 0 1h-.564l-1.205 8.838A2.5 2.5 0 0 1 9.754 15H6.246a2.5 2.5 0 0 1-2.477-2.162L2.564 4H2a.5.5 0 0 1 0-1h4Zm1 3.5a.5.5 0 0 0-1 0v5a.5.5 0 0 0 1 0v-5ZM9.5 6a.5.5 0 0 0-.5.5v5a.5.5 0 0 0 1 0v-5a.5.5 0 0 0-.5-.5Z" }) }) })] })), showToolbar && (_jsxs("div", { className: "bn-step-toolbar", "aria-label": `${label} controls`, children: [showFormattingButtons && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: `bn-step-toolbar__button${activeFormats.has("bold") ? " bn-step-toolbar__button--active" : ""}`, "data-tooltip": "Bold", onMouseDown: (event) => {
1166
1286
  event.preventDefault();
1167
1287
  handleToolbarAction("toggleBold");
1168
- }, "aria-label": "Bold", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 2.66675H8.33333C8.92064 2.66677 9.49502 2.83918 9.98525 3.1626C10.4755 3.48602 10.86 3.94622 11.0911 4.48613C11.3223 5.02604 11.3898 5.62192 11.2855 6.19988C11.1811 6.77783 10.9094 7.31244 10.504 7.73741C11.0752 8.06825 11.5213 8.57823 11.7733 9.18833C12.0252 9.79844 12.0689 10.4746 11.8976 11.1121C11.7263 11.7495 11.3495 12.3127 10.8256 12.7143C10.3018 13.1159 9.66008 13.3335 9 13.3334H4V12.0001H4.66667V4.00008H4V2.66675ZM6 7.33341H8.33333C8.77536 7.33341 9.19928 7.15782 9.51184 6.84526C9.8244 6.5327 10 6.10878 10 5.66675C10 5.22472 9.8244 4.8008 9.51184 4.48824C9.19928 4.17568 8.77536 4.00008 8.33333 4.00008H6V7.33341ZM6 8.66675V12.0001H9C9.44203 12.0001 9.86595 11.8245 10.1785 11.5119C10.4911 11.1994 10.6667 10.7754 10.6667 10.3334C10.6667 9.89139 10.4911 9.46746 10.1785 9.1549C9.86595 8.84234 9.44203 8.66675 9 8.66675H6Z", fill: "currentColor" }) }) }), _jsx("button", { type: "button", className: "bn-step-toolbar__button", "data-tooltip": "Italic", onMouseDown: (event) => {
1288
+ }, "aria-label": "Bold", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 2.66675H8.33333C8.92064 2.66677 9.49502 2.83918 9.98525 3.1626C10.4755 3.48602 10.86 3.94622 11.0911 4.48613C11.3223 5.02604 11.3898 5.62192 11.2855 6.19988C11.1811 6.77783 10.9094 7.31244 10.504 7.73741C11.0752 8.06825 11.5213 8.57823 11.7733 9.18833C12.0252 9.79844 12.0689 10.4746 11.8976 11.1121C11.7263 11.7495 11.3495 12.3127 10.8256 12.7143C10.3018 13.1159 9.66008 13.3335 9 13.3334H4V12.0001H4.66667V4.00008H4V2.66675ZM6 7.33341H8.33333C8.77536 7.33341 9.19928 7.15782 9.51184 6.84526C9.8244 6.5327 10 6.10878 10 5.66675C10 5.22472 9.8244 4.8008 9.51184 4.48824C9.19928 4.17568 8.77536 4.00008 8.33333 4.00008H6V7.33341ZM6 8.66675V12.0001H9C9.44203 12.0001 9.86595 11.8245 10.1785 11.5119C10.4911 11.1994 10.6667 10.7754 10.6667 10.3334C10.6667 9.89139 10.4911 9.46746 10.1785 9.1549C9.86595 8.84234 9.44203 8.66675 9 8.66675H6Z", fill: "currentColor" }) }) }), _jsx("button", { type: "button", className: `bn-step-toolbar__button${activeFormats.has("italic") ? " bn-step-toolbar__button--active" : ""}`, "data-tooltip": "Italic", onMouseDown: (event) => {
1169
1289
  event.preventDefault();
1170
1290
  handleToolbarAction("toggleItalic");
1171
- }, "aria-label": "Italic", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M8.66699 13.3334H4.66699V12.0001H5.95166L8.69566 4.00008H7.33366V2.66675H11.3337V4.00008H10.049L7.30499 12.0001H8.66699V13.3334Z", fill: "currentColor" }) }) }), _jsx("button", { type: "button", className: "bn-step-toolbar__button", "data-tooltip": "Code", onMouseDown: (event) => {
1291
+ }, "aria-label": "Italic", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M8.66699 13.3334H4.66699V12.0001H5.95166L8.69566 4.00008H7.33366V2.66675H11.3337V4.00008H10.049L7.30499 12.0001H8.66699V13.3334Z", fill: "currentColor" }) }) }), _jsx("button", { type: "button", className: `bn-step-toolbar__button${activeFormats.has("code") ? " bn-step-toolbar__button--active" : ""}`, "data-tooltip": "Code", onMouseDown: (event) => {
1172
1292
  event.preventDefault();
1173
1293
  handleToolbarAction("toggleCode");
1174
1294
  }, "aria-label": "Code", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M10.333 12.6667L14 8.00008L10.333 3.33341L9.15833 4.28341L12.1583 8.00008L9.15833 11.7167L10.333 12.6667ZM5.66699 12.6667L6.84166 11.7167L3.84166 8.00008L6.84166 4.28341L5.66699 3.33341L2 8.00008L5.66699 12.6667Z", fill: "currentColor" }) }) })] })), enableImageUpload && uploadImage && showImageButton && (_jsx("button", { type: "button", className: "bn-step-toolbar__button", "data-tooltip": "Insert image", onMouseDown: (event) => {
1175
1295
  var _a;
1176
1296
  event.preventDefault();
1177
1297
  (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
1178
- }, "aria-label": "Insert image", tabIndex: -1, disabled: isUploading, children: _jsx(ImageUploadIcon, {}) })), showFormattingButtons && Components && (_jsxs(Components.Generic.Popover.Root, { opened: showLinkPopover, position: "top", children: [_jsx(Components.Generic.Popover.Trigger, { children: _jsx("button", { type: "button", className: "bn-step-toolbar__button", "data-tooltip": "Insert link", onMouseDown: (event) => {
1298
+ }, "aria-label": "Insert image", tabIndex: -1, disabled: isUploading, children: _jsx(ImageUploadIcon, {}) })), showFormattingButtons && Components && (_jsxs(Components.Generic.Popover.Root, { opened: showLinkPopover, position: "top", children: [_jsx(Components.Generic.Popover.Trigger, { children: _jsx("button", { type: "button", className: `bn-step-toolbar__button${linkActive ? " bn-step-toolbar__button--active" : ""}`, "data-tooltip": "Insert link", onMouseDown: (event) => {
1179
1299
  event.preventDefault();
1180
1300
  if (showLinkPopover) {
1181
1301
  setShowLinkPopover(false);
@@ -942,6 +942,11 @@ html.dark .bn-step-image-preview__content {
942
942
  background: var(--step-bg-button-hover);
943
943
  }
944
944
 
945
+ .bn-step-toolbar__button--active {
946
+ background: var(--step-bg-button-hover);
947
+ color: var(--step-input-border-focus);
948
+ }
949
+
945
950
  [data-tooltip] {
946
951
  position: relative;
947
952
  }
@@ -1074,8 +1079,7 @@ html.dark .bn-step-image-preview__content {
1074
1079
  }
1075
1080
 
1076
1081
  .bn-step-editor .overtype-wrapper .overtype-preview strong.step-preview-bold {
1077
- -webkit-text-stroke: 0.5px currentColor;
1078
- font-weight: inherit !important;
1082
+ font-weight: 600 !important;
1079
1083
  color: inherit !important;
1080
1084
  }
1081
1085
 
@@ -1085,9 +1089,10 @@ html.dark .bn-step-image-preview__content {
1085
1089
  }
1086
1090
 
1087
1091
  .bn-step-editor .overtype-wrapper .overtype-preview code.step-preview-code {
1088
- background-color: rgba(135, 131, 120, 0.15) !important;
1092
+ background-color: transparent !important;
1089
1093
  border-radius: 3px !important;
1090
- color: inherit !important;
1094
+ font-family: "Fira Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
1095
+ color: rgb(146, 64, 14) !important;
1091
1096
  }
1092
1097
 
1093
1098
  .bn-step-custom-caret {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.46",
3
+ "version": "0.4.47",
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",
@@ -81,6 +81,22 @@ type ExtractedImage = {
81
81
 
82
82
  type LinkMeta = { start: number; end: number; url: string };
83
83
  type FormattingMeta = { start: number; end: number; type: "bold" | "italic" | "code" };
84
+ type FormatType = "bold" | "italic" | "code";
85
+
86
+ function getActiveFormats(
87
+ formatting: FormattingMeta[],
88
+ selStart: number,
89
+ selEnd: number,
90
+ ): Set<FormatType> {
91
+ const active = new Set<FormatType>();
92
+ if (selStart === selEnd) return active;
93
+ for (const f of formatting) {
94
+ if (selStart < f.end && selEnd > f.start) {
95
+ active.add(f.type);
96
+ }
97
+ }
98
+ return active;
99
+ }
84
100
 
85
101
 
86
102
  function stripInlineMarkdown(markdown: string): {
@@ -345,8 +361,6 @@ function getCaretRectInPreview(preview: HTMLElement, offset: number, textareaVal
345
361
  }
346
362
 
347
363
  function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingMeta[], textareaValue?: string) {
348
- if (formatting.length === 0) return;
349
-
350
364
  // Remove previous formatting highlights
351
365
  const existingBold = preview.querySelectorAll("strong.step-preview-bold");
352
366
  for (let i = 0; i < existingBold.length; i++) {
@@ -386,6 +400,8 @@ function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingM
386
400
  // so the tree walker sees clean text nodes matching the original structure.
387
401
  preview.normalize();
388
402
 
403
+ if (formatting.length === 0) return;
404
+
389
405
  // OverType splits textarea lines into <div> elements, discarding the \n
390
406
  // characters. Convert textarea-space positions (with \n) to preview-space
391
407
  // positions (without \n) so we can find the correct text nodes.
@@ -632,10 +648,14 @@ export function StepField({
632
648
  const linkSelectionRef = useRef<{ start: number; end: number; text: string } | null>(null);
633
649
  const linksRef = useRef<LinkMeta[]>([]);
634
650
  const formattingRef = useRef<FormattingMeta[]>([]);
651
+ const formattingUndoRef = useRef<Array<{ formatting: FormattingMeta[]; links: LinkMeta[] }>>([]);
652
+ const formattingRedoRef = useRef<Array<{ formatting: FormattingMeta[]; links: LinkMeta[] }>>([]);
635
653
  const caretRef = useRef<HTMLDivElement | null>(null);
636
654
  const prevTextRef = useRef("");
637
655
  const isSyncingRef = useRef(false);
638
656
  const [cursorLink, setCursorLink] = useState<LinkMeta | null>(null);
657
+ const [activeFormats, setActiveFormats] = useState<Set<FormatType>>(new Set());
658
+ const [linkActive, setLinkActive] = useState(false);
639
659
  const Components = useComponentsContext();
640
660
  const resolvedPlaceholder = placeholder ?? "";
641
661
 
@@ -658,6 +678,8 @@ export function StepField({
658
678
 
659
679
  linksRef.current = adjustLinksForEdit(linksRef.current, editPos, delta);
660
680
  formattingRef.current = adjustFormattingForEdit(formattingRef.current, editPos, delta);
681
+ formattingUndoRef.current = [];
682
+ formattingRedoRef.current = [];
661
683
  prevTextRef.current = nextValue;
662
684
 
663
685
  const markdown = buildFullMarkdown(nextValue, linksRef.current, formattingRef.current);
@@ -1024,14 +1046,32 @@ export function StepField({
1024
1046
  textareaNode.focus();
1025
1047
 
1026
1048
  const fmtType: "bold" | "italic" | "code" = action === "toggleBold" ? "bold" : action === "toggleCode" ? "code" : "italic";
1027
- const start = textareaNode.selectionStart ?? 0;
1028
- const end = textareaNode.selectionEnd ?? 0;
1049
+ const rawStart = textareaNode.selectionStart ?? 0;
1050
+ const rawEnd = textareaNode.selectionEnd ?? 0;
1051
+
1052
+ // Trim leading/trailing whitespace from the selection so that
1053
+ // formatting markers wrap only the meaningful content.
1054
+ const selectedText = textareaNode.value.slice(rawStart, rawEnd);
1055
+ const leadingWs = selectedText.match(/^(\s*)/)?.[1].length ?? 0;
1056
+ const trailingWs = selectedText.match(/(\s*)$/)?.[1].length ?? 0;
1057
+ const start = rawStart + leadingWs;
1058
+ const end = rawEnd - trailingWs;
1059
+
1060
+ // If selection is all whitespace, nothing to format
1061
+ if (start >= end) return;
1029
1062
 
1030
1063
  // Check if selection is already formatted
1031
1064
  const existingIdx = formattingRef.current.findIndex(
1032
1065
  (f) => f.type === fmtType && f.start <= start && f.end >= end,
1033
1066
  );
1034
1067
 
1068
+ // Save current state for undo before modifying
1069
+ formattingUndoRef.current = [
1070
+ ...formattingUndoRef.current,
1071
+ { formatting: [...formattingRef.current], links: [...linksRef.current] },
1072
+ ];
1073
+ formattingRedoRef.current = [];
1074
+
1035
1075
  if (existingIdx !== -1) {
1036
1076
  // Remove formatting
1037
1077
  formattingRef.current = formattingRef.current.filter((_, i) => i !== existingIdx);
@@ -1040,6 +1080,7 @@ export function StepField({
1040
1080
  formattingRef.current = [...formattingRef.current, { start, end, type: fmtType }];
1041
1081
  } else {
1042
1082
  // No selection — nothing to format
1083
+ formattingUndoRef.current = formattingUndoRef.current.slice(0, -1);
1043
1084
  return;
1044
1085
  }
1045
1086
 
@@ -1057,6 +1098,46 @@ export function StepField({
1057
1098
  [textareaNode],
1058
1099
  );
1059
1100
 
1101
+ const updateActiveFormats = useCallback(() => {
1102
+ if (!textareaNode) return;
1103
+ const selStart = textareaNode.selectionStart ?? 0;
1104
+ const selEnd = textareaNode.selectionEnd ?? 0;
1105
+ const next = getActiveFormats(formattingRef.current, selStart, selEnd);
1106
+ setActiveFormats((prev) => {
1107
+ if (prev.size === next.size && [...prev].every((t) => next.has(t))) return prev;
1108
+ return next;
1109
+ });
1110
+ const hasLink = selStart !== selEnd && linksRef.current.some((l) => selStart < l.end && selEnd > l.start);
1111
+ setLinkActive(hasLink);
1112
+ }, [textareaNode]);
1113
+
1114
+ useEffect(() => {
1115
+ if (!textareaNode) return;
1116
+
1117
+ const onSelectionChange = () => {
1118
+ if (document.activeElement === textareaNode) {
1119
+ updateActiveFormats();
1120
+ }
1121
+ };
1122
+
1123
+ const onBlur = () => {
1124
+ setActiveFormats(new Set());
1125
+ setLinkActive(false);
1126
+ };
1127
+
1128
+ document.addEventListener("selectionchange", onSelectionChange);
1129
+ textareaNode.addEventListener("keyup", updateActiveFormats);
1130
+ textareaNode.addEventListener("mouseup", updateActiveFormats);
1131
+ textareaNode.addEventListener("blur", onBlur);
1132
+
1133
+ return () => {
1134
+ document.removeEventListener("selectionchange", onSelectionChange);
1135
+ textareaNode.removeEventListener("keyup", updateActiveFormats);
1136
+ textareaNode.removeEventListener("mouseup", updateActiveFormats);
1137
+ textareaNode.removeEventListener("blur", onBlur);
1138
+ };
1139
+ }, [textareaNode, updateActiveFormats]);
1140
+
1060
1141
  const linkPopoverRef = useRef<HTMLDivElement>(null);
1061
1142
 
1062
1143
  // Close link popover on outside click
@@ -1310,6 +1391,54 @@ export function StepField({
1310
1391
  handleToolbarAction("toggleCode");
1311
1392
  return;
1312
1393
  }
1394
+ if (event.key === "z" || event.key === "Z") {
1395
+ const undoStack = formattingUndoRef.current;
1396
+ if (undoStack.length > 0) {
1397
+ event.preventDefault();
1398
+ event.stopImmediatePropagation();
1399
+ formattingRedoRef.current = [
1400
+ ...formattingRedoRef.current,
1401
+ { formatting: [...formattingRef.current], links: [...linksRef.current] },
1402
+ ];
1403
+ const prev = undoStack[undoStack.length - 1];
1404
+ formattingUndoRef.current = undoStack.slice(0, -1);
1405
+ formattingRef.current = prev.formatting;
1406
+ linksRef.current = prev.links;
1407
+ const instance = editorInstanceRef.current;
1408
+ if (instance) {
1409
+ const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
1410
+ onChangeRef.current?.(markdown);
1411
+ setPlainTextValue(markdownToPlainText(markdown));
1412
+ applyFormattingHighlights(instance.preview, formattingRef.current, instance.textarea?.value);
1413
+ applyLinkHighlights(instance.preview, linksRef.current);
1414
+ }
1415
+ return;
1416
+ }
1417
+ }
1418
+ }
1419
+ if (modKey && event.shiftKey && (event.key === "z" || event.key === "Z")) {
1420
+ const redoStack = formattingRedoRef.current;
1421
+ if (redoStack.length > 0) {
1422
+ event.preventDefault();
1423
+ event.stopImmediatePropagation();
1424
+ formattingUndoRef.current = [
1425
+ ...formattingUndoRef.current,
1426
+ { formatting: [...formattingRef.current], links: [...linksRef.current] },
1427
+ ];
1428
+ const next = redoStack[redoStack.length - 1];
1429
+ formattingRedoRef.current = redoStack.slice(0, -1);
1430
+ formattingRef.current = next.formatting;
1431
+ linksRef.current = next.links;
1432
+ const instance = editorInstanceRef.current;
1433
+ if (instance) {
1434
+ const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
1435
+ onChangeRef.current?.(markdown);
1436
+ setPlainTextValue(markdownToPlainText(markdown));
1437
+ applyFormattingHighlights(instance.preview, formattingRef.current, instance.textarea?.value);
1438
+ applyLinkHighlights(instance.preview, linksRef.current);
1439
+ }
1440
+ return;
1441
+ }
1313
1442
  }
1314
1443
 
1315
1444
  if (enableAutocomplete && shouldShowAutocomplete) {
@@ -1489,7 +1618,7 @@ export function StepField({
1489
1618
  <>
1490
1619
  <button
1491
1620
  type="button"
1492
- className="bn-step-toolbar__button"
1621
+ className={`bn-step-toolbar__button${activeFormats.has("bold") ? " bn-step-toolbar__button--active" : ""}`}
1493
1622
  data-tooltip="Bold"
1494
1623
  onMouseDown={(event) => {
1495
1624
  event.preventDefault();
@@ -1504,7 +1633,7 @@ export function StepField({
1504
1633
  </button>
1505
1634
  <button
1506
1635
  type="button"
1507
- className="bn-step-toolbar__button"
1636
+ className={`bn-step-toolbar__button${activeFormats.has("italic") ? " bn-step-toolbar__button--active" : ""}`}
1508
1637
  data-tooltip="Italic"
1509
1638
  onMouseDown={(event) => {
1510
1639
  event.preventDefault();
@@ -1519,7 +1648,7 @@ export function StepField({
1519
1648
  </button>
1520
1649
  <button
1521
1650
  type="button"
1522
- className="bn-step-toolbar__button"
1651
+ className={`bn-step-toolbar__button${activeFormats.has("code") ? " bn-step-toolbar__button--active" : ""}`}
1523
1652
  data-tooltip="Code"
1524
1653
  onMouseDown={(event) => {
1525
1654
  event.preventDefault();
@@ -1558,7 +1687,7 @@ export function StepField({
1558
1687
  <Components.Generic.Popover.Trigger>
1559
1688
  <button
1560
1689
  type="button"
1561
- className="bn-step-toolbar__button"
1690
+ className={`bn-step-toolbar__button${linkActive ? " bn-step-toolbar__button--active" : ""}`}
1562
1691
  data-tooltip="Insert link"
1563
1692
  onMouseDown={(event) => {
1564
1693
  event.preventDefault();
@@ -942,6 +942,11 @@ html.dark .bn-step-image-preview__content {
942
942
  background: var(--step-bg-button-hover);
943
943
  }
944
944
 
945
+ .bn-step-toolbar__button--active {
946
+ background: var(--step-bg-button-hover);
947
+ color: var(--step-input-border-focus);
948
+ }
949
+
945
950
  [data-tooltip] {
946
951
  position: relative;
947
952
  }
@@ -1074,8 +1079,7 @@ html.dark .bn-step-image-preview__content {
1074
1079
  }
1075
1080
 
1076
1081
  .bn-step-editor .overtype-wrapper .overtype-preview strong.step-preview-bold {
1077
- -webkit-text-stroke: 0.5px currentColor;
1078
- font-weight: inherit !important;
1082
+ font-weight: 600 !important;
1079
1083
  color: inherit !important;
1080
1084
  }
1081
1085
 
@@ -1085,9 +1089,10 @@ html.dark .bn-step-image-preview__content {
1085
1089
  }
1086
1090
 
1087
1091
  .bn-step-editor .overtype-wrapper .overtype-preview code.step-preview-code {
1088
- background-color: rgba(135, 131, 120, 0.15) !important;
1092
+ background-color: transparent !important;
1089
1093
  border-radius: 3px !important;
1090
- color: inherit !important;
1094
+ font-family: "Fira Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
1095
+ color: rgb(146, 64, 14) !important;
1091
1096
  }
1092
1097
 
1093
1098
  .bn-step-custom-caret {