testomatio-editor-blocks 0.3.0 → 0.4.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/snippet.js +127 -2
- package/package/editor/blocks/step.js +82 -15
- package/package/editor/blocks/stepField.d.ts +6 -1
- package/package/editor/blocks/stepField.js +20 -2
- package/package/editor/blocks/useAutoResize.d.ts +8 -0
- package/package/editor/blocks/useAutoResize.js +31 -0
- package/package/editor/customMarkdownConverter.js +150 -21
- package/package/styles.css +204 -71
- package/package.json +5 -2
- package/src/App.tsx +1 -1
- package/src/editor/blocks/markdown.ts +27 -7
- package/src/editor/blocks/snippet.tsx +202 -26
- package/src/editor/blocks/step.tsx +132 -36
- package/src/editor/blocks/stepField.tsx +552 -267
- package/src/editor/blocks/useAutoResize.ts +44 -0
- package/src/editor/customMarkdownConverter.test.ts +99 -2
- package/src/editor/customMarkdownConverter.ts +166 -19
- package/src/editor/customSchema.test.ts +35 -0
- package/src/editor/markdownToBlocks.test.ts +119 -0
- package/src/editor/styles.css +342 -71
|
@@ -1,8 +1,131 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createReactBlockSpec } from "@blocknote/react";
|
|
3
|
-
import
|
|
3
|
+
import OverType from "overtype";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
5
|
import { StepField } from "./stepField";
|
|
5
6
|
import { useSnippetAutocomplete } from "../snippetAutocomplete";
|
|
7
|
+
import { useStepImageUpload } from "../stepImageUpload";
|
|
8
|
+
function SnippetDataField({ label, value, placeholder, onChange, onFieldFocus, fieldName, enableImageUpload = false, }) {
|
|
9
|
+
const containerRef = useRef(null);
|
|
10
|
+
const instanceRef = useRef(null);
|
|
11
|
+
const uploadImage = useStepImageUpload();
|
|
12
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
13
|
+
const onChangeRef = useRef(onChange);
|
|
14
|
+
const initialValueRef = useRef(value);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
onChangeRef.current = onChange;
|
|
17
|
+
}, [onChange]);
|
|
18
|
+
const insertImageMarkdown = useCallback((url) => {
|
|
19
|
+
var _a, _b, _c;
|
|
20
|
+
const instance = instanceRef.current;
|
|
21
|
+
const textarea = instance === null || instance === void 0 ? void 0 : instance.textarea;
|
|
22
|
+
if (!instance || !textarea) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const currentValue = instance.getValue();
|
|
26
|
+
const selectionStart = (_a = textarea.selectionStart) !== null && _a !== void 0 ? _a : currentValue.length;
|
|
27
|
+
const selectionEnd = (_b = textarea.selectionEnd) !== null && _b !== void 0 ? _b : currentValue.length;
|
|
28
|
+
const before = currentValue.slice(0, selectionStart);
|
|
29
|
+
const after = currentValue.slice(selectionEnd);
|
|
30
|
+
const needsNewlineBefore = before.length > 0 && !before.endsWith("\n");
|
|
31
|
+
const needsNewlineAfter = after.length > 0 && !after.startsWith("\n");
|
|
32
|
+
const markdown = `${needsNewlineBefore ? "\n" : ""}${needsNewlineAfter ? "\n" : ""}`;
|
|
33
|
+
const nextValue = `${before}${markdown}${after}`;
|
|
34
|
+
instance.setValue(nextValue);
|
|
35
|
+
(_c = onChangeRef.current) === null || _c === void 0 ? void 0 : _c.call(onChangeRef, nextValue);
|
|
36
|
+
requestAnimationFrame(() => {
|
|
37
|
+
const cursor = selectionStart + markdown.length;
|
|
38
|
+
textarea.selectionStart = cursor;
|
|
39
|
+
textarea.selectionEnd = cursor;
|
|
40
|
+
textarea.focus();
|
|
41
|
+
});
|
|
42
|
+
}, []);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const container = containerRef.current;
|
|
45
|
+
if (!container) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const [instance] = OverType.init(container, {
|
|
49
|
+
value: initialValueRef.current,
|
|
50
|
+
placeholder,
|
|
51
|
+
autoResize: true,
|
|
52
|
+
minHeight: "5rem",
|
|
53
|
+
toolbar: false,
|
|
54
|
+
onChange: (nextValue) => {
|
|
55
|
+
var _a;
|
|
56
|
+
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, nextValue);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
instanceRef.current = instance;
|
|
60
|
+
const textarea = instance.textarea;
|
|
61
|
+
if (fieldName) {
|
|
62
|
+
textarea.dataset.stepField = fieldName;
|
|
63
|
+
}
|
|
64
|
+
const handleFocus = () => {
|
|
65
|
+
setIsFocused(true);
|
|
66
|
+
onFieldFocus === null || onFieldFocus === void 0 ? void 0 : onFieldFocus();
|
|
67
|
+
};
|
|
68
|
+
const handleBlur = () => setIsFocused(false);
|
|
69
|
+
textarea.addEventListener("focus", handleFocus);
|
|
70
|
+
textarea.addEventListener("blur", handleBlur);
|
|
71
|
+
return () => {
|
|
72
|
+
textarea.removeEventListener("focus", handleFocus);
|
|
73
|
+
textarea.removeEventListener("blur", handleBlur);
|
|
74
|
+
instance.destroy();
|
|
75
|
+
instanceRef.current = null;
|
|
76
|
+
};
|
|
77
|
+
}, [fieldName, onFieldFocus, placeholder]);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const instance = instanceRef.current;
|
|
80
|
+
if (!instance) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (instance.getValue() !== value) {
|
|
84
|
+
instance.setValue(value);
|
|
85
|
+
}
|
|
86
|
+
}, [value]);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
var _a;
|
|
89
|
+
if (!enableImageUpload || !uploadImage) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const textarea = (_a = instanceRef.current) === null || _a === void 0 ? void 0 : _a.textarea;
|
|
93
|
+
if (!textarea) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const handlePaste = async (event) => {
|
|
97
|
+
var _a, _b;
|
|
98
|
+
const items = Array.from((_b = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.items) !== null && _b !== void 0 ? _b : []);
|
|
99
|
+
const imageItem = items.find((item) => item.kind === "file" && item.type.startsWith("image/"));
|
|
100
|
+
const file = imageItem === null || imageItem === void 0 ? void 0 : imageItem.getAsFile();
|
|
101
|
+
if (!file) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
try {
|
|
106
|
+
const response = await uploadImage(file);
|
|
107
|
+
if (response === null || response === void 0 ? void 0 : response.url) {
|
|
108
|
+
insertImageMarkdown(response.url);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error("Failed to upload pasted image", error);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
textarea.addEventListener("paste", handlePaste);
|
|
116
|
+
return () => {
|
|
117
|
+
textarea.removeEventListener("paste", handlePaste);
|
|
118
|
+
};
|
|
119
|
+
}, [enableImageUpload, insertImageMarkdown, uploadImage]);
|
|
120
|
+
const editorClassName = useMemo(() => [
|
|
121
|
+
"bn-step-editor",
|
|
122
|
+
"bn-step-editor--multiline",
|
|
123
|
+
isFocused ? "bn-step-editor--focused" : "",
|
|
124
|
+
]
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.join(" "), [isFocused]);
|
|
127
|
+
return (_jsxs("div", { className: "bn-step-field", children: [_jsx("div", { className: "bn-step-field__top", children: _jsx("span", { className: "bn-step-field__label", children: label }) }), _jsx("div", { ref: containerRef, className: editorClassName, "data-step-field": fieldName })] }));
|
|
128
|
+
}
|
|
6
129
|
export const snippetBlock = createReactBlockSpec({
|
|
7
130
|
type: "snippet",
|
|
8
131
|
content: "none",
|
|
@@ -24,8 +147,10 @@ export const snippetBlock = createReactBlockSpec({
|
|
|
24
147
|
render: ({ block, editor }) => {
|
|
25
148
|
const snippetTitle = block.props.snippetTitle || "";
|
|
26
149
|
const snippetData = block.props.snippetData || "";
|
|
150
|
+
const snippetId = block.props.snippetId || "";
|
|
27
151
|
const snippetSuggestions = useSnippetAutocomplete();
|
|
28
152
|
const hasSnippets = snippetSuggestions.length > 0;
|
|
153
|
+
const isSnippetSelected = snippetId.length > 0;
|
|
29
154
|
const handleSnippetChange = useCallback((nextTitle) => {
|
|
30
155
|
if (nextTitle === snippetTitle) {
|
|
31
156
|
return;
|
|
@@ -67,6 +192,6 @@ export const snippetBlock = createReactBlockSpec({
|
|
|
67
192
|
if (!hasSnippets) {
|
|
68
193
|
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
194
|
}
|
|
70
|
-
return (
|
|
195
|
+
return (_jsx("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, children: !isSnippetSelected ? (_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, readOnly: false })) : (_jsx(SnippetDataField, { label: `Snippet: ${snippetTitle}`, value: snippetData, onChange: handleSnippetDataChange, fieldName: "snippet-data", enableImageUpload: true, onFieldFocus: handleFieldFocus, placeholder: "Snippet data will appear here..." })) }));
|
|
71
196
|
},
|
|
72
197
|
});
|
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createReactBlockSpec } from "@blocknote/react";
|
|
3
|
-
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { StepField } from "./stepField";
|
|
5
5
|
import { useStepImageUpload } from "../stepImageUpload";
|
|
6
|
+
const EXPECTED_COLLAPSED_KEY = "bn-expected-collapsed";
|
|
7
|
+
const readExpectedCollapsedPreference = () => {
|
|
8
|
+
if (typeof window === "undefined") {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return window.localStorage.getItem(EXPECTED_COLLAPSED_KEY) === "true";
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const writeExpectedCollapsedPreference = (collapsed) => {
|
|
19
|
+
if (typeof window === "undefined") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
window.localStorage.setItem(EXPECTED_COLLAPSED_KEY, collapsed ? "true" : "false");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
//
|
|
27
|
+
}
|
|
28
|
+
};
|
|
6
29
|
export const stepBlock = createReactBlockSpec({
|
|
7
30
|
type: "testStep",
|
|
8
31
|
content: "none",
|
|
@@ -22,22 +45,25 @@ export const stepBlock = createReactBlockSpec({
|
|
|
22
45
|
const stepTitle = block.props.stepTitle || "";
|
|
23
46
|
const stepData = block.props.stepData || "";
|
|
24
47
|
const expectedResult = block.props.expectedResult || "";
|
|
25
|
-
const
|
|
26
|
-
const
|
|
48
|
+
const expectedHasContent = expectedResult.trim().length > 0;
|
|
49
|
+
const storedExpectedCollapsed = useMemo(() => readExpectedCollapsedPreference(), []);
|
|
50
|
+
const dataHasContent = stepData.trim().length > 0;
|
|
51
|
+
const [isExpectedVisible, setIsExpectedVisible] = useState(expectedHasContent ? true : !storedExpectedCollapsed);
|
|
52
|
+
const [isDataVisible, setIsDataVisible] = useState(dataHasContent);
|
|
27
53
|
const [shouldFocusDataField, setShouldFocusDataField] = useState(false);
|
|
28
54
|
const uploadImage = useStepImageUpload();
|
|
55
|
+
// Calculate step number based on position in document
|
|
56
|
+
const stepNumber = useMemo(() => {
|
|
57
|
+
const allBlocks = editor.document;
|
|
58
|
+
const stepBlocks = allBlocks.filter((b) => b.type === "testStep");
|
|
59
|
+
const index = stepBlocks.findIndex((b) => b.id === block.id);
|
|
60
|
+
return index >= 0 ? index + 1 : 1;
|
|
61
|
+
}, [editor.document, block.id]);
|
|
29
62
|
useEffect(() => {
|
|
30
|
-
if (
|
|
63
|
+
if (dataHasContent && !isDataVisible) {
|
|
31
64
|
setIsDataVisible(true);
|
|
32
65
|
}
|
|
33
|
-
}, [
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (shouldFocusDataField && isDataVisible) {
|
|
36
|
-
const timer = setTimeout(() => setShouldFocusDataField(false), 0);
|
|
37
|
-
return () => clearTimeout(timer);
|
|
38
|
-
}
|
|
39
|
-
return undefined;
|
|
40
|
-
}, [isDataVisible, shouldFocusDataField]);
|
|
66
|
+
}, [dataHasContent, isDataVisible]);
|
|
41
67
|
const handleStepTitleChange = useCallback((next) => {
|
|
42
68
|
if (next === stepTitle) {
|
|
43
69
|
return;
|
|
@@ -58,10 +84,13 @@ export const stepBlock = createReactBlockSpec({
|
|
|
58
84
|
},
|
|
59
85
|
});
|
|
60
86
|
}, [editor, block.id, stepData]);
|
|
61
|
-
const
|
|
87
|
+
const handleShowData = useCallback(() => {
|
|
62
88
|
setIsDataVisible(true);
|
|
63
89
|
setShouldFocusDataField(true);
|
|
64
90
|
}, []);
|
|
91
|
+
const handleHideData = useCallback(() => {
|
|
92
|
+
setIsDataVisible(false);
|
|
93
|
+
}, []);
|
|
65
94
|
const handleExpectedChange = useCallback((next) => {
|
|
66
95
|
if (next === expectedResult) {
|
|
67
96
|
return;
|
|
@@ -88,7 +117,25 @@ export const stepBlock = createReactBlockSpec({
|
|
|
88
117
|
const handleFieldFocus = useCallback(() => {
|
|
89
118
|
editor.setSelection(block.id, block.id);
|
|
90
119
|
}, [editor, block.id]);
|
|
91
|
-
|
|
120
|
+
const [dataFocusSignal] = useState(0);
|
|
121
|
+
const [expectedFocusSignal, setExpectedFocusSignal] = useState(0);
|
|
122
|
+
const handleShowExpected = useCallback(() => {
|
|
123
|
+
setIsExpectedVisible(true);
|
|
124
|
+
setExpectedFocusSignal((value) => value + 1);
|
|
125
|
+
writeExpectedCollapsedPreference(false);
|
|
126
|
+
}, []);
|
|
127
|
+
const handleHideExpected = useCallback(() => {
|
|
128
|
+
setIsExpectedVisible(false);
|
|
129
|
+
writeExpectedCollapsedPreference(true);
|
|
130
|
+
}, []);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (expectedHasContent && !isExpectedVisible) {
|
|
133
|
+
setIsExpectedVisible(true);
|
|
134
|
+
}
|
|
135
|
+
}, [expectedHasContent, isExpectedVisible]);
|
|
136
|
+
const canToggleData = !dataHasContent;
|
|
137
|
+
const canToggleExpected = !expectedHasContent;
|
|
138
|
+
return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, children: [_jsx(StepField, { label: `Step ${stepNumber}`, value: stepTitle, onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: handleFieldFocus, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
|
|
92
139
|
if (!uploadImage) {
|
|
93
140
|
return;
|
|
94
141
|
}
|
|
@@ -108,6 +155,26 @@ export const stepBlock = createReactBlockSpec({
|
|
|
108
155
|
catch (error) {
|
|
109
156
|
console.error("Failed to upload image to Step Data", error);
|
|
110
157
|
}
|
|
111
|
-
} }), isDataVisible
|
|
158
|
+
} }), isDataVisible ? (_jsx(StepField, { label: "Step Data", labelToggle: canToggleData
|
|
159
|
+
? {
|
|
160
|
+
onClick: handleHideData,
|
|
161
|
+
expanded: true,
|
|
162
|
+
}
|
|
163
|
+
: undefined, value: stepData, onChange: handleStepDataChange, autoFocus: shouldFocusDataField, focusSignal: dataFocusSignal, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })) : (_jsx("div", { className: "bn-step-field bn-step-field--collapsed", children: _jsx("span", { className: "bn-step-field__label bn-step-field__label--toggle", role: "button", tabIndex: -1, onClick: handleShowData, onKeyDown: (event) => {
|
|
164
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
handleShowData();
|
|
167
|
+
}
|
|
168
|
+
}, "aria-expanded": "false", children: "Step Data" }) })), isExpectedVisible ? (_jsx(StepField, { label: "Expected Result", labelToggle: canToggleExpected
|
|
169
|
+
? {
|
|
170
|
+
onClick: handleHideExpected,
|
|
171
|
+
expanded: true,
|
|
172
|
+
}
|
|
173
|
+
: undefined, value: expectedResult, onChange: handleExpectedChange, multiline: true, focusSignal: expectedFocusSignal, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })) : (_jsx("div", { className: "bn-step-field bn-step-field--collapsed", children: _jsx("span", { className: "bn-step-field__label bn-step-field__label--toggle", role: "button", tabIndex: -1, onClick: handleShowExpected, onKeyDown: (event) => {
|
|
174
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
handleShowExpected();
|
|
177
|
+
}
|
|
178
|
+
}, "aria-expanded": "false", children: "Expected Result" }) })), _jsx("button", { type: "button", className: "bn-step-add", onClick: handleInsertNextStep, children: "+ Step" })] }));
|
|
112
179
|
},
|
|
113
180
|
});
|
|
@@ -4,9 +4,14 @@ import { type SnippetSuggestion } from "../snippetAutocomplete";
|
|
|
4
4
|
type Suggestion = StepSuggestion | SnippetSuggestion;
|
|
5
5
|
type StepFieldProps = {
|
|
6
6
|
label: string;
|
|
7
|
+
labelToggle?: {
|
|
8
|
+
onClick: () => void;
|
|
9
|
+
expanded: boolean;
|
|
10
|
+
};
|
|
7
11
|
value: string;
|
|
8
12
|
onChange: (nextValue: string) => void;
|
|
9
13
|
autoFocus?: boolean;
|
|
14
|
+
focusSignal?: number;
|
|
10
15
|
multiline?: boolean;
|
|
11
16
|
enableAutocomplete?: boolean;
|
|
12
17
|
fieldName?: string;
|
|
@@ -22,5 +27,5 @@ type StepFieldProps = {
|
|
|
22
27
|
showImageButton?: boolean;
|
|
23
28
|
onFieldFocus?: () => void;
|
|
24
29
|
};
|
|
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;
|
|
30
|
+
export declare function StepField({ label, labelToggle, value, onChange, autoFocus, focusSignal, multiline, enableAutocomplete, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly, showSuggestionsOnFocus, enableImageUpload, onImageFile, rightAction, showFormattingButtons, showImageButton, onFieldFocus, }: StepFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
26
31
|
export {};
|
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
4
4
|
import { useStepAutocomplete } from "../stepAutocomplete";
|
|
5
5
|
import { useStepImageUpload } from "../stepImageUpload";
|
|
6
6
|
import { escapeMarkdownText, normalizePlainText } from "./markdown";
|
|
7
|
+
import { useAutoResize } from "./useAutoResize";
|
|
7
8
|
const READ_ONLY_ALLOWED_KEYS = new Set([
|
|
8
9
|
"ArrowDown",
|
|
9
10
|
"ArrowUp",
|
|
@@ -30,7 +31,7 @@ function markdownToPlainText(markdown) {
|
|
|
30
31
|
return markdown.replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/\[[^\]]*]\([^)]+\)/g, "").replace(/[*_`~]/g, "").replace(/\s+/g, " ").trim();
|
|
31
32
|
}
|
|
32
33
|
}
|
|
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, }) {
|
|
34
|
+
export function StepField({ label, labelToggle, value, onChange, autoFocus, focusSignal, multiline = false, enableAutocomplete = false, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly = false, showSuggestionsOnFocus = false, enableImageUpload = false, onImageFile, rightAction, showFormattingButtons = false, showImageButton = false, onFieldFocus, }) {
|
|
34
35
|
const stepSuggestions = useStepAutocomplete();
|
|
35
36
|
const suggestions = suggestionsOverride !== null && suggestionsOverride !== void 0 ? suggestionsOverride : stepSuggestions;
|
|
36
37
|
const uploadImage = useStepImageUpload();
|
|
@@ -86,6 +87,12 @@ export function StepField({ label, value, onChange, autoFocus, multiline = false
|
|
|
86
87
|
textareaNode.focus();
|
|
87
88
|
}
|
|
88
89
|
}, [textareaNode]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!textareaNode || !focusSignal) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
textareaNode.focus();
|
|
95
|
+
}, [focusSignal, textareaNode]);
|
|
89
96
|
useEffect(() => {
|
|
90
97
|
const instance = editorInstanceRef.current;
|
|
91
98
|
if (!instance) {
|
|
@@ -120,6 +127,12 @@ export function StepField({ label, value, onChange, autoFocus, multiline = false
|
|
|
120
127
|
}
|
|
121
128
|
textareaNode.readOnly = readOnly;
|
|
122
129
|
}, [readOnly, textareaNode]);
|
|
130
|
+
useAutoResize({
|
|
131
|
+
textarea: textareaNode,
|
|
132
|
+
multiline,
|
|
133
|
+
minRows: 3,
|
|
134
|
+
maxRows: 16,
|
|
135
|
+
});
|
|
123
136
|
useEffect(() => {
|
|
124
137
|
if (!textareaNode) {
|
|
125
138
|
return;
|
|
@@ -409,7 +422,12 @@ export function StepField({ label, value, onChange, autoFocus, multiline = false
|
|
|
409
422
|
]
|
|
410
423
|
.filter(Boolean)
|
|
411
424
|
.join(" ");
|
|
412
|
-
return (_jsxs("div", { className: "bn-step-field", children: [_jsxs("div", { className: "bn-step-field__top", children: [_jsxs("
|
|
425
|
+
return (_jsxs("div", { className: "bn-step-field", children: [_jsxs("div", { className: "bn-step-field__top", children: [_jsxs("div", { className: "bn-step-field__label-row", children: [labelToggle ? (_jsx("span", { className: "bn-step-field__label bn-step-field__label--toggle", role: "button", tabIndex: -1, onClick: labelToggle.onClick, onKeyDown: (event) => {
|
|
426
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
427
|
+
event.preventDefault();
|
|
428
|
+
labelToggle.onClick();
|
|
429
|
+
}
|
|
430
|
+
}, "aria-expanded": labelToggle.expanded, children: label })) : (_jsx("span", { className: "bn-step-field__label", children: label })), enableAutocomplete && (_jsx("button", { type: "button", className: "bn-step-suggestions-toggle", onMouseDown: (event) => {
|
|
413
431
|
event.preventDefault();
|
|
414
432
|
setShowAllSuggestions(true);
|
|
415
433
|
textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.focus();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
export function useAutoResize({ textarea, multiline = false, minRows = 2, maxRows = 12 }) {
|
|
3
|
+
const frameRef = useRef(0);
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
if (!textarea || !multiline) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const resize = () => {
|
|
9
|
+
textarea.style.height = "auto";
|
|
10
|
+
const lineHeight = parseFloat(getComputedStyle(textarea).lineHeight || "16");
|
|
11
|
+
const minHeight = lineHeight * minRows;
|
|
12
|
+
const maxHeight = lineHeight * maxRows;
|
|
13
|
+
textarea.style.height = `${Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight)}px`;
|
|
14
|
+
};
|
|
15
|
+
const observer = new MutationObserver(resize);
|
|
16
|
+
observer.observe(textarea, { childList: true, characterData: true, subtree: true });
|
|
17
|
+
const handleInput = () => {
|
|
18
|
+
var _a;
|
|
19
|
+
cancelAnimationFrame((_a = frameRef.current) !== null && _a !== void 0 ? _a : 0);
|
|
20
|
+
frameRef.current = requestAnimationFrame(resize);
|
|
21
|
+
};
|
|
22
|
+
textarea.addEventListener("input", handleInput);
|
|
23
|
+
resize();
|
|
24
|
+
return () => {
|
|
25
|
+
var _a;
|
|
26
|
+
observer.disconnect();
|
|
27
|
+
textarea.removeEventListener("input", handleInput);
|
|
28
|
+
cancelAnimationFrame((_a = frameRef.current) !== null && _a !== void 0 ? _a : 0);
|
|
29
|
+
};
|
|
30
|
+
}, [textarea, multiline, minRows, maxRows]);
|
|
31
|
+
}
|