@bettercms-ai/astro 0.14.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/components/BcmsForm.astro +80 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -132,6 +132,25 @@ or on-demand-revalidation hook.
|
|
|
132
132
|
|
|
133
133
|
Markup is class-driven and unstyled — you own the CSS.
|
|
134
134
|
|
|
135
|
+
### Phone fields with a country code picker
|
|
136
|
+
|
|
137
|
+
A phone field whose author enabled **Country code picker** in the builder renders a country
|
|
138
|
+
control beside the number. Whatever the visitor picks, the field still submits **one value**,
|
|
139
|
+
under its own key, in E.164 (`+14155550123`) — there is no second key to handle.
|
|
140
|
+
|
|
141
|
+
Two classes are yours to style:
|
|
142
|
+
|
|
143
|
+
| Class | Element |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `bcms-phone` | the wrapper around the country control and the number |
|
|
146
|
+
| `bcms-phone-country` | the country control itself |
|
|
147
|
+
|
|
148
|
+
The country control is a native `<select>` built at build/SSR time, so **no phone library
|
|
149
|
+
reaches the visitor's browser**. The trade is that the number is not reformatted as it is
|
|
150
|
+
typed — the value is still composed correctly on submit. `@bettercms-ai/next` makes the other
|
|
151
|
+
choice and ships a formatter in a lazy chunk.
|
|
152
|
+
|
|
153
|
+
|
|
135
154
|
## Live editing on a static site — `<BcmsLiveBlocks>`
|
|
136
155
|
|
|
137
156
|
When the Visual Editor frames a static build it can only show what was built, so editing a
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import config from "bettercms:config";
|
|
11
11
|
import type { DeliveryForm, DeliveryFormField } from "@bettercms-ai/sdk";
|
|
12
|
+
import { getCountries, getCountryCallingCode, isSupportedCountry, getExampleNumber } from "libphonenumber-js/min";
|
|
13
|
+
import phoneExamples from "libphonenumber-js/mobile/examples";
|
|
12
14
|
|
|
13
15
|
interface Props {
|
|
14
16
|
form: DeliveryForm;
|
|
@@ -32,6 +34,46 @@ const INPUT_TYPE: Partial<Record<DeliveryFormField["type"], string>> = {
|
|
|
32
34
|
url: "url",
|
|
33
35
|
};
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Calling-code options for a phone picker. Frontmatter only — this runs at build/SSR time and
|
|
39
|
+
* ships ZERO bytes of libphonenumber to the visitor; the inline script below never sees it.
|
|
40
|
+
*
|
|
41
|
+
* Every libphonenumber helper is wrapped in an arrow rather than passed as a bare callback:
|
|
42
|
+
* `.filter`/`.map` hand it the array INDEX as its `metadata` argument and it throws.
|
|
43
|
+
*/
|
|
44
|
+
const REGION_NAMES =
|
|
45
|
+
typeof Intl !== "undefined" && typeof Intl.DisplayNames === "function"
|
|
46
|
+
? new Intl.DisplayNames(["en"], { type: "region" })
|
|
47
|
+
: null;
|
|
48
|
+
|
|
49
|
+
let countryCache: { iso: string; name: string; code: string; trunk: boolean }[] | null = null;
|
|
50
|
+
const countryList = () => {
|
|
51
|
+
if (countryCache) return countryCache;
|
|
52
|
+
countryCache = getCountries()
|
|
53
|
+
.filter((iso) => isSupportedCountry(iso))
|
|
54
|
+
.map((iso) => {
|
|
55
|
+
let name = iso as string;
|
|
56
|
+
try {
|
|
57
|
+
name = REGION_NAMES?.of(iso) ?? iso;
|
|
58
|
+
} catch {
|
|
59
|
+
name = iso;
|
|
60
|
+
}
|
|
61
|
+
let trunk = false;
|
|
62
|
+
try {
|
|
63
|
+
const ex = getExampleNumber(iso, phoneExamples);
|
|
64
|
+
if (ex) {
|
|
65
|
+
const national = ex.formatNational().replace(/\D/g, "");
|
|
66
|
+
trunk = national.length > String(ex.nationalNumber).length && national.startsWith("0");
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
trunk = false;
|
|
70
|
+
}
|
|
71
|
+
return { iso: iso as string, name, code: getCountryCallingCode(iso), trunk };
|
|
72
|
+
})
|
|
73
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
74
|
+
return countryCache;
|
|
75
|
+
};
|
|
76
|
+
|
|
35
77
|
const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
36
78
|
---
|
|
37
79
|
|
|
@@ -98,6 +140,29 @@ const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
|
98
140
|
<select id={`bcms-f-${f.key}`} name={f.key} required={f.required}>
|
|
99
141
|
{(f.options ?? []).map((o) => <option value={o} data-bcms-form-part="option">{o}</option>)}
|
|
100
142
|
</select>
|
|
143
|
+
) : f.type === "phone" && f.ui?.countryPicker ? (
|
|
144
|
+
/* A country select plus a tel input, submitting ONE E.164 value. `required` belongs
|
|
145
|
+
on the number only — a select always has a value and would never block. */
|
|
146
|
+
<span class="bcms-phone">
|
|
147
|
+
<select class="bcms-phone-country" name={`${f.key}__country`} aria-label="Country code">
|
|
148
|
+
<option value="" selected={!f.ui.defaultCountry}>International</option>
|
|
149
|
+
{countryList().map((c) => (
|
|
150
|
+
<option value={c.iso} data-cc={c.code} data-trunk={c.trunk ? "1" : undefined} selected={c.iso === f.ui!.defaultCountry}>
|
|
151
|
+
{c.name} +{c.code}
|
|
152
|
+
</option>
|
|
153
|
+
))}
|
|
154
|
+
</select>
|
|
155
|
+
<input
|
|
156
|
+
type="tel"
|
|
157
|
+
id={`bcms-f-${f.key}`}
|
|
158
|
+
name={f.key}
|
|
159
|
+
placeholder={f.placeholder}
|
|
160
|
+
value={f.defaultValue}
|
|
161
|
+
required={f.required}
|
|
162
|
+
autocomplete="tel"
|
|
163
|
+
dir="ltr"
|
|
164
|
+
/>
|
|
165
|
+
</span>
|
|
101
166
|
) : (
|
|
102
167
|
<input
|
|
103
168
|
type={INPUT_TYPE[f.type] ?? "text"}
|
|
@@ -126,6 +191,13 @@ const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
|
126
191
|
{showTurnstile && <script is:inline src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>}
|
|
127
192
|
|
|
128
193
|
<script is:inline>
|
|
194
|
+
// Compose one E.164 value from a country-picker phone field. CANONICAL COPY is
|
|
195
|
+
// PHONE_COMPOSE_JS in the backend's src/lib/sites/render-page.ts, and this file is pinned
|
|
196
|
+
// against it byte for byte by src/__tests__/content/form-script-pin.test.ts. All three
|
|
197
|
+
// vanilla lanes are inline script text on someone else's page and cannot import at runtime,
|
|
198
|
+
// so the rule genuinely exists more than once.
|
|
199
|
+
function bcmsPhone(cc,raw,trunk){var d=String(raw||'').trim();if(!d)return '';if(d.charAt(0)==='+'){var i=d.replace(/\D/g,'');return i?'+'+i:'';}if(!cc)return d;var n=d.replace(/\D/g,'');if(trunk&&n.charAt(0)==='0')n=n.slice(1);return n?'+'+cc+n:'';}
|
|
200
|
+
|
|
129
201
|
// One copy of this script ships per <BcmsForm> instance, and each copy sees EVERY
|
|
130
202
|
// form on the page — so two forms meant four bindings and a double submit. The
|
|
131
203
|
// `bcmsFormBound` flag makes binding idempotent no matter how many copies run,
|
|
@@ -177,6 +249,14 @@ const showTurnstile = Boolean(form.turnstileEnabled && turnstileSiteKey);
|
|
|
177
249
|
if (k in data) data[k] = Array.isArray(data[k]) ? [...data[k], v] : [data[k], v];
|
|
178
250
|
else data[k] = v;
|
|
179
251
|
});
|
|
252
|
+
// Compose AFTER the FormData loop. A showIf pointing at a phone key must keep comparing
|
|
253
|
+
// the raw digits, and applyConds runs on every input event.
|
|
254
|
+
form.querySelectorAll('select[name$="__country"]').forEach((sel) => {
|
|
255
|
+
const k = sel.name.slice(0, -9);
|
|
256
|
+
const o = sel.options[sel.selectedIndex];
|
|
257
|
+
data[k] = bcmsPhone(o ? (o.getAttribute("data-cc") || "") : "", data[k], o ? o.getAttribute("data-trunk") === "1" : false);
|
|
258
|
+
delete data[k + "__country"];
|
|
259
|
+
});
|
|
180
260
|
const tsEl = form.querySelector('[name="cf-turnstile-response"]');
|
|
181
261
|
const body = { data };
|
|
182
262
|
if (tsEl?.value) body["cf-turnstile-response"] = tsEl.value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bettercms-ai/astro",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The BetterCMS adapter for Astro — a `bettercms()` integration, a `bettercms:client` virtual module, typed content loaders, draft preview, and native .astro rendering components.",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,11 +54,12 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@bettercms-ai/image-url": "^0.3.0",
|
|
57
|
-
"@bettercms-ai/
|
|
57
|
+
"@bettercms-ai/next": "^0.13.0",
|
|
58
|
+
"@bettercms-ai/richtext": "^0.3.0",
|
|
59
|
+
"@bettercms-ai/sdk": "^1.21.0",
|
|
58
60
|
"@bettercms-ai/types": "^1.11.0",
|
|
59
61
|
"@bettercms-ai/ui": "^0.7.0",
|
|
60
|
-
"
|
|
61
|
-
"@bettercms-ai/next": "^0.13.0"
|
|
62
|
+
"libphonenumber-js": "^1.13.13"
|
|
62
63
|
},
|
|
63
64
|
"peerDependencies": {
|
|
64
65
|
"astro": ">=4",
|