testomatio-editor-blocks 0.4.74 → 0.4.76
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/blocks/testMeta.d.ts +19 -0
- package/package/editor/blocks/testMeta.js +65 -1
- package/package/index.d.ts +1 -1
- package/package/index.js +1 -1
- package/package/styles.css +89 -0
- package/package.json +1 -1
- package/src/App.tsx +14 -8
- package/src/editor/blocks/step.tsx +137 -106
- package/src/editor/blocks/stepField.tsx +163 -5
- package/src/editor/blocks/testMeta.tsx +74 -1
- package/src/editor/styles.css +89 -0
- package/src/index.ts +1 -1
- 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,5 +1,5 @@
|
|
|
1
1
|
import OverType, { type OverType as OverTypeInstance } from "overtype";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import type { ReactNode, ChangeEvent } from "react";
|
|
4
4
|
import { useComponentsContext } from "@blocknote/react";
|
|
5
5
|
import { EditLinkMenuItems } from "@blocknote/react";
|
|
@@ -63,7 +63,26 @@ const READ_ONLY_ALLOWED_KEYS = new Set([
|
|
|
63
63
|
|
|
64
64
|
const AUTOCOMPLETE_TRIGGER_KEYS = new Set([" ", "Space"]);
|
|
65
65
|
|
|
66
|
-
const markdownParser = (
|
|
66
|
+
const markdownParser = (
|
|
67
|
+
OverType as {
|
|
68
|
+
MarkdownParser?: {
|
|
69
|
+
parse: (
|
|
70
|
+
markdown: string,
|
|
71
|
+
activeLine?: number,
|
|
72
|
+
showActiveLineRaw?: boolean,
|
|
73
|
+
instanceHighlighter?: unknown,
|
|
74
|
+
isPreviewMode?: boolean,
|
|
75
|
+
) => string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
).MarkdownParser;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `useLayoutEffect` that degrades to `useEffect` outside the browser so SSR
|
|
82
|
+
* doesn't warn. Static previews and the OverType mount run pre-paint to avoid a
|
|
83
|
+
* blank/jumping frame on the click→edit swap.
|
|
84
|
+
*/
|
|
85
|
+
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
67
86
|
|
|
68
87
|
function ImageUploadIcon() {
|
|
69
88
|
return (
|
|
@@ -674,6 +693,136 @@ function markdownToPlainText(markdown: string): string {
|
|
|
674
693
|
}
|
|
675
694
|
}
|
|
676
695
|
|
|
696
|
+
const IMAGE_SYNTAX = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Render a run of plain text, turning any `` markdown into real
|
|
700
|
+
* `<img>` elements. Returns a string when there are no images (so simple text
|
|
701
|
+
* stays a plain text node), otherwise a keyed array of strings and images.
|
|
702
|
+
*/
|
|
703
|
+
function renderTextWithImages(text: string, key: string): ReactNode {
|
|
704
|
+
if (!text.includes("![")) {
|
|
705
|
+
return text;
|
|
706
|
+
}
|
|
707
|
+
const nodes: ReactNode[] = [];
|
|
708
|
+
IMAGE_SYNTAX.lastIndex = 0;
|
|
709
|
+
let last = 0;
|
|
710
|
+
let part = 0;
|
|
711
|
+
let match: RegExpExecArray | null;
|
|
712
|
+
while ((match = IMAGE_SYNTAX.exec(text)) !== null) {
|
|
713
|
+
if (match.index > last) {
|
|
714
|
+
nodes.push(<Fragment key={`${key}-t${part++}`}>{text.slice(last, match.index)}</Fragment>);
|
|
715
|
+
}
|
|
716
|
+
nodes.push(<img key={`${key}-i${part++}`} src={match[2]} alt={match[1] || "Step image"} />);
|
|
717
|
+
last = match.index + match[0].length;
|
|
718
|
+
}
|
|
719
|
+
if (last < text.length) {
|
|
720
|
+
nodes.push(<Fragment key={`${key}-t${part++}`}>{text.slice(last)}</Fragment>);
|
|
721
|
+
}
|
|
722
|
+
return nodes;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Render a step field's markdown as a faithful, read-only reading view: the same
|
|
727
|
+
* clean text + bold/italic/code/link decorations + inline images the live
|
|
728
|
+
* OverType preview shows, but as plain React children (no editor, no refs, no
|
|
729
|
+
* imperative DOM — BlockNote's node-view renderer doesn't attach refs the way a
|
|
730
|
+
* normal React commit does, so the content must be declarative).
|
|
731
|
+
*
|
|
732
|
+
* Decorations are applied by slicing the plain text at every formatting/link
|
|
733
|
+
* boundary and wrapping each segment in the same `step-preview-*` elements the
|
|
734
|
+
* live editor uses, so all existing CSS applies unchanged.
|
|
735
|
+
*/
|
|
736
|
+
function renderStepFieldContent(value: string): ReactNode {
|
|
737
|
+
const { plainText, links, formatting } = stripInlineMarkdown(value);
|
|
738
|
+
if (!plainText) {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
if (formatting.length === 0 && links.length === 0) {
|
|
742
|
+
return renderTextWithImages(plainText, "p");
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const len = plainText.length;
|
|
746
|
+
const points = new Set<number>([0, len]);
|
|
747
|
+
for (const f of formatting) {
|
|
748
|
+
points.add(Math.max(0, f.start));
|
|
749
|
+
points.add(Math.min(len, f.end));
|
|
750
|
+
}
|
|
751
|
+
for (const l of links) {
|
|
752
|
+
points.add(Math.max(0, l.start));
|
|
753
|
+
points.add(Math.min(len, l.end));
|
|
754
|
+
}
|
|
755
|
+
const sorted = [...points].sort((a, b) => a - b);
|
|
756
|
+
|
|
757
|
+
const out: ReactNode[] = [];
|
|
758
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
759
|
+
const a = sorted[i];
|
|
760
|
+
const b = sorted[i + 1];
|
|
761
|
+
if (a >= b) {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const text = plainText.slice(a, b);
|
|
765
|
+
const fmts = new Set(
|
|
766
|
+
formatting.filter((f) => f.start <= a && f.end >= b).map((f) => f.type),
|
|
767
|
+
);
|
|
768
|
+
const link = links.find((l) => l.start <= a && l.end >= b);
|
|
769
|
+
|
|
770
|
+
let node: ReactNode = renderTextWithImages(text, `s${i}`);
|
|
771
|
+
if (fmts.has("code")) {
|
|
772
|
+
node = <code className="step-preview-code">{node}</code>;
|
|
773
|
+
}
|
|
774
|
+
if (fmts.has("italic")) {
|
|
775
|
+
node = <em className="step-preview-italic">{node}</em>;
|
|
776
|
+
}
|
|
777
|
+
if (fmts.has("bold")) {
|
|
778
|
+
node = <strong className="step-preview-bold">{node}</strong>;
|
|
779
|
+
}
|
|
780
|
+
if (link) {
|
|
781
|
+
node = (
|
|
782
|
+
<a className="step-preview-link" href={link.url}>
|
|
783
|
+
{node}
|
|
784
|
+
</a>
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
out.push(<Fragment key={i}>{node}</Fragment>);
|
|
788
|
+
}
|
|
789
|
+
return out;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Lightweight, non-interactive stand-in for {@link StepField}. Mounts no
|
|
794
|
+
* OverType editor, observers, or event handlers — just a styled
|
|
795
|
+
* `.bn-step-editor--preview` box whose markdown is rendered declaratively. Used
|
|
796
|
+
* for every step that isn't currently being edited.
|
|
797
|
+
*/
|
|
798
|
+
export function StepFieldPreview({
|
|
799
|
+
value,
|
|
800
|
+
fieldName,
|
|
801
|
+
multiline = true,
|
|
802
|
+
}: {
|
|
803
|
+
value: string;
|
|
804
|
+
fieldName?: string;
|
|
805
|
+
multiline?: boolean;
|
|
806
|
+
}) {
|
|
807
|
+
const content = useMemo(() => renderStepFieldContent(value), [value]);
|
|
808
|
+
|
|
809
|
+
const editorClassName = [
|
|
810
|
+
"bn-step-editor",
|
|
811
|
+
multiline ? "bn-step-editor--multiline" : "",
|
|
812
|
+
"bn-step-editor--preview",
|
|
813
|
+
]
|
|
814
|
+
.filter(Boolean)
|
|
815
|
+
.join(" ");
|
|
816
|
+
|
|
817
|
+
return (
|
|
818
|
+
<div className="bn-step-field">
|
|
819
|
+
<div className={editorClassName} data-step-field={fieldName}>
|
|
820
|
+
{content}
|
|
821
|
+
</div>
|
|
822
|
+
</div>
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
677
826
|
export function StepField({
|
|
678
827
|
label,
|
|
679
828
|
showLabel = true,
|
|
@@ -712,6 +861,10 @@ export function StepField({
|
|
|
712
861
|
const autoFocusRef = useRef(false);
|
|
713
862
|
const pendingFocusRef = useRef(false);
|
|
714
863
|
const initialValueRef = useRef(value);
|
|
864
|
+
// Read at OverType init so the editor mounts already-collapsed in compact
|
|
865
|
+
// mode (no tall first frame before the compact layout effect runs).
|
|
866
|
+
const compactModeRef = useRef(compactMode);
|
|
867
|
+
compactModeRef.current = compactMode;
|
|
715
868
|
const onChangeRef = useRef(onChange);
|
|
716
869
|
const [plainTextValue, setPlainTextValue] = useState(() => markdownToPlainText(value));
|
|
717
870
|
const [isFocused, setIsFocused] = useState(false);
|
|
@@ -799,7 +952,7 @@ export function StepField({
|
|
|
799
952
|
onChangeRef.current?.(markdown);
|
|
800
953
|
}, [pushUndoSnapshot]);
|
|
801
954
|
|
|
802
|
-
|
|
955
|
+
useIsomorphicLayoutEffect(() => {
|
|
803
956
|
const container = editorContainerRef.current;
|
|
804
957
|
if (!container) {
|
|
805
958
|
return;
|
|
@@ -820,11 +973,16 @@ export function StepField({
|
|
|
820
973
|
value: plainText,
|
|
821
974
|
placeholder: resolvedPlaceholder,
|
|
822
975
|
autoResize: multiline,
|
|
823
|
-
|
|
976
|
+
// Seed the compact floor at init so a clicked step paints already
|
|
977
|
+
// collapsed — the compact layout effect below keeps it in sync after.
|
|
978
|
+
minHeight: compactModeRef.current ? "0px" : multiline ? "4rem" : "2.5rem",
|
|
824
979
|
padding: "0.5rem 0.75rem",
|
|
825
980
|
fontSize: "0.95rem",
|
|
826
981
|
onChange: handleEditorChange,
|
|
827
982
|
});
|
|
983
|
+
if (compactModeRef.current && instance.textarea) {
|
|
984
|
+
instance.textarea.rows = 1;
|
|
985
|
+
}
|
|
828
986
|
|
|
829
987
|
// Monkey-patch updatePreview to add link highlights
|
|
830
988
|
const originalUpdatePreview = instance.updatePreview.bind(instance);
|
|
@@ -976,7 +1134,7 @@ export function StepField({
|
|
|
976
1134
|
// so caret and value survive. Driven by the stable compactMode flag (not
|
|
977
1135
|
// `compact`) so collapsed and expanded share one height — focusing never
|
|
978
1136
|
// shifts the layout.
|
|
979
|
-
|
|
1137
|
+
useIsomorphicLayoutEffect(() => {
|
|
980
1138
|
const instance = editorInstanceRef.current as
|
|
981
1139
|
| (OverTypeInstance & {
|
|
982
1140
|
options?: { minHeight?: string };
|
|
@@ -31,6 +31,77 @@ export function serializeMetaFields(fields: MetaField[]): string {
|
|
|
31
31
|
return JSON.stringify(fields);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
type TestEditorLike = {
|
|
35
|
+
document: any[];
|
|
36
|
+
getTextCursorPosition?: () => { block?: { id?: string } };
|
|
37
|
+
insertBlocks: (
|
|
38
|
+
blocks: any[],
|
|
39
|
+
referenceId: string,
|
|
40
|
+
placement: "before" | "after",
|
|
41
|
+
) => any[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Reads the `labels` value from the first suite `testMeta` block, if any. */
|
|
45
|
+
function suiteLabelsFromDocument(document: any[]): string {
|
|
46
|
+
for (const block of document) {
|
|
47
|
+
if (block?.type !== "testMeta") continue;
|
|
48
|
+
if ((block.props as any)?.metaKind !== "suite") continue;
|
|
49
|
+
const fields = parseMetaFields((block.props as any)?.metaFields);
|
|
50
|
+
const labels = fields.find((f) => f.key.trim().toLowerCase() === "labels");
|
|
51
|
+
if (labels) return labels.value;
|
|
52
|
+
}
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Insert a new test: a `testMeta` panel (labelled "NEW TEST" until it gets an id)
|
|
58
|
+
* seeded with default metadata — `type: manual`, `priority: normal`, and `labels`
|
|
59
|
+
* copied from the suite block — followed by an empty H2 heading for the test title.
|
|
60
|
+
*
|
|
61
|
+
* Inserts at the text cursor when the editor is focused, otherwise at the end of
|
|
62
|
+
* the document. Returns the title heading's block id (for focusing), or null.
|
|
63
|
+
*/
|
|
64
|
+
export function addTestBlock(editor: TestEditorLike): string | null {
|
|
65
|
+
const fields: MetaField[] = [
|
|
66
|
+
{ key: "type", value: "manual" },
|
|
67
|
+
{ key: "priority", value: "normal" },
|
|
68
|
+
{ key: "labels", value: suiteLabelsFromDocument(editor.document) },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const metaBlock = {
|
|
72
|
+
type: "testMeta" as const,
|
|
73
|
+
props: {
|
|
74
|
+
metaKind: "test",
|
|
75
|
+
metaFields: serializeMetaFields(fields),
|
|
76
|
+
metaInline: false,
|
|
77
|
+
},
|
|
78
|
+
children: [],
|
|
79
|
+
};
|
|
80
|
+
const titleHeading = {
|
|
81
|
+
type: "heading" as const,
|
|
82
|
+
props: { level: 2 },
|
|
83
|
+
content: [],
|
|
84
|
+
children: [],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Prefer the text cursor position — BlockNote keeps it in the editor state even
|
|
88
|
+
// after the editor blurs (e.g. when a toolbar button is clicked), so the test
|
|
89
|
+
// lands where the user left the caret. Fall back to the document end only when
|
|
90
|
+
// there is no cursor at all.
|
|
91
|
+
const docs = editor.document;
|
|
92
|
+
let referenceId: string | undefined;
|
|
93
|
+
try {
|
|
94
|
+
referenceId = editor.getTextCursorPosition?.().block?.id;
|
|
95
|
+
} catch {
|
|
96
|
+
referenceId = undefined;
|
|
97
|
+
}
|
|
98
|
+
if (!referenceId) referenceId = docs[docs.length - 1]?.id;
|
|
99
|
+
if (!referenceId) return null;
|
|
100
|
+
|
|
101
|
+
const inserted = editor.insertBlocks([metaBlock, titleHeading], referenceId, "after");
|
|
102
|
+
return inserted?.[1]?.id ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
34
105
|
type AddFieldMenuProps = {
|
|
35
106
|
kind: "test" | "suite";
|
|
36
107
|
usedKeys: string[];
|
|
@@ -214,7 +285,9 @@ export const testMetaBlock = createReactBlockSpec(
|
|
|
214
285
|
draggable={false}
|
|
215
286
|
>
|
|
216
287
|
<div className="bn-testmeta__header">
|
|
217
|
-
<span className="bn-testmeta__label">
|
|
288
|
+
<span className="bn-testmeta__label">
|
|
289
|
+
{kind === "test" && !idField?.value ? "NEW TEST" : kind.toUpperCase()}
|
|
290
|
+
</span>
|
|
218
291
|
{idField?.value && <span className="bn-testmeta__id">{idField.value}</span>}
|
|
219
292
|
{!expanded && (
|
|
220
293
|
<button
|
package/src/editor/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
|
}
|
|
@@ -974,6 +998,19 @@ html.dark .testomatio-editor [data-content-type="heading"] .bn-tag-badge {
|
|
|
974
998
|
color: var(--text-muted);
|
|
975
999
|
}
|
|
976
1000
|
|
|
1001
|
+
/*
|
|
1002
|
+
* The "new test" title heading is inserted directly after a testMeta panel, so it
|
|
1003
|
+
* is the block-outer immediately following the one containing a testMeta block.
|
|
1004
|
+
* Override BlockNote's injected "Heading" placeholder just for that heading.
|
|
1005
|
+
* !important beats the dynamically injected placeholder rule.
|
|
1006
|
+
*/
|
|
1007
|
+
.bn-block-outer:has(.bn-block-content[data-content-type="testMeta"])
|
|
1008
|
+
+ .bn-block-outer
|
|
1009
|
+
.bn-block-content[data-content-type="heading"]
|
|
1010
|
+
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
|
|
1011
|
+
content: "Enter test title" !important;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
977
1014
|
.bn-snippet-dropdown {
|
|
978
1015
|
position: relative;
|
|
979
1016
|
}
|
|
@@ -1432,6 +1469,58 @@ html.dark .bn-step-editor--preview {
|
|
|
1432
1469
|
color: #e5e5e5;
|
|
1433
1470
|
}
|
|
1434
1471
|
|
|
1472
|
+
/* Wrapper around a non-edited step's read-only preview. Focusable so Tab enters
|
|
1473
|
+
a step (which mounts its editor); no lingering outline since focus moves to
|
|
1474
|
+
the freshly-mounted field immediately. */
|
|
1475
|
+
.bn-teststep-preview-wrapper {
|
|
1476
|
+
outline: none;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/* Inline decorations inside the read-only preview mirror the live OverType
|
|
1480
|
+
preview, which uses the same step-preview-* classes. The live rules are
|
|
1481
|
+
scoped to `.overtype-wrapper .overtype-preview` (which the static preview
|
|
1482
|
+
doesn't render), so the same declarations are repeated here for the flow
|
|
1483
|
+
`--preview` container. */
|
|
1484
|
+
.bn-step-editor--preview a.step-preview-link {
|
|
1485
|
+
color: #4f46e5;
|
|
1486
|
+
text-decoration: underline;
|
|
1487
|
+
pointer-events: none;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
.bn-step-editor--preview strong.step-preview-bold {
|
|
1491
|
+
-webkit-text-stroke: 0.5px currentColor;
|
|
1492
|
+
font-weight: inherit;
|
|
1493
|
+
color: inherit;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
.bn-step-editor--preview em.step-preview-italic {
|
|
1497
|
+
font-style: italic;
|
|
1498
|
+
color: inherit;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
.bn-step-editor--preview code.step-preview-code {
|
|
1502
|
+
background-color: transparent;
|
|
1503
|
+
font-family: inherit;
|
|
1504
|
+
font-size: inherit;
|
|
1505
|
+
color: rgb(146, 64, 14);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
.bn-step-editor--preview img {
|
|
1509
|
+
display: block;
|
|
1510
|
+
max-width: 100%;
|
|
1511
|
+
border-radius: 0.65rem;
|
|
1512
|
+
margin: 0.5rem 0;
|
|
1513
|
+
pointer-events: none;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
html.dark .bn-step-editor--preview code.step-preview-code {
|
|
1517
|
+
color: rgba(251, 191, 36, 1);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
html.dark .bn-step-editor--preview a.step-preview-link {
|
|
1521
|
+
color: rgba(129, 140, 248, 1);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1435
1524
|
.bn-step-editor.bn-step-editor--focused {
|
|
1436
1525
|
outline: none;
|
|
1437
1526
|
box-shadow: none;
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ export {
|
|
|
6
6
|
} from "./editor/customSchema";
|
|
7
7
|
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
|
|
8
8
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
9
|
-
export { testMetaBlock } from "./editor/blocks/testMeta";
|
|
9
|
+
export { testMetaBlock, addTestBlock } from "./editor/blocks/testMeta";
|
|
10
10
|
export {
|
|
11
11
|
setMetaFieldSuggestions,
|
|
12
12
|
getMetaFieldSuggestions,
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Defers mounting of expensive block content until the element is at (or near)
|
|
3
|
-
* the viewport. Heavy blocks (e.g. test steps that each spin up an OverType
|
|
4
|
-
* editor) render a cheap placeholder first; the real interactive content is
|
|
5
|
-
* mounted only once the block scrolls into view. This keeps pasting/loading a
|
|
6
|
-
* large document fast — only the visible steps pay the editor-init cost up
|
|
7
|
-
* front, the rest are upgraded lazily as the user scrolls.
|
|
8
|
-
*
|
|
9
|
-
* Returns a ref to attach to the wrapper element and a boolean that flips to
|
|
10
|
-
* `true` once (and stays true — we never tear an editor back down).
|
|
11
|
-
*
|
|
12
|
-
* `activate(focus)` lets the caller upgrade eagerly on interaction. Passing
|
|
13
|
-
* `focus: true` (a click/focus on the placeholder) records that the freshly
|
|
14
|
-
* mounted content should take focus, so a single click on a preview starts
|
|
15
|
-
* editing. Passive activation (hover pre-warm, scroll-into-view) leaves focus
|
|
16
|
-
* alone via `shouldFocusOnActivate === false`.
|
|
17
|
-
*/
|
|
18
|
-
export declare function useDeferredMount<T extends HTMLElement>(options?: {
|
|
19
|
-
rootMargin?: string;
|
|
20
|
-
initiallyActive?: boolean;
|
|
21
|
-
}): {
|
|
22
|
-
ref: React.RefObject<T | null>;
|
|
23
|
-
active: boolean;
|
|
24
|
-
activate: (focus?: boolean) => void;
|
|
25
|
-
shouldFocusOnActivate: boolean;
|
|
26
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
/**
|
|
3
|
-
* Defers mounting of expensive block content until the element is at (or near)
|
|
4
|
-
* the viewport. Heavy blocks (e.g. test steps that each spin up an OverType
|
|
5
|
-
* editor) render a cheap placeholder first; the real interactive content is
|
|
6
|
-
* mounted only once the block scrolls into view. This keeps pasting/loading a
|
|
7
|
-
* large document fast — only the visible steps pay the editor-init cost up
|
|
8
|
-
* front, the rest are upgraded lazily as the user scrolls.
|
|
9
|
-
*
|
|
10
|
-
* Returns a ref to attach to the wrapper element and a boolean that flips to
|
|
11
|
-
* `true` once (and stays true — we never tear an editor back down).
|
|
12
|
-
*
|
|
13
|
-
* `activate(focus)` lets the caller upgrade eagerly on interaction. Passing
|
|
14
|
-
* `focus: true` (a click/focus on the placeholder) records that the freshly
|
|
15
|
-
* mounted content should take focus, so a single click on a preview starts
|
|
16
|
-
* editing. Passive activation (hover pre-warm, scroll-into-view) leaves focus
|
|
17
|
-
* alone via `shouldFocusOnActivate === false`.
|
|
18
|
-
*/
|
|
19
|
-
export function useDeferredMount(options = {}) {
|
|
20
|
-
const { rootMargin = "300px 0px", initiallyActive = false } = options;
|
|
21
|
-
const ref = useRef(null);
|
|
22
|
-
const [active, setActive] = useState(initiallyActive);
|
|
23
|
-
const activeRef = useRef(active);
|
|
24
|
-
activeRef.current = active;
|
|
25
|
-
const focusOnActivateRef = useRef(false);
|
|
26
|
-
const activate = (focus = false) => {
|
|
27
|
-
if (activeRef.current)
|
|
28
|
-
return;
|
|
29
|
-
if (focus)
|
|
30
|
-
focusOnActivateRef.current = true;
|
|
31
|
-
setActive(true);
|
|
32
|
-
};
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if (activeRef.current)
|
|
35
|
-
return;
|
|
36
|
-
const el = ref.current;
|
|
37
|
-
if (!el)
|
|
38
|
-
return;
|
|
39
|
-
// Environments without IntersectionObserver (or SSR) just mount eagerly.
|
|
40
|
-
if (typeof IntersectionObserver === "undefined") {
|
|
41
|
-
setActive(true);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const observer = new IntersectionObserver((entries) => {
|
|
45
|
-
if (entries.some((entry) => entry.isIntersecting)) {
|
|
46
|
-
setActive(true);
|
|
47
|
-
observer.disconnect();
|
|
48
|
-
}
|
|
49
|
-
}, { rootMargin });
|
|
50
|
-
observer.observe(el);
|
|
51
|
-
return () => observer.disconnect();
|
|
52
|
-
}, [rootMargin]);
|
|
53
|
-
return { ref, active, activate, shouldFocusOnActivate: focusOnActivateRef.current };
|
|
54
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Defers mounting of expensive block content until the element is at (or near)
|
|
5
|
-
* the viewport. Heavy blocks (e.g. test steps that each spin up an OverType
|
|
6
|
-
* editor) render a cheap placeholder first; the real interactive content is
|
|
7
|
-
* mounted only once the block scrolls into view. This keeps pasting/loading a
|
|
8
|
-
* large document fast — only the visible steps pay the editor-init cost up
|
|
9
|
-
* front, the rest are upgraded lazily as the user scrolls.
|
|
10
|
-
*
|
|
11
|
-
* Returns a ref to attach to the wrapper element and a boolean that flips to
|
|
12
|
-
* `true` once (and stays true — we never tear an editor back down).
|
|
13
|
-
*
|
|
14
|
-
* `activate(focus)` lets the caller upgrade eagerly on interaction. Passing
|
|
15
|
-
* `focus: true` (a click/focus on the placeholder) records that the freshly
|
|
16
|
-
* mounted content should take focus, so a single click on a preview starts
|
|
17
|
-
* editing. Passive activation (hover pre-warm, scroll-into-view) leaves focus
|
|
18
|
-
* alone via `shouldFocusOnActivate === false`.
|
|
19
|
-
*/
|
|
20
|
-
export function useDeferredMount<T extends HTMLElement>(
|
|
21
|
-
options: { rootMargin?: string; initiallyActive?: boolean } = {},
|
|
22
|
-
): {
|
|
23
|
-
ref: React.RefObject<T | null>;
|
|
24
|
-
active: boolean;
|
|
25
|
-
activate: (focus?: boolean) => void;
|
|
26
|
-
shouldFocusOnActivate: boolean;
|
|
27
|
-
} {
|
|
28
|
-
const { rootMargin = "300px 0px", initiallyActive = false } = options;
|
|
29
|
-
const ref = useRef<T>(null);
|
|
30
|
-
const [active, setActive] = useState(initiallyActive);
|
|
31
|
-
const activeRef = useRef(active);
|
|
32
|
-
activeRef.current = active;
|
|
33
|
-
const focusOnActivateRef = useRef(false);
|
|
34
|
-
|
|
35
|
-
const activate = (focus = false) => {
|
|
36
|
-
if (activeRef.current) return;
|
|
37
|
-
if (focus) focusOnActivateRef.current = true;
|
|
38
|
-
setActive(true);
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
if (activeRef.current) return;
|
|
43
|
-
const el = ref.current;
|
|
44
|
-
if (!el) return;
|
|
45
|
-
|
|
46
|
-
// Environments without IntersectionObserver (or SSR) just mount eagerly.
|
|
47
|
-
if (typeof IntersectionObserver === "undefined") {
|
|
48
|
-
setActive(true);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const observer = new IntersectionObserver(
|
|
53
|
-
(entries) => {
|
|
54
|
-
if (entries.some((entry) => entry.isIntersecting)) {
|
|
55
|
-
setActive(true);
|
|
56
|
-
observer.disconnect();
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
{ rootMargin },
|
|
60
|
-
);
|
|
61
|
-
observer.observe(el);
|
|
62
|
-
return () => observer.disconnect();
|
|
63
|
-
}, [rootMargin]);
|
|
64
|
-
|
|
65
|
-
return { ref, active, activate, shouldFocusOnActivate: focusOnActivateRef.current };
|
|
66
|
-
}
|