testomatio-editor-blocks 0.4.46 → 0.4.48
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.
- package/package/editor/blocks/step.d.ts +11 -1
- package/package/editor/blocks/step.js +56 -0
- package/package/editor/blocks/stepField.js +134 -12
- package/package/editor/customMarkdownConverter.js +8 -35
- package/package/index.d.ts +1 -1
- package/package/index.js +1 -1
- package/package/styles.css +14 -4
- package/package.json +1 -1
- package/src/App.tsx +27 -48
- package/src/editor/blocks/step.tsx +61 -1
- package/src/editor/blocks/stepField.tsx +141 -8
- package/src/editor/customMarkdownConverter.test.ts +113 -15
- package/src/editor/customMarkdownConverter.ts +8 -36
- package/src/editor/markdownToBlocks.test.ts +15 -3
- package/src/editor/styles.css +14 -4
- package/src/index.ts +1 -1
|
@@ -22,7 +22,17 @@ export declare function canInsertStepOrSnippet(editor: {
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function addStepsBlock(editor: {
|
|
24
24
|
document: any[];
|
|
25
|
-
insertBlocks: (blocks: any[], referenceId: string, placement:
|
|
25
|
+
insertBlocks: (blocks: any[], referenceId: string, placement: "before" | "after") => any[];
|
|
26
|
+
}): string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Programmatically add an empty snippet block to the editor.
|
|
29
|
+
* - If a "Steps" heading exists, inserts after the last step/snippet under it.
|
|
30
|
+
* - Otherwise, appends a "Steps" heading + empty snippet at the end.
|
|
31
|
+
* Returns the inserted snippet's block ID (for focusing), or null.
|
|
32
|
+
*/
|
|
33
|
+
export declare function addSnippetBlock(editor: {
|
|
34
|
+
document: any[];
|
|
35
|
+
insertBlocks: (blocks: any[], referenceId: string, placement: "before" | "after") => any[];
|
|
26
36
|
}): string | null;
|
|
27
37
|
export declare const stepBlock: {
|
|
28
38
|
config: {
|
|
@@ -142,6 +142,62 @@ export function addStepsBlock(editor) {
|
|
|
142
142
|
const inserted = editor.insertBlocks([stepsHeading, emptyStep], lastBlock.id, "after");
|
|
143
143
|
return (_d = (_c = inserted === null || inserted === void 0 ? void 0 : inserted[1]) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : null;
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Programmatically add an empty snippet block to the editor.
|
|
147
|
+
* - If a "Steps" heading exists, inserts after the last step/snippet under it.
|
|
148
|
+
* - Otherwise, appends a "Steps" heading + empty snippet at the end.
|
|
149
|
+
* Returns the inserted snippet's block ID (for focusing), or null.
|
|
150
|
+
*/
|
|
151
|
+
export function addSnippetBlock(editor) {
|
|
152
|
+
var _a, _b, _c, _d;
|
|
153
|
+
const allBlocks = editor.document;
|
|
154
|
+
const emptySnippet = {
|
|
155
|
+
type: "snippet",
|
|
156
|
+
props: { snippetId: "", snippetTitle: "", snippetData: "", snippetExpectedResult: "" },
|
|
157
|
+
children: [],
|
|
158
|
+
};
|
|
159
|
+
let stepsHeadingIndex = -1;
|
|
160
|
+
for (let i = 0; i < allBlocks.length; i++) {
|
|
161
|
+
const b = allBlocks[i];
|
|
162
|
+
if (b.type !== "heading")
|
|
163
|
+
continue;
|
|
164
|
+
const text = (Array.isArray(b.content) ? b.content : [])
|
|
165
|
+
.filter((n) => n.type === "text")
|
|
166
|
+
.map((n) => { var _a; return (_a = n.text) !== null && _a !== void 0 ? _a : ""; })
|
|
167
|
+
.join("")
|
|
168
|
+
.trim()
|
|
169
|
+
.toLowerCase()
|
|
170
|
+
.replace(/[:\-–—]$/, "");
|
|
171
|
+
if (isStepsHeading(text)) {
|
|
172
|
+
stepsHeadingIndex = i;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (stepsHeadingIndex >= 0) {
|
|
177
|
+
let lastIndex = stepsHeadingIndex;
|
|
178
|
+
for (let i = stepsHeadingIndex + 1; i < allBlocks.length; i++) {
|
|
179
|
+
const b = allBlocks[i];
|
|
180
|
+
if (b.type === "testStep" || b.type === "snippet") {
|
|
181
|
+
lastIndex = i;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (isEmptyParagraph(b))
|
|
185
|
+
continue;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
const inserted = editor.insertBlocks([emptySnippet], allBlocks[lastIndex].id, "after");
|
|
189
|
+
return (_b = (_a = inserted === null || inserted === void 0 ? void 0 : inserted[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
|
|
190
|
+
}
|
|
191
|
+
const lastBlock = allBlocks[allBlocks.length - 1];
|
|
192
|
+
const stepsHeading = {
|
|
193
|
+
type: "heading",
|
|
194
|
+
props: { level: 3 },
|
|
195
|
+
content: [{ type: "text", text: "Steps" }],
|
|
196
|
+
children: [],
|
|
197
|
+
};
|
|
198
|
+
const inserted = editor.insertBlocks([stepsHeading, emptySnippet], lastBlock.id, "after");
|
|
199
|
+
return (_d = (_c = inserted === null || inserted === void 0 ? void 0 : inserted[1]) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : null;
|
|
200
|
+
}
|
|
145
201
|
export const stepBlock = createReactBlockSpec({
|
|
146
202
|
type: "testStep",
|
|
147
203
|
content: "none",
|
|
@@ -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,38 +848,95 @@ 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
|
|
842
|
-
const
|
|
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);
|
|
848
881
|
}
|
|
849
882
|
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);
|
|
850
885
|
// Add formatting for selection
|
|
851
886
|
formattingRef.current = [...formattingRef.current, { start, end, type: fmtType }];
|
|
852
887
|
}
|
|
853
888
|
else {
|
|
854
889
|
// No selection — nothing to format
|
|
890
|
+
formattingUndoRef.current = formattingUndoRef.current.slice(0, -1);
|
|
855
891
|
return;
|
|
856
892
|
}
|
|
857
893
|
const currentValue = instance.getValue();
|
|
858
894
|
prevTextRef.current = currentValue;
|
|
859
895
|
const markdown = buildFullMarkdown(currentValue, linksRef.current, formattingRef.current);
|
|
860
|
-
(
|
|
896
|
+
(_g = onChangeRef.current) === null || _g === void 0 ? void 0 : _g.call(onChangeRef, markdown);
|
|
861
897
|
setPlainTextValue(markdownToPlainText(markdown));
|
|
862
898
|
// Re-apply highlights
|
|
863
899
|
applyFormattingHighlights(instance.preview, formattingRef.current, textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.value);
|
|
864
900
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
865
901
|
}, [textareaNode]);
|
|
902
|
+
const updateActiveFormats = useCallback(() => {
|
|
903
|
+
var _a, _b;
|
|
904
|
+
if (!textareaNode)
|
|
905
|
+
return;
|
|
906
|
+
const selStart = (_a = textareaNode.selectionStart) !== null && _a !== void 0 ? _a : 0;
|
|
907
|
+
const selEnd = (_b = textareaNode.selectionEnd) !== null && _b !== void 0 ? _b : 0;
|
|
908
|
+
const next = getActiveFormats(formattingRef.current, selStart, selEnd);
|
|
909
|
+
setActiveFormats((prev) => {
|
|
910
|
+
if (prev.size === next.size && [...prev].every((t) => next.has(t)))
|
|
911
|
+
return prev;
|
|
912
|
+
return next;
|
|
913
|
+
});
|
|
914
|
+
const hasLink = selStart !== selEnd && linksRef.current.some((l) => selStart < l.end && selEnd > l.start);
|
|
915
|
+
setLinkActive(hasLink);
|
|
916
|
+
}, [textareaNode]);
|
|
917
|
+
useEffect(() => {
|
|
918
|
+
if (!textareaNode)
|
|
919
|
+
return;
|
|
920
|
+
const onSelectionChange = () => {
|
|
921
|
+
if (document.activeElement === textareaNode) {
|
|
922
|
+
updateActiveFormats();
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
const onBlur = () => {
|
|
926
|
+
setActiveFormats(new Set());
|
|
927
|
+
setLinkActive(false);
|
|
928
|
+
};
|
|
929
|
+
document.addEventListener("selectionchange", onSelectionChange);
|
|
930
|
+
textareaNode.addEventListener("keyup", updateActiveFormats);
|
|
931
|
+
textareaNode.addEventListener("mouseup", updateActiveFormats);
|
|
932
|
+
textareaNode.addEventListener("blur", onBlur);
|
|
933
|
+
return () => {
|
|
934
|
+
document.removeEventListener("selectionchange", onSelectionChange);
|
|
935
|
+
textareaNode.removeEventListener("keyup", updateActiveFormats);
|
|
936
|
+
textareaNode.removeEventListener("mouseup", updateActiveFormats);
|
|
937
|
+
textareaNode.removeEventListener("blur", onBlur);
|
|
938
|
+
};
|
|
939
|
+
}, [textareaNode, updateActiveFormats]);
|
|
866
940
|
const linkPopoverRef = useRef(null);
|
|
867
941
|
// Close link popover on outside click
|
|
868
942
|
useEffect(() => {
|
|
@@ -1044,7 +1118,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1044
1118
|
const keydownHandlerRef = useRef(null);
|
|
1045
1119
|
useEffect(() => {
|
|
1046
1120
|
keydownHandlerRef.current = (event) => {
|
|
1047
|
-
var _a, _b;
|
|
1121
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1048
1122
|
if (readOnly) {
|
|
1049
1123
|
const openKeys = enableAutocomplete && (event.metaKey || event.ctrlKey) && AUTOCOMPLETE_TRIGGER_KEYS.has(event.code);
|
|
1050
1124
|
if (!READ_ONLY_ALLOWED_KEYS.has(event.key) && !openKeys) {
|
|
@@ -1073,6 +1147,54 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1073
1147
|
handleToolbarAction("toggleCode");
|
|
1074
1148
|
return;
|
|
1075
1149
|
}
|
|
1150
|
+
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
|
+
}
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
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
|
+
}
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1076
1198
|
}
|
|
1077
1199
|
if (enableAutocomplete && shouldShowAutocomplete) {
|
|
1078
1200
|
if (event.key === "ArrowDown") {
|
|
@@ -1087,7 +1209,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1087
1209
|
}
|
|
1088
1210
|
if (event.key === "Enter" || event.key === "Tab") {
|
|
1089
1211
|
event.preventDefault();
|
|
1090
|
-
const suggestion = (
|
|
1212
|
+
const suggestion = (_f = filteredSuggestions[activeSuggestionIndex]) !== null && _f !== void 0 ? _f : filteredSuggestions[0];
|
|
1091
1213
|
if (suggestion) {
|
|
1092
1214
|
applySuggestion(suggestion);
|
|
1093
1215
|
}
|
|
@@ -1162,20 +1284,20 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1162
1284
|
}, 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
1285
|
event.preventDefault();
|
|
1164
1286
|
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"
|
|
1287
|
+
}, 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
1288
|
event.preventDefault();
|
|
1167
1289
|
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"
|
|
1290
|
+
}, "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
1291
|
event.preventDefault();
|
|
1170
1292
|
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"
|
|
1293
|
+
}, "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
1294
|
event.preventDefault();
|
|
1173
1295
|
handleToolbarAction("toggleCode");
|
|
1174
1296
|
}, "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
1297
|
var _a;
|
|
1176
1298
|
event.preventDefault();
|
|
1177
1299
|
(_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"
|
|
1300
|
+
}, "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
1301
|
event.preventDefault();
|
|
1180
1302
|
if (showLinkPopover) {
|
|
1181
1303
|
setShowLinkPopover(false);
|
|
@@ -685,12 +685,9 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
685
685
|
if (detectedType !== listType) {
|
|
686
686
|
break;
|
|
687
687
|
}
|
|
688
|
-
// Only try to parse as testStep for top-level items
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (indentLevel === 0 && (allowEmptySteps || isLikelyStep(lines, index))) {
|
|
692
|
-
const looksLikeTestStep = listType === "bullet" ||
|
|
693
|
-
(listType === "numbered" && (allowEmptySteps || isLikelyStep(lines, index)));
|
|
688
|
+
// Only try to parse as testStep for top-level items under a Steps heading
|
|
689
|
+
if (indentLevel === 0 && allowEmptySteps) {
|
|
690
|
+
const looksLikeTestStep = listType === "bullet" || listType === "numbered";
|
|
694
691
|
if (looksLikeTestStep) {
|
|
695
692
|
const nextStep = parseTestStep(lines, index, allowEmptySteps);
|
|
696
693
|
if (nextStep) {
|
|
@@ -736,32 +733,6 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
736
733
|
}
|
|
737
734
|
return { items, nextIndex: index };
|
|
738
735
|
}
|
|
739
|
-
function isLikelyStep(lines, index) {
|
|
740
|
-
// Look ahead to see if there's indented content or expected result
|
|
741
|
-
// Look ahead through subsequent lines for expected result markers or indented content
|
|
742
|
-
for (let i = index + 1; i < lines.length; i++) {
|
|
743
|
-
const line = lines[i];
|
|
744
|
-
const trimmed = line.trim();
|
|
745
|
-
// Stop at blank lines
|
|
746
|
-
if (!trimmed)
|
|
747
|
-
break;
|
|
748
|
-
// Check for indented content (step data) first — indented lines indicate a test step
|
|
749
|
-
const hasIndent = /^\s{2,}/.test(line);
|
|
750
|
-
if (hasIndent)
|
|
751
|
-
return true;
|
|
752
|
-
// Stop at new list items, headings, or other block-level elements (only if not indented)
|
|
753
|
-
if (/^[-*+](\s|$)/.test(trimmed) || /^\d+[.)]\s/.test(trimmed))
|
|
754
|
-
break;
|
|
755
|
-
if (trimmed.startsWith("#") || trimmed.startsWith(">") || trimmed.startsWith("|") || trimmed.startsWith("```") || trimmed.startsWith(":::"))
|
|
756
|
-
break;
|
|
757
|
-
// Check for expected result markers
|
|
758
|
-
if (EXPECTED_LABEL_REGEX.test(trimmed))
|
|
759
|
-
return true;
|
|
760
|
-
if (trimmed.match(/^\*[^*]*expected/i))
|
|
761
|
-
return true;
|
|
762
|
-
}
|
|
763
|
-
return false;
|
|
764
|
-
}
|
|
765
736
|
function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
766
737
|
const current = lines[index];
|
|
767
738
|
const trimmed = current.trim();
|
|
@@ -1225,14 +1196,16 @@ export function markdownToBlocks(markdown, options) {
|
|
|
1225
1196
|
}
|
|
1226
1197
|
continue;
|
|
1227
1198
|
}
|
|
1228
|
-
const snippetWrapper =
|
|
1199
|
+
const snippetWrapper = stepsHeadingLevel !== null
|
|
1200
|
+
? parseSnippetWrapper(lines, index)
|
|
1201
|
+
: null;
|
|
1229
1202
|
if (snippetWrapper) {
|
|
1230
1203
|
blocks.push(snippetWrapper.block);
|
|
1231
1204
|
index = snippetWrapper.nextIndex;
|
|
1232
1205
|
continue;
|
|
1233
1206
|
}
|
|
1234
|
-
const stepLikeBlock =
|
|
1235
|
-
? parseTestStep(lines, index,
|
|
1207
|
+
const stepLikeBlock = stepsHeadingLevel !== null
|
|
1208
|
+
? parseTestStep(lines, index, true)
|
|
1236
1209
|
: null;
|
|
1237
1210
|
if (stepLikeBlock) {
|
|
1238
1211
|
blocks.push(stepLikeBlock.block);
|
package/package/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { customSchema, type CustomSchema, type CustomBlock, type CustomEditor, } from "./editor/customSchema";
|
|
2
|
-
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock } from "./editor/blocks/step";
|
|
2
|
+
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
|
|
3
3
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
4
4
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
5
5
|
export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, type MarkdownToBlocksOptions, } from "./editor/customMarkdownConverter";
|
package/package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { customSchema, } from "./editor/customSchema";
|
|
2
|
-
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock } from "./editor/blocks/step";
|
|
2
|
+
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
|
|
3
3
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
4
4
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
5
5
|
export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
|
package/package/styles.css
CHANGED
|
@@ -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
|
-
-
|
|
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:
|
|
1092
|
+
background-color: transparent !important;
|
|
1089
1093
|
border-radius: 3px !important;
|
|
1090
|
-
|
|
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 {
|
|
@@ -1334,6 +1339,11 @@ html.dark .bn-step-image-preview__content {
|
|
|
1334
1339
|
pointer-events: none;
|
|
1335
1340
|
}
|
|
1336
1341
|
|
|
1342
|
+
.bn-suggestion-menu .mantine-Badge-label {
|
|
1343
|
+
text-transform: none;
|
|
1344
|
+
font-size: var(--badge-fz-sm);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1337
1347
|
.bn-suggestion-icon {
|
|
1338
1348
|
display: inline-flex;
|
|
1339
1349
|
align-items: center;
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -15,13 +15,14 @@ import {
|
|
|
15
15
|
blocksToMarkdown,
|
|
16
16
|
markdownToBlocks,
|
|
17
17
|
type CustomEditorBlock,
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
} from "./editor/customMarkdownConverter";
|
|
20
20
|
import { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
|
|
21
21
|
import { customSchema, type CustomEditor } from "./editor/customSchema";
|
|
22
22
|
import { setStepsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
|
|
23
23
|
import { setSnippetFetcher, type SnippetJsonApiDocument } from "./editor/snippetAutocomplete";
|
|
24
24
|
import { setImageUploadHandler } from "./editor/stepImageUpload";
|
|
25
|
+
import { canInsertStepOrSnippet, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
|
|
25
26
|
import "./App.css";
|
|
26
27
|
|
|
27
28
|
const focusStepField = (
|
|
@@ -291,7 +292,18 @@ function CustomSlashMenu() {
|
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
const getItems = async (query: string) => {
|
|
294
|
-
const
|
|
295
|
+
const isMac =
|
|
296
|
+
typeof navigator !== "undefined" &&
|
|
297
|
+
(/Mac/.test(navigator.platform) ||
|
|
298
|
+
(/AppleWebKit/.test(navigator.userAgent) &&
|
|
299
|
+
/Mobile\/\w+/.test(navigator.userAgent)));
|
|
300
|
+
|
|
301
|
+
const defaultItems = getDefaultReactSlashMenuItems(editor).map((item) => {
|
|
302
|
+
if (item.badge && isMac) {
|
|
303
|
+
return { ...item, badge: item.badge.replace("Alt", "Option") };
|
|
304
|
+
}
|
|
305
|
+
return item;
|
|
306
|
+
});
|
|
295
307
|
|
|
296
308
|
const stepItem = {
|
|
297
309
|
key: "test_step" as any,
|
|
@@ -334,7 +346,12 @@ function CustomSlashMenu() {
|
|
|
334
346
|
},
|
|
335
347
|
};
|
|
336
348
|
|
|
337
|
-
|
|
349
|
+
const currentBlock = editor.getTextCursorPosition().block;
|
|
350
|
+
const canInsert = canInsertStepOrSnippet(editor, currentBlock.id);
|
|
351
|
+
const items = canInsert
|
|
352
|
+
? [...defaultItems, stepItem, snippetItem]
|
|
353
|
+
: defaultItems;
|
|
354
|
+
return filterSuggestionItems(items, query);
|
|
338
355
|
};
|
|
339
356
|
|
|
340
357
|
return <SuggestionMenuController triggerCharacter="/" getItems={getItems} />;
|
|
@@ -451,52 +468,14 @@ function App() {
|
|
|
451
468
|
};
|
|
452
469
|
}, [editor, uploadStepImage]);
|
|
453
470
|
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
},
|
|
462
|
-
children: [],
|
|
463
|
-
});
|
|
464
|
-
}, []);
|
|
465
|
-
|
|
466
|
-
const createSnippetBlock = useMemo<() => CustomPartialBlock>(() => {
|
|
467
|
-
return () => ({
|
|
468
|
-
type: "snippet",
|
|
469
|
-
props: {
|
|
470
|
-
snippetId: "",
|
|
471
|
-
snippetTitle: "",
|
|
472
|
-
snippetData: "",
|
|
473
|
-
snippetExpectedResult: "",
|
|
474
|
-
},
|
|
475
|
-
children: [],
|
|
476
|
-
});
|
|
477
|
-
}, []);
|
|
478
|
-
|
|
479
|
-
const insertBlockAfterSelection = (createBlock: () => CustomPartialBlock, focusFieldName?: string) => {
|
|
480
|
-
const selection = editor.getSelection();
|
|
481
|
-
const selectedBlocks = selection?.blocks ?? [];
|
|
482
|
-
const selectedBlock = selectedBlocks[selectedBlocks.length - 1];
|
|
483
|
-
const documentBlocks = editor.document;
|
|
484
|
-
const fallbackBlock = documentBlocks[documentBlocks.length - 1];
|
|
485
|
-
const referenceId = selectedBlock?.id ?? fallbackBlock?.id;
|
|
486
|
-
|
|
487
|
-
if (!referenceId) {
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const inserted = editor.insertBlocks([createBlock()], referenceId, "after");
|
|
492
|
-
const firstInserted = inserted[0];
|
|
493
|
-
if (firstInserted && focusFieldName) {
|
|
494
|
-
focusStepField(editor, firstInserted.id, focusFieldName);
|
|
495
|
-
}
|
|
471
|
+
const insertTestStep = () => {
|
|
472
|
+
const id = addStepsBlock(editor);
|
|
473
|
+
if (id) focusStepField(editor, id, "title");
|
|
474
|
+
};
|
|
475
|
+
const insertSnippet = () => {
|
|
476
|
+
const id = addSnippetBlock(editor);
|
|
477
|
+
if (id) focusStepField(editor, id, "snippet-title");
|
|
496
478
|
};
|
|
497
|
-
|
|
498
|
-
const insertTestStep = () => insertBlockAfterSelection(createTestStepBlock, "title");
|
|
499
|
-
const insertSnippet = () => insertBlockAfterSelection(createSnippetBlock, "snippet-title");
|
|
500
479
|
|
|
501
480
|
const handleCopyMarkdown = async () => {
|
|
502
481
|
if (conversionError) {
|