testomatio-editor-blocks 0.1.2 → 0.2.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/README.md +13 -6
- package/package/editor/blocks/markdown.d.ts +5 -0
- package/package/editor/blocks/markdown.js +160 -0
- package/package/editor/blocks/snippet.d.ts +38 -0
- package/package/editor/blocks/snippet.js +65 -0
- package/package/editor/blocks/step.d.ts +32 -0
- package/package/editor/blocks/step.js +97 -0
- package/package/editor/blocks/stepField.d.ts +26 -0
- package/package/editor/blocks/stepField.js +316 -0
- package/package/editor/customMarkdownConverter.js +111 -80
- package/package/editor/customSchema.d.ts +31 -45
- package/package/editor/customSchema.js +6 -616
- package/package/editor/snippetAutocomplete.d.ts +28 -0
- package/package/editor/snippetAutocomplete.js +94 -0
- package/package/editor/stepAutocomplete.d.ts +1 -1
- package/package/editor/stepAutocomplete.js +1 -1
- package/package/index.d.ts +1 -1
- package/package/index.js +1 -1
- package/package/styles.css +57 -0
- package/package.json +1 -1
- package/src/App.tsx +143 -41
- package/src/editor/blocks/blocks.test.ts +22 -0
- package/src/editor/blocks/markdown.ts +199 -0
- package/src/editor/blocks/snippet.tsx +109 -0
- package/src/editor/blocks/step.tsx +175 -0
- package/src/editor/blocks/stepField.tsx +487 -0
- package/src/editor/customMarkdownConverter.test.ts +121 -36
- package/src/editor/customMarkdownConverter.ts +128 -85
- package/src/editor/customSchema.tsx +6 -935
- package/src/editor/snippetAutocomplete.test.ts +54 -0
- package/src/editor/snippetAutocomplete.ts +133 -0
- package/src/editor/stepAutocomplete.test.ts +3 -3
- package/src/editor/stepAutocomplete.tsx +1 -1
- package/src/editor/styles.css +57 -0
- package/src/index.ts +1 -1
- package/src/editor/customSchema.test.ts +0 -47
|
@@ -1,623 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { defaultBlockSpecs, defaultProps } from "@blocknote/core";
|
|
1
|
+
import { defaultBlockSpecs } from "@blocknote/core";
|
|
3
2
|
import { BlockNoteSchema } from "@blocknote/core";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { useStepImageUpload } from "./stepImageUpload";
|
|
8
|
-
function escapeHtml(text) {
|
|
9
|
-
return text
|
|
10
|
-
.replace(/&/g, "&")
|
|
11
|
-
.replace(/</g, "<")
|
|
12
|
-
.replace(/>/g, ">")
|
|
13
|
-
.replace(/\"/g, """)
|
|
14
|
-
.replace(/'/g, "'");
|
|
15
|
-
}
|
|
16
|
-
const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
17
|
-
function markdownToHtml(markdown) {
|
|
18
|
-
if (!markdown) {
|
|
19
|
-
return "";
|
|
20
|
-
}
|
|
21
|
-
const lines = markdown.split(/\n/);
|
|
22
|
-
const htmlLines = lines.map((line) => {
|
|
23
|
-
const inline = parseInlineMarkdown(line);
|
|
24
|
-
const html = inlineToHtml(inline);
|
|
25
|
-
if (!html) {
|
|
26
|
-
return html;
|
|
27
|
-
}
|
|
28
|
-
return html.replace(IMAGE_MARKDOWN_REGEX, (_match, alt = "", src = "") => `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" class="bn-inline-image" contenteditable="false" draggable="false" />`);
|
|
29
|
-
});
|
|
30
|
-
return htmlLines.join("<br />");
|
|
31
|
-
}
|
|
32
|
-
function parseInlineMarkdown(text) {
|
|
33
|
-
if (!text) {
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
const normalized = text.replace(/\\([*_`~])/g, "\uE000$1");
|
|
37
|
-
const rawSegments = normalized
|
|
38
|
-
.split(/(\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/)
|
|
39
|
-
.filter(Boolean);
|
|
40
|
-
return rawSegments.map((segment) => {
|
|
41
|
-
const baseStyles = { bold: false, italic: false, underline: false };
|
|
42
|
-
if (/^\*\*(.+)\*\*$/.test(segment) || /^__(.+)__$/.test(segment)) {
|
|
43
|
-
const content = segment.slice(2, -2);
|
|
44
|
-
return {
|
|
45
|
-
text: restoreEscapes(content),
|
|
46
|
-
styles: { ...baseStyles, bold: true },
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
if (/^\*(.+)\*$/.test(segment) || /^_(.+)_$/.test(segment)) {
|
|
50
|
-
const content = segment.slice(1, -1);
|
|
51
|
-
return {
|
|
52
|
-
text: restoreEscapes(content),
|
|
53
|
-
styles: { ...baseStyles, italic: true },
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
if (/^<u>(.+)<\/u>$/.test(segment)) {
|
|
57
|
-
const content = segment.slice(3, -4);
|
|
58
|
-
return {
|
|
59
|
-
text: restoreEscapes(content),
|
|
60
|
-
styles: { ...baseStyles, underline: true },
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
text: restoreEscapes(segment),
|
|
65
|
-
styles: { ...baseStyles },
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
function inlineToHtml(inline) {
|
|
70
|
-
return inline
|
|
71
|
-
.map(({ text, styles }) => {
|
|
72
|
-
let html = escapeHtml(text);
|
|
73
|
-
if (styles.bold) {
|
|
74
|
-
html = `<strong>${html}</strong>`;
|
|
75
|
-
}
|
|
76
|
-
if (styles.italic) {
|
|
77
|
-
html = `<em>${html}</em>`;
|
|
78
|
-
}
|
|
79
|
-
if (styles.underline) {
|
|
80
|
-
html = `<u>${html}</u>`;
|
|
81
|
-
}
|
|
82
|
-
return html;
|
|
83
|
-
})
|
|
84
|
-
.join("");
|
|
85
|
-
}
|
|
86
|
-
function restoreEscapes(text) {
|
|
87
|
-
return text.replace(/\uE000/g, "\\");
|
|
88
|
-
}
|
|
89
|
-
function htmlToMarkdown(html) {
|
|
90
|
-
if (typeof document === "undefined") {
|
|
91
|
-
return fallbackHtmlToMarkdown(html);
|
|
92
|
-
}
|
|
93
|
-
const temp = document.createElement("div");
|
|
94
|
-
temp.innerHTML = html;
|
|
95
|
-
const traverse = (node) => {
|
|
96
|
-
var _a, _b, _c;
|
|
97
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
98
|
-
const text = (_a = node.textContent) !== null && _a !== void 0 ? _a : "";
|
|
99
|
-
return escapeMarkdownText(text);
|
|
100
|
-
}
|
|
101
|
-
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
102
|
-
return "";
|
|
103
|
-
}
|
|
104
|
-
const element = node;
|
|
105
|
-
const children = Array.from(element.childNodes)
|
|
106
|
-
.map(traverse)
|
|
107
|
-
.join("");
|
|
108
|
-
switch (element.tagName.toLowerCase()) {
|
|
109
|
-
case "strong":
|
|
110
|
-
case "b":
|
|
111
|
-
return children ? `**${children}**` : children;
|
|
112
|
-
case "em":
|
|
113
|
-
case "i":
|
|
114
|
-
return children ? `*${children}*` : children;
|
|
115
|
-
case "u":
|
|
116
|
-
return children ? `<u>${children}</u>` : children;
|
|
117
|
-
case "br":
|
|
118
|
-
return "\n";
|
|
119
|
-
case "div":
|
|
120
|
-
case "p":
|
|
121
|
-
return children + "\n";
|
|
122
|
-
case "img": {
|
|
123
|
-
const src = (_b = element.getAttribute("src")) !== null && _b !== void 0 ? _b : "";
|
|
124
|
-
const alt = (_c = element.getAttribute("alt")) !== null && _c !== void 0 ? _c : "";
|
|
125
|
-
return ``;
|
|
126
|
-
}
|
|
127
|
-
default:
|
|
128
|
-
return children;
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
const markdown = Array.from(temp.childNodes).map(traverse).join("");
|
|
132
|
-
return markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
133
|
-
}
|
|
134
|
-
function fallbackHtmlToMarkdown(html) {
|
|
135
|
-
if (!html) {
|
|
136
|
-
return "";
|
|
137
|
-
}
|
|
138
|
-
let result = html;
|
|
139
|
-
result = result.replace(/<img[^>]*>/gi, (match) => {
|
|
140
|
-
var _a, _b, _c, _d;
|
|
141
|
-
const src = (_b = (_a = match.match(/src="([^"]*)"/i)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : "";
|
|
142
|
-
const alt = (_d = (_c = match.match(/alt="([^"]*)"/i)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : "";
|
|
143
|
-
return ``;
|
|
144
|
-
});
|
|
145
|
-
result = result
|
|
146
|
-
.replace(/<br\s*\/?>/gi, "\n")
|
|
147
|
-
.replace(/<\/?(div|p)>/gi, "\n")
|
|
148
|
-
.replace(/<strong>(.*?)<\/strong>/gis, (_m, content) => `**${content}**`)
|
|
149
|
-
.replace(/<(em|i)>(.*?)<\/(em|i)>/gis, (_m, _tag, content) => `*${content}*`)
|
|
150
|
-
.replace(/<span[^>]*>/gi, "")
|
|
151
|
-
.replace(/<\/span>/gi, "")
|
|
152
|
-
.replace(/<u>(.*?)<\/u>/gis, (_m, content) => `<u>${content}</u>`);
|
|
153
|
-
result = result.replace(/<\/?[^>]+>/g, "");
|
|
154
|
-
return result
|
|
155
|
-
.split("\n")
|
|
156
|
-
.map((line) => line.trimEnd())
|
|
157
|
-
.join("\n")
|
|
158
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
159
|
-
.trim();
|
|
160
|
-
}
|
|
161
|
-
const MARKDOWN_ESCAPE_REGEX = /([*_\\])/g;
|
|
162
|
-
function escapeMarkdownText(text) {
|
|
163
|
-
return text.replace(MARKDOWN_ESCAPE_REGEX, "\\$1");
|
|
164
|
-
}
|
|
165
|
-
function normalizePlainText(text) {
|
|
166
|
-
return text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
167
|
-
}
|
|
168
|
-
function StepField({ label, value, placeholder, onChange, autoFocus, multiline = false, enableAutocomplete = false, fieldName, enableImageUpload = false, onImageFile, }) {
|
|
169
|
-
const editorRef = useRef(null);
|
|
170
|
-
const [isFocused, setIsFocused] = useState(false);
|
|
171
|
-
const autoFocusRef = useRef(false);
|
|
172
|
-
const [plainTextValue, setPlainTextValue] = useState("");
|
|
173
|
-
const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(0);
|
|
174
|
-
const [showAllSuggestions, setShowAllSuggestions] = useState(false);
|
|
175
|
-
const suggestions = useStepAutocomplete();
|
|
176
|
-
const uploadImage = useStepImageUpload();
|
|
177
|
-
const fileInputRef = useRef(null);
|
|
178
|
-
const [isUploading, setIsUploading] = useState(false);
|
|
179
|
-
const normalizedQuery = normalizePlainText(plainTextValue);
|
|
180
|
-
const filteredSuggestions = useMemo(() => {
|
|
181
|
-
if (!enableAutocomplete) {
|
|
182
|
-
return [];
|
|
183
|
-
}
|
|
184
|
-
const pool = showAllSuggestions || !normalizedQuery
|
|
185
|
-
? suggestions
|
|
186
|
-
: suggestions.filter((item) => normalizePlainText(item.title).startsWith(normalizedQuery));
|
|
187
|
-
return pool.slice(0, 8);
|
|
188
|
-
}, [enableAutocomplete, normalizedQuery, showAllSuggestions, suggestions]);
|
|
189
|
-
const hasExactMatch = filteredSuggestions.some((item) => normalizePlainText(item.title) === normalizedQuery);
|
|
190
|
-
const shouldShowAutocomplete = enableAutocomplete &&
|
|
191
|
-
isFocused &&
|
|
192
|
-
filteredSuggestions.length > 0 &&
|
|
193
|
-
(!hasExactMatch || showAllSuggestions) &&
|
|
194
|
-
(showAllSuggestions || normalizedQuery.length >= 1);
|
|
195
|
-
useEffect(() => {
|
|
196
|
-
setActiveSuggestionIndex(0);
|
|
197
|
-
}, [normalizedQuery, filteredSuggestions.length, showAllSuggestions]);
|
|
198
|
-
useEffect(() => {
|
|
199
|
-
if (normalizedQuery.length > 0) {
|
|
200
|
-
setShowAllSuggestions(false);
|
|
201
|
-
}
|
|
202
|
-
}, [normalizedQuery]);
|
|
203
|
-
useEffect(() => {
|
|
204
|
-
var _a;
|
|
205
|
-
const element = editorRef.current;
|
|
206
|
-
if (!element || isFocused) {
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
if (value.trim().length === 0) {
|
|
210
|
-
element.innerHTML = "";
|
|
211
|
-
setPlainTextValue("");
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
element.innerHTML = markdownToHtml(value);
|
|
215
|
-
setPlainTextValue((_a = element.textContent) !== null && _a !== void 0 ? _a : "");
|
|
216
|
-
}
|
|
217
|
-
}, [value, isFocused]);
|
|
218
|
-
const syncValue = useCallback(() => {
|
|
219
|
-
var _a;
|
|
220
|
-
const element = editorRef.current;
|
|
221
|
-
if (!element) {
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const markdown = htmlToMarkdown(element.innerHTML);
|
|
225
|
-
if (markdown !== value) {
|
|
226
|
-
onChange(markdown);
|
|
227
|
-
}
|
|
228
|
-
setPlainTextValue((_a = element.innerText) !== null && _a !== void 0 ? _a : "");
|
|
229
|
-
if (!markdown && element.innerHTML !== "") {
|
|
230
|
-
element.innerHTML = "";
|
|
231
|
-
}
|
|
232
|
-
}, [onChange, value]);
|
|
233
|
-
useEffect(() => {
|
|
234
|
-
if (!autoFocus || autoFocusRef.current || !editorRef.current) {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
autoFocusRef.current = true;
|
|
238
|
-
const element = editorRef.current;
|
|
239
|
-
const focusElement = () => {
|
|
240
|
-
var _a;
|
|
241
|
-
element.focus();
|
|
242
|
-
setIsFocused(true);
|
|
243
|
-
const selection = typeof window !== "undefined" ? (_a = window.getSelection) === null || _a === void 0 ? void 0 : _a.call(window) : null;
|
|
244
|
-
if (selection) {
|
|
245
|
-
selection.selectAllChildren(element);
|
|
246
|
-
selection.collapseToEnd();
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
if (typeof requestAnimationFrame === "function") {
|
|
250
|
-
const frame = requestAnimationFrame(focusElement);
|
|
251
|
-
return () => cancelAnimationFrame(frame);
|
|
252
|
-
}
|
|
253
|
-
const timeout = setTimeout(focusElement, 0);
|
|
254
|
-
return () => clearTimeout(timeout);
|
|
255
|
-
}, [autoFocus]);
|
|
256
|
-
const applyFormat = useCallback((command) => {
|
|
257
|
-
document.execCommand(command);
|
|
258
|
-
syncValue();
|
|
259
|
-
}, [syncValue]);
|
|
260
|
-
const ensureCaretInEditor = useCallback(() => {
|
|
261
|
-
var _a;
|
|
262
|
-
const element = editorRef.current;
|
|
263
|
-
if (!element) {
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
const selection = (_a = window.getSelection) === null || _a === void 0 ? void 0 : _a.call(window);
|
|
267
|
-
if (!selection) {
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
if (selection.rangeCount === 0 || !element.contains(selection.anchorNode)) {
|
|
271
|
-
const range = document.createRange();
|
|
272
|
-
range.selectNodeContents(element);
|
|
273
|
-
range.collapse(false);
|
|
274
|
-
selection.removeAllRanges();
|
|
275
|
-
selection.addRange(range);
|
|
276
|
-
}
|
|
277
|
-
element.focus();
|
|
278
|
-
return true;
|
|
279
|
-
}, []);
|
|
280
|
-
const insertImageAtCursor = useCallback((url) => {
|
|
281
|
-
const element = editorRef.current;
|
|
282
|
-
if (!element) {
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
const escapedUrl = escapeHtml(url);
|
|
286
|
-
const imgHtml = `<img src="${escapedUrl}" alt="" class="bn-inline-image" contenteditable="false" draggable="false" />`;
|
|
287
|
-
element.focus();
|
|
288
|
-
ensureCaretInEditor();
|
|
289
|
-
document.execCommand("insertHTML", false, imgHtml);
|
|
290
|
-
syncValue();
|
|
291
|
-
}, [ensureCaretInEditor, syncValue]);
|
|
292
|
-
const handleImagePick = useCallback(async () => {
|
|
293
|
-
if (!enableImageUpload || !uploadImage) {
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
const fileInput = fileInputRef.current;
|
|
297
|
-
if (fileInput) {
|
|
298
|
-
fileInput.click();
|
|
299
|
-
}
|
|
300
|
-
}, [enableImageUpload, uploadImage]);
|
|
301
|
-
const handleFileChange = useCallback(async (event) => {
|
|
302
|
-
var _a;
|
|
303
|
-
const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
304
|
-
if (!file || !uploadImage) {
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
try {
|
|
308
|
-
setIsUploading(true);
|
|
309
|
-
const response = await uploadImage(file);
|
|
310
|
-
if (response === null || response === void 0 ? void 0 : response.url) {
|
|
311
|
-
insertImageAtCursor(response.url);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
console.error("Failed to upload image", error);
|
|
316
|
-
}
|
|
317
|
-
finally {
|
|
318
|
-
setIsUploading(false);
|
|
319
|
-
event.target.value = "";
|
|
320
|
-
}
|
|
321
|
-
}, [insertImageAtCursor, uploadImage]);
|
|
322
|
-
const handlePaste = useCallback(async (event) => {
|
|
323
|
-
var _a, _b, _c;
|
|
324
|
-
if ((enableImageUpload && uploadImage) || onImageFile) {
|
|
325
|
-
const items = Array.from((_a = event.clipboardData.items) !== null && _a !== void 0 ? _a : []);
|
|
326
|
-
const imageItem = items.find((item) => item.kind === "file" && item.type.startsWith("image/"));
|
|
327
|
-
const file = imageItem === null || imageItem === void 0 ? void 0 : imageItem.getAsFile();
|
|
328
|
-
if (file) {
|
|
329
|
-
event.preventDefault();
|
|
330
|
-
if (onImageFile) {
|
|
331
|
-
await onImageFile(file);
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
if (enableImageUpload && uploadImage) {
|
|
335
|
-
try {
|
|
336
|
-
setIsUploading(true);
|
|
337
|
-
const result = await uploadImage(file);
|
|
338
|
-
if (result === null || result === void 0 ? void 0 : result.url) {
|
|
339
|
-
ensureCaretInEditor();
|
|
340
|
-
document.execCommand("insertHTML", false, `<img src="${escapeHtml(result.url)}" alt="" class="bn-inline-image" contenteditable="false" draggable="false" />`);
|
|
341
|
-
syncValue();
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
console.error("Failed to upload image from paste", error);
|
|
346
|
-
}
|
|
347
|
-
finally {
|
|
348
|
-
setIsUploading(false);
|
|
349
|
-
}
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
event.preventDefault();
|
|
355
|
-
const text = (_c = (_b = event.clipboardData) === null || _b === void 0 ? void 0 : _b.getData("text/plain")) !== null && _c !== void 0 ? _c : "";
|
|
356
|
-
const html = markdownToHtml(text);
|
|
357
|
-
ensureCaretInEditor();
|
|
358
|
-
document.execCommand("insertHTML", false, html);
|
|
359
|
-
syncValue();
|
|
360
|
-
}, [enableImageUpload, ensureCaretInEditor, syncValue, uploadImage]);
|
|
361
|
-
const applySuggestion = useCallback((suggestion) => {
|
|
362
|
-
var _a;
|
|
363
|
-
const escaped = escapeMarkdownText(suggestion.title);
|
|
364
|
-
onChange(escaped);
|
|
365
|
-
setPlainTextValue(suggestion.title);
|
|
366
|
-
setActiveSuggestionIndex(0);
|
|
367
|
-
setShowAllSuggestions(false);
|
|
368
|
-
if (editorRef.current) {
|
|
369
|
-
editorRef.current.innerHTML = markdownToHtml(escaped);
|
|
370
|
-
editorRef.current.focus();
|
|
371
|
-
const selection = typeof window !== "undefined" ? (_a = window.getSelection) === null || _a === void 0 ? void 0 : _a.call(window) : null;
|
|
372
|
-
if (selection && editorRef.current.firstChild) {
|
|
373
|
-
const range = document.createRange();
|
|
374
|
-
range.selectNodeContents(editorRef.current);
|
|
375
|
-
range.collapse(false);
|
|
376
|
-
selection.removeAllRanges();
|
|
377
|
-
selection.addRange(range);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}, [onChange]);
|
|
381
|
-
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) => {
|
|
382
|
-
var _a;
|
|
383
|
-
event.preventDefault();
|
|
384
|
-
setShowAllSuggestions(true);
|
|
385
|
-
(_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
386
|
-
}, "aria-label": "Show step suggestions", tabIndex: -1, children: "\u2304" }))] }), _jsxs("div", { className: "bn-step-toolbar", "aria-label": `${label} formatting`, children: [_jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
|
|
387
|
-
var _a;
|
|
388
|
-
event.preventDefault();
|
|
389
|
-
(_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
390
|
-
applyFormat("bold");
|
|
391
|
-
}, "aria-label": "Bold", tabIndex: -1, children: "B" }), _jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
|
|
392
|
-
var _a;
|
|
393
|
-
event.preventDefault();
|
|
394
|
-
(_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
395
|
-
applyFormat("italic");
|
|
396
|
-
}, "aria-label": "Italic", tabIndex: -1, children: "I" }), _jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
|
|
397
|
-
var _a;
|
|
398
|
-
event.preventDefault();
|
|
399
|
-
(_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
400
|
-
applyFormat("underline");
|
|
401
|
-
}, "aria-label": "Underline", tabIndex: -1, children: "U" }), enableImageUpload && uploadImage && (_jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
|
|
402
|
-
event.preventDefault();
|
|
403
|
-
handleImagePick();
|
|
404
|
-
}, "aria-label": "Insert image", tabIndex: -1, disabled: isUploading, children: "Img" }))] })] }), enableImageUpload && (_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", style: { display: "none" }, onChange: handleFileChange })), _jsx("div", { ref: editorRef, className: "bn-step-editor", contentEditable: true, suppressContentEditableWarning: true, "data-placeholder": placeholder, "data-multiline": multiline ? "true" : "false", "data-step-field": fieldName, onFocus: () => {
|
|
405
|
-
var _a, _b;
|
|
406
|
-
setIsFocused(true);
|
|
407
|
-
setPlainTextValue((_b = (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.innerText) !== null && _b !== void 0 ? _b : "");
|
|
408
|
-
}, onBlur: () => {
|
|
409
|
-
setIsFocused(false);
|
|
410
|
-
syncValue();
|
|
411
|
-
}, onInput: syncValue, onPaste: handlePaste, onKeyDown: (event) => {
|
|
412
|
-
var _a, _b;
|
|
413
|
-
if ((event.key === "a" || event.key === "A") && (event.metaKey || event.ctrlKey)) {
|
|
414
|
-
event.preventDefault();
|
|
415
|
-
const selection = (_a = window.getSelection) === null || _a === void 0 ? void 0 : _a.call(window);
|
|
416
|
-
const node = editorRef.current;
|
|
417
|
-
if (selection && node) {
|
|
418
|
-
const range = document.createRange();
|
|
419
|
-
range.selectNodeContents(node);
|
|
420
|
-
selection.removeAllRanges();
|
|
421
|
-
selection.addRange(range);
|
|
422
|
-
}
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
if (enableAutocomplete && shouldShowAutocomplete) {
|
|
426
|
-
if (event.key === "ArrowDown") {
|
|
427
|
-
event.preventDefault();
|
|
428
|
-
setActiveSuggestionIndex((prev) => prev + 1 >= filteredSuggestions.length ? 0 : prev + 1);
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
if (event.key === "ArrowUp") {
|
|
432
|
-
event.preventDefault();
|
|
433
|
-
setActiveSuggestionIndex((prev) => prev - 1 < 0 ? filteredSuggestions.length - 1 : prev - 1);
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
if (event.key === "Enter" || event.key === "Tab") {
|
|
437
|
-
event.preventDefault();
|
|
438
|
-
const suggestion = (_b = filteredSuggestions[activeSuggestionIndex]) !== null && _b !== void 0 ? _b : filteredSuggestions[0];
|
|
439
|
-
if (suggestion) {
|
|
440
|
-
applySuggestion(suggestion);
|
|
441
|
-
}
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
if (enableAutocomplete && (event.metaKey || event.ctrlKey) && (event.code === "Space" || event.key === "" || event.key === " ")) {
|
|
446
|
-
event.preventDefault();
|
|
447
|
-
setShowAllSuggestions(true);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
if (event.key === "Enter") {
|
|
451
|
-
event.preventDefault();
|
|
452
|
-
if (multiline && event.shiftKey) {
|
|
453
|
-
document.execCommand("insertLineBreak");
|
|
454
|
-
document.execCommand("insertLineBreak");
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
document.execCommand("insertLineBreak");
|
|
458
|
-
}
|
|
459
|
-
syncValue();
|
|
460
|
-
}
|
|
461
|
-
} }), 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
|
|
462
|
-
? "bn-step-suggestion bn-step-suggestion--active"
|
|
463
|
-
: "bn-step-suggestion", onMouseDown: (event) => {
|
|
464
|
-
event.preventDefault();
|
|
465
|
-
applySuggestion(suggestion);
|
|
466
|
-
}, 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))) }))] }));
|
|
467
|
-
}
|
|
468
|
-
const statusOptions = ["draft", "ready", "blocked"];
|
|
469
|
-
const statusLabels = {
|
|
470
|
-
draft: "Draft",
|
|
471
|
-
ready: "Ready",
|
|
472
|
-
blocked: "Blocked",
|
|
473
|
-
};
|
|
474
|
-
const statusClassNames = {
|
|
475
|
-
draft: "bn-testcase--draft",
|
|
476
|
-
ready: "bn-testcase--ready",
|
|
477
|
-
blocked: "bn-testcase--blocked",
|
|
478
|
-
};
|
|
479
|
-
const testStepBlock = createReactBlockSpec({
|
|
480
|
-
type: "testStep",
|
|
481
|
-
content: "none",
|
|
482
|
-
propSchema: {
|
|
483
|
-
stepTitle: {
|
|
484
|
-
default: "",
|
|
485
|
-
},
|
|
486
|
-
stepData: {
|
|
487
|
-
default: "",
|
|
488
|
-
},
|
|
489
|
-
expectedResult: {
|
|
490
|
-
default: "",
|
|
491
|
-
},
|
|
492
|
-
},
|
|
493
|
-
}, {
|
|
494
|
-
render: ({ block, editor }) => {
|
|
495
|
-
const stepTitle = block.props.stepTitle || "";
|
|
496
|
-
const stepData = block.props.stepData || "";
|
|
497
|
-
const expectedResult = block.props.expectedResult || "";
|
|
498
|
-
const showExpectedField = stepTitle.trim().length > 0 || stepData.trim().length > 0 || expectedResult.trim().length > 0;
|
|
499
|
-
const [isDataVisible, setIsDataVisible] = useState(() => stepData.trim().length > 0);
|
|
500
|
-
const [shouldFocusDataField, setShouldFocusDataField] = useState(false);
|
|
501
|
-
const uploadImage = useStepImageUpload();
|
|
502
|
-
useEffect(() => {
|
|
503
|
-
if (stepData.trim().length > 0 && !isDataVisible) {
|
|
504
|
-
setIsDataVisible(true);
|
|
505
|
-
}
|
|
506
|
-
}, [isDataVisible, stepData]);
|
|
507
|
-
useEffect(() => {
|
|
508
|
-
if (shouldFocusDataField && isDataVisible) {
|
|
509
|
-
const timer = setTimeout(() => setShouldFocusDataField(false), 0);
|
|
510
|
-
return () => clearTimeout(timer);
|
|
511
|
-
}
|
|
512
|
-
return undefined;
|
|
513
|
-
}, [isDataVisible, shouldFocusDataField]);
|
|
514
|
-
const handleStepTitleChange = useCallback((next) => {
|
|
515
|
-
if (next === stepTitle) {
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
editor.updateBlock(block.id, {
|
|
519
|
-
props: {
|
|
520
|
-
stepTitle: next,
|
|
521
|
-
},
|
|
522
|
-
});
|
|
523
|
-
}, [editor, block.id, stepTitle]);
|
|
524
|
-
const handleStepDataChange = useCallback((next) => {
|
|
525
|
-
if (next === stepData) {
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
editor.updateBlock(block.id, {
|
|
529
|
-
props: {
|
|
530
|
-
stepData: next,
|
|
531
|
-
},
|
|
532
|
-
});
|
|
533
|
-
}, [editor, block.id, stepData]);
|
|
534
|
-
const handleShowDataField = useCallback(() => {
|
|
535
|
-
setIsDataVisible(true);
|
|
536
|
-
setShouldFocusDataField(true);
|
|
537
|
-
}, []);
|
|
538
|
-
const handleExpectedChange = useCallback((next) => {
|
|
539
|
-
if (next === expectedResult) {
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
editor.updateBlock(block.id, {
|
|
543
|
-
props: {
|
|
544
|
-
expectedResult: next,
|
|
545
|
-
},
|
|
546
|
-
});
|
|
547
|
-
}, [editor, block.id, expectedResult]);
|
|
548
|
-
return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, children: [_jsx(StepField, { label: "Step Title", value: stepTitle, placeholder: "Describe the action to perform", onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0, enableAutocomplete: true, fieldName: "title", enableImageUpload: false, onImageFile: async (file) => {
|
|
549
|
-
if (!uploadImage) {
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
setIsDataVisible(true);
|
|
553
|
-
setShouldFocusDataField(true);
|
|
554
|
-
try {
|
|
555
|
-
const result = await uploadImage(file);
|
|
556
|
-
if (result === null || result === void 0 ? void 0 : result.url) {
|
|
557
|
-
const nextValue = stepData.trim().length > 0 ? `${stepData}\n` : ``;
|
|
558
|
-
editor.updateBlock(block.id, {
|
|
559
|
-
props: {
|
|
560
|
-
stepData: nextValue,
|
|
561
|
-
},
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
catch (error) {
|
|
566
|
-
console.error("Failed to upload image to Step Data", error);
|
|
567
|
-
}
|
|
568
|
-
} }), !isDataVisible && (_jsx("button", { type: "button", className: "bn-teststep__toggle", onClick: handleShowDataField, "aria-expanded": "false", tabIndex: -1, children: "+ Step Data" })), isDataVisible && (_jsx(StepField, { label: "Step Data", value: stepData, placeholder: "Provide additional data about the step", onChange: handleStepDataChange, autoFocus: shouldFocusDataField, multiline: true, enableImageUpload: true })), showExpectedField && (_jsx(StepField, { label: "Expected Result", value: expectedResult, placeholder: "What should happen?", onChange: handleExpectedChange, multiline: true, enableImageUpload: true }))] }));
|
|
569
|
-
},
|
|
570
|
-
});
|
|
571
|
-
const testCaseBlock = createReactBlockSpec({
|
|
572
|
-
type: "testCase",
|
|
573
|
-
content: "inline",
|
|
574
|
-
propSchema: {
|
|
575
|
-
textAlignment: defaultProps.textAlignment,
|
|
576
|
-
textColor: defaultProps.textColor,
|
|
577
|
-
backgroundColor: defaultProps.backgroundColor,
|
|
578
|
-
status: {
|
|
579
|
-
default: "draft",
|
|
580
|
-
values: Array.from(statusOptions),
|
|
581
|
-
},
|
|
582
|
-
reference: {
|
|
583
|
-
default: "",
|
|
584
|
-
},
|
|
585
|
-
},
|
|
586
|
-
}, {
|
|
587
|
-
render: ({ block, contentRef, editor }) => {
|
|
588
|
-
const status = block.props.status;
|
|
589
|
-
const handleStatusChange = (event) => {
|
|
590
|
-
const nextStatus = event.target.value;
|
|
591
|
-
editor.updateBlock(block.id, {
|
|
592
|
-
props: {
|
|
593
|
-
status: nextStatus,
|
|
594
|
-
},
|
|
595
|
-
});
|
|
596
|
-
};
|
|
597
|
-
const handleReferenceChange = (event) => {
|
|
598
|
-
editor.updateBlock(block.id, {
|
|
599
|
-
props: {
|
|
600
|
-
reference: event.target.value,
|
|
601
|
-
},
|
|
602
|
-
});
|
|
603
|
-
};
|
|
604
|
-
const style = {
|
|
605
|
-
textAlign: block.props.textAlignment,
|
|
606
|
-
color: block.props.textColor === "default"
|
|
607
|
-
? undefined
|
|
608
|
-
: block.props.textColor,
|
|
609
|
-
backgroundColor: block.props.backgroundColor === "default"
|
|
610
|
-
? undefined
|
|
611
|
-
: block.props.backgroundColor,
|
|
612
|
-
};
|
|
613
|
-
return (_jsxs("div", { className: "bn-testcase " + statusClassNames[status], "data-reference": block.props.reference || undefined, style: style, children: [_jsxs("div", { className: "bn-testcase__header", children: [_jsxs("div", { className: "bn-testcase__meta", children: [_jsx("span", { className: "bn-testcase__label", children: "Test Case" }), _jsx("input", { className: "bn-testcase__reference", placeholder: "Reference ID", value: block.props.reference, onChange: handleReferenceChange })] }), _jsxs("label", { className: "bn-testcase__status", children: [_jsx("span", { children: "Status:" }), _jsx("select", { value: status, onChange: handleStatusChange, children: statusOptions.map((option) => (_jsx("option", { value: option, children: statusLabels[option] }, option))) })] })] }), _jsx("div", { className: "bn-testcase__body", ref: contentRef })] }));
|
|
614
|
-
},
|
|
615
|
-
});
|
|
3
|
+
import { stepBlock } from "./blocks/step";
|
|
4
|
+
import { snippetBlock } from "./blocks/snippet";
|
|
5
|
+
import { htmlToMarkdown, markdownToHtml } from "./blocks/markdown";
|
|
616
6
|
export const customSchema = BlockNoteSchema.create({
|
|
617
7
|
blockSpecs: {
|
|
618
8
|
...defaultBlockSpecs,
|
|
619
|
-
|
|
620
|
-
|
|
9
|
+
testStep: stepBlock,
|
|
10
|
+
snippet: snippetBlock,
|
|
621
11
|
},
|
|
622
12
|
});
|
|
623
13
|
export const __markdownTestUtils = {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type SnippetSuggestion = {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
body?: string | null;
|
|
5
|
+
description?: string | null;
|
|
6
|
+
usageCount?: number | null;
|
|
7
|
+
isSnippet?: boolean | null;
|
|
8
|
+
};
|
|
9
|
+
export type SnippetJsonApiAttributes = {
|
|
10
|
+
title?: string | null;
|
|
11
|
+
body?: string | null;
|
|
12
|
+
description?: string | null;
|
|
13
|
+
"usage-count"?: number | string | null;
|
|
14
|
+
};
|
|
15
|
+
export type SnippetJsonApiResource = {
|
|
16
|
+
id?: string | number | null;
|
|
17
|
+
type?: string | null;
|
|
18
|
+
attributes?: SnippetJsonApiAttributes | null;
|
|
19
|
+
};
|
|
20
|
+
export type SnippetJsonApiDocument = {
|
|
21
|
+
data?: SnippetJsonApiResource[] | null;
|
|
22
|
+
};
|
|
23
|
+
export type SnippetSuggestionsFetcher = () => Promise<SnippetInput> | SnippetInput;
|
|
24
|
+
type SnippetInput = SnippetSuggestion[] | SnippetJsonApiDocument | SnippetJsonApiResource[] | null | undefined;
|
|
25
|
+
export declare function setSnippetFetcher(fetcher: SnippetSuggestionsFetcher | null): void;
|
|
26
|
+
export declare function useSnippetAutocomplete(): SnippetSuggestion[];
|
|
27
|
+
export declare function parseSnippetsFromJsonApi(document: SnippetJsonApiDocument | SnippetJsonApiResource[] | null | undefined): SnippetSuggestion[];
|
|
28
|
+
export {};
|