testomatio-editor-blocks 0.4.73 → 0.4.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package/editor/blocks/step.js +106 -73
- package/package/editor/blocks/stepField.d.ts +11 -0
- package/package/editor/blocks/stepField.js +120 -4
- package/package/editor/customMarkdownConverter.js +54 -34
- package/package/styles.css +76 -0
- package/package.json +1 -1
- package/src/editor/blocks/step.tsx +137 -106
- package/src/editor/blocks/stepField.tsx +163 -5
- package/src/editor/customMarkdownConverter.test.ts +59 -0
- package/src/editor/customMarkdownConverter.ts +53 -36
- package/src/editor/styles.css +76 -0
- package/package/editor/blocks/useDeferredMount.d.ts +0 -26
- package/package/editor/blocks/useDeferredMount.js +0 -54
- package/src/editor/blocks/useDeferredMount.ts +0 -66
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createReactBlockSpec, useEditorChange } from "@blocknote/react";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { StepField } from "./stepField";
|
|
4
|
+
import { StepField, StepFieldPreview } from "./stepField";
|
|
5
5
|
import { StepHorizontalView } from "./stepHorizontalView";
|
|
6
|
-
import { useDeferredMount } from "./useDeferredMount";
|
|
7
6
|
import { useStepImageUpload } from "../stepImageUpload";
|
|
8
7
|
const EXPECTED_COLLAPSED_KEY = "bn-expected-collapsed";
|
|
9
8
|
const VIEW_MODE_KEY = "bn-step-view-mode";
|
|
@@ -46,6 +45,35 @@ const writeStepViewMode = (mode) => {
|
|
|
46
45
|
//
|
|
47
46
|
}
|
|
48
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* Subscribes to the globally-shared step view mode. The mode lives in
|
|
50
|
+
* localStorage and changes are broadcast via the `bn-step-view-mode` event
|
|
51
|
+
* (same tab) and the `storage` event (other tabs), so toggling it on any step
|
|
52
|
+
* re-renders every step — including the read-only previews — into the new mode.
|
|
53
|
+
*/
|
|
54
|
+
function useStepViewMode() {
|
|
55
|
+
const [viewMode, setViewMode] = useState(() => readStepViewMode());
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (typeof window === "undefined") {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const handleStorage = (event) => {
|
|
61
|
+
if (event.key === VIEW_MODE_KEY) {
|
|
62
|
+
setViewMode(readStepViewMode());
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const handleLocal = () => {
|
|
66
|
+
setViewMode(readStepViewMode());
|
|
67
|
+
};
|
|
68
|
+
window.addEventListener("storage", handleStorage);
|
|
69
|
+
window.addEventListener("bn-step-view-mode", handleLocal);
|
|
70
|
+
return () => {
|
|
71
|
+
window.removeEventListener("storage", handleStorage);
|
|
72
|
+
window.removeEventListener("bn-step-view-mode", handleLocal);
|
|
73
|
+
};
|
|
74
|
+
}, []);
|
|
75
|
+
return viewMode;
|
|
76
|
+
}
|
|
49
77
|
/**
|
|
50
78
|
* Returns true when a normalised (lowercased, trailing-punctuation-stripped)
|
|
51
79
|
* heading text looks like a "Steps" heading.
|
|
@@ -232,30 +260,28 @@ export function computeStepNumber(allBlocks, blockId) {
|
|
|
232
260
|
}
|
|
233
261
|
return count;
|
|
234
262
|
}
|
|
235
|
-
/** Strip the most common inline markdown markers for a readable static preview. */
|
|
236
|
-
function stripMarkdownForPreview(text) {
|
|
237
|
-
return text
|
|
238
|
-
.replace(/!\[[^\]]*\]\([^)]*\)/g, "")
|
|
239
|
-
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
240
|
-
.replace(/(\*\*|__|\*|_|~~|`)/g, "")
|
|
241
|
-
.replace(/<\/?[^>]+>/g, "")
|
|
242
|
-
.trim();
|
|
243
|
-
}
|
|
244
263
|
/**
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
264
|
+
* Read-only stand-in rendered for every step that isn't currently being edited.
|
|
265
|
+
* It mirrors the live step's structure for the active view mode and renders each
|
|
266
|
+
* field via {@link StepFieldPreview} — a faithful, formatted reading view with
|
|
267
|
+
* no OverType editor. Only the focused step ever mounts an editor, so scrolling
|
|
268
|
+
* never mounts/tears down editors and the list stays flicker-free.
|
|
269
|
+
*
|
|
270
|
+
* Field visibility is gated on the raw trimmed props (matching the live step's
|
|
271
|
+
* `isDataVisible`/`isExpectedVisible`) so the same fields appear in both states.
|
|
248
272
|
*/
|
|
249
|
-
function TestStepPreview({ blockId, stepNumber, stepTitle, stepData, expectedResult, }) {
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
return (_jsxs("div", { className: "bn-teststep"
|
|
273
|
+
function TestStepPreview({ blockId, stepNumber, viewMode, stepTitle, stepData, expectedResult, }) {
|
|
274
|
+
const compactMode = viewMode === "compact";
|
|
275
|
+
const hasData = stepData.trim().length > 0;
|
|
276
|
+
const hasExpected = expectedResult.trim().length > 0;
|
|
277
|
+
return (_jsxs("div", { className: `bn-teststep${compactMode ? " bn-teststep--compact bn-teststep--collapsed" : ""}`, "data-block-id": blockId, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [!compactMode && (_jsx("div", { className: "bn-teststep__header", children: _jsx("span", { className: "bn-teststep__title", children: "Step" }) })), _jsx(StepFieldPreview, { value: stepTitle, fieldName: "title" }), hasData ? _jsx(StepFieldPreview, { value: stepData, fieldName: "data" }) : null, hasExpected ? _jsx(StepFieldPreview, { value: expectedResult, fieldName: "expected" }) : null] })] }));
|
|
254
278
|
}
|
|
255
279
|
/**
|
|
256
|
-
* Wrapper that
|
|
257
|
-
*
|
|
258
|
-
*
|
|
280
|
+
* Wrapper that mounts the (expensive) interactive step editor only while the
|
|
281
|
+
* step is being edited. Every other step renders the read-only
|
|
282
|
+
* {@link TestStepPreview}, so a document of any size keeps at most one OverType
|
|
283
|
+
* editor alive — scrolling never mounts or tears down editors, which is what
|
|
284
|
+
* keeps the list flicker-free (and pasting/loading a large document fast).
|
|
259
285
|
*
|
|
260
286
|
* The step number is tracked here and pushed down as a prop. We subscribe to
|
|
261
287
|
* editor changes but bail out of re-rendering when the number is unchanged, so
|
|
@@ -264,13 +290,16 @@ function TestStepPreview({ blockId, stepNumber, stepTitle, stepData, expectedRes
|
|
|
264
290
|
function TestStepBlock({ block, editor }) {
|
|
265
291
|
// An empty step is almost always a freshly-inserted one that needs to focus
|
|
266
292
|
// its title immediately, so mount its real editor eagerly. Steps with content
|
|
267
|
-
//
|
|
293
|
+
// start as a cheap read-only preview and upgrade on click/focus.
|
|
268
294
|
const isEmptyStep = !(block.props.stepTitle || "") &&
|
|
269
295
|
!(block.props.stepData || "") &&
|
|
270
296
|
!(block.props.expectedResult || "");
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
297
|
+
const viewMode = useStepViewMode();
|
|
298
|
+
const [editing, setEditing] = useState(isEmptyStep);
|
|
299
|
+
// Set when editing begins from a click/focus on the preview, so the freshly
|
|
300
|
+
// mounted editor takes focus (a single click starts editing). Cleared after
|
|
301
|
+
// the editor consumes it.
|
|
302
|
+
const focusOnMountRef = useRef(false);
|
|
274
303
|
const [stepNumber, setStepNumber] = useState(() => computeStepNumber(editor.document, block.id));
|
|
275
304
|
useEditorChange(() => {
|
|
276
305
|
// Recompute on change, but bail out of the state update (and therefore the
|
|
@@ -280,16 +309,29 @@ function TestStepBlock({ block, editor }) {
|
|
|
280
309
|
const next = computeStepNumber(editor.document, block.id);
|
|
281
310
|
setStepNumber((prev) => (prev === next ? prev : next));
|
|
282
311
|
}, editor);
|
|
283
|
-
|
|
312
|
+
const beginEditing = useCallback(() => {
|
|
313
|
+
focusOnMountRef.current = true;
|
|
314
|
+
setEditing(true);
|
|
315
|
+
}, []);
|
|
316
|
+
// Mousedown rather than click so the editor mounts before focus settles, and
|
|
317
|
+
// preventDefault so the browser doesn't move focus to <body> when the preview
|
|
318
|
+
// (the mousedown target) unmounts mid-click — that stray blur would otherwise
|
|
319
|
+
// immediately tear the new editor back down.
|
|
320
|
+
const beginEditingFromPointer = useCallback((event) => {
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
beginEditing();
|
|
323
|
+
}, [beginEditing]);
|
|
324
|
+
const endEditing = useCallback(() => setEditing(false), []);
|
|
325
|
+
if (editing) {
|
|
284
326
|
// Empty steps mounted eagerly (freshly inserted) auto-focus their title.
|
|
285
327
|
// A preview upgraded by a click focuses its field too, so a single click
|
|
286
|
-
// starts editing.
|
|
287
|
-
//
|
|
288
|
-
return (_jsx(TestStepContent, { block: block, editor: editor, stepNumber: stepNumber, autoFocusEnabled: isEmptyStep, focusOnMount:
|
|
328
|
+
// starts editing. The editor tears back down to a preview when focus
|
|
329
|
+
// leaves the step (see TestStepContent's blur handling).
|
|
330
|
+
return (_jsx(TestStepContent, { block: block, editor: editor, stepNumber: stepNumber, viewMode: viewMode, autoFocusEnabled: isEmptyStep, focusOnMount: focusOnMountRef.current, onEditEnd: endEditing }));
|
|
289
331
|
}
|
|
290
|
-
return (_jsx("div", {
|
|
332
|
+
return (_jsx("div", { className: "bn-teststep-preview-wrapper", tabIndex: 0, onMouseDownCapture: beginEditingFromPointer, onFocusCapture: beginEditing, children: _jsx(TestStepPreview, { blockId: block.id, stepNumber: stepNumber, viewMode: viewMode, stepTitle: block.props.stepTitle || "", stepData: block.props.stepData || "", expectedResult: block.props.expectedResult || "" }) }));
|
|
291
333
|
}
|
|
292
|
-
function TestStepContent({ block, editor, stepNumber, autoFocusEnabled = false, focusOnMount = false, }) {
|
|
334
|
+
function TestStepContent({ block, editor, stepNumber, viewMode, autoFocusEnabled = false, focusOnMount = false, onEditEnd, }) {
|
|
293
335
|
// When a preview is upgraded by a click, focus its primary field once on
|
|
294
336
|
// mount so a single click starts editing (caret at end).
|
|
295
337
|
const mountFocusSignal = focusOnMount ? 1 : 0;
|
|
@@ -303,12 +345,8 @@ function TestStepContent({ block, editor, stepNumber, autoFocusEnabled = false,
|
|
|
303
345
|
const [isDataVisible, setIsDataVisible] = useState(dataHasContent);
|
|
304
346
|
const [shouldFocusDataField, setShouldFocusDataField] = useState(false);
|
|
305
347
|
const uploadImage = useStepImageUpload();
|
|
306
|
-
const [viewMode, setViewMode] = useState(() => readStepViewMode());
|
|
307
348
|
const containerRef = useRef(null);
|
|
308
349
|
const [forceVertical, setForceVertical] = useState(false);
|
|
309
|
-
// In compact mode each step collapses to a reading-focused row and only
|
|
310
|
-
// expands to the full editing layout while one of its fields has focus.
|
|
311
|
-
const [expanded, setExpanded] = useState(false);
|
|
312
350
|
useEffect(() => {
|
|
313
351
|
var _a;
|
|
314
352
|
const el = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.parentElement;
|
|
@@ -324,28 +362,38 @@ function TestStepContent({ block, editor, stepNumber, autoFocusEnabled = false,
|
|
|
324
362
|
}, []);
|
|
325
363
|
const compactMode = viewMode === "compact";
|
|
326
364
|
const effectiveHorizontal = viewMode === "horizontal" && !forceVertical;
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
365
|
+
// A mounted step is, by definition, the one being edited, so it always
|
|
366
|
+
// shows the full editing layout — the collapsed reading row is now the
|
|
367
|
+
// read-only preview's job.
|
|
368
|
+
const compactCollapsed = false;
|
|
369
|
+
// Tear the editor back down to the read-only preview once focus leaves the
|
|
370
|
+
// whole step. Re-checked on the next frame so transient blurs (clicking a
|
|
371
|
+
// toolbar button, or the link popover that portals to <body>) don't
|
|
372
|
+
// collapse an active edit. Edits persist to block props on change, so
|
|
373
|
+
// unmounting never loses data. Re-bound when the layout (and thus the root
|
|
374
|
+
// element) changes so it always listens on the live root.
|
|
330
375
|
useEffect(() => {
|
|
331
|
-
|
|
376
|
+
const root = containerRef.current;
|
|
377
|
+
if (!root || !onEditEnd) {
|
|
332
378
|
return;
|
|
333
379
|
}
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
380
|
+
const handleFocusOut = () => {
|
|
381
|
+
// Defer to the next frame so focus moving *within* the step (or to a
|
|
382
|
+
// popover that portals to <body>, e.g. the link editor) has settled
|
|
383
|
+
// before we decide whether editing has really ended.
|
|
384
|
+
requestAnimationFrame(() => {
|
|
385
|
+
const active = document.activeElement;
|
|
386
|
+
if (active &&
|
|
387
|
+
(root.contains(active) ||
|
|
388
|
+
active.closest(".bn-popover-content, .bn-form-popover, [role='dialog']"))) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
onEditEnd();
|
|
392
|
+
});
|
|
341
393
|
};
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
window.removeEventListener("storage", handleStorage);
|
|
346
|
-
window.removeEventListener("bn-step-view-mode", handleLocal);
|
|
347
|
-
};
|
|
348
|
-
}, []);
|
|
394
|
+
root.addEventListener("focusout", handleFocusOut);
|
|
395
|
+
return () => root.removeEventListener("focusout", handleFocusOut);
|
|
396
|
+
}, [onEditEnd, effectiveHorizontal]);
|
|
349
397
|
const combinedStepValue = useMemo(() => {
|
|
350
398
|
if (!stepData) {
|
|
351
399
|
return stepTitle;
|
|
@@ -455,28 +503,13 @@ function TestStepContent({ block, editor, stepNumber, autoFocusEnabled = false,
|
|
|
455
503
|
next = "vertical";
|
|
456
504
|
}
|
|
457
505
|
writeStepViewMode(next);
|
|
458
|
-
|
|
506
|
+
// The shared useStepViewMode hook (in every step, including this one)
|
|
507
|
+
// listens for this event and re-reads the mode, so we don't track it
|
|
508
|
+
// locally here.
|
|
459
509
|
if (typeof window !== "undefined") {
|
|
460
510
|
window.dispatchEvent(new Event("bn-step-view-mode"));
|
|
461
511
|
}
|
|
462
512
|
}, [viewMode, forceVertical]);
|
|
463
|
-
const handleContentFocusCapture = useCallback(() => {
|
|
464
|
-
if (viewMode === "compact") {
|
|
465
|
-
setExpanded(true);
|
|
466
|
-
}
|
|
467
|
-
}, [viewMode]);
|
|
468
|
-
const handleContentBlurCapture = useCallback((event) => {
|
|
469
|
-
if (viewMode !== "compact") {
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
// Keep the step expanded while focus stays inside it (e.g. moving to a
|
|
473
|
-
// toolbar or action button); collapse only when focus leaves entirely.
|
|
474
|
-
const nextTarget = event.relatedTarget;
|
|
475
|
-
if (nextTarget && event.currentTarget.contains(nextTarget)) {
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
setExpanded(false);
|
|
479
|
-
}, [viewMode]);
|
|
480
513
|
const [dataFocusSignal] = useState(0);
|
|
481
514
|
const [expectedFocusSignal, setExpectedFocusSignal] = useState(0);
|
|
482
515
|
const handleShowExpected = useCallback(() => {
|
|
@@ -500,7 +533,7 @@ function TestStepContent({ block, editor, stepNumber, autoFocusEnabled = false,
|
|
|
500
533
|
if (effectiveHorizontal) {
|
|
501
534
|
return (_jsx(StepHorizontalView, { ref: containerRef, blockId: block.id, stepNumber: stepNumber, stepValue: combinedStepValue, expectedResult: expectedResult, onStepChange: handleCombinedStepChange, onExpectedChange: handleExpectedChange, onInsertNextStep: handleInsertNextStep, onFieldFocus: handleFieldFocus, viewToggle: viewToggleButton, focusSignal: mountFocusSignal }));
|
|
502
535
|
}
|
|
503
|
-
return (_jsxs("div", { className: `bn-teststep${compactMode ? " bn-teststep--compact" : ""}${compactCollapsed ? " bn-teststep--collapsed" : ""}`, "data-block-id": block.id, ref: containerRef, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content",
|
|
536
|
+
return (_jsxs("div", { className: `bn-teststep${compactMode ? " bn-teststep--compact" : ""}${compactCollapsed ? " bn-teststep--collapsed" : ""}`, "data-block-id": block.id, ref: containerRef, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__header", children: [!compactMode && _jsx("span", { className: "bn-teststep__title", children: "Step" }), viewToggleButton] }), _jsx(StepField, { label: "Step", showLabel: false, value: stepTitle, placeholder: STEP_TITLE_PLACEHOLDER, onChange: handleStepTitleChange, autoFocus: autoFocusEnabled && stepTitle.length === 0, focusSignal: mountFocusSignal, multiline: true, disableNewlines: true, enableAutocomplete: true, fieldName: "title", compact: compactCollapsed, compactMode: compactMode, suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: handleFieldFocus, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
|
|
504
537
|
if (!uploadImage) {
|
|
505
538
|
return;
|
|
506
539
|
}
|
|
@@ -67,5 +67,16 @@ export declare function applyInlineExclusion(formatting: FormattingMeta[], links
|
|
|
67
67
|
links: LinkMeta[];
|
|
68
68
|
};
|
|
69
69
|
export declare function buildFullMarkdown(plainText: string, links: LinkMeta[], formatting: FormattingMeta[]): string;
|
|
70
|
+
/**
|
|
71
|
+
* Lightweight, non-interactive stand-in for {@link StepField}. Mounts no
|
|
72
|
+
* OverType editor, observers, or event handlers — just a styled
|
|
73
|
+
* `.bn-step-editor--preview` box whose markdown is rendered declaratively. Used
|
|
74
|
+
* for every step that isn't currently being edited.
|
|
75
|
+
*/
|
|
76
|
+
export declare function StepFieldPreview({ value, fieldName, multiline, }: {
|
|
77
|
+
value: string;
|
|
78
|
+
fieldName?: string;
|
|
79
|
+
multiline?: boolean;
|
|
80
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
70
81
|
export declare function StepField({ label, showLabel, labelToggle, labelAction, placeholder, value, onChange, autoFocus, focusSignal, multiline, disableNewlines, enableAutocomplete, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly, showSuggestionsOnFocus, enableImageUpload, onImageFile, rightAction, showFormattingButtons, showImageButton, onFieldFocus, compact, compactMode, }: StepFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
71
82
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import OverType from "overtype";
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { useComponentsContext } from "@blocknote/react";
|
|
5
5
|
import { EditLinkMenuItems } from "@blocknote/react";
|
|
6
6
|
import { useStepAutocomplete } from "../stepAutocomplete";
|
|
@@ -15,6 +15,12 @@ const READ_ONLY_ALLOWED_KEYS = new Set([
|
|
|
15
15
|
]);
|
|
16
16
|
const AUTOCOMPLETE_TRIGGER_KEYS = new Set([" ", "Space"]);
|
|
17
17
|
const markdownParser = OverType.MarkdownParser;
|
|
18
|
+
/**
|
|
19
|
+
* `useLayoutEffect` that degrades to `useEffect` outside the browser so SSR
|
|
20
|
+
* doesn't warn. Static previews and the OverType mount run pre-paint to avoid a
|
|
21
|
+
* blank/jumping frame on the click→edit swap.
|
|
22
|
+
*/
|
|
23
|
+
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
18
24
|
function ImageUploadIcon() {
|
|
19
25
|
return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M12.667 2C13.0335 2.00008 13.3474 2.13057 13.6084 2.3916C13.8694 2.65264 13.9999 2.96648 14 3.33301V12.667C13.9999 13.0335 13.8694 13.3474 13.6084 13.6084C13.3474 13.8694 13.0335 13.9999 12.667 14H3.33301C2.96648 13.9999 2.65264 13.8694 2.3916 13.6084C2.13057 13.3474 2.00008 13.0335 2 12.667V3.33301C2.00008 2.96648 2.13057 2.65264 2.3916 2.3916C2.65264 2.13057 2.96648 2.00008 3.33301 2H12.667ZM3.33301 12.667H12.667V3.33301H3.33301V12.667ZM12 11.333H4L6 8.66699L7.5 10.667L9.5 8L12 11.333ZM5.66699 4.66699C5.94455 4.66707 6.18066 4.76375 6.375 4.95801C6.56944 5.15245 6.66699 5.38921 6.66699 5.66699C6.66692 5.94463 6.56937 6.18063 6.375 6.375C6.18063 6.56937 5.94463 6.66692 5.66699 6.66699C5.38921 6.66699 5.15245 6.56944 4.95801 6.375C4.76375 6.18066 4.66707 5.94455 4.66699 5.66699C4.66699 5.38921 4.76356 5.15245 4.95801 4.95801C5.15245 4.76356 5.38921 4.66699 5.66699 4.66699Z", fill: "currentColor" }) }));
|
|
20
26
|
}
|
|
@@ -532,6 +538,107 @@ function markdownToPlainText(markdown) {
|
|
|
532
538
|
return markdown.replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/\[[^\]]*]\([^)]+\)/g, "").replace(/[*_`~]/g, "").replace(/\s+/g, " ").trim();
|
|
533
539
|
}
|
|
534
540
|
}
|
|
541
|
+
const IMAGE_SYNTAX = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
542
|
+
/**
|
|
543
|
+
* Render a run of plain text, turning any `` markdown into real
|
|
544
|
+
* `<img>` elements. Returns a string when there are no images (so simple text
|
|
545
|
+
* stays a plain text node), otherwise a keyed array of strings and images.
|
|
546
|
+
*/
|
|
547
|
+
function renderTextWithImages(text, key) {
|
|
548
|
+
if (!text.includes("![")) {
|
|
549
|
+
return text;
|
|
550
|
+
}
|
|
551
|
+
const nodes = [];
|
|
552
|
+
IMAGE_SYNTAX.lastIndex = 0;
|
|
553
|
+
let last = 0;
|
|
554
|
+
let part = 0;
|
|
555
|
+
let match;
|
|
556
|
+
while ((match = IMAGE_SYNTAX.exec(text)) !== null) {
|
|
557
|
+
if (match.index > last) {
|
|
558
|
+
nodes.push(_jsx(Fragment, { children: text.slice(last, match.index) }, `${key}-t${part++}`));
|
|
559
|
+
}
|
|
560
|
+
nodes.push(_jsx("img", { src: match[2], alt: match[1] || "Step image" }, `${key}-i${part++}`));
|
|
561
|
+
last = match.index + match[0].length;
|
|
562
|
+
}
|
|
563
|
+
if (last < text.length) {
|
|
564
|
+
nodes.push(_jsx(Fragment, { children: text.slice(last) }, `${key}-t${part++}`));
|
|
565
|
+
}
|
|
566
|
+
return nodes;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Render a step field's markdown as a faithful, read-only reading view: the same
|
|
570
|
+
* clean text + bold/italic/code/link decorations + inline images the live
|
|
571
|
+
* OverType preview shows, but as plain React children (no editor, no refs, no
|
|
572
|
+
* imperative DOM — BlockNote's node-view renderer doesn't attach refs the way a
|
|
573
|
+
* normal React commit does, so the content must be declarative).
|
|
574
|
+
*
|
|
575
|
+
* Decorations are applied by slicing the plain text at every formatting/link
|
|
576
|
+
* boundary and wrapping each segment in the same `step-preview-*` elements the
|
|
577
|
+
* live editor uses, so all existing CSS applies unchanged.
|
|
578
|
+
*/
|
|
579
|
+
function renderStepFieldContent(value) {
|
|
580
|
+
const { plainText, links, formatting } = stripInlineMarkdown(value);
|
|
581
|
+
if (!plainText) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
if (formatting.length === 0 && links.length === 0) {
|
|
585
|
+
return renderTextWithImages(plainText, "p");
|
|
586
|
+
}
|
|
587
|
+
const len = plainText.length;
|
|
588
|
+
const points = new Set([0, len]);
|
|
589
|
+
for (const f of formatting) {
|
|
590
|
+
points.add(Math.max(0, f.start));
|
|
591
|
+
points.add(Math.min(len, f.end));
|
|
592
|
+
}
|
|
593
|
+
for (const l of links) {
|
|
594
|
+
points.add(Math.max(0, l.start));
|
|
595
|
+
points.add(Math.min(len, l.end));
|
|
596
|
+
}
|
|
597
|
+
const sorted = [...points].sort((a, b) => a - b);
|
|
598
|
+
const out = [];
|
|
599
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
600
|
+
const a = sorted[i];
|
|
601
|
+
const b = sorted[i + 1];
|
|
602
|
+
if (a >= b) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const text = plainText.slice(a, b);
|
|
606
|
+
const fmts = new Set(formatting.filter((f) => f.start <= a && f.end >= b).map((f) => f.type));
|
|
607
|
+
const link = links.find((l) => l.start <= a && l.end >= b);
|
|
608
|
+
let node = renderTextWithImages(text, `s${i}`);
|
|
609
|
+
if (fmts.has("code")) {
|
|
610
|
+
node = _jsx("code", { className: "step-preview-code", children: node });
|
|
611
|
+
}
|
|
612
|
+
if (fmts.has("italic")) {
|
|
613
|
+
node = _jsx("em", { className: "step-preview-italic", children: node });
|
|
614
|
+
}
|
|
615
|
+
if (fmts.has("bold")) {
|
|
616
|
+
node = _jsx("strong", { className: "step-preview-bold", children: node });
|
|
617
|
+
}
|
|
618
|
+
if (link) {
|
|
619
|
+
node = (_jsx("a", { className: "step-preview-link", href: link.url, children: node }));
|
|
620
|
+
}
|
|
621
|
+
out.push(_jsx(Fragment, { children: node }, i));
|
|
622
|
+
}
|
|
623
|
+
return out;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Lightweight, non-interactive stand-in for {@link StepField}. Mounts no
|
|
627
|
+
* OverType editor, observers, or event handlers — just a styled
|
|
628
|
+
* `.bn-step-editor--preview` box whose markdown is rendered declaratively. Used
|
|
629
|
+
* for every step that isn't currently being edited.
|
|
630
|
+
*/
|
|
631
|
+
export function StepFieldPreview({ value, fieldName, multiline = true, }) {
|
|
632
|
+
const content = useMemo(() => renderStepFieldContent(value), [value]);
|
|
633
|
+
const editorClassName = [
|
|
634
|
+
"bn-step-editor",
|
|
635
|
+
multiline ? "bn-step-editor--multiline" : "",
|
|
636
|
+
"bn-step-editor--preview",
|
|
637
|
+
]
|
|
638
|
+
.filter(Boolean)
|
|
639
|
+
.join(" ");
|
|
640
|
+
return (_jsx("div", { className: "bn-step-field", children: _jsx("div", { className: editorClassName, "data-step-field": fieldName, children: content }) }));
|
|
641
|
+
}
|
|
535
642
|
export function StepField({ label, showLabel = true, labelToggle, labelAction, placeholder, value, onChange, autoFocus, focusSignal, multiline = false, disableNewlines = false, enableAutocomplete = false, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly = false, showSuggestionsOnFocus = false, enableImageUpload = false, onImageFile, rightAction, showFormattingButtons = false, showImageButton = false, onFieldFocus, compact = false, compactMode = false, }) {
|
|
536
643
|
var _a, _b;
|
|
537
644
|
const stepSuggestions = useStepAutocomplete();
|
|
@@ -544,6 +651,10 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
544
651
|
const autoFocusRef = useRef(false);
|
|
545
652
|
const pendingFocusRef = useRef(false);
|
|
546
653
|
const initialValueRef = useRef(value);
|
|
654
|
+
// Read at OverType init so the editor mounts already-collapsed in compact
|
|
655
|
+
// mode (no tall first frame before the compact layout effect runs).
|
|
656
|
+
const compactModeRef = useRef(compactMode);
|
|
657
|
+
compactModeRef.current = compactMode;
|
|
547
658
|
const onChangeRef = useRef(onChange);
|
|
548
659
|
const [plainTextValue, setPlainTextValue] = useState(() => markdownToPlainText(value));
|
|
549
660
|
const [isFocused, setIsFocused] = useState(false);
|
|
@@ -611,7 +722,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
611
722
|
});
|
|
612
723
|
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, markdown);
|
|
613
724
|
}, [pushUndoSnapshot]);
|
|
614
|
-
|
|
725
|
+
useIsomorphicLayoutEffect(() => {
|
|
615
726
|
const container = editorContainerRef.current;
|
|
616
727
|
if (!container) {
|
|
617
728
|
return;
|
|
@@ -629,11 +740,16 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
629
740
|
value: plainText,
|
|
630
741
|
placeholder: resolvedPlaceholder,
|
|
631
742
|
autoResize: multiline,
|
|
632
|
-
|
|
743
|
+
// Seed the compact floor at init so a clicked step paints already
|
|
744
|
+
// collapsed — the compact layout effect below keeps it in sync after.
|
|
745
|
+
minHeight: compactModeRef.current ? "0px" : multiline ? "4rem" : "2.5rem",
|
|
633
746
|
padding: "0.5rem 0.75rem",
|
|
634
747
|
fontSize: "0.95rem",
|
|
635
748
|
onChange: handleEditorChange,
|
|
636
749
|
});
|
|
750
|
+
if (compactModeRef.current && instance.textarea) {
|
|
751
|
+
instance.textarea.rows = 1;
|
|
752
|
+
}
|
|
637
753
|
// Monkey-patch updatePreview to add link highlights
|
|
638
754
|
const originalUpdatePreview = instance.updatePreview.bind(instance);
|
|
639
755
|
instance.updatePreview = function () {
|
|
@@ -767,7 +883,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
767
883
|
// so caret and value survive. Driven by the stable compactMode flag (not
|
|
768
884
|
// `compact`) so collapsed and expanded share one height — focusing never
|
|
769
885
|
// shifts the layout.
|
|
770
|
-
|
|
886
|
+
useIsomorphicLayoutEffect(() => {
|
|
771
887
|
var _a;
|
|
772
888
|
const instance = editorInstanceRef.current;
|
|
773
889
|
if (!(instance === null || instance === void 0 ? void 0 : instance.options)) {
|
|
@@ -43,8 +43,46 @@ function escapeMarkdown(text) {
|
|
|
43
43
|
result += text.slice(lastIndex).replace(SPECIAL_CHAR_REGEX, "\\$1");
|
|
44
44
|
return result;
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// Creates a stateful escaper that escapes dots outside Markdown code while
|
|
47
|
+
// leaving code spans (inline `…`, fenced ``` … ```) verbatim — backslash
|
|
48
|
+
// escapes are ignored inside code, so `\.` would render literally.
|
|
49
|
+
//
|
|
50
|
+
// The state (an open, not-yet-closed backtick run) carries across calls so a
|
|
51
|
+
// fence can be opened in one segment and closed in another. This matters
|
|
52
|
+
// because the step editor splits a multi-line code block across props: the
|
|
53
|
+
// opening ``` lands in `stepTitle` while the body and closing ``` land in
|
|
54
|
+
// `stepData`.
|
|
55
|
+
function makeStepEscaper() {
|
|
56
|
+
let fence = null;
|
|
57
|
+
return (text) => {
|
|
58
|
+
let result = "";
|
|
59
|
+
let i = 0;
|
|
60
|
+
while (i < text.length) {
|
|
61
|
+
if (text[i] === "`") {
|
|
62
|
+
let ticks = 0;
|
|
63
|
+
while (text[i + ticks] === "`")
|
|
64
|
+
ticks++;
|
|
65
|
+
const run = "`".repeat(ticks);
|
|
66
|
+
result += run;
|
|
67
|
+
i += ticks;
|
|
68
|
+
if (fence === null) {
|
|
69
|
+
fence = run; // open a code span/fence
|
|
70
|
+
}
|
|
71
|
+
else if (fence === run) {
|
|
72
|
+
fence = null; // matching run closes it
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (fence !== null) {
|
|
77
|
+
result += text[i]; // inside code — copy verbatim
|
|
78
|
+
i++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
result += text[i] === "." ? "\\." : text[i];
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
};
|
|
48
86
|
}
|
|
49
87
|
function stripHtmlWrappers(text) {
|
|
50
88
|
return text
|
|
@@ -380,53 +418,35 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
380
418
|
.join(" ");
|
|
381
419
|
const normalizedExpectedForCheck = stripExpectedPrefix(expectedResult).trim();
|
|
382
420
|
const hasContent = stepData.length > 0 || normalizedExpectedForCheck.length > 0;
|
|
421
|
+
// One escaper threads code-fence state from the title into the data: the
|
|
422
|
+
// editor splits a multi-line code block so the opening ``` sits in the
|
|
423
|
+
// title and the body + closing ``` sit in the data. Both must be treated
|
|
424
|
+
// as a single code region so their dots stay literal.
|
|
425
|
+
const escapeBody = makeStepEscaper();
|
|
383
426
|
if (normalizedTitle.length > 0 || hasContent) {
|
|
384
427
|
const listStyle = (_o = block.props.listStyle) !== null && _o !== void 0 ? _o : "bullet";
|
|
385
428
|
const prefix = listStyle === "ordered" ? `${(stepIndex !== null && stepIndex !== void 0 ? stepIndex : 0) + 1}.` : "*";
|
|
386
|
-
lines.push(normalizedTitle.length > 0 ? `${prefix} ${
|
|
429
|
+
lines.push(normalizedTitle.length > 0 ? `${prefix} ${escapeBody(normalizedTitle)}` : `${prefix} `);
|
|
387
430
|
}
|
|
388
431
|
if (stepData.length > 0) {
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
dataLines.forEach((dataLine) => {
|
|
432
|
+
const escaped = escapeBody(stepData);
|
|
433
|
+
escaped.split(/\r?\n/).forEach((dataLine) => {
|
|
392
434
|
const trimmedLine = dataLine.trim();
|
|
393
|
-
|
|
394
|
-
lines.push(" ");
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
// Don't escape dots inside fenced code blocks (or on the fence lines
|
|
398
|
-
// themselves) — Markdown ignores backslash escapes there, so `\.`
|
|
399
|
-
// would render literally.
|
|
400
|
-
const isFence = trimmedLine.startsWith("```");
|
|
401
|
-
const content = insideCodeFence || isFence ? trimmedLine : escapeStepContent(trimmedLine);
|
|
402
|
-
lines.push(` ${content}`);
|
|
403
|
-
if (isFence) {
|
|
404
|
-
insideCodeFence = !insideCodeFence;
|
|
405
|
-
}
|
|
435
|
+
lines.push(trimmedLine.length === 0 ? " " : ` ${trimmedLine}`);
|
|
406
436
|
});
|
|
407
437
|
}
|
|
408
438
|
const normalizedExpected = stripExpectedPrefix(expectedResult).trim();
|
|
409
439
|
if (normalizedExpected.length > 0) {
|
|
410
|
-
const
|
|
440
|
+
const escaped = makeStepEscaper()(normalizedExpected);
|
|
411
441
|
const label = "*Expected result*";
|
|
412
|
-
let
|
|
413
|
-
|
|
442
|
+
let isFirst = true;
|
|
443
|
+
escaped.split(/\r?\n/).forEach((expectedLine) => {
|
|
414
444
|
const trimmedLine = expectedLine.trim();
|
|
415
445
|
if (trimmedLine.length === 0) {
|
|
416
446
|
return;
|
|
417
447
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const content = insideCodeFence || isFence ? trimmedLine : escapeStepContent(trimmedLine);
|
|
421
|
-
if (index === 0) {
|
|
422
|
-
lines.push(` ${label}: ${content}`);
|
|
423
|
-
}
|
|
424
|
-
else {
|
|
425
|
-
lines.push(` ${content}`);
|
|
426
|
-
}
|
|
427
|
-
if (isFence) {
|
|
428
|
-
insideCodeFence = !insideCodeFence;
|
|
429
|
-
}
|
|
448
|
+
lines.push(isFirst ? ` ${label}: ${trimmedLine}` : ` ${trimmedLine}`);
|
|
449
|
+
isFirst = false;
|
|
430
450
|
});
|
|
431
451
|
}
|
|
432
452
|
return lines;
|