testomatio-editor-blocks 0.2.3 → 0.3.0
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/markdown.d.ts +5 -0
- package/package/editor/blocks/markdown.js +30 -5
- package/package/editor/blocks/snippet.js +1 -1
- package/package/editor/blocks/step.js +2 -2
- package/package/editor/blocks/stepField.d.ts +2 -3
- package/package/editor/blocks/stepField.js +376 -229
- package/package/styles.css +133 -10
- package/package.json +1 -1
- package/src/editor/blocks/markdown.ts +12 -2
- package/src/editor/customMarkdownConverter.test.ts +15 -0
|
@@ -3,3 +3,8 @@ export declare function markdownToHtml(markdown: string): string;
|
|
|
3
3
|
export declare function escapeMarkdownText(text: string): string;
|
|
4
4
|
export declare function normalizePlainText(text: string): string;
|
|
5
5
|
export declare function htmlToMarkdown(html: string): string;
|
|
6
|
+
declare function cleanupEscapedFormatting(markdown: string): string;
|
|
7
|
+
export declare const __markdownStringUtils: {
|
|
8
|
+
cleanupEscapedFormatting: typeof cleanupEscapedFormatting;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
2
2
|
const MARKDOWN_ESCAPE_REGEX = /([*_\\])/g;
|
|
3
|
+
const INLINE_SEGMENT_REGEX = /(\*\*\*[^*]+\*\*\*|___[^_]+___|\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/;
|
|
3
4
|
export function escapeHtml(text) {
|
|
4
5
|
return text
|
|
5
6
|
.replace(/&/g, "&")
|
|
@@ -16,11 +17,16 @@ function parseInlineMarkdown(text) {
|
|
|
16
17
|
return [];
|
|
17
18
|
}
|
|
18
19
|
const normalized = text.replace(/\\([*_`~])/g, "\uE000$1");
|
|
19
|
-
const rawSegments = normalized
|
|
20
|
-
.split(/(\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/)
|
|
21
|
-
.filter(Boolean);
|
|
20
|
+
const rawSegments = normalized.split(INLINE_SEGMENT_REGEX).filter(Boolean);
|
|
22
21
|
return rawSegments.map((segment) => {
|
|
23
22
|
const baseStyles = { bold: false, italic: false, underline: false };
|
|
23
|
+
if (/^\*\*\*(.+)\*\*\*$/.test(segment) || /^___(.+)___$/.test(segment)) {
|
|
24
|
+
const content = segment.slice(3, -3);
|
|
25
|
+
return {
|
|
26
|
+
text: restoreEscapes(content),
|
|
27
|
+
styles: { bold: true, italic: true, underline: false },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
24
30
|
if (/^\*\*(.+)\*\*$/.test(segment) || /^__(.+)__$/.test(segment)) {
|
|
25
31
|
const content = segment.slice(2, -2);
|
|
26
32
|
return {
|
|
@@ -106,12 +112,13 @@ function fallbackHtmlToMarkdown(html) {
|
|
|
106
112
|
.replace(/<\/span>/gi, "")
|
|
107
113
|
.replace(/<u>(.*?)<\/u>/gis, (_m, content) => `<u>${content}</u>`);
|
|
108
114
|
result = result.replace(/<\/?[^>]+>/g, "");
|
|
109
|
-
|
|
115
|
+
const markdown = result
|
|
110
116
|
.split("\n")
|
|
111
117
|
.map((line) => line.trimEnd())
|
|
112
118
|
.join("\n")
|
|
113
119
|
.replace(/\n{3,}/g, "\n\n")
|
|
114
120
|
.trim();
|
|
121
|
+
return cleanupEscapedFormatting(markdown);
|
|
115
122
|
}
|
|
116
123
|
export function htmlToMarkdown(html) {
|
|
117
124
|
if (typeof document === "undefined") {
|
|
@@ -156,5 +163,23 @@ export function htmlToMarkdown(html) {
|
|
|
156
163
|
}
|
|
157
164
|
};
|
|
158
165
|
const markdown = Array.from(temp.childNodes).map(traverse).join("");
|
|
159
|
-
return markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
166
|
+
return cleanupEscapedFormatting(markdown).replace(/\n{3,}/g, "\n\n").trim();
|
|
167
|
+
}
|
|
168
|
+
function cleanupEscapedFormatting(markdown) {
|
|
169
|
+
return markdown.replace(/(\\+)([*_]+)/g, (_match, slashes, markers) => {
|
|
170
|
+
if (markers.length === 0) {
|
|
171
|
+
return slashes + markers;
|
|
172
|
+
}
|
|
173
|
+
const shouldClean = markers.length === 3 ||
|
|
174
|
+
markers.length === 2 ||
|
|
175
|
+
markers.length === 1;
|
|
176
|
+
if (!shouldClean) {
|
|
177
|
+
return slashes + markers;
|
|
178
|
+
}
|
|
179
|
+
const hasPrintable = slashes.length % 2 === 0;
|
|
180
|
+
return hasPrintable ? markers : slashes + markers;
|
|
181
|
+
});
|
|
160
182
|
}
|
|
183
|
+
export const __markdownStringUtils = {
|
|
184
|
+
cleanupEscapedFormatting,
|
|
185
|
+
};
|
|
@@ -67,6 +67,6 @@ export const snippetBlock = createReactBlockSpec({
|
|
|
67
67
|
if (!hasSnippets) {
|
|
68
68
|
return (_jsx("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, children: _jsx("p", { className: "bn-snippet__empty", children: "No snippets in this project." }) }));
|
|
69
69
|
}
|
|
70
|
-
return (_jsxs("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, children: [_jsx(StepField, { label: "Snippet Title", value: snippetTitle,
|
|
70
|
+
return (_jsxs("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, children: [_jsx(StepField, { label: "Snippet Title", value: snippetTitle, onChange: handleSnippetChange, autoFocus: snippetTitle.length === 0, enableAutocomplete: true, suggestionFilter: (suggestion) => suggestion.isSnippet === true, suggestionsOverride: snippetSuggestions, onSuggestionSelect: handleSnippetSelect, fieldName: "snippet-title", showSuggestionsOnFocus: true, enableImageUpload: false, onFieldFocus: handleFieldFocus }), _jsx(StepField, { label: "Snippet Data", value: snippetData, onChange: handleSnippetDataChange, multiline: true, fieldName: "snippet-data", enableImageUpload: true, onFieldFocus: handleFieldFocus })] }));
|
|
71
71
|
},
|
|
72
72
|
});
|
|
@@ -88,7 +88,7 @@ export const stepBlock = createReactBlockSpec({
|
|
|
88
88
|
const handleFieldFocus = useCallback(() => {
|
|
89
89
|
editor.setSelection(block.id, block.id);
|
|
90
90
|
}, [editor, block.id]);
|
|
91
|
-
return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, children: [_jsx(StepField, { label: "Step Title", value: stepTitle,
|
|
91
|
+
return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, children: [_jsx(StepField, { label: "Step Title", value: stepTitle, onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: handleFieldFocus, rightAction: !isDataVisible ? (_jsx("button", { type: "button", className: "bn-teststep__toggle", onClick: handleShowDataField, "aria-expanded": "false", tabIndex: -1, children: "+ Step Data" })) : null, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
|
|
92
92
|
if (!uploadImage) {
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
@@ -108,6 +108,6 @@ export const stepBlock = createReactBlockSpec({
|
|
|
108
108
|
catch (error) {
|
|
109
109
|
console.error("Failed to upload image to Step Data", error);
|
|
110
110
|
}
|
|
111
|
-
} }), isDataVisible && (_jsx(StepField, { label: "Step Data", value: stepData,
|
|
111
|
+
} }), isDataVisible && (_jsx(StepField, { label: "Step Data", value: stepData, onChange: handleStepDataChange, autoFocus: shouldFocusDataField, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })), showExpectedField && (_jsx(StepField, { label: "Expected Result", value: expectedResult, onChange: handleExpectedChange, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })), _jsx("button", { type: "button", className: "bn-step-add", onClick: handleInsertNextStep, children: "+ Step" })] }));
|
|
112
112
|
},
|
|
113
113
|
});
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
1
2
|
import { type StepSuggestion } from "../stepAutocomplete";
|
|
2
3
|
import { type SnippetSuggestion } from "../snippetAutocomplete";
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
4
|
type Suggestion = StepSuggestion | SnippetSuggestion;
|
|
5
5
|
type StepFieldProps = {
|
|
6
6
|
label: string;
|
|
7
7
|
value: string;
|
|
8
|
-
placeholder: string;
|
|
9
8
|
onChange: (nextValue: string) => void;
|
|
10
9
|
autoFocus?: boolean;
|
|
11
10
|
multiline?: boolean;
|
|
@@ -23,5 +22,5 @@ type StepFieldProps = {
|
|
|
23
22
|
showImageButton?: boolean;
|
|
24
23
|
onFieldFocus?: () => void;
|
|
25
24
|
};
|
|
26
|
-
export declare function StepField({ label, value,
|
|
25
|
+
export declare function StepField({ label, value, onChange, autoFocus, multiline, enableAutocomplete, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly, showSuggestionsOnFocus, enableImageUpload, onImageFile, rightAction, showFormattingButtons, showImageButton, onFieldFocus, }: StepFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
27
26
|
export {};
|
|
@@ -1,21 +1,239 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import OverType from "overtype";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
4
|
import { useStepAutocomplete } from "../stepAutocomplete";
|
|
3
5
|
import { useStepImageUpload } from "../stepImageUpload";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
import { escapeMarkdownText, normalizePlainText } from "./markdown";
|
|
7
|
+
const READ_ONLY_ALLOWED_KEYS = new Set([
|
|
8
|
+
"ArrowDown",
|
|
9
|
+
"ArrowUp",
|
|
10
|
+
"Enter",
|
|
11
|
+
"Tab",
|
|
12
|
+
]);
|
|
13
|
+
const AUTOCOMPLETE_TRIGGER_KEYS = new Set([" ", "Space"]);
|
|
14
|
+
const markdownParser = OverType.MarkdownParser;
|
|
15
|
+
function markdownToPlainText(markdown) {
|
|
16
|
+
var _a;
|
|
17
|
+
if (!markdown) {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const html = (markdownParser === null || markdownParser === void 0 ? void 0 : markdownParser.parse) ? markdownParser.parse(markdown) : markdown;
|
|
22
|
+
if (typeof document === "undefined") {
|
|
23
|
+
return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
24
|
+
}
|
|
25
|
+
const temp = document.createElement("div");
|
|
26
|
+
temp.innerHTML = html;
|
|
27
|
+
return ((_a = temp.textContent) !== null && _a !== void 0 ? _a : "").replace(/\s+/g, " ").trim();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return markdown.replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/\[[^\]]*]\([^)]+\)/g, "").replace(/[*_`~]/g, "").replace(/\s+/g, " ").trim();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function StepField({ label, value, onChange, autoFocus, multiline = false, enableAutocomplete = false, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly = false, showSuggestionsOnFocus = false, enableImageUpload = false, onImageFile, rightAction, showFormattingButtons = false, showImageButton = false, onFieldFocus, }) {
|
|
13
34
|
const stepSuggestions = useStepAutocomplete();
|
|
14
35
|
const suggestions = suggestionsOverride !== null && suggestionsOverride !== void 0 ? suggestionsOverride : stepSuggestions;
|
|
15
36
|
const uploadImage = useStepImageUpload();
|
|
16
37
|
const fileInputRef = useRef(null);
|
|
38
|
+
const editorContainerRef = useRef(null);
|
|
39
|
+
const editorInstanceRef = useRef(null);
|
|
40
|
+
const [textareaNode, setTextareaNode] = useState(null);
|
|
41
|
+
const autoFocusRef = useRef(false);
|
|
42
|
+
const pendingFocusRef = useRef(false);
|
|
43
|
+
const initialValueRef = useRef(value);
|
|
44
|
+
const onChangeRef = useRef(onChange);
|
|
45
|
+
const [plainTextValue, setPlainTextValue] = useState(() => markdownToPlainText(value));
|
|
46
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
47
|
+
const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(0);
|
|
48
|
+
const [showAllSuggestions, setShowAllSuggestions] = useState(false);
|
|
17
49
|
const [isUploading, setIsUploading] = useState(false);
|
|
18
|
-
const
|
|
50
|
+
const [previewImageUrl, setPreviewImageUrl] = useState(null);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
onChangeRef.current = onChange;
|
|
53
|
+
}, [onChange]);
|
|
54
|
+
const handleEditorChange = useCallback((nextValue) => {
|
|
55
|
+
var _a;
|
|
56
|
+
setPlainTextValue((prev) => {
|
|
57
|
+
const normalized = markdownToPlainText(nextValue);
|
|
58
|
+
return prev === normalized ? prev : normalized;
|
|
59
|
+
});
|
|
60
|
+
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, nextValue);
|
|
61
|
+
}, []);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const container = editorContainerRef.current;
|
|
64
|
+
if (!container) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const [instance] = OverType.init(container, {
|
|
68
|
+
value: initialValueRef.current,
|
|
69
|
+
autoResize: multiline,
|
|
70
|
+
minHeight: multiline ? "4rem" : "2.5rem",
|
|
71
|
+
padding: "0.5rem 0.75rem",
|
|
72
|
+
fontSize: "0.95rem",
|
|
73
|
+
onChange: handleEditorChange,
|
|
74
|
+
});
|
|
75
|
+
editorInstanceRef.current = instance;
|
|
76
|
+
setTextareaNode(instance.textarea);
|
|
77
|
+
return () => {
|
|
78
|
+
instance.destroy();
|
|
79
|
+
editorInstanceRef.current = null;
|
|
80
|
+
setTextareaNode(null);
|
|
81
|
+
};
|
|
82
|
+
}, [handleEditorChange, multiline]);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (pendingFocusRef.current && textareaNode) {
|
|
85
|
+
pendingFocusRef.current = false;
|
|
86
|
+
textareaNode.focus();
|
|
87
|
+
}
|
|
88
|
+
}, [textareaNode]);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const instance = editorInstanceRef.current;
|
|
91
|
+
if (!instance) {
|
|
92
|
+
setPlainTextValue((prev) => {
|
|
93
|
+
const normalized = markdownToPlainText(value);
|
|
94
|
+
return prev === normalized ? prev : normalized;
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (instance.getValue() !== value) {
|
|
99
|
+
instance.setValue(value);
|
|
100
|
+
}
|
|
101
|
+
setPlainTextValue((prev) => {
|
|
102
|
+
const normalized = markdownToPlainText(value);
|
|
103
|
+
return prev === normalized ? prev : normalized;
|
|
104
|
+
});
|
|
105
|
+
}, [value]);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!textareaNode) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (fieldName) {
|
|
111
|
+
textareaNode.dataset.stepField = fieldName;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
delete textareaNode.dataset.stepField;
|
|
115
|
+
}
|
|
116
|
+
}, [fieldName, textareaNode]);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!textareaNode) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
textareaNode.readOnly = readOnly;
|
|
122
|
+
}, [readOnly, textareaNode]);
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!textareaNode) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const handleFocus = () => {
|
|
128
|
+
setIsFocused(true);
|
|
129
|
+
if (showSuggestionsOnFocus && enableAutocomplete) {
|
|
130
|
+
setShowAllSuggestions(true);
|
|
131
|
+
}
|
|
132
|
+
onFieldFocus === null || onFieldFocus === void 0 ? void 0 : onFieldFocus();
|
|
133
|
+
};
|
|
134
|
+
const handleBlur = () => {
|
|
135
|
+
setIsFocused(false);
|
|
136
|
+
setShowAllSuggestions(false);
|
|
137
|
+
};
|
|
138
|
+
textareaNode.addEventListener("focus", handleFocus);
|
|
139
|
+
textareaNode.addEventListener("blur", handleBlur);
|
|
140
|
+
return () => {
|
|
141
|
+
textareaNode.removeEventListener("focus", handleFocus);
|
|
142
|
+
textareaNode.removeEventListener("blur", handleBlur);
|
|
143
|
+
};
|
|
144
|
+
}, [enableAutocomplete, onFieldFocus, showSuggestionsOnFocus, textareaNode]);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!autoFocus || autoFocusRef.current || !textareaNode) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
autoFocusRef.current = true;
|
|
150
|
+
const focus = () => {
|
|
151
|
+
textareaNode.focus();
|
|
152
|
+
if (showSuggestionsOnFocus && enableAutocomplete) {
|
|
153
|
+
setShowAllSuggestions(true);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
if (typeof requestAnimationFrame === "function") {
|
|
157
|
+
const frame = requestAnimationFrame(focus);
|
|
158
|
+
return () => cancelAnimationFrame(frame);
|
|
159
|
+
}
|
|
160
|
+
const timeout = setTimeout(focus, 0);
|
|
161
|
+
return () => clearTimeout(timeout);
|
|
162
|
+
}, [autoFocus, enableAutocomplete, showSuggestionsOnFocus, textareaNode]);
|
|
163
|
+
const insertImageMarkdown = useCallback((url) => {
|
|
164
|
+
var _a, _b, _c;
|
|
165
|
+
const instance = editorInstanceRef.current;
|
|
166
|
+
const textarea = textareaNode;
|
|
167
|
+
if (!instance || !textarea) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const currentValue = instance.getValue();
|
|
171
|
+
const start = (_a = textarea.selectionStart) !== null && _a !== void 0 ? _a : currentValue.length;
|
|
172
|
+
const end = (_b = textarea.selectionEnd) !== null && _b !== void 0 ? _b : currentValue.length;
|
|
173
|
+
const before = currentValue.slice(0, start);
|
|
174
|
+
const after = currentValue.slice(end);
|
|
175
|
+
const needsBeforeNewline = before.length > 0 && !before.endsWith("\n");
|
|
176
|
+
const needsAfterNewline = after.length > 0 && !after.startsWith("\n");
|
|
177
|
+
const insertText = `${needsBeforeNewline ? "\n" : ""}${needsAfterNewline ? "\n" : ""}`;
|
|
178
|
+
const nextValue = `${before}${insertText}${after}`;
|
|
179
|
+
instance.setValue(nextValue);
|
|
180
|
+
setPlainTextValue(markdownToPlainText(nextValue));
|
|
181
|
+
(_c = onChangeRef.current) === null || _c === void 0 ? void 0 : _c.call(onChangeRef, nextValue);
|
|
182
|
+
requestAnimationFrame(() => {
|
|
183
|
+
textarea.selectionStart = start + insertText.length;
|
|
184
|
+
textarea.selectionEnd = start + insertText.length;
|
|
185
|
+
textarea.focus();
|
|
186
|
+
});
|
|
187
|
+
}, [textareaNode]);
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!textareaNode) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const handlePaste = async (event) => {
|
|
193
|
+
var _a, _b;
|
|
194
|
+
if (!onImageFile && !(enableImageUpload && uploadImage)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const items = Array.from((_b = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.items) !== null && _b !== void 0 ? _b : []);
|
|
198
|
+
const imageItem = items.find((item) => item.kind === "file" && item.type.startsWith("image/"));
|
|
199
|
+
const file = imageItem === null || imageItem === void 0 ? void 0 : imageItem.getAsFile();
|
|
200
|
+
if (!file) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
if (onImageFile) {
|
|
205
|
+
await onImageFile(file);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (enableImageUpload && uploadImage) {
|
|
209
|
+
try {
|
|
210
|
+
const result = await uploadImage(file);
|
|
211
|
+
if (result === null || result === void 0 ? void 0 : result.url) {
|
|
212
|
+
insertImageMarkdown(result.url);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
console.error("Failed to upload pasted image", error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const listener = (event) => {
|
|
221
|
+
void handlePaste(event);
|
|
222
|
+
};
|
|
223
|
+
textareaNode.addEventListener("paste", listener);
|
|
224
|
+
return () => {
|
|
225
|
+
textareaNode.removeEventListener("paste", listener);
|
|
226
|
+
};
|
|
227
|
+
}, [enableImageUpload, insertImageMarkdown, onImageFile, textareaNode, uploadImage]);
|
|
228
|
+
const handleToolbarAction = useCallback((action) => {
|
|
229
|
+
var _a;
|
|
230
|
+
const shortcuts = (_a = editorInstanceRef.current) === null || _a === void 0 ? void 0 : _a.shortcuts;
|
|
231
|
+
if (!textareaNode || !(shortcuts === null || shortcuts === void 0 ? void 0 : shortcuts.handleAction)) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
textareaNode.focus();
|
|
235
|
+
shortcuts.handleAction(action);
|
|
236
|
+
}, [textareaNode]);
|
|
19
237
|
const suggestionPool = useMemo(() => {
|
|
20
238
|
if (!suggestionFilter) {
|
|
21
239
|
return suggestions;
|
|
@@ -23,6 +241,12 @@ export function StepField({ label, value, placeholder, onChange, autoFocus, mult
|
|
|
23
241
|
const filtered = suggestions.filter(suggestionFilter);
|
|
24
242
|
return filtered.length > 0 ? filtered : suggestions;
|
|
25
243
|
}, [suggestionFilter, suggestions]);
|
|
244
|
+
const normalizedQuery = normalizePlainText(plainTextValue);
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
if (normalizedQuery.length > 0) {
|
|
247
|
+
setShowAllSuggestions(false);
|
|
248
|
+
}
|
|
249
|
+
}, [normalizedQuery]);
|
|
26
250
|
const filteredSuggestions = useMemo(() => {
|
|
27
251
|
if (!enableAutocomplete) {
|
|
28
252
|
return [];
|
|
@@ -41,169 +265,164 @@ export function StepField({ label, value, placeholder, onChange, autoFocus, mult
|
|
|
41
265
|
useEffect(() => {
|
|
42
266
|
setActiveSuggestionIndex(0);
|
|
43
267
|
}, [normalizedQuery, filteredSuggestions.length, showAllSuggestions]);
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
268
|
+
const extractedImages = useMemo(() => {
|
|
269
|
+
if (!value) {
|
|
270
|
+
return [];
|
|
47
271
|
}
|
|
48
|
-
|
|
49
|
-
|
|
272
|
+
const regex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
273
|
+
const results = [];
|
|
274
|
+
let match;
|
|
275
|
+
while ((match = regex.exec(value)) !== null) {
|
|
276
|
+
const [, alt = "", url = ""] = match;
|
|
277
|
+
results.push({
|
|
278
|
+
id: `${match.index}-${url}-${results.length}`,
|
|
279
|
+
url,
|
|
280
|
+
alt,
|
|
281
|
+
start: match.index,
|
|
282
|
+
end: match.index + match[0].length,
|
|
283
|
+
markdown: match[0],
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return results;
|
|
287
|
+
}, [value]);
|
|
288
|
+
const handleRemoveImage = useCallback((image) => {
|
|
50
289
|
var _a;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
290
|
+
const before = value.slice(0, image.start);
|
|
291
|
+
const after = value.slice(image.end);
|
|
292
|
+
const nextValue = `${before}${after}`.replace(/\n{3,}/g, "\n\n");
|
|
293
|
+
if (editorInstanceRef.current) {
|
|
294
|
+
editorInstanceRef.current.setValue(nextValue);
|
|
54
295
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
296
|
+
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, nextValue);
|
|
297
|
+
setPlainTextValue(markdownToPlainText(nextValue));
|
|
298
|
+
setPreviewImageUrl((prev) => (prev === image.url ? null : prev));
|
|
299
|
+
}, [value]);
|
|
300
|
+
const handleImageClick = useCallback((url) => {
|
|
301
|
+
setPreviewImageUrl(url);
|
|
302
|
+
}, []);
|
|
303
|
+
const focusAdjacentField = useCallback((direction) => {
|
|
304
|
+
if (!textareaNode || typeof document === "undefined") {
|
|
305
|
+
return false;
|
|
58
306
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
307
|
+
const selector = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"], [data-step-field]';
|
|
308
|
+
const focusable = Array.from(document.querySelectorAll(selector)).filter((element) => {
|
|
309
|
+
if (element.getAttribute("aria-hidden") === "true" || element.tabIndex === -1 || element.hasAttribute("disabled")) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
const isVisible = element.offsetWidth > 0 || element.offsetHeight > 0 || element.getClientRects().length > 0;
|
|
313
|
+
return isVisible;
|
|
314
|
+
});
|
|
315
|
+
const currentIndex = focusable.findIndex((element) => element === textareaNode);
|
|
316
|
+
const target = currentIndex === -1 ? null : focusable[currentIndex + direction];
|
|
317
|
+
if (!target) {
|
|
318
|
+
return false;
|
|
62
319
|
}
|
|
63
|
-
|
|
64
|
-
|
|
320
|
+
target.focus();
|
|
321
|
+
return true;
|
|
322
|
+
}, [textareaNode]);
|
|
323
|
+
const applySuggestion = useCallback((suggestion) => {
|
|
65
324
|
var _a;
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const markdown = htmlToMarkdown(element.innerHTML);
|
|
71
|
-
if (markdown !== value) {
|
|
72
|
-
onChange(markdown);
|
|
73
|
-
}
|
|
74
|
-
setPlainTextValue((_a = element.innerText) !== null && _a !== void 0 ? _a : "");
|
|
75
|
-
if (!markdown && element.innerHTML !== "") {
|
|
76
|
-
element.innerHTML = "";
|
|
325
|
+
const escaped = escapeMarkdownText(suggestion.title);
|
|
326
|
+
const instance = editorInstanceRef.current;
|
|
327
|
+
if (instance) {
|
|
328
|
+
instance.setValue(escaped);
|
|
77
329
|
}
|
|
78
|
-
|
|
330
|
+
setPlainTextValue(suggestion.title);
|
|
331
|
+
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, escaped);
|
|
332
|
+
onSuggestionSelect === null || onSuggestionSelect === void 0 ? void 0 : onSuggestionSelect(suggestion);
|
|
333
|
+
setActiveSuggestionIndex(0);
|
|
334
|
+
setShowAllSuggestions(false);
|
|
335
|
+
requestAnimationFrame(() => {
|
|
336
|
+
textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.focus();
|
|
337
|
+
if (textareaNode) {
|
|
338
|
+
textareaNode.selectionStart = escaped.length;
|
|
339
|
+
textareaNode.selectionEnd = escaped.length;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}, [onSuggestionSelect, textareaNode]);
|
|
343
|
+
const keydownHandlerRef = useRef(null);
|
|
79
344
|
useEffect(() => {
|
|
80
|
-
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
autoFocusRef.current = true;
|
|
84
|
-
const element = editorRef.current;
|
|
85
|
-
const focusElement = () => {
|
|
345
|
+
keydownHandlerRef.current = (event) => {
|
|
86
346
|
var _a;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (selection) {
|
|
94
|
-
selection.selectAllChildren(element);
|
|
95
|
-
selection.collapseToEnd();
|
|
347
|
+
if (readOnly) {
|
|
348
|
+
const openKeys = enableAutocomplete && (event.metaKey || event.ctrlKey) && AUTOCOMPLETE_TRIGGER_KEYS.has(event.code);
|
|
349
|
+
if (!READ_ONLY_ALLOWED_KEYS.has(event.key) && !openKeys) {
|
|
350
|
+
event.preventDefault();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
96
353
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
const timeout = setTimeout(focusElement, 0);
|
|
103
|
-
return () => clearTimeout(timeout);
|
|
104
|
-
}, [autoFocus, enableAutocomplete, showSuggestionsOnFocus]);
|
|
105
|
-
const ensureCaretInEditor = useCallback(() => {
|
|
106
|
-
var _a;
|
|
107
|
-
const element = editorRef.current;
|
|
108
|
-
if (!element) {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
const selection = (_a = window.getSelection) === null || _a === void 0 ? void 0 : _a.call(window);
|
|
112
|
-
if (!selection) {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
if (selection.rangeCount === 0 || !element.contains(selection.anchorNode)) {
|
|
116
|
-
const range = document.createRange();
|
|
117
|
-
range.selectNodeContents(element);
|
|
118
|
-
range.collapse(false);
|
|
119
|
-
selection.removeAllRanges();
|
|
120
|
-
selection.addRange(range);
|
|
121
|
-
}
|
|
122
|
-
element.focus();
|
|
123
|
-
return true;
|
|
124
|
-
}, []);
|
|
125
|
-
const handlePaste = useCallback(async (event) => {
|
|
126
|
-
var _a, _b, _c, _d, _e;
|
|
127
|
-
if ((enableImageUpload && uploadImage) || onImageFile) {
|
|
128
|
-
const items = Array.from((_a = event.clipboardData.items) !== null && _a !== void 0 ? _a : []);
|
|
129
|
-
const imageItem = items.find((item) => item.kind === "file" && item.type.startsWith("image/"));
|
|
130
|
-
const file = imageItem === null || imageItem === void 0 ? void 0 : imageItem.getAsFile();
|
|
131
|
-
if (file) {
|
|
132
|
-
event.preventDefault();
|
|
133
|
-
if (onImageFile) {
|
|
134
|
-
await onImageFile(file);
|
|
354
|
+
if (enableAutocomplete && shouldShowAutocomplete) {
|
|
355
|
+
if (event.key === "ArrowDown") {
|
|
356
|
+
event.preventDefault();
|
|
357
|
+
setActiveSuggestionIndex((prev) => prev + 1 >= filteredSuggestions.length ? 0 : prev + 1);
|
|
135
358
|
return;
|
|
136
359
|
}
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
console.error("Failed to upload image from paste", error);
|
|
360
|
+
if (event.key === "ArrowUp") {
|
|
361
|
+
event.preventDefault();
|
|
362
|
+
setActiveSuggestionIndex((prev) => prev - 1 < 0 ? filteredSuggestions.length - 1 : prev - 1);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
366
|
+
event.preventDefault();
|
|
367
|
+
const suggestion = (_a = filteredSuggestions[activeSuggestionIndex]) !== null && _a !== void 0 ? _a : filteredSuggestions[0];
|
|
368
|
+
if (suggestion) {
|
|
369
|
+
applySuggestion(suggestion);
|
|
151
370
|
}
|
|
152
371
|
return;
|
|
153
372
|
}
|
|
154
373
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
setPlainTextValue(suggestion.title);
|
|
169
|
-
setActiveSuggestionIndex(0);
|
|
170
|
-
setShowAllSuggestions(false);
|
|
171
|
-
if (editorRef.current) {
|
|
172
|
-
editorRef.current.innerHTML = markdownToHtml(escaped);
|
|
173
|
-
editorRef.current.focus();
|
|
174
|
-
const selection = typeof window !== "undefined" ? (_a = window.getSelection) === null || _a === void 0 ? void 0 : _a.call(window) : null;
|
|
175
|
-
if (selection && editorRef.current.firstChild) {
|
|
176
|
-
const range = document.createRange();
|
|
177
|
-
range.selectNodeContents(editorRef.current);
|
|
178
|
-
range.collapse(false);
|
|
179
|
-
selection.removeAllRanges();
|
|
180
|
-
selection.addRange(range);
|
|
374
|
+
if (enableAutocomplete &&
|
|
375
|
+
(event.metaKey || event.ctrlKey) &&
|
|
376
|
+
(AUTOCOMPLETE_TRIGGER_KEYS.has(event.code) || AUTOCOMPLETE_TRIGGER_KEYS.has(event.key))) {
|
|
377
|
+
event.preventDefault();
|
|
378
|
+
setShowAllSuggestions(true);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (event.key === "Tab") {
|
|
382
|
+
const moved = focusAdjacentField(event.shiftKey ? -1 : 1);
|
|
383
|
+
if (moved) {
|
|
384
|
+
event.preventDefault();
|
|
385
|
+
event.stopImmediatePropagation();
|
|
386
|
+
}
|
|
181
387
|
}
|
|
388
|
+
};
|
|
389
|
+
}, [activeSuggestionIndex, applySuggestion, enableAutocomplete, filteredSuggestions, focusAdjacentField, readOnly, shouldShowAutocomplete]);
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (!textareaNode) {
|
|
392
|
+
return;
|
|
182
393
|
}
|
|
183
|
-
|
|
394
|
+
const listener = (event) => {
|
|
395
|
+
var _a;
|
|
396
|
+
(_a = keydownHandlerRef.current) === null || _a === void 0 ? void 0 : _a.call(keydownHandlerRef, event);
|
|
397
|
+
};
|
|
398
|
+
const keydownOptions = { capture: true };
|
|
399
|
+
textareaNode.addEventListener("keydown", listener, keydownOptions);
|
|
400
|
+
return () => {
|
|
401
|
+
textareaNode.removeEventListener("keydown", listener, keydownOptions);
|
|
402
|
+
};
|
|
403
|
+
}, [textareaNode]);
|
|
404
|
+
const editorClassName = [
|
|
405
|
+
"bn-step-editor",
|
|
406
|
+
multiline ? "bn-step-editor--multiline" : "",
|
|
407
|
+
isFocused ? "bn-step-editor--focused" : "",
|
|
408
|
+
readOnly ? "bn-step-editor--readonly" : "",
|
|
409
|
+
]
|
|
410
|
+
.filter(Boolean)
|
|
411
|
+
.join(" ");
|
|
184
412
|
return (_jsxs("div", { className: "bn-step-field", children: [_jsxs("div", { className: "bn-step-field__top", children: [_jsxs("span", { className: "bn-step-field__label", children: [label, enableAutocomplete && (_jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
|
|
185
|
-
var _a;
|
|
186
413
|
event.preventDefault();
|
|
187
414
|
setShowAllSuggestions(true);
|
|
188
|
-
|
|
415
|
+
textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.focus();
|
|
189
416
|
}, "aria-label": "Show suggestions", tabIndex: -1, children: "\u2304" }))] }), _jsxs("div", { className: "bn-step-toolbar", "aria-label": `${label} controls`, children: [showFormattingButtons && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
|
|
190
|
-
var _a;
|
|
191
417
|
event.preventDefault();
|
|
192
|
-
(
|
|
193
|
-
document.execCommand("bold");
|
|
194
|
-
syncValue();
|
|
418
|
+
handleToolbarAction("toggleBold");
|
|
195
419
|
}, "aria-label": "Bold", tabIndex: -1, children: "B" }), _jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
|
|
196
|
-
var _a;
|
|
197
420
|
event.preventDefault();
|
|
198
|
-
(
|
|
199
|
-
document.execCommand("italic");
|
|
200
|
-
syncValue();
|
|
421
|
+
handleToolbarAction("toggleItalic");
|
|
201
422
|
}, "aria-label": "Italic", tabIndex: -1, children: "I" })] })), enableImageUpload && uploadImage && showImageButton && (_jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
|
|
423
|
+
var _a;
|
|
202
424
|
event.preventDefault();
|
|
203
|
-
|
|
204
|
-
if (input) {
|
|
205
|
-
input.click();
|
|
206
|
-
}
|
|
425
|
+
(_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
207
426
|
}, "aria-label": "Insert image", tabIndex: -1, disabled: isUploading, children: "Img" })), rightAction] })] }), enableImageUpload && (_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", style: { display: "none" }, onChange: async (event) => {
|
|
208
427
|
var _a;
|
|
209
428
|
const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
@@ -214,17 +433,7 @@ export function StepField({ label, value, placeholder, onChange, autoFocus, mult
|
|
|
214
433
|
setIsUploading(true);
|
|
215
434
|
const response = await uploadImage(file);
|
|
216
435
|
if (response === null || response === void 0 ? void 0 : response.url) {
|
|
217
|
-
|
|
218
|
-
if (element) {
|
|
219
|
-
const escapedUrl = escapeHtml(response.url);
|
|
220
|
-
const needsBreak = element.innerHTML.trim().length > 0;
|
|
221
|
-
const imgHtml = (needsBreak ? "<br />" : "") +
|
|
222
|
-
`<img src="${escapedUrl}" alt="" class="bn-inline-image" contenteditable="false" draggable="false" />`;
|
|
223
|
-
element.focus();
|
|
224
|
-
ensureCaretInEditor();
|
|
225
|
-
document.execCommand("insertHTML", false, imgHtml);
|
|
226
|
-
syncValue();
|
|
227
|
-
}
|
|
436
|
+
insertImageMarkdown(response.url);
|
|
228
437
|
}
|
|
229
438
|
}
|
|
230
439
|
catch (error) {
|
|
@@ -234,84 +443,22 @@ export function StepField({ label, value, placeholder, onChange, autoFocus, mult
|
|
|
234
443
|
setIsUploading(false);
|
|
235
444
|
event.target.value = "";
|
|
236
445
|
}
|
|
237
|
-
} })), _jsx("div", { ref:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
setShowAllSuggestions(true);
|
|
242
|
-
}
|
|
243
|
-
onFieldFocus === null || onFieldFocus === void 0 ? void 0 : onFieldFocus();
|
|
244
|
-
setPlainTextValue((_b = (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.innerText) !== null && _b !== void 0 ? _b : "");
|
|
245
|
-
}, onBlur: () => {
|
|
246
|
-
setIsFocused(false);
|
|
247
|
-
syncValue();
|
|
248
|
-
}, onInput: readOnly ? undefined : syncValue, onPaste: readOnly ? (event) => event.preventDefault() : handlePaste, onKeyDown: (event) => {
|
|
249
|
-
var _a, _b;
|
|
250
|
-
if (readOnly) {
|
|
251
|
-
const allowedKeys = new Set([
|
|
252
|
-
"ArrowDown",
|
|
253
|
-
"ArrowUp",
|
|
254
|
-
"Enter",
|
|
255
|
-
"Tab",
|
|
256
|
-
]);
|
|
257
|
-
const openKeys = enableAutocomplete && (event.metaKey || event.ctrlKey) && (event.code === "Space" || event.key === "" || event.key === " ");
|
|
258
|
-
if (!allowedKeys.has(event.key) && !openKeys) {
|
|
259
|
-
event.preventDefault();
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if ((event.key === "a" || event.key === "A") && (event.metaKey || event.ctrlKey)) {
|
|
264
|
-
event.preventDefault();
|
|
265
|
-
const selection = (_a = window.getSelection) === null || _a === void 0 ? void 0 : _a.call(window);
|
|
266
|
-
const node = editorRef.current;
|
|
267
|
-
if (selection && node) {
|
|
268
|
-
const range = document.createRange();
|
|
269
|
-
range.selectNodeContents(node);
|
|
270
|
-
selection.removeAllRanges();
|
|
271
|
-
selection.addRange(range);
|
|
272
|
-
}
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (enableAutocomplete && shouldShowAutocomplete) {
|
|
276
|
-
if (event.key === "ArrowDown") {
|
|
277
|
-
event.preventDefault();
|
|
278
|
-
setActiveSuggestionIndex((prev) => prev + 1 >= filteredSuggestions.length ? 0 : prev + 1);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
if (event.key === "ArrowUp") {
|
|
282
|
-
event.preventDefault();
|
|
283
|
-
setActiveSuggestionIndex((prev) => prev - 1 < 0 ? filteredSuggestions.length - 1 : prev - 1);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
if (event.key === "Enter" || event.key === "Tab") {
|
|
287
|
-
event.preventDefault();
|
|
288
|
-
const suggestion = (_b = filteredSuggestions[activeSuggestionIndex]) !== null && _b !== void 0 ? _b : filteredSuggestions[0];
|
|
289
|
-
if (suggestion) {
|
|
290
|
-
applySuggestion(suggestion);
|
|
291
|
-
}
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (enableAutocomplete && (event.metaKey || event.ctrlKey) && (event.code === "Space" || event.key === "" || event.key === " ")) {
|
|
296
|
-
event.preventDefault();
|
|
297
|
-
setShowAllSuggestions(true);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
if (event.key === "Enter") {
|
|
301
|
-
event.preventDefault();
|
|
302
|
-
if (multiline && event.shiftKey) {
|
|
303
|
-
document.execCommand("insertLineBreak");
|
|
304
|
-
document.execCommand("insertLineBreak");
|
|
446
|
+
} })), _jsx("div", { ref: editorContainerRef, className: editorClassName, "data-step-field": fieldName, tabIndex: -1, onFocus: (event) => {
|
|
447
|
+
if (event.target === editorContainerRef.current) {
|
|
448
|
+
if (textareaNode) {
|
|
449
|
+
textareaNode.focus();
|
|
305
450
|
}
|
|
306
451
|
else {
|
|
307
|
-
|
|
452
|
+
pendingFocusRef.current = true;
|
|
308
453
|
}
|
|
309
|
-
syncValue();
|
|
310
454
|
}
|
|
311
|
-
} }),
|
|
455
|
+
} }), extractedImages.length > 0 && (_jsx("div", { className: "bn-step-images", role: "list", children: extractedImages.map((image) => (_jsxs("div", { className: "bn-step-image-thumb", role: "listitem", children: [_jsx("button", { type: "button", className: "bn-step-image-thumb__button", onClick: () => handleImageClick(image.url), "aria-label": "Preview image", children: _jsx("img", { src: image.url, alt: image.alt || "Step image" }) }), _jsx("button", { type: "button", className: "bn-step-image-thumb__remove", onClick: (event) => {
|
|
456
|
+
event.stopPropagation();
|
|
457
|
+
handleRemoveImage(image);
|
|
458
|
+
}, "aria-label": "Remove image", children: "\u00D7" })] }, image.id))) })), shouldShowAutocomplete && (_jsx("div", { className: "bn-step-suggestions", role: "listbox", "aria-label": `${label} suggestions`, children: filteredSuggestions.map((suggestion, index) => (_jsxs("button", { type: "button", role: "option", "aria-selected": index === activeSuggestionIndex, className: index === activeSuggestionIndex
|
|
312
459
|
? "bn-step-suggestion bn-step-suggestion--active"
|
|
313
460
|
: "bn-step-suggestion", onMouseDown: (event) => {
|
|
314
461
|
event.preventDefault();
|
|
315
462
|
applySuggestion(suggestion);
|
|
316
|
-
}, tabIndex: -1, children: [_jsx("span", { className: "bn-step-suggestion__title", children: suggestion.title }), typeof suggestion.usageCount === "number" && suggestion.usageCount > 0 && (_jsxs("span", { className: "bn-step-suggestion__meta", children: [suggestion.usageCount, " uses"] }))] }, suggestion.id))) }))] }));
|
|
463
|
+
}, tabIndex: -1, children: [_jsx("span", { className: "bn-step-suggestion__title", children: suggestion.title }), typeof suggestion.usageCount === "number" && suggestion.usageCount > 0 && (_jsxs("span", { className: "bn-step-suggestion__meta", children: [suggestion.usageCount, " uses"] }))] }, suggestion.id))) })), previewImageUrl && (_jsx("div", { className: "bn-step-image-preview", role: "dialog", "aria-label": "Image preview", onClick: () => setPreviewImageUrl(null), children: _jsxs("div", { className: "bn-step-image-preview__content", role: "document", onClick: (event) => event.stopPropagation(), children: [_jsx("img", { src: previewImageUrl, alt: "Full size step" }), _jsx("button", { type: "button", className: "bn-step-image-preview__close", onClick: () => setPreviewImageUrl(null), "aria-label": "Close preview", children: "\u00D7" })] }) }))] }));
|
|
317
464
|
}
|
package/package/styles.css
CHANGED
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
border-color: rgba(16, 185, 129, 0.25);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
.bn-snippet .bn-step-editor
|
|
154
|
+
.bn-snippet .bn-step-editor.bn-step-editor--focused {
|
|
155
155
|
border-color: rgba(16, 185, 129, 0.7);
|
|
156
156
|
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
|
|
157
157
|
}
|
|
@@ -275,35 +275,158 @@
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
.bn-step-editor {
|
|
278
|
+
position: relative;
|
|
278
279
|
border-radius: 0.6rem;
|
|
279
280
|
border: 1px solid rgba(37, 99, 235, 0.25);
|
|
280
|
-
padding: 0.5rem 0.75rem;
|
|
281
|
-
font-size: 0.95rem;
|
|
282
281
|
background: rgba(255, 255, 255, 0.92);
|
|
283
282
|
transition:
|
|
284
283
|
border-color 120ms ease,
|
|
285
284
|
box-shadow 120ms ease;
|
|
286
285
|
min-height: 2.5rem;
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
font-size: 0.95rem;
|
|
287
|
+
overflow: hidden;
|
|
289
288
|
}
|
|
290
289
|
|
|
291
|
-
.bn-step-editor
|
|
290
|
+
.bn-step-editor--multiline {
|
|
292
291
|
min-height: 4rem;
|
|
293
292
|
}
|
|
294
293
|
|
|
295
|
-
.bn-step-editor
|
|
294
|
+
.bn-step-editor.bn-step-editor--focused {
|
|
296
295
|
outline: none;
|
|
297
296
|
border-color: rgba(37, 99, 235, 0.7);
|
|
298
297
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
|
299
298
|
}
|
|
300
299
|
|
|
301
|
-
.bn-step-editor
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
.bn-step-editor .overtype-container {
|
|
301
|
+
height: 100%;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.bn-step-editor .overtype-wrapper {
|
|
305
|
+
min-height: 100%;
|
|
306
|
+
background: transparent !important;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.bn-step-editor .overtype-wrapper .overtype-input,
|
|
310
|
+
.bn-step-editor .overtype-wrapper .overtype-preview {
|
|
311
|
+
padding: 0.5rem 0.75rem !important;
|
|
312
|
+
min-height: inherit !important;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.bn-step-editor .overtype-wrapper .overtype-preview {
|
|
316
|
+
color: #0f172a !important;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.bn-step-editor .overtype-wrapper .overtype-preview img {
|
|
320
|
+
display: block;
|
|
321
|
+
max-width: 100%;
|
|
322
|
+
border-radius: 0.65rem;
|
|
323
|
+
margin: 0.5rem 0;
|
|
304
324
|
pointer-events: none;
|
|
305
325
|
}
|
|
306
326
|
|
|
327
|
+
.bn-step-field:not(:focus-within) .overtype-wrapper .overtype-input::selection {
|
|
328
|
+
background-color: transparent !important;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.bn-step-field:focus-within .overtype-wrapper .overtype-input::selection {
|
|
332
|
+
background-color: rgba(244, 211, 94, 0.4) !important;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.bn-step-images {
|
|
336
|
+
display: flex;
|
|
337
|
+
flex-wrap: wrap;
|
|
338
|
+
gap: 0.5rem;
|
|
339
|
+
margin-top: 0.35rem;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.bn-step-image-thumb {
|
|
343
|
+
position: relative;
|
|
344
|
+
width: 4rem;
|
|
345
|
+
height: 4rem;
|
|
346
|
+
border-radius: 0.5rem;
|
|
347
|
+
border: 1px solid rgba(37, 99, 235, 0.25);
|
|
348
|
+
overflow: hidden;
|
|
349
|
+
background: rgba(15, 23, 42, 0.05);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.bn-step-image-thumb__button {
|
|
353
|
+
width: 100%;
|
|
354
|
+
height: 100%;
|
|
355
|
+
border: none;
|
|
356
|
+
padding: 0;
|
|
357
|
+
background: transparent;
|
|
358
|
+
cursor: pointer;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.bn-step-image-thumb__button img {
|
|
362
|
+
width: 100%;
|
|
363
|
+
height: 100%;
|
|
364
|
+
object-fit: cover;
|
|
365
|
+
display: block;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.bn-step-image-thumb__remove {
|
|
369
|
+
position: absolute;
|
|
370
|
+
top: 0.15rem;
|
|
371
|
+
right: 0.15rem;
|
|
372
|
+
border: none;
|
|
373
|
+
border-radius: 999px;
|
|
374
|
+
width: 1.35rem;
|
|
375
|
+
height: 1.35rem;
|
|
376
|
+
background: rgba(15, 23, 42, 0.75);
|
|
377
|
+
color: white;
|
|
378
|
+
cursor: pointer;
|
|
379
|
+
opacity: 0;
|
|
380
|
+
transition: opacity 120ms ease;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.bn-step-image-thumb:hover .bn-step-image-thumb__remove,
|
|
384
|
+
.bn-step-image-thumb__remove:focus-visible {
|
|
385
|
+
opacity: 1;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.bn-step-image-preview {
|
|
389
|
+
position: fixed;
|
|
390
|
+
inset: 0;
|
|
391
|
+
background: rgba(15, 23, 42, 0.75);
|
|
392
|
+
display: flex;
|
|
393
|
+
align-items: center;
|
|
394
|
+
justify-content: center;
|
|
395
|
+
padding: 1.5rem;
|
|
396
|
+
z-index: 15;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.bn-step-image-preview__content {
|
|
400
|
+
position: relative;
|
|
401
|
+
max-width: min(90vw, 800px);
|
|
402
|
+
width: 100%;
|
|
403
|
+
background: white;
|
|
404
|
+
border-radius: 0.75rem;
|
|
405
|
+
padding: 1rem;
|
|
406
|
+
box-shadow: 0 25px 45px rgba(15, 23, 42, 0.35);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.bn-step-image-preview__content img {
|
|
410
|
+
width: 100%;
|
|
411
|
+
height: auto;
|
|
412
|
+
display: block;
|
|
413
|
+
border-radius: 0.5rem;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.bn-step-image-preview__close {
|
|
417
|
+
position: absolute;
|
|
418
|
+
top: 0.35rem;
|
|
419
|
+
right: 0.35rem;
|
|
420
|
+
border: none;
|
|
421
|
+
background: rgba(15, 23, 42, 0.65);
|
|
422
|
+
color: white;
|
|
423
|
+
border-radius: 999px;
|
|
424
|
+
width: 2rem;
|
|
425
|
+
height: 2rem;
|
|
426
|
+
cursor: pointer;
|
|
427
|
+
font-size: 1.1rem;
|
|
428
|
+
}
|
|
429
|
+
|
|
307
430
|
.bn-step-suggestions {
|
|
308
431
|
position: absolute;
|
|
309
432
|
left: 0;
|
package/package.json
CHANGED
|
@@ -139,12 +139,13 @@ function fallbackHtmlToMarkdown(html: string): string {
|
|
|
139
139
|
|
|
140
140
|
result = result.replace(/<\/?[^>]+>/g, "");
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
const markdown = result
|
|
143
143
|
.split("\n")
|
|
144
144
|
.map((line) => line.trimEnd())
|
|
145
145
|
.join("\n")
|
|
146
146
|
.replace(/\n{3,}/g, "\n\n")
|
|
147
147
|
.trim();
|
|
148
|
+
return cleanupEscapedFormatting(markdown);
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
export function htmlToMarkdown(html: string): string {
|
|
@@ -195,5 +196,14 @@ export function htmlToMarkdown(html: string): string {
|
|
|
195
196
|
};
|
|
196
197
|
|
|
197
198
|
const markdown = Array.from(temp.childNodes).map(traverse).join("");
|
|
198
|
-
return markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
199
|
+
return cleanupEscapedFormatting(markdown).replace(/\n{3,}/g, "\n\n").trim();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function cleanupEscapedFormatting(markdown: string): string {
|
|
203
|
+
return markdown.replace(/\\([*_])([^]*?)\\\1/g, (match, marker, inner) => {
|
|
204
|
+
if (inner.includes("\\")) {
|
|
205
|
+
return match;
|
|
206
|
+
}
|
|
207
|
+
return `${marker}${inner}${marker}`;
|
|
208
|
+
});
|
|
199
209
|
}
|
|
@@ -157,6 +157,21 @@ describe("blocksToMarkdown", () => {
|
|
|
157
157
|
);
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
+
it("cleans escaped formatting markers when toggling styles repeatedly", () => {
|
|
161
|
+
const blocks: CustomEditorBlock[] = [
|
|
162
|
+
{
|
|
163
|
+
id: "esc1",
|
|
164
|
+
type: "paragraph",
|
|
165
|
+
props: baseProps,
|
|
166
|
+
content: [{ type: "text", text: "text", styles: { bold: true, italic: true } }],
|
|
167
|
+
children: [],
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const markdown = blocksToMarkdown(blocks);
|
|
172
|
+
expect(markdown).toBe("***text***");
|
|
173
|
+
});
|
|
174
|
+
|
|
160
175
|
it("keeps inline formatting inside step fields", () => {
|
|
161
176
|
const blocks: CustomEditorBlock[] = [
|
|
162
177
|
{
|