@authhero/widget 0.37.0 → 0.38.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/authhero-widget/authhero-widget.esm.js +1 -1
- package/dist/authhero-widget/index.esm.js +1 -1
- package/dist/authhero-widget/p-b5f14fa6.entry.js +1 -0
- package/dist/authhero-widget/p-f94037cd.entry.js +1 -0
- package/dist/cjs/authhero-node.cjs.entry.js +458 -13
- package/dist/cjs/authhero-widget.cjs.entry.js +79 -10
- package/dist/cjs/authhero-widget.cjs.js +1 -1
- package/dist/cjs/index.cjs.js +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/collection/components/authhero-node/authhero-node.css +94 -0
- package/dist/collection/components/authhero-node/authhero-node.js +302 -13
- package/dist/collection/components/authhero-widget/authhero-widget.js +98 -10
- package/dist/collection/utils/date-format.js +176 -0
- package/dist/components/authhero-node.js +1 -1
- package/dist/components/authhero-widget.js +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/p-C9ZfIQiS.js +1 -0
- package/dist/esm/authhero-node.entry.js +458 -13
- package/dist/esm/authhero-widget.entry.js +79 -10
- package/dist/esm/authhero-widget.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/types/components/authhero-node/authhero-node.d.ts +83 -0
- package/dist/types/components/authhero-widget/authhero-widget.d.ts +19 -0
- package/dist/types/components.d.ts +18 -0
- package/dist/types/utils/date-format.d.ts +52 -0
- package/hydrate/index.js +541 -24
- package/hydrate/index.mjs +541 -24
- package/package.json +2 -2
- package/dist/authhero-widget/p-cdfc4555.entry.js +0 -1
- package/dist/authhero-widget/p-e95c436f.entry.js +0 -1
- package/dist/components/p-BP46GHTc.js +0 -1
|
@@ -236,7 +236,184 @@ function getCountryByCode(code) {
|
|
|
236
236
|
countries.find((c) => c.code === "US"));
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
const authheroNodeCss = () => `:host{display:block}.input-wrapper{display:flex;flex-direction:column;position:relative;margin-top:8px;margin-bottom:16px}.input-container{position:relative;width:100%}.input-label{position:absolute;left:16px;top:50%;transform:translateY(-50%);font-size:var(--ah-font-size-label, 16px);font-weight:var(--ah-font-weight-label, 400);color:var(--ah-color-text-muted, #65676e);pointer-events:none;transition:all 0.15s ease-out;background-color:transparent;padding:0;z-index:1}.input-label.floating,.input-field:focus+.input-label,.input-field:not(:placeholder-shown)+.input-label,select.input-field+.input-label,input[type="date"].input-field+.input-label{top:-8px;transform:translateY(0);font-size:12px;background-color:var(--ah-color-bg, #ffffff);padding:0 4px;left:12px;color:var(--ah-color-text-muted, #65676e)}.input-field:focus+.input-label{color:var(--ah-color-primary, #635dff)}.required{color:var(--ah-color-error, #d03c38);margin-left:2px}.input-field{width:100%;padding:16px;font-size:16px;font-family:inherit;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:var(--ah-input-radius, 3px);outline:none;transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out;box-sizing:border-box}.input-field::placeholder{color:transparent}.input-field:hover{border-color:var(--ah-color-border-hover, #65676e)}.input-field:focus{border-color:var(--ah-color-primary, #635dff);box-shadow:inset 0 0 0 1px var(--ah-color-primary, #635dff)}.input-field.has-error{border-color:var(--ah-color-error, #d03c38)}.input-field.has-error:focus{box-shadow:inset 0 0 0 1px var(--ah-color-error, #d03c38)}.input-field:disabled{background-color:var(--ah-color-bg-disabled, #f5f5f5);border-color:var(--ah-color-border-disabled, #e0e1e3);cursor:not-allowed;opacity:0.7}.password-container{position:relative;display:flex;align-items:center}.password-container .input-field{padding-right:48px}.password-toggle{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;color:var(--ah-color-icon, var(--ah-color-text-muted, #65676e));transition:color 0.15s ease}.password-toggle:hover{color:var(--ah-color-icon, var(--ah-color-text, #1e212a))}.password-toggle svg{width:20px;height:20px}.error-text{font-size:12px;color:var(--ah-color-error, #d03c38);margin-top:4px;line-height:1.4}.helper-text{font-size:12px;color:var(--ah-color-text-muted, #65676e);margin-top:4px;line-height:1.4}.field-link{display:block;text-align:left;margin-top:8px;margin-bottom:16px}.field-link a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:var(--ah-font-size-link, 14px);font-weight:var(--ah-font-weight-link, 400)}.field-link a:hover{text-decoration:underline}.checkbox-wrapper{display:flex;align-items:flex-start;gap:10px;cursor:pointer;margin-bottom:16px}.checkbox-wrapper input[type="checkbox"]{width:18px;height:18px;margin:0;accent-color:var(--ah-color-primary, #635dff);cursor:pointer;flex-shrink:0}.checkbox-label{font-size:var(--ah-font-size-body, 14px);font-weight:var(--ah-font-weight-body, 400);color:var(--ah-color-text, #1e212a);line-height:1.5}.btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;width:100%;padding:14px 20px;font-size:var(--ah-font-size-btn, 16px);font-weight:var(--ah-font-weight-btn, 400);font-family:inherit;line-height:1.25;text-align:center;text-decoration:none;border:none;border-radius:var(--ah-btn-radius, 3px);cursor:pointer;transition:background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;box-sizing:border-box}.btn:disabled{opacity:0.6;cursor:not-allowed}.btn:not(:disabled):active{transform:scale(0.98)}.btn:focus-visible{outline:2px solid var(--ah-color-focus-ring, var(--ah-color-primary, #635dff));outline-offset:2px}.btn-primary{background-color:var(--ah-color-primary, #635dff);color:var(--ah-color-text-on-primary, #ffffff);margin-top:12px}.btn-primary:not(:disabled):hover{background-color:var( --ah-color-primary-hover, color-mix(in srgb, var(--ah-color-primary, #635dff) 85%, black) )}.btn-secondary{background-color:var(--ah-color-bg, #ffffff);color:var(--ah-btn-secondary-text, var(--ah-color-text, #1e212a));border:var(--ah-btn-border-width, 1px) solid var(--ah-btn-secondary-border, var(--ah-color-border, #c9cace))}.btn-secondary:not(:disabled):hover{background-color:var(--ah-color-bg-hover, #f5f5f5);border-color:var(--ah-color-border-hover, #65676e)}.btn-link{background:none;border:none;color:var(--ah-color-link, #635dff);padding:8px 0;font-weight:var(--ah-font-weight-link, 400);text-decoration:none}.btn-link:hover{text-decoration:underline}.phone-input-wrapper{display:flex;align-items:stretch;gap:0;width:100%}.country-select{flex-shrink:0;width:auto;padding:12px 8px;font-size:var(--ah-font-size-body, 16px);font-family:var(--ah-font-family, inherit);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-right:none;border-radius:var(--ah-input-radius, 4px) 0 0 var(--ah-input-radius, 4px);background-color:var(--ah-color-input-bg, var(--ah-color-surface, #fff));color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));cursor:pointer;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 6px center;padding-right:22px}.country-select:focus{outline:none;border-color:var(--ah-color-primary, #635dff);box-shadow:0 0 0 1px var(--ah-color-primary, #635dff)}.country-select:disabled{opacity:0.5;cursor:not-allowed}.phone-input-wrapper .input-container{flex:1;min-width:0}.phone-input-wrapper .input-field{border-radius:0 var(--ah-input-radius, 4px) var(--ah-input-radius, 4px) 0}.phone-input-wrapper .input-label.floating,.phone-input-wrapper .input-field:focus+.input-label,.phone-input-wrapper .input-field:not(:placeholder-shown)+.input-label{left:8px}.social-buttons{display:flex;flex-direction:column;gap:12px}.btn-social{display:flex;align-items:center;justify-content:center;gap:12px;position:relative}.btn-social-badge{position:absolute;right:12px;top:0;transform:translateY(-50%);font-size:11px;font-weight:500;line-height:1;padding:4px 9px;border-radius:999px;background:var(--ah-color-last-used, var(--ah-color-primary, #635dff));color:var(--ah-color-last-used-text, var(--ah-color-text-on-primary, #fff));white-space:nowrap;pointer-events:none}.btn-social.btn-social-last-used{border-color:var(--ah-color-last-used, var(--ah-color-primary, #635dff));box-shadow:inset 0 0 0 1px var(--ah-color-last-used, var(--ah-color-primary, #635dff))}.input-badge{position:absolute;right:12px;top:0;transform:translateY(-50%);font-size:11px;font-weight:500;line-height:1;padding:4px 9px;border-radius:999px;background:var(--ah-color-last-used, var(--ah-color-primary, #635dff));color:var(--ah-color-last-used-text, var(--ah-color-text-on-primary, #fff));white-space:nowrap;pointer-events:none;z-index:2}.btn-social-content{display:flex;flex-direction:column;align-items:center;text-align:center}.btn-social-subtitle{font-size:12px;font-style:italic;opacity:0.8;margin-top:2px}.btn-social-subtitle:empty{display:none}.social-icon{width:20px;height:20px;flex-shrink:0}@media (max-width: 480px){.social-buttons:has(.btn-social:nth-child(3)){flex-direction:row;flex-wrap:nowrap;justify-content:stretch;gap:8px}.social-buttons:has(.btn-social:nth-child(3)) .btn-social{width:auto;min-width:0;padding:12px;flex:1 1 0}.social-buttons:has(.btn-social:nth-child(3)) .btn-social span{display:none}.social-buttons:has(.btn-social:nth-child(3)) .social-icon{width:24px;height:24px}}.btn-icon{width:20px;height:20px;flex-shrink:0}.btn-icon img{width:100%;height:100%;object-fit:contain}.text-title{font-size:var(--ah-font-size-title, 20px);font-weight:var(--ah-font-weight-title, 400);color:var(--ah-color-text-header, var(--ah-color-header, #1e212a));margin:8px 0;line-height:1.3}.text-title.text-success{color:var(--ah-color-success, #13a769)}.text-description{font-size:var(--ah-font-size-subtitle, 14px);font-weight:var(--ah-font-weight-subtitle, 400);color:var(--ah-color-text-muted, #65676e);margin:4px 0;line-height:1.5}.image{display:block;max-width:100%;height:auto;border-radius:4px}.image-centered{margin:0 auto 16px;width:52px;height:52px;object-fit:contain}.link{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:var(--ah-font-size-link, 14px);transition:color 0.15s ease}.link:hover{text-decoration:underline}.link:focus-visible{outline:2px solid var(--ah-color-focus-ring, var(--ah-color-link, #635dff));outline-offset:2px;border-radius:2px}.node-error{padding:12px 16px;background-color:var(--ah-color-error-bg, #ffeaea);color:var(--ah-color-error, #d03c38);border-left:3px solid var(--ah-color-error, #d03c38);border-radius:0;font-size:14px;margin-bottom:16px}.node-success{padding:12px 16px;background-color:var(--ah-color-success-bg, #e6f9f1);color:var(--ah-color-success, #13a769);border-left:3px solid var(--ah-color-success, #13a769);border-radius:0;font-size:14px;margin-bottom:16px}.divider{display:flex;align-items:center;text-align:center;margin:16px 0}.divider::before,.divider::after{content:"";flex:1;border-bottom:1px solid var(--ah-color-border-muted, #c9cace)}.divider-text{padding:0 10px;font-size:12px;font-weight:400;color:var(--ah-color-text-muted, #65676e);text-transform:uppercase;letter-spacing:0.5px}.rich-text{font-size:var(--ah-font-size-body, 14px);font-weight:var(--ah-font-weight-body, 400);line-height:1.5;color:var(--ah-color-text, #1e212a)}.rich-text a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);transition:color 0.15s ease}.rich-text a:hover{text-decoration:underline}.rich-text .terms-text{margin-top:16px;text-align:center;font-size:12px;color:var(--ah-color-text-muted, #65676e)}.rich-text .terms-text a{font-size:12px}.rich-text .forgot-password-link{text-align:right;font-size:13px;margin-top:4px}.rich-text .forgot-password-link a{font-size:13px;color:var(--ah-color-link, #635dff)}.rich-text .passwordless-link{margin-top:16px;text-align:center;font-size:14px}.rich-text .passwordless-link a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none)}.rich-text .passwordless-link a:hover{text-decoration:underline}.rich-text .signup-link{margin-top:16px;text-align:center;font-size:14px;color:var(--ah-color-text, #1e212a)}.code-field{position:relative}.code-input{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:none;background:transparent;color:transparent;caret-color:transparent;font-family:inherit;letter-spacing:0;outline:none;cursor:text;-webkit-text-fill-color:transparent}.code-boxes{display:flex;align-items:center;gap:var(--ah-code-group-gap, 12px);justify-content:center}.code-group{display:flex;gap:0}.code-separator{color:var(--ah-color-text-muted, #65676e);font-size:var(--ah-code-font-size, 24px);user-select:none}.code-box-placeholder{color:var(--ah-color-text-muted, #65676e);opacity:0.4}.code-box{position:relative;flex:0 0 auto;width:var(--ah-code-box-width, 48px);height:var(--ah-code-box-height, 56px);display:flex;align-items:center;justify-content:center;font-size:var(--ah-code-font-size, 24px);font-family:inherit;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:0;transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out;box-sizing:border-box}.code-box+.code-box{margin-left:calc(-1 * var(--ah-input-border-width, 1px))}.code-group .code-box:first-child{border-top-left-radius:var(--ah-input-radius, 3px);border-bottom-left-radius:var(--ah-input-radius, 3px)}.code-group .code-box:last-child{border-top-right-radius:var(--ah-input-radius, 3px);border-bottom-right-radius:var(--ah-input-radius, 3px)}.code-field:focus-within .code-box.is-active{z-index:1;border-color:var(--ah-color-primary, #635dff);box-shadow:inset 0 0 0 1px var(--ah-color-primary, #635dff)}.code-field.has-error .code-box{border-color:var(--ah-color-error, #d03c38)}.code-input:disabled{cursor:not-allowed}.code-input:disabled~.code-boxes .code-box{background-color:var(--ah-color-bg-disabled, #f5f5f5);border-color:var(--ah-color-border-disabled, #e0e1e3)}`;
|
|
239
|
+
/**
|
|
240
|
+
* Locale-aware layout for the segmented DATE input.
|
|
241
|
+
*
|
|
242
|
+
* The segment order is resolved from a static table rather than from
|
|
243
|
+
* `Intl.DateTimeFormat`, deliberately: the widget is server-rendered and then
|
|
244
|
+
* hydrated in the browser, and the two runtimes do not ship the same ICU data
|
|
245
|
+
* (a Workers runtime commonly falls back to en-US ordering). A static table
|
|
246
|
+
* gives the server and the client the same answer, so the hydrated DOM matches
|
|
247
|
+
* the rendered one.
|
|
248
|
+
*/
|
|
249
|
+
/** Languages that write dates big-endian: year, month, day. */
|
|
250
|
+
const YEAR_FIRST_LANGUAGES = new Set([
|
|
251
|
+
"bo",
|
|
252
|
+
"dz",
|
|
253
|
+
"hu",
|
|
254
|
+
"ja",
|
|
255
|
+
"ko",
|
|
256
|
+
"lt",
|
|
257
|
+
"mn",
|
|
258
|
+
"sv",
|
|
259
|
+
"ug",
|
|
260
|
+
"zh",
|
|
261
|
+
]);
|
|
262
|
+
/**
|
|
263
|
+
* Regions that write month before day. Month-first is essentially a US
|
|
264
|
+
* convention plus the places that adopted it.
|
|
265
|
+
*/
|
|
266
|
+
const MONTH_FIRST_REGIONS = new Set(["us", "ph", "fm", "mh", "pw", "as", "gu"]);
|
|
267
|
+
/**
|
|
268
|
+
* Locales whose language is normally year-first but which write dates
|
|
269
|
+
* day-first in a particular region — Finland-Swedish is d.m.yyyy, unlike
|
|
270
|
+
* Sweden-Swedish.
|
|
271
|
+
*/
|
|
272
|
+
const DAY_FIRST_TAGS = new Set(["sv-fi"]);
|
|
273
|
+
const DEFAULT_TOKENS = {
|
|
274
|
+
day: "DD",
|
|
275
|
+
month: "MM",
|
|
276
|
+
year: "YYYY",
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Split a BCP-47 tag into a lowercase language subtag and, when present, a
|
|
280
|
+
* two-letter region subtag. Script and variant subtags are ignored.
|
|
281
|
+
*/
|
|
282
|
+
function parseTag(locale) {
|
|
283
|
+
const tag = (locale || "").toLowerCase().replace(/_/g, "-");
|
|
284
|
+
const parts = tag.split("-").filter(Boolean);
|
|
285
|
+
const language = parts[0] || "";
|
|
286
|
+
const region = parts.slice(1).find((part) => /^[a-z]{2}$/.test(part));
|
|
287
|
+
return { language, region };
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Resolve the segment order for a locale. Falls back to day-month-year, which
|
|
291
|
+
* is what most of the world writes.
|
|
292
|
+
*/
|
|
293
|
+
function getDateOrder(locale) {
|
|
294
|
+
const { language, region } = parseTag(locale);
|
|
295
|
+
if (region && MONTH_FIRST_REGIONS.has(region)) {
|
|
296
|
+
return ["month", "day", "year"];
|
|
297
|
+
}
|
|
298
|
+
if (region && DAY_FIRST_TAGS.has(`${language}-${region}`)) {
|
|
299
|
+
return ["day", "month", "year"];
|
|
300
|
+
}
|
|
301
|
+
if (YEAR_FIRST_LANGUAGES.has(language)) {
|
|
302
|
+
return ["year", "month", "day"];
|
|
303
|
+
}
|
|
304
|
+
// A bare "en" carries no region — treat it the way Intl does and assume
|
|
305
|
+
// en-US. Regional English (en-gb, en-au, …) falls through to day-first.
|
|
306
|
+
if (language === "en" && !region) {
|
|
307
|
+
return ["month", "day", "year"];
|
|
308
|
+
}
|
|
309
|
+
return ["day", "month", "year"];
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Parse an explicit format string from the component config, e.g.
|
|
313
|
+
* "DD/MM/YYYY" or "YYYY-MM-DD". Returns null when the string does not name
|
|
314
|
+
* all three segments.
|
|
315
|
+
*/
|
|
316
|
+
function parseFormat(format) {
|
|
317
|
+
const matches = format.match(/y+|m+|d+/gi);
|
|
318
|
+
if (!matches)
|
|
319
|
+
return null;
|
|
320
|
+
const order = [];
|
|
321
|
+
const tokens = { ...DEFAULT_TOKENS };
|
|
322
|
+
for (const match of matches) {
|
|
323
|
+
const initial = match[0].toLowerCase();
|
|
324
|
+
const segment = initial === "y" ? "year" : initial === "m" ? "month" : "day";
|
|
325
|
+
if (order.includes(segment))
|
|
326
|
+
continue;
|
|
327
|
+
order.push(segment);
|
|
328
|
+
// A "YY" format still collects a four-digit year — the segment holds four
|
|
329
|
+
// characters and `toIsoDate` rejects anything shorter, with a two-digit
|
|
330
|
+
// entry expanded on blur. Showing "YY" as the placeholder would promise an
|
|
331
|
+
// input the field does not accept, so short year tokens are widened.
|
|
332
|
+
tokens[segment] =
|
|
333
|
+
segment === "year" && match.length < 4 ? DEFAULT_TOKENS.year : match;
|
|
334
|
+
}
|
|
335
|
+
if (order.length !== 3)
|
|
336
|
+
return null;
|
|
337
|
+
const separator = format.match(/[^a-z\s]/i)?.[0] ?? "/";
|
|
338
|
+
return { order, tokens, separator };
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Resolve the full layout for a DATE field. An explicit `config.format` wins;
|
|
342
|
+
* otherwise the locale decides the order.
|
|
343
|
+
*/
|
|
344
|
+
function getDateLayout(format, locale) {
|
|
345
|
+
if (format) {
|
|
346
|
+
const parsed = parseFormat(format);
|
|
347
|
+
if (parsed)
|
|
348
|
+
return parsed;
|
|
349
|
+
}
|
|
350
|
+
const order = getDateOrder(locale);
|
|
351
|
+
return {
|
|
352
|
+
order,
|
|
353
|
+
tokens: DEFAULT_TOKENS,
|
|
354
|
+
separator: order[0] === "year" ? "-" : "/",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/** Number of days in a given month, honouring leap years. */
|
|
358
|
+
function daysInMonth(year, month) {
|
|
359
|
+
if (month < 1 || month > 12)
|
|
360
|
+
return 0;
|
|
361
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
362
|
+
}
|
|
363
|
+
/** Split an ISO "YYYY-MM-DD" value into its segments. */
|
|
364
|
+
function parseIsoDate(value) {
|
|
365
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec((value || "").trim());
|
|
366
|
+
if (!match)
|
|
367
|
+
return null;
|
|
368
|
+
return { year: match[1], month: match[2], day: match[3] };
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Build an ISO "YYYY-MM-DD" string from segment values. Returns "" unless all
|
|
372
|
+
* three segments are complete and form a real calendar date — a half-filled or
|
|
373
|
+
* impossible date (31 February) is not a value worth submitting.
|
|
374
|
+
*/
|
|
375
|
+
function toIsoDate(segments) {
|
|
376
|
+
const { year, month, day } = segments;
|
|
377
|
+
if (year.length !== 4 || month.length === 0 || day.length === 0)
|
|
378
|
+
return "";
|
|
379
|
+
const y = Number(year);
|
|
380
|
+
const m = Number(month);
|
|
381
|
+
const d = Number(day);
|
|
382
|
+
if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) {
|
|
383
|
+
return "";
|
|
384
|
+
}
|
|
385
|
+
if (m < 1 || m > 12)
|
|
386
|
+
return "";
|
|
387
|
+
if (d < 1 || d > daysInMonth(y, m))
|
|
388
|
+
return "";
|
|
389
|
+
return `${String(y).padStart(4, "0")}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Expand a two-digit year into the most recent matching year at or before
|
|
393
|
+
* `anchorYear` — "85" becomes 1985, "05" becomes 2005. Anything that is not
|
|
394
|
+
* exactly two digits is returned unchanged, so a fully typed year is never
|
|
395
|
+
* second-guessed.
|
|
396
|
+
*/
|
|
397
|
+
function expandTwoDigitYear(year, anchorYear) {
|
|
398
|
+
if (!/^\d{2}$/.test(year))
|
|
399
|
+
return year;
|
|
400
|
+
const century = Math.floor(anchorYear / 100) * 100;
|
|
401
|
+
const candidate = century + Number(year);
|
|
402
|
+
return String(candidate > anchorYear ? candidate - 100 : candidate);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The latest year a two-digit entry may expand to. `config.max` pins it when
|
|
406
|
+
* the field has an upper bound; otherwise we assume a date in the past (the
|
|
407
|
+
* birthdate case) and anchor on the current year.
|
|
408
|
+
*/
|
|
409
|
+
function resolveYearAnchor(max, today) {
|
|
410
|
+
const parsed = parseIsoDate(max);
|
|
411
|
+
if (parsed)
|
|
412
|
+
return Number(parsed.year);
|
|
413
|
+
return today.getUTCFullYear();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const authheroNodeCss = () => `:host{display:block}.input-wrapper{display:flex;flex-direction:column;position:relative;margin-top:8px;margin-bottom:16px}.input-container{position:relative;width:100%}.input-label{position:absolute;left:16px;top:50%;transform:translateY(-50%);font-size:var(--ah-font-size-label, 16px);font-weight:var(--ah-font-weight-label, 400);color:var(--ah-color-text-muted, #65676e);pointer-events:none;transition:all 0.15s ease-out;background-color:transparent;padding:0;z-index:1}.input-label.floating,.input-field:focus+.input-label,.input-field:not(:placeholder-shown)+.input-label,select.input-field+.input-label,input[type="date"].input-field+.input-label{top:-8px;transform:translateY(0);font-size:12px;background-color:var(--ah-color-bg, #ffffff);padding:0 4px;left:12px;color:var(--ah-color-text-muted, #65676e)}.input-field:focus+.input-label{color:var(--ah-color-primary, #635dff)}.required{color:var(--ah-color-error, #d03c38);margin-left:2px}.input-field{width:100%;padding:16px;font-size:16px;font-family:inherit;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:var(--ah-input-radius, 3px);outline:none;transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out;box-sizing:border-box}.input-field::placeholder{color:transparent}.input-field:hover{border-color:var(--ah-color-border-hover, #65676e)}.input-field:focus{border-color:var(--ah-color-primary, #635dff);box-shadow:inset 0 0 0 1px var(--ah-color-primary, #635dff)}.input-field.has-error{border-color:var(--ah-color-error, #d03c38)}.input-field.has-error:focus{box-shadow:inset 0 0 0 1px var(--ah-color-error, #d03c38)}.input-field:disabled{background-color:var(--ah-color-bg-disabled, #f5f5f5);border-color:var(--ah-color-border-disabled, #e0e1e3);cursor:not-allowed;opacity:0.7}.password-container{position:relative;display:flex;align-items:center}.password-container .input-field{padding-right:48px}.password-toggle{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;color:var(--ah-color-icon, var(--ah-color-text-muted, #65676e));transition:color 0.15s ease}.password-toggle:hover{color:var(--ah-color-icon, var(--ah-color-text, #1e212a))}.password-toggle svg{width:20px;height:20px}.error-text{font-size:12px;color:var(--ah-color-error, #d03c38);margin-top:4px;line-height:1.4}.helper-text{font-size:12px;color:var(--ah-color-text-muted, #65676e);margin-top:4px;line-height:1.4}.field-link{display:block;text-align:left;margin-top:8px;margin-bottom:16px}.field-link a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:var(--ah-font-size-link, 14px);font-weight:var(--ah-font-weight-link, 400)}.field-link a:hover{text-decoration:underline}.checkbox-wrapper{display:flex;align-items:flex-start;gap:10px;cursor:pointer;margin-bottom:16px}.checkbox-wrapper input[type="checkbox"]{width:18px;height:18px;margin:0;accent-color:var(--ah-color-primary, #635dff);cursor:pointer;flex-shrink:0}.checkbox-label{font-size:var(--ah-font-size-body, 14px);font-weight:var(--ah-font-weight-body, 400);color:var(--ah-color-text, #1e212a);line-height:1.5}.btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;width:100%;padding:14px 20px;font-size:var(--ah-font-size-btn, 16px);font-weight:var(--ah-font-weight-btn, 400);font-family:inherit;line-height:1.25;text-align:center;text-decoration:none;border:none;border-radius:var(--ah-btn-radius, 3px);cursor:pointer;transition:background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;box-sizing:border-box}.btn:disabled{opacity:0.6;cursor:not-allowed}.btn:not(:disabled):active{transform:scale(0.98)}.btn:focus-visible{outline:2px solid var(--ah-color-focus-ring, var(--ah-color-primary, #635dff));outline-offset:2px}.btn-primary{background-color:var(--ah-color-primary, #635dff);color:var(--ah-color-text-on-primary, #ffffff);margin-top:12px}.btn-primary:not(:disabled):hover{background-color:var( --ah-color-primary-hover, color-mix(in srgb, var(--ah-color-primary, #635dff) 85%, black) )}.btn-secondary{background-color:var(--ah-color-bg, #ffffff);color:var(--ah-btn-secondary-text, var(--ah-color-text, #1e212a));border:var(--ah-btn-border-width, 1px) solid var(--ah-btn-secondary-border, var(--ah-color-border, #c9cace))}.btn-secondary:not(:disabled):hover{background-color:var(--ah-color-bg-hover, #f5f5f5);border-color:var(--ah-color-border-hover, #65676e)}.btn-link{background:none;border:none;color:var(--ah-color-link, #635dff);padding:8px 0;font-weight:var(--ah-font-weight-link, 400);text-decoration:none}.btn-link:hover{text-decoration:underline}.phone-input-wrapper{display:flex;align-items:stretch;gap:0;width:100%}.country-select{flex-shrink:0;width:auto;padding:12px 8px;font-size:var(--ah-font-size-body, 16px);font-family:var(--ah-font-family, inherit);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-right:none;border-radius:var(--ah-input-radius, 4px) 0 0 var(--ah-input-radius, 4px);background-color:var(--ah-color-input-bg, var(--ah-color-surface, #fff));color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));cursor:pointer;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 6px center;padding-right:22px}.country-select:focus{outline:none;border-color:var(--ah-color-primary, #635dff);box-shadow:0 0 0 1px var(--ah-color-primary, #635dff)}.country-select:disabled{opacity:0.5;cursor:not-allowed}.phone-input-wrapper .input-container{flex:1;min-width:0}.phone-input-wrapper .input-field{border-radius:0 var(--ah-input-radius, 4px) var(--ah-input-radius, 4px) 0}.phone-input-wrapper .input-label.floating,.phone-input-wrapper .input-field:focus+.input-label,.phone-input-wrapper .input-field:not(:placeholder-shown)+.input-label{left:8px}.date-input{display:flex;align-items:center;width:100%;padding:16px;box-sizing:border-box;background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:var(--ah-input-radius, 3px);transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out}.date-input:hover{border-color:var(--ah-color-border-hover, #65676e)}.date-input:focus-within{border-color:var(--ah-color-primary, #635dff);box-shadow:inset 0 0 0 1px var(--ah-color-primary, #635dff)}.date-input.has-error{border-color:var(--ah-color-error, #d03c38)}.date-input.has-error:focus-within{box-shadow:inset 0 0 0 1px var(--ah-color-error, #d03c38)}@media (forced-colors: active){.date-input:focus-within,.date-input.has-error:focus-within{outline:2px solid Highlight;outline-offset:1px}}.date-segment{width:3.25ch;min-width:0;padding:0;font-size:16px;font-family:inherit;text-align:center;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background:transparent;border:none;outline:none}.date-segment[data-date-segment="year"]{width:5.5ch}.date-segment::placeholder{color:var(--ah-color-text-muted, #65676e);opacity:1}.date-segment:disabled{color:var(--ah-color-text-disabled, #a0a0a0);cursor:not-allowed}.date-input:has(.date-segment:disabled){background-color:var(--ah-color-bg-disabled, #f5f5f5)}.date-separator{padding:0 4px;font-size:16px;color:var(--ah-color-text-muted, #65676e);user-select:none}.social-buttons{display:flex;flex-direction:column;gap:12px}.btn-social{display:flex;align-items:center;justify-content:center;gap:12px;position:relative}.btn-social-badge{position:absolute;right:12px;top:0;transform:translateY(-50%);font-size:11px;font-weight:500;line-height:1;padding:4px 9px;border-radius:999px;background:var(--ah-color-last-used, var(--ah-color-primary, #635dff));color:var(--ah-color-last-used-text, var(--ah-color-text-on-primary, #fff));white-space:nowrap;pointer-events:none}.btn-social.btn-social-last-used{border-color:var(--ah-color-last-used, var(--ah-color-primary, #635dff));box-shadow:inset 0 0 0 1px var(--ah-color-last-used, var(--ah-color-primary, #635dff))}.input-badge{position:absolute;right:12px;top:0;transform:translateY(-50%);font-size:11px;font-weight:500;line-height:1;padding:4px 9px;border-radius:999px;background:var(--ah-color-last-used, var(--ah-color-primary, #635dff));color:var(--ah-color-last-used-text, var(--ah-color-text-on-primary, #fff));white-space:nowrap;pointer-events:none;z-index:2}.btn-social-content{display:flex;flex-direction:column;align-items:center;text-align:center}.btn-social-subtitle{font-size:12px;font-style:italic;opacity:0.8;margin-top:2px}.btn-social-subtitle:empty{display:none}.social-icon{width:20px;height:20px;flex-shrink:0}@media (max-width: 480px){.social-buttons:has(.btn-social:nth-child(3)){flex-direction:row;flex-wrap:nowrap;justify-content:stretch;gap:8px}.social-buttons:has(.btn-social:nth-child(3)) .btn-social{width:auto;min-width:0;padding:12px;flex:1 1 0}.social-buttons:has(.btn-social:nth-child(3)) .btn-social span{display:none}.social-buttons:has(.btn-social:nth-child(3)) .social-icon{width:24px;height:24px}}.btn-icon{width:20px;height:20px;flex-shrink:0}.btn-icon img{width:100%;height:100%;object-fit:contain}.text-title{font-size:var(--ah-font-size-title, 20px);font-weight:var(--ah-font-weight-title, 400);color:var(--ah-color-text-header, var(--ah-color-header, #1e212a));margin:8px 0;line-height:1.3}.text-title.text-success{color:var(--ah-color-success, #13a769)}.text-description{font-size:var(--ah-font-size-subtitle, 14px);font-weight:var(--ah-font-weight-subtitle, 400);color:var(--ah-color-text-muted, #65676e);margin:4px 0;line-height:1.5}.image{display:block;max-width:100%;height:auto;border-radius:4px}.image-centered{margin:0 auto 16px;width:52px;height:52px;object-fit:contain}.link{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);font-size:var(--ah-font-size-link, 14px);transition:color 0.15s ease}.link:hover{text-decoration:underline}.link:focus-visible{outline:2px solid var(--ah-color-focus-ring, var(--ah-color-link, #635dff));outline-offset:2px;border-radius:2px}.node-error{padding:12px 16px;background-color:var(--ah-color-error-bg, #ffeaea);color:var(--ah-color-error, #d03c38);border-left:3px solid var(--ah-color-error, #d03c38);border-radius:0;font-size:14px;margin-bottom:16px}.node-success{padding:12px 16px;background-color:var(--ah-color-success-bg, #e6f9f1);color:var(--ah-color-success, #13a769);border-left:3px solid var(--ah-color-success, #13a769);border-radius:0;font-size:14px;margin-bottom:16px}.divider{display:flex;align-items:center;text-align:center;margin:16px 0}.divider::before,.divider::after{content:"";flex:1;border-bottom:1px solid var(--ah-color-border-muted, #c9cace)}.divider-text{padding:0 10px;font-size:12px;font-weight:400;color:var(--ah-color-text-muted, #65676e);text-transform:uppercase;letter-spacing:0.5px}.rich-text{font-size:var(--ah-font-size-body, 14px);font-weight:var(--ah-font-weight-body, 400);line-height:1.5;color:var(--ah-color-text, #1e212a)}.rich-text a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none);transition:color 0.15s ease}.rich-text a:hover{text-decoration:underline}.rich-text .terms-text{margin-top:16px;text-align:center;font-size:12px;color:var(--ah-color-text-muted, #65676e)}.rich-text .terms-text a{font-size:12px}.rich-text .forgot-password-link{text-align:right;font-size:13px;margin-top:4px}.rich-text .forgot-password-link a{font-size:13px;color:var(--ah-color-link, #635dff)}.rich-text .passwordless-link{margin-top:16px;text-align:center;font-size:14px}.rich-text .passwordless-link a{color:var(--ah-color-link, #635dff);text-decoration:var(--ah-link-decoration, none)}.rich-text .passwordless-link a:hover{text-decoration:underline}.rich-text .signup-link{margin-top:16px;text-align:center;font-size:14px;color:var(--ah-color-text, #1e212a)}.code-field{position:relative}.code-input{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:none;background:transparent;color:transparent;caret-color:transparent;font-family:inherit;letter-spacing:0;outline:none;cursor:text;-webkit-text-fill-color:transparent}.code-boxes{display:flex;align-items:center;gap:var(--ah-code-group-gap, 12px);justify-content:center}.code-group{display:flex;gap:0}.code-separator{color:var(--ah-color-text-muted, #65676e);font-size:var(--ah-code-font-size, 24px);user-select:none}.code-box-placeholder{color:var(--ah-color-text-muted, #65676e);opacity:0.4}.code-box{position:relative;flex:0 0 auto;width:var(--ah-code-box-width, 48px);height:var(--ah-code-box-height, 56px);display:flex;align-items:center;justify-content:center;font-size:var(--ah-code-font-size, 24px);font-family:inherit;color:var(--ah-color-input-text, var(--ah-color-text, #1e212a));background-color:var(--ah-color-input-bg, #ffffff);border:var(--ah-input-border-width, 1px) solid var(--ah-color-border, #c9cace);border-radius:0;transition:border-color 0.15s ease-out, box-shadow 0.15s ease-out;box-sizing:border-box}.code-box+.code-box{margin-left:calc(-1 * var(--ah-input-border-width, 1px))}.code-group .code-box:first-child{border-top-left-radius:var(--ah-input-radius, 3px);border-bottom-left-radius:var(--ah-input-radius, 3px)}.code-group .code-box:last-child{border-top-right-radius:var(--ah-input-radius, 3px);border-bottom-right-radius:var(--ah-input-radius, 3px)}.code-field:focus-within .code-box.is-active{z-index:1;border-color:var(--ah-color-primary, #635dff);box-shadow:inset 0 0 0 1px var(--ah-color-primary, #635dff)}.code-field.has-error .code-box{border-color:var(--ah-color-error, #d03c38)}.code-input:disabled{cursor:not-allowed}.code-input:disabled~.code-boxes .code-box{background-color:var(--ah-color-bg-disabled, #f5f5f5);border-color:var(--ah-color-border-disabled, #e0e1e3)}`;
|
|
240
417
|
|
|
241
418
|
const AuthheroNode = class {
|
|
242
419
|
constructor(hostRef) {
|
|
@@ -254,6 +431,12 @@ const AuthheroNode = class {
|
|
|
254
431
|
* Whether the component is disabled.
|
|
255
432
|
*/
|
|
256
433
|
disabled = false;
|
|
434
|
+
/**
|
|
435
|
+
* BCP-47 locale used for locale-dependent field layout (currently the
|
|
436
|
+
* segment order of DATE fields). Resolved server-side so SSR and hydration
|
|
437
|
+
* agree; falls back to day-month-year when absent.
|
|
438
|
+
*/
|
|
439
|
+
locale;
|
|
257
440
|
/**
|
|
258
441
|
* Whether the password field is visible.
|
|
259
442
|
*/
|
|
@@ -275,6 +458,15 @@ const AuthheroNode = class {
|
|
|
275
458
|
* When true, the value is emitted as-is without dial code prefix.
|
|
276
459
|
*/
|
|
277
460
|
telEmailMode = false;
|
|
461
|
+
/**
|
|
462
|
+
* Segment values for the DATE input, each held as typed (unpadded while the
|
|
463
|
+
* user is mid-entry) and normalised on blur.
|
|
464
|
+
*/
|
|
465
|
+
dateSegments = {
|
|
466
|
+
year: "",
|
|
467
|
+
month: "",
|
|
468
|
+
day: "",
|
|
469
|
+
};
|
|
278
470
|
/**
|
|
279
471
|
* Emitted when a field value changes.
|
|
280
472
|
*/
|
|
@@ -286,13 +478,16 @@ const AuthheroNode = class {
|
|
|
286
478
|
componentChanged() {
|
|
287
479
|
this.initCountryFromConfig();
|
|
288
480
|
this.initTelValue();
|
|
481
|
+
this.initDateValue();
|
|
289
482
|
}
|
|
290
483
|
valueChanged() {
|
|
291
484
|
this.initTelValue();
|
|
485
|
+
this.initDateValue();
|
|
292
486
|
}
|
|
293
487
|
componentWillLoad() {
|
|
294
488
|
this.initCountryFromConfig();
|
|
295
489
|
this.initTelValue();
|
|
490
|
+
this.initDateValue();
|
|
296
491
|
}
|
|
297
492
|
componentDidLoad() {
|
|
298
493
|
// Auto-focus a code field so the caret starts in the first (leftmost) box.
|
|
@@ -318,6 +513,20 @@ const AuthheroNode = class {
|
|
|
318
513
|
}
|
|
319
514
|
}
|
|
320
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* The last value this field emitted for a TEL component. The widget mirrors
|
|
518
|
+
* every emitted value back onto the `value` prop, and that echo must not be
|
|
519
|
+
* re-parsed: the wire format carries only a dial code, so a country picked
|
|
520
|
+
* from the list would be replaced by the first entry sharing that code — a
|
|
521
|
+
* user who chose Canada would be silently moved to the US on their next
|
|
522
|
+
* keystroke.
|
|
523
|
+
*/
|
|
524
|
+
lastEmittedTel;
|
|
525
|
+
/** Emit a TEL value and remember it, so the echo can be recognised. */
|
|
526
|
+
emitTelValue(value) {
|
|
527
|
+
this.lastEmittedTel = value;
|
|
528
|
+
this.fieldChange.emit({ id: this.component.id, value });
|
|
529
|
+
}
|
|
321
530
|
/**
|
|
322
531
|
* Hydrate localPhoneNumber (and selectedCountry) from the effective value
|
|
323
532
|
* for TEL fields. The full value is stored as `{dialCode}{localNumber}`,
|
|
@@ -326,6 +535,10 @@ const AuthheroNode = class {
|
|
|
326
535
|
initTelValue() {
|
|
327
536
|
if (this.component?.type !== "TEL")
|
|
328
537
|
return;
|
|
538
|
+
if (this.lastEmittedTel !== undefined &&
|
|
539
|
+
this.getEffectiveValue() === this.lastEmittedTel) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
329
542
|
const config = this.component.config;
|
|
330
543
|
const allowEmail = config?.allow_email === true;
|
|
331
544
|
const fullValue = this.getEffectiveValue();
|
|
@@ -360,11 +573,16 @@ const AuthheroNode = class {
|
|
|
360
573
|
handleCountryChange = (e) => {
|
|
361
574
|
const target = e.target;
|
|
362
575
|
this.selectedCountry = getCountryByCode(target.value);
|
|
576
|
+
// A half-typed "+xx" prefix is replaced by the picked country, not kept
|
|
577
|
+
// as a local number (it would emit "+46+4").
|
|
578
|
+
if (this.localPhoneNumber.startsWith("+")) {
|
|
579
|
+
this.localPhoneNumber = "";
|
|
580
|
+
}
|
|
363
581
|
// Re-emit the full phone number with new dial code
|
|
364
582
|
const fullNumber = this.localPhoneNumber
|
|
365
583
|
? `${this.selectedCountry.dialCode}${this.localPhoneNumber}`
|
|
366
584
|
: "";
|
|
367
|
-
this.
|
|
585
|
+
this.emitTelValue(fullNumber);
|
|
368
586
|
};
|
|
369
587
|
/**
|
|
370
588
|
* Try to detect a dial code prefix in the raw input (e.g. "+46", "0046")
|
|
@@ -402,7 +620,21 @@ const AuthheroNode = class {
|
|
|
402
620
|
target.value = cleanedLocal;
|
|
403
621
|
this.localPhoneNumber = cleanedLocal;
|
|
404
622
|
const fullNumber = `${this.selectedCountry.dialCode}${cleanedLocal}`;
|
|
405
|
-
this.
|
|
623
|
+
this.emitTelValue(fullNumber);
|
|
624
|
+
}
|
|
625
|
+
else if (value.startsWith("+")) {
|
|
626
|
+
// A leading "+" is an international prefix the user is still typing.
|
|
627
|
+
// Keep it in the field so the dial code can build up until it matches
|
|
628
|
+
// a country — stripping it per keystroke means "+46" never resolves to
|
|
629
|
+
// Sweden, since each character is discarded before the next arrives.
|
|
630
|
+
const cleaned = `+${value.slice(1).replace(/[^\d\s\-()]/g, "")}`;
|
|
631
|
+
if (cleaned !== value) {
|
|
632
|
+
target.value = cleaned;
|
|
633
|
+
}
|
|
634
|
+
this.localPhoneNumber = cleaned;
|
|
635
|
+
// Emit the partial prefix as-is; prefixing it with the (not yet chosen)
|
|
636
|
+
// country's dial code would produce nonsense like "+1+4".
|
|
637
|
+
this.emitTelValue(cleaned);
|
|
406
638
|
}
|
|
407
639
|
else {
|
|
408
640
|
const cleaned = allowPlus
|
|
@@ -415,7 +647,7 @@ const AuthheroNode = class {
|
|
|
415
647
|
const fullNumber = cleaned
|
|
416
648
|
? `${this.selectedCountry.dialCode}${cleaned}`
|
|
417
649
|
: "";
|
|
418
|
-
this.
|
|
650
|
+
this.emitTelValue(fullNumber);
|
|
419
651
|
}
|
|
420
652
|
}
|
|
421
653
|
handlePhoneInput = (e) => {
|
|
@@ -435,7 +667,7 @@ const AuthheroNode = class {
|
|
|
435
667
|
else {
|
|
436
668
|
// Email or text — emit as-is
|
|
437
669
|
this.localPhoneNumber = value;
|
|
438
|
-
this.
|
|
670
|
+
this.emitTelValue(value);
|
|
439
671
|
}
|
|
440
672
|
return;
|
|
441
673
|
}
|
|
@@ -446,6 +678,196 @@ const AuthheroNode = class {
|
|
|
446
678
|
const target = e.target;
|
|
447
679
|
this.fieldChange.emit({ id: this.component.id, value: target.value });
|
|
448
680
|
};
|
|
681
|
+
// ===========================================================================
|
|
682
|
+
// DATE segment handling
|
|
683
|
+
// ===========================================================================
|
|
684
|
+
/**
|
|
685
|
+
* The last ISO value this field emitted. The widget mirrors every emitted
|
|
686
|
+
* value back onto the `value` prop, and that echo must not be treated as an
|
|
687
|
+
* external change: it would re-format the segments under the caret while
|
|
688
|
+
* the user is still typing (a day of "3" comes back as "03", swallowing the
|
|
689
|
+
* next digit).
|
|
690
|
+
*/
|
|
691
|
+
lastEmittedDate;
|
|
692
|
+
/** Hydrate the day/month/year segments from the effective ISO value. */
|
|
693
|
+
initDateValue() {
|
|
694
|
+
if (this.component?.type !== "DATE")
|
|
695
|
+
return;
|
|
696
|
+
if (this.lastEmittedDate !== undefined &&
|
|
697
|
+
this.getEffectiveValue() === this.lastEmittedDate) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const parsed = parseIsoDate(this.getEffectiveValue());
|
|
701
|
+
if (parsed) {
|
|
702
|
+
this.dateSegments = parsed;
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
// No parseable value. Only clear when we currently hold a complete date,
|
|
706
|
+
// i.e. this is a genuine external reset: a half-typed date emits "" and
|
|
707
|
+
// the parent echoes that straight back as the value prop, which would
|
|
708
|
+
// otherwise wipe the segment the user just typed into.
|
|
709
|
+
if (toIsoDate(this.dateSegments) !== "") {
|
|
710
|
+
this.dateSegments = { year: "", month: "", day: "" };
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
dateConfig() {
|
|
714
|
+
return this.component.config;
|
|
715
|
+
}
|
|
716
|
+
getDateLayoutForField() {
|
|
717
|
+
const config = this.dateConfig();
|
|
718
|
+
return getDateLayout(config?.format, this.locale);
|
|
719
|
+
}
|
|
720
|
+
static SEGMENT_LENGTH = {
|
|
721
|
+
day: 2,
|
|
722
|
+
month: 2,
|
|
723
|
+
year: 4,
|
|
724
|
+
};
|
|
725
|
+
/** Birthdate autofill tokens — the common case for a typed date. */
|
|
726
|
+
static SEGMENT_AUTOCOMPLETE = {
|
|
727
|
+
day: "bday-day",
|
|
728
|
+
month: "bday-month",
|
|
729
|
+
year: "bday-year",
|
|
730
|
+
};
|
|
731
|
+
/**
|
|
732
|
+
* The submittable value for the current segments: an ISO date, or "" when
|
|
733
|
+
* it is incomplete, impossible, or outside the field's `min`/`max`. ISO
|
|
734
|
+
* strings compare chronologically, so the bounds need no parsing.
|
|
735
|
+
*/
|
|
736
|
+
dateIsoValue(segments) {
|
|
737
|
+
const iso = toIsoDate(segments);
|
|
738
|
+
if (!iso)
|
|
739
|
+
return "";
|
|
740
|
+
const config = this.dateConfig();
|
|
741
|
+
const min = config?.min;
|
|
742
|
+
const max = config?.max;
|
|
743
|
+
if (typeof min === "string" && parseIsoDate(min) && iso < min)
|
|
744
|
+
return "";
|
|
745
|
+
if (typeof max === "string" && parseIsoDate(max) && iso > max)
|
|
746
|
+
return "";
|
|
747
|
+
return iso;
|
|
748
|
+
}
|
|
749
|
+
setDateSegments(segments) {
|
|
750
|
+
this.dateSegments = segments;
|
|
751
|
+
this.lastEmittedDate = this.dateIsoValue(segments);
|
|
752
|
+
this.fieldChange.emit({
|
|
753
|
+
id: this.component.id,
|
|
754
|
+
value: this.lastEmittedDate,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
focusDateSegment(segment) {
|
|
758
|
+
const input = this.host.shadowRoot?.querySelector(`input[data-date-segment="${segment}"]`);
|
|
759
|
+
input?.focus();
|
|
760
|
+
input?.select();
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Whether typing this digit completes the segment. A segment is complete at
|
|
764
|
+
* its full width, and also as soon as a further digit could not produce a
|
|
765
|
+
* valid number — no day starts with 4 and no month starts with 2.
|
|
766
|
+
*/
|
|
767
|
+
isDateSegmentComplete(segment, value) {
|
|
768
|
+
if (value.length >= AuthheroNode.SEGMENT_LENGTH[segment])
|
|
769
|
+
return true;
|
|
770
|
+
if (value.length !== 1)
|
|
771
|
+
return false;
|
|
772
|
+
if (segment === "day")
|
|
773
|
+
return Number(value) > 3;
|
|
774
|
+
if (segment === "month")
|
|
775
|
+
return Number(value) > 1;
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
handleDateSegmentInput = (segment) => (e) => {
|
|
779
|
+
const target = e.target;
|
|
780
|
+
const digits = target.value
|
|
781
|
+
.replace(/\D/g, "")
|
|
782
|
+
.slice(0, AuthheroNode.SEGMENT_LENGTH[segment]);
|
|
783
|
+
if (digits !== target.value) {
|
|
784
|
+
target.value = digits;
|
|
785
|
+
}
|
|
786
|
+
this.setDateSegments({ ...this.dateSegments, [segment]: digits });
|
|
787
|
+
if (this.isDateSegmentComplete(segment, digits)) {
|
|
788
|
+
const order = this.getDateLayoutForField().order;
|
|
789
|
+
const next = order[order.indexOf(segment) + 1];
|
|
790
|
+
if (next)
|
|
791
|
+
this.focusDateSegment(next);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
/**
|
|
795
|
+
* Pad a single digit ("5" -> "05") and expand a two-digit year against the
|
|
796
|
+
* field's upper bound ("85" -> 1985). Shared by blur and paste so a pasted
|
|
797
|
+
* date is read the same way as a typed one.
|
|
798
|
+
*/
|
|
799
|
+
normalizeDateSegment(segment, value) {
|
|
800
|
+
if (segment !== "year")
|
|
801
|
+
return value.padStart(2, "0");
|
|
802
|
+
return expandTwoDigitYear(value, resolveYearAnchor(this.dateConfig()?.max, new Date()));
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Normalise on blur: pad a single digit ("5" -> "05") and expand a two-digit
|
|
806
|
+
* year against the field's upper bound ("85" -> 1985).
|
|
807
|
+
*/
|
|
808
|
+
handleDateSegmentBlur = (segment) => (e) => {
|
|
809
|
+
const target = e.target;
|
|
810
|
+
const value = target.value.replace(/\D/g, "");
|
|
811
|
+
if (!value)
|
|
812
|
+
return;
|
|
813
|
+
const normalized = this.normalizeDateSegment(segment, value);
|
|
814
|
+
if (normalized === value)
|
|
815
|
+
return;
|
|
816
|
+
target.value = normalized;
|
|
817
|
+
this.setDateSegments({ ...this.dateSegments, [segment]: normalized });
|
|
818
|
+
};
|
|
819
|
+
/**
|
|
820
|
+
* Backspace at the start of an empty segment moves to the previous one, so
|
|
821
|
+
* the whole date can be cleared without reaching for the mouse.
|
|
822
|
+
*/
|
|
823
|
+
handleDateSegmentKeyDown = (segment) => (e) => {
|
|
824
|
+
if (e.key === "Enter") {
|
|
825
|
+
e.preventDefault();
|
|
826
|
+
this.buttonClick.emit({ id: "submit", type: "submit", value: "next" });
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (e.key !== "Backspace")
|
|
830
|
+
return;
|
|
831
|
+
const target = e.target;
|
|
832
|
+
if (target.value.length > 0 || target.selectionStart !== 0)
|
|
833
|
+
return;
|
|
834
|
+
const order = this.getDateLayoutForField().order;
|
|
835
|
+
const previous = order[order.indexOf(segment) - 1];
|
|
836
|
+
if (previous) {
|
|
837
|
+
e.preventDefault();
|
|
838
|
+
this.focusDateSegment(previous);
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
/**
|
|
842
|
+
* Pasting a whole date into any segment fills all three. An ISO value is
|
|
843
|
+
* read year-first; anything else follows the field's displayed order. A
|
|
844
|
+
* two-digit year is expanded the same way a typed one is, so "15/03/85"
|
|
845
|
+
* pastes as 1985 rather than falling through to the browser's own paste.
|
|
846
|
+
*/
|
|
847
|
+
handleDatePaste = (e) => {
|
|
848
|
+
const text = e.clipboardData?.getData("text") ?? "";
|
|
849
|
+
const groups = text.match(/\d+/g);
|
|
850
|
+
if (!groups || groups.length < 3)
|
|
851
|
+
return;
|
|
852
|
+
const [first, second, third] = groups;
|
|
853
|
+
const isoLike = first.length === 4;
|
|
854
|
+
const order = isoLike
|
|
855
|
+
? ["year", "month", "day"]
|
|
856
|
+
: this.getDateLayoutForField().order;
|
|
857
|
+
const segments = {
|
|
858
|
+
year: "",
|
|
859
|
+
month: "",
|
|
860
|
+
day: "",
|
|
861
|
+
};
|
|
862
|
+
[first, second, third].forEach((group, index) => {
|
|
863
|
+
const segment = order[index];
|
|
864
|
+
segments[segment] = this.normalizeDateSegment(segment, group.slice(0, AuthheroNode.SEGMENT_LENGTH[segment]));
|
|
865
|
+
});
|
|
866
|
+
if (!toIsoDate(segments))
|
|
867
|
+
return;
|
|
868
|
+
e.preventDefault();
|
|
869
|
+
this.setDateSegments(segments);
|
|
870
|
+
};
|
|
449
871
|
handleKeyDown = (e) => {
|
|
450
872
|
if (e.key === "Enter") {
|
|
451
873
|
e.preventDefault();
|
|
@@ -458,7 +880,7 @@ const AuthheroNode = class {
|
|
|
458
880
|
if (config?.allow_email === true && target.value.length === 0) {
|
|
459
881
|
this.telEmailMode = true;
|
|
460
882
|
this.localPhoneNumber = "";
|
|
461
|
-
this.
|
|
883
|
+
this.emitTelValue("");
|
|
462
884
|
}
|
|
463
885
|
}
|
|
464
886
|
};
|
|
@@ -733,17 +1155,40 @@ const AuthheroNode = class {
|
|
|
733
1155
|
const effectiveValue = this.getEffectiveValue();
|
|
734
1156
|
return (h("div", { class: "input-wrapper", part: "input-wrapper" }, this.renderLabel(component.label, inputId, component.required), h("input", { id: inputId, class: this.getInputFieldClass(errors.length > 0), part: "input", type: "url", name: component.id, value: effectiveValue ?? "", placeholder: component.config?.placeholder, required: component.required, disabled: this.disabled, onInput: this.handleInput, onKeyDown: this.handleKeyDown }), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
|
|
735
1157
|
}
|
|
1158
|
+
/**
|
|
1159
|
+
* DATE is rendered as three numeric segments rather than `input type=date`.
|
|
1160
|
+
* A native date input renders differently in every browser (Safari adds a
|
|
1161
|
+
* stepper), keeps its dd/mm/yyyy hint visible while half-filled, and asks
|
|
1162
|
+
* for a calendar gesture to reach a year decades back — all wrong for a
|
|
1163
|
+
* date typed from memory, which is what these fields collect.
|
|
1164
|
+
*/
|
|
736
1165
|
renderDateField(component) {
|
|
737
|
-
const inputId = `input-${component.id}`;
|
|
738
1166
|
const errors = this.getErrors();
|
|
739
|
-
const {
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
1167
|
+
const { format } = component.config ?? {};
|
|
1168
|
+
const layout = getDateLayout(format, this.locale);
|
|
1169
|
+
const isoValue = this.dateIsoValue(this.dateSegments);
|
|
1170
|
+
// Every segment filled but no ISO value means the date does not exist
|
|
1171
|
+
// (31 February) or falls outside min/max. Flag it rather than silently
|
|
1172
|
+
// submitting nothing — the widget posts JSON, so the browser's own
|
|
1173
|
+
// constraint validation never runs.
|
|
1174
|
+
const isIncomplete = layout.order.some((segment) => this.dateSegments[segment].length === 0);
|
|
1175
|
+
const hasInvalidDate = !isoValue && !isIncomplete;
|
|
1176
|
+
const hasError = errors.length > 0 || hasInvalidDate;
|
|
1177
|
+
const firstInputId = `input-${component.id}`;
|
|
1178
|
+
const segmentInput = (segment) => (h("input", { key: segment, id: segment === layout.order[0] ? firstInputId : undefined, class: "date-segment", part: `input date-segment date-segment-${segment}`, type: "text", inputmode: "numeric", autocomplete: AuthheroNode.SEGMENT_AUTOCOMPLETE[segment], "data-date-segment": segment, maxlength: AuthheroNode.SEGMENT_LENGTH[segment], size: AuthheroNode.SEGMENT_LENGTH[segment], value: this.dateSegments[segment], placeholder: layout.tokens[segment], "aria-label": layout.tokens[segment], "aria-invalid": hasError ? "true" : "false", required: component.required, disabled: this.disabled, onInput: this.handleDateSegmentInput(segment), onBlur: this.handleDateSegmentBlur(segment), onKeyDown: this.handleDateSegmentKeyDown(segment), onPaste: this.handleDatePaste }));
|
|
1179
|
+
return (h("div", { class: "input-wrapper", part: "input-wrapper" }, h("div", { class: "input-container" }, h("div", { class: hasError ? "date-input has-error" : "date-input", part: "date-input", role: "group", "aria-label": component.label }, layout.order.map((segment, index) => [
|
|
1180
|
+
index > 0 && (h("span", { key: `sep-${segment}`, class: "date-separator", part: "date-separator", "aria-hidden": "true" }, layout.separator)),
|
|
1181
|
+
segmentInput(segment),
|
|
1182
|
+
])), this.renderFloatingLabel(component.label, firstInputId, component.required, true)), h("input", { type: "hidden", name: component.id, "data-input-name": component.id, value: isoValue }), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
|
|
744
1183
|
}
|
|
745
1184
|
renderBooleanField(component) {
|
|
746
|
-
return (h("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, h("input", { type: "checkbox", part: "checkbox", name: component.id, checked:
|
|
1185
|
+
return (h("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, h("input", { type: "checkbox", part: "checkbox", name: component.id, checked:
|
|
1186
|
+
// Once the field has a value it is authoritative — falling back to
|
|
1187
|
+
// the default here would keep a ticked-by-default box ticked after
|
|
1188
|
+
// the user unticks it.
|
|
1189
|
+
this.value === undefined
|
|
1190
|
+
? component.config?.default_value === true
|
|
1191
|
+
: this.value === "true", required: component.required, disabled: this.disabled, onChange: this.handleCheckbox }), h("span", { class: "checkbox-label", part: "checkbox-label" }, component.label)));
|
|
747
1192
|
}
|
|
748
1193
|
renderLegalField(component) {
|
|
749
1194
|
const text = component.config?.text ?? component.label ?? "";
|