@bettercms-ai/next 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocks.d.ts +12 -1
- package/dist/blocks.js +144 -15
- package/dist/blocks.js.map +1 -1
- package/dist/draft-bridge.js +144 -15
- package/dist/draft-bridge.js.map +1 -1
- package/dist/form.js +120 -6
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/blocks.d.ts
CHANGED
|
@@ -29,8 +29,19 @@ interface BcmsBlocksProps {
|
|
|
29
29
|
apiBase?: string;
|
|
30
30
|
/** Class for the wrapping <div>. */
|
|
31
31
|
className?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The instant block windows (`block.window`) are read against. Defaults to the render time: the
|
|
34
|
+
* build for a static page, the request for a server-rendered one.
|
|
35
|
+
*/
|
|
36
|
+
now?: Date;
|
|
37
|
+
/**
|
|
38
|
+
* Draw the blocks outside their window too, marked `data-bcms-window="pending|expired"`.
|
|
39
|
+
* For a draft preview, where an author must be able to find what they scheduled. Off, a block
|
|
40
|
+
* outside its window is not rendered, at any depth.
|
|
41
|
+
*/
|
|
42
|
+
showScheduled?: boolean;
|
|
32
43
|
}
|
|
33
44
|
/** Render a page's blocks. Unknown/empty blocks render nothing (fail-soft). */
|
|
34
|
-
declare function BcmsBlocks({ blocks, forms, components, turnstileSiteKey, apiBase, className, }: BcmsBlocksProps): ReactElement;
|
|
45
|
+
declare function BcmsBlocks({ blocks: all, forms, components, turnstileSiteKey, apiBase, className, now, showScheduled, }: BcmsBlocksProps): ReactElement;
|
|
35
46
|
|
|
36
47
|
export { BcmsBlocks, type BcmsBlocksProps };
|
package/dist/blocks.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
isValidElement,
|
|
8
8
|
useState as useState2
|
|
9
9
|
} from "react";
|
|
10
|
-
import { blockStyleToCss, blockStyleToInnerCss, splitButtonStyle, buttonIconOf, buttonIconSvg, buttonVariantOf, headingPaintCss, withButtonLookProps } from "@bettercms-ai/types";
|
|
10
|
+
import { RESPONSIVE_CSS, blockStyleToCss, blockStyleToInnerCss, hideOnAttr, usesHideOn, visibleBlocks, windowState, splitButtonStyle, buttonIconOf, buttonIconSvg, buttonVariantOf, headingPaintCss, withButtonLookProps } from "@bettercms-ai/types";
|
|
11
11
|
import { componentInstanceAddresses } from "@bettercms-ai/types";
|
|
12
12
|
import { applyOverrides } from "@bettercms-ai/ui/resolve";
|
|
13
13
|
import { PortableText } from "@bettercms-ai/ui/blocks";
|
|
@@ -15,7 +15,7 @@ import { embeddedComponentBlock, readStructuredContent } from "@bettercms-ai/ric
|
|
|
15
15
|
import { headingParts, richtextHeadingLevel } from "@bettercms-ai/richtext/heading";
|
|
16
16
|
|
|
17
17
|
// src/form.tsx
|
|
18
|
-
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
|
|
18
|
+
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react";
|
|
19
19
|
import {
|
|
20
20
|
submitForm,
|
|
21
21
|
shouldShowField,
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
asStringArray,
|
|
24
24
|
toggleOption
|
|
25
25
|
} from "@bettercms-ai/sdk";
|
|
26
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
26
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
27
27
|
var TURNSTILE_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
|
28
28
|
var PhoneField = lazy(() => import("./phone-field.js"));
|
|
29
29
|
var INPUT_TYPES = {
|
|
@@ -42,6 +42,21 @@ function readPrefill() {
|
|
|
42
42
|
});
|
|
43
43
|
return out;
|
|
44
44
|
}
|
|
45
|
+
function splitSteps(fields) {
|
|
46
|
+
if (!fields.some((f) => f.type === "step")) return null;
|
|
47
|
+
const pages = [];
|
|
48
|
+
let cur = { label: null, key: null, fields: [] };
|
|
49
|
+
for (const f of fields) {
|
|
50
|
+
if (f.type !== "step") {
|
|
51
|
+
cur.fields.push(f);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (cur.fields.length) pages.push(cur);
|
|
55
|
+
cur = { label: f.label, key: f.key, fields: [] };
|
|
56
|
+
}
|
|
57
|
+
if (cur.fields.length) pages.push(cur);
|
|
58
|
+
return pages;
|
|
59
|
+
}
|
|
45
60
|
function resetTurnstile() {
|
|
46
61
|
const t = globalThis.turnstile;
|
|
47
62
|
if (t) {
|
|
@@ -66,6 +81,37 @@ function BcmsForm({
|
|
|
66
81
|
});
|
|
67
82
|
const [status, setStatus] = useState({ state: "idle" });
|
|
68
83
|
const [fieldErrors, setFieldErrors] = useState({});
|
|
84
|
+
const pages = useMemo(() => splitSteps(form.fields), [form.fields]);
|
|
85
|
+
const [enhanced, setEnhanced] = useState(false);
|
|
86
|
+
const [step, setStep] = useState(0);
|
|
87
|
+
const pageRefs = useRef([]);
|
|
88
|
+
const headingRefs = useRef([]);
|
|
89
|
+
const moveFocus = useRef(false);
|
|
90
|
+
const last = pages ? pages.length - 1 : 0;
|
|
91
|
+
const paged = Boolean(pages && enhanced);
|
|
92
|
+
useEffect(() => setEnhanced(true), []);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!moveFocus.current) return;
|
|
95
|
+
moveFocus.current = false;
|
|
96
|
+
headingRefs.current[step]?.focus();
|
|
97
|
+
}, [step]);
|
|
98
|
+
function pageInvalid(i) {
|
|
99
|
+
const controls = pageRefs.current[i]?.querySelectorAll("input,select,textarea") ?? [];
|
|
100
|
+
for (const c of controls) {
|
|
101
|
+
if (!c.checkValidity()) {
|
|
102
|
+
c.reportValidity();
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
function goTo(i) {
|
|
109
|
+
moveFocus.current = true;
|
|
110
|
+
setStep(i);
|
|
111
|
+
}
|
|
112
|
+
function next() {
|
|
113
|
+
if (!pageInvalid(step)) goTo(step + 1);
|
|
114
|
+
}
|
|
69
115
|
useEffect(() => {
|
|
70
116
|
if (!showTurnstile || document.querySelector(`script[src="${TURNSTILE_SRC}"]`)) return;
|
|
71
117
|
const script = document.createElement("script");
|
|
@@ -75,12 +121,13 @@ function BcmsForm({
|
|
|
75
121
|
document.head.appendChild(script);
|
|
76
122
|
}, [showTurnstile]);
|
|
77
123
|
const visibleFields = useMemo(
|
|
78
|
-
() => form.fields.filter((f) => f.type !== "hidden" && shouldShowField(f, values)),
|
|
124
|
+
() => form.fields.filter((f) => f.type !== "hidden" && f.type !== "step" && shouldShowField(f, values)),
|
|
79
125
|
[form.fields, values]
|
|
80
126
|
);
|
|
81
127
|
const setValue = (key, value) => setValues((prev) => ({ ...prev, [key]: value }));
|
|
82
128
|
async function handleSubmit(e) {
|
|
83
129
|
e.preventDefault();
|
|
130
|
+
if (paged && step < last) return next();
|
|
84
131
|
setStatus({ state: "submitting" });
|
|
85
132
|
setFieldErrors({});
|
|
86
133
|
const tokenEl = e.currentTarget.querySelector(
|
|
@@ -89,6 +136,7 @@ function BcmsForm({
|
|
|
89
136
|
const turnstileToken = tokenEl?.value || void 0;
|
|
90
137
|
const data = {};
|
|
91
138
|
for (const field of form.fields) {
|
|
139
|
+
if (field.type === "step") continue;
|
|
92
140
|
if (field.type === "hidden" || shouldShowField(field, values)) {
|
|
93
141
|
data[field.key] = values[field.key];
|
|
94
142
|
}
|
|
@@ -104,6 +152,7 @@ function BcmsForm({
|
|
|
104
152
|
const reset = formInitialValues(form);
|
|
105
153
|
if (form.honeypotField) reset[form.honeypotField] = "";
|
|
106
154
|
setValues(reset);
|
|
155
|
+
setStep(0);
|
|
107
156
|
setStatus({
|
|
108
157
|
state: "success",
|
|
109
158
|
message: form.successMessage ?? "Thanks! Your submission was received."
|
|
@@ -111,7 +160,11 @@ function BcmsForm({
|
|
|
111
160
|
resetTurnstile();
|
|
112
161
|
} catch (err) {
|
|
113
162
|
const e2 = err;
|
|
114
|
-
if (e2.fieldErrors)
|
|
163
|
+
if (e2.fieldErrors) {
|
|
164
|
+
setFieldErrors(e2.fieldErrors);
|
|
165
|
+
const bad = pages?.findIndex((p) => p.fields.some((f) => e2.fieldErrors?.[f.key]));
|
|
166
|
+
if (bad !== void 0 && bad >= 0) goTo(bad);
|
|
167
|
+
}
|
|
115
168
|
setStatus({ state: "error", message: e2.message || "Something went wrong. Please try again." });
|
|
116
169
|
resetTurnstile();
|
|
117
170
|
}
|
|
@@ -121,7 +174,59 @@ function BcmsForm({
|
|
|
121
174
|
// preview frame and patch its copy live. Valued rather than bare: the runtime
|
|
122
175
|
// selectors are `form[data-bcms-form]`, which a valued attribute still matches.
|
|
123
176
|
/* @__PURE__ */ jsxs("form", { className, onSubmit: handleSubmit, noValidate: true, "data-bcms-form": form.id, children: [
|
|
124
|
-
|
|
177
|
+
pages ? pages.map((page, i) => /* @__PURE__ */ jsxs(
|
|
178
|
+
"fieldset",
|
|
179
|
+
{
|
|
180
|
+
ref: (el) => {
|
|
181
|
+
pageRefs.current[i] = el;
|
|
182
|
+
},
|
|
183
|
+
className: "bcms-step",
|
|
184
|
+
"data-bcms-step": i + 1,
|
|
185
|
+
hidden: paged && i !== step,
|
|
186
|
+
"aria-labelledby": `bcms-step-${form.id}-${i + 1}`,
|
|
187
|
+
children: [
|
|
188
|
+
/* @__PURE__ */ jsxs(
|
|
189
|
+
"h3",
|
|
190
|
+
{
|
|
191
|
+
id: `bcms-step-${form.id}-${i + 1}`,
|
|
192
|
+
className: "bcms-step-title",
|
|
193
|
+
tabIndex: -1,
|
|
194
|
+
ref: (el) => {
|
|
195
|
+
headingRefs.current[i] = el;
|
|
196
|
+
},
|
|
197
|
+
"data-bcms-form-field": page.key ?? void 0,
|
|
198
|
+
children: [
|
|
199
|
+
/* @__PURE__ */ jsxs("span", { className: "bcms-step-count", children: [
|
|
200
|
+
"Step ",
|
|
201
|
+
i + 1,
|
|
202
|
+
" of ",
|
|
203
|
+
pages.length
|
|
204
|
+
] }),
|
|
205
|
+
page.label ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
206
|
+
" ",
|
|
207
|
+
/* @__PURE__ */ jsx("span", { "data-bcms-form-part": "label", children: page.label })
|
|
208
|
+
] }) : null
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
),
|
|
212
|
+
visibleFields.filter((f) => page.fields.includes(f)).map((field) => /* @__PURE__ */ jsx(
|
|
213
|
+
Field,
|
|
214
|
+
{
|
|
215
|
+
field,
|
|
216
|
+
value: values[field.key],
|
|
217
|
+
error: fieldErrors[field.key],
|
|
218
|
+
onChange: (v) => setValue(field.key, v)
|
|
219
|
+
},
|
|
220
|
+
field.key
|
|
221
|
+
)),
|
|
222
|
+
paged ? /* @__PURE__ */ jsxs("div", { className: "bcms-step-nav", children: [
|
|
223
|
+
i > 0 ? /* @__PURE__ */ jsx("button", { type: "button", "data-bcms-step-back": true, onClick: () => goTo(i - 1), children: "Back" }) : null,
|
|
224
|
+
i < last ? /* @__PURE__ */ jsx("button", { type: "button", "data-bcms-step-next": true, onClick: next, children: "Next" }) : null
|
|
225
|
+
] }) : null
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
page.key ?? "first"
|
|
229
|
+
)) : visibleFields.map((field) => /* @__PURE__ */ jsx(
|
|
125
230
|
Field,
|
|
126
231
|
{
|
|
127
232
|
field,
|
|
@@ -143,7 +248,16 @@ function BcmsForm({
|
|
|
143
248
|
}
|
|
144
249
|
) }) : null,
|
|
145
250
|
showTurnstile ? /* @__PURE__ */ jsx("div", { className: "cf-turnstile", "data-sitekey": turnstileSiteKey }) : null,
|
|
146
|
-
/* @__PURE__ */ jsx(
|
|
251
|
+
/* @__PURE__ */ jsx(
|
|
252
|
+
"button",
|
|
253
|
+
{
|
|
254
|
+
type: "submit",
|
|
255
|
+
hidden: paged && step < last,
|
|
256
|
+
disabled: status.state === "submitting",
|
|
257
|
+
"data-bcms-form-part": "submit",
|
|
258
|
+
children: status.state === "submitting" ? "Submitting\u2026" : form.submitLabel ?? "Submit"
|
|
259
|
+
}
|
|
260
|
+
),
|
|
147
261
|
status.message ? /* @__PURE__ */ jsx(
|
|
148
262
|
"p",
|
|
149
263
|
{
|
|
@@ -310,16 +424,20 @@ function bindProps(id, entries, ctx) {
|
|
|
310
424
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
311
425
|
var MAX_COMPONENT_DEPTH = 10;
|
|
312
426
|
function BcmsBlocks({
|
|
313
|
-
blocks,
|
|
427
|
+
blocks: all,
|
|
314
428
|
forms,
|
|
315
429
|
components,
|
|
316
430
|
turnstileSiteKey,
|
|
317
431
|
apiBase,
|
|
318
|
-
className
|
|
432
|
+
className,
|
|
433
|
+
now,
|
|
434
|
+
showScheduled
|
|
319
435
|
}) {
|
|
320
436
|
const formsById = new Map((forms ?? []).map((f) => [f.id, f]));
|
|
321
437
|
const componentsById = new Map((components ?? []).map((cmp) => [cmp.id, cmp]));
|
|
322
|
-
const
|
|
438
|
+
const at = now ?? /* @__PURE__ */ new Date();
|
|
439
|
+
const blocks = showScheduled ? all : visibleBlocks(all, at);
|
|
440
|
+
const ctx = { formsById, componentsById, turnstileSiteKey, apiBase, depth: 0, seen: /* @__PURE__ */ new Set(), now: at, showScheduled };
|
|
323
441
|
return (
|
|
324
442
|
/**
|
|
325
443
|
* 🔴 `data-bcms-page` marks THIS SUBTREE as ours to edit — the SDK's equivalent of the hosted
|
|
@@ -335,7 +453,10 @@ function BcmsBlocks({
|
|
|
335
453
|
* NO blocks yet is still recognised — otherwise there is no way to add the first section,
|
|
336
454
|
* which is exactly the state a new page starts in.
|
|
337
455
|
*/
|
|
338
|
-
/* @__PURE__ */
|
|
456
|
+
/* @__PURE__ */ jsxs2("div", { className, "data-bcms-page": "", children: [
|
|
457
|
+
usesHideOn([blocks, components]) && /* @__PURE__ */ jsx2("style", { dangerouslySetInnerHTML: { __html: RESPONSIVE_CSS } }),
|
|
458
|
+
blocks.map((block) => tagTopLevel(block, renderBlock(block, ctx)))
|
|
459
|
+
] })
|
|
339
460
|
);
|
|
340
461
|
}
|
|
341
462
|
function tagTopLevel(block, node) {
|
|
@@ -345,14 +466,21 @@ function tagTopLevel(block, node) {
|
|
|
345
466
|
});
|
|
346
467
|
}
|
|
347
468
|
function renderBlock(block, ctx) {
|
|
469
|
+
const node = renderStyledBlock(block, ctx);
|
|
470
|
+
const state = ctx.showScheduled && ctx.now ? windowState(block, ctx.now) : "live";
|
|
471
|
+
if (state === "live" || !isValidElement(node)) return node;
|
|
472
|
+
return cloneElement(node, { "data-bcms-window": state });
|
|
473
|
+
}
|
|
474
|
+
function renderStyledBlock(block, ctx) {
|
|
348
475
|
const node = renderBlockInner(block, ctx);
|
|
349
476
|
const own = block.type === "button" ? splitButtonStyle(block.style).placement : block.style;
|
|
350
477
|
const style = blockStyleToCss(own);
|
|
351
478
|
const inner = blockStyleToInnerCss(own);
|
|
352
|
-
|
|
479
|
+
const hide = hideOnAttr(own);
|
|
480
|
+
if (!style && !inner && !hide || node == null) return node;
|
|
353
481
|
const column = inner ? /* @__PURE__ */ jsx2("div", { className: "bcms-block-inner", style: inner, children: node }, block.id) : node;
|
|
354
|
-
if (!style) return column;
|
|
355
|
-
return /* @__PURE__ */ jsx2("div", { style, children: column }, block.id);
|
|
482
|
+
if (!style && !hide) return column;
|
|
483
|
+
return /* @__PURE__ */ jsx2("div", { style, ...hide ? { "data-bcms-hide": hide } : {}, children: column }, block.id);
|
|
356
484
|
}
|
|
357
485
|
function renderBlockInner(block, ctx) {
|
|
358
486
|
switch (block.type) {
|
|
@@ -626,6 +754,7 @@ function renderComponent({ id, props }, ctx) {
|
|
|
626
754
|
if (!def) return renderMissingComponent(props.componentId, ctx);
|
|
627
755
|
if (ctx.depth >= MAX_COMPONENT_DEPTH || ctx.seen.has(def.id)) return null;
|
|
628
756
|
const resolved = applyOverrides(def.blockJson, withButtonLookProps(def), props.overrides ?? {});
|
|
757
|
+
const shown = ctx.showScheduled || !ctx.now ? resolved : visibleBlocks(resolved, ctx.now);
|
|
629
758
|
const childCtx = {
|
|
630
759
|
...ctx,
|
|
631
760
|
depth: ctx.depth + 1,
|
|
@@ -640,7 +769,7 @@ function renderComponent({ id, props }, ctx) {
|
|
|
640
769
|
"data-bcms-component": def.id,
|
|
641
770
|
"data-bcms-variant": "base",
|
|
642
771
|
"data-bcms-instance": id,
|
|
643
|
-
children:
|
|
772
|
+
children: shown.map((child) => renderBlock(child, childCtx))
|
|
644
773
|
},
|
|
645
774
|
id
|
|
646
775
|
);
|