testomatio-editor-blocks 0.4.74 → 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/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/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)) {
|
package/package/styles.css
CHANGED
|
@@ -631,6 +631,30 @@ html.dark .bn-step-editor .overtype-wrapper .overtype-preview a.step-preview-lin
|
|
|
631
631
|
vertical-align: 1px;
|
|
632
632
|
}
|
|
633
633
|
|
|
634
|
+
/* Read-only preview parity for compact reading rows: the live compact field
|
|
635
|
+
tightens padding on the OverType layers (above), which the static preview
|
|
636
|
+
doesn't have, so apply the same padding to the flow `--preview` box. */
|
|
637
|
+
.bn-teststep--compact .bn-step-editor--preview {
|
|
638
|
+
padding: 4px 12px;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/* Same "Expected" reading badge for the static preview. The static preview is a
|
|
642
|
+
single flow box (not OverType's per-line divs), so the badge goes on its own
|
|
643
|
+
::before. */
|
|
644
|
+
.bn-teststep--collapsed .bn-step-editor--preview[data-step-field="expected"]::before {
|
|
645
|
+
content: "Expected";
|
|
646
|
+
display: inline-block;
|
|
647
|
+
margin-right: 8px;
|
|
648
|
+
padding: 0 6px;
|
|
649
|
+
border-radius: 4px;
|
|
650
|
+
background: var(--step-bg-light);
|
|
651
|
+
color: var(--step-muted);
|
|
652
|
+
font-size: 11px;
|
|
653
|
+
font-weight: 600;
|
|
654
|
+
line-height: 18px;
|
|
655
|
+
vertical-align: 1px;
|
|
656
|
+
}
|
|
657
|
+
|
|
634
658
|
.bn-teststep__view-toggle--compact svg {
|
|
635
659
|
color: var(--step-muted);
|
|
636
660
|
}
|
|
@@ -1432,6 +1456,58 @@ html.dark .bn-step-editor--preview {
|
|
|
1432
1456
|
color: #e5e5e5;
|
|
1433
1457
|
}
|
|
1434
1458
|
|
|
1459
|
+
/* Wrapper around a non-edited step's read-only preview. Focusable so Tab enters
|
|
1460
|
+
a step (which mounts its editor); no lingering outline since focus moves to
|
|
1461
|
+
the freshly-mounted field immediately. */
|
|
1462
|
+
.bn-teststep-preview-wrapper {
|
|
1463
|
+
outline: none;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/* Inline decorations inside the read-only preview mirror the live OverType
|
|
1467
|
+
preview, which uses the same step-preview-* classes. The live rules are
|
|
1468
|
+
scoped to `.overtype-wrapper .overtype-preview` (which the static preview
|
|
1469
|
+
doesn't render), so the same declarations are repeated here for the flow
|
|
1470
|
+
`--preview` container. */
|
|
1471
|
+
.bn-step-editor--preview a.step-preview-link {
|
|
1472
|
+
color: #4f46e5;
|
|
1473
|
+
text-decoration: underline;
|
|
1474
|
+
pointer-events: none;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
.bn-step-editor--preview strong.step-preview-bold {
|
|
1478
|
+
-webkit-text-stroke: 0.5px currentColor;
|
|
1479
|
+
font-weight: inherit;
|
|
1480
|
+
color: inherit;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
.bn-step-editor--preview em.step-preview-italic {
|
|
1484
|
+
font-style: italic;
|
|
1485
|
+
color: inherit;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
.bn-step-editor--preview code.step-preview-code {
|
|
1489
|
+
background-color: transparent;
|
|
1490
|
+
font-family: inherit;
|
|
1491
|
+
font-size: inherit;
|
|
1492
|
+
color: rgb(146, 64, 14);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
.bn-step-editor--preview img {
|
|
1496
|
+
display: block;
|
|
1497
|
+
max-width: 100%;
|
|
1498
|
+
border-radius: 0.65rem;
|
|
1499
|
+
margin: 0.5rem 0;
|
|
1500
|
+
pointer-events: none;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
html.dark .bn-step-editor--preview code.step-preview-code {
|
|
1504
|
+
color: rgba(251, 191, 36, 1);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
html.dark .bn-step-editor--preview a.step-preview-link {
|
|
1508
|
+
color: rgba(129, 140, 248, 1);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1435
1511
|
.bn-step-editor.bn-step-editor--focused {
|
|
1436
1512
|
outline: none;
|
|
1437
1513
|
box-shadow: none;
|
package/package.json
CHANGED