@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
package/hydrate/index.mjs
CHANGED
|
@@ -5498,7 +5498,184 @@ function getCountryByCode(code) {
|
|
|
5498
5498
|
countries.find((c) => c.code === "US"));
|
|
5499
5499
|
}
|
|
5500
5500
|
|
|
5501
|
-
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)}`;
|
|
5501
|
+
/**
|
|
5502
|
+
* Locale-aware layout for the segmented DATE input.
|
|
5503
|
+
*
|
|
5504
|
+
* The segment order is resolved from a static table rather than from
|
|
5505
|
+
* `Intl.DateTimeFormat`, deliberately: the widget is server-rendered and then
|
|
5506
|
+
* hydrated in the browser, and the two runtimes do not ship the same ICU data
|
|
5507
|
+
* (a Workers runtime commonly falls back to en-US ordering). A static table
|
|
5508
|
+
* gives the server and the client the same answer, so the hydrated DOM matches
|
|
5509
|
+
* the rendered one.
|
|
5510
|
+
*/
|
|
5511
|
+
/** Languages that write dates big-endian: year, month, day. */
|
|
5512
|
+
const YEAR_FIRST_LANGUAGES = new Set([
|
|
5513
|
+
"bo",
|
|
5514
|
+
"dz",
|
|
5515
|
+
"hu",
|
|
5516
|
+
"ja",
|
|
5517
|
+
"ko",
|
|
5518
|
+
"lt",
|
|
5519
|
+
"mn",
|
|
5520
|
+
"sv",
|
|
5521
|
+
"ug",
|
|
5522
|
+
"zh",
|
|
5523
|
+
]);
|
|
5524
|
+
/**
|
|
5525
|
+
* Regions that write month before day. Month-first is essentially a US
|
|
5526
|
+
* convention plus the places that adopted it.
|
|
5527
|
+
*/
|
|
5528
|
+
const MONTH_FIRST_REGIONS = new Set(["us", "ph", "fm", "mh", "pw", "as", "gu"]);
|
|
5529
|
+
/**
|
|
5530
|
+
* Locales whose language is normally year-first but which write dates
|
|
5531
|
+
* day-first in a particular region — Finland-Swedish is d.m.yyyy, unlike
|
|
5532
|
+
* Sweden-Swedish.
|
|
5533
|
+
*/
|
|
5534
|
+
const DAY_FIRST_TAGS = new Set(["sv-fi"]);
|
|
5535
|
+
const DEFAULT_TOKENS = {
|
|
5536
|
+
day: "DD",
|
|
5537
|
+
month: "MM",
|
|
5538
|
+
year: "YYYY",
|
|
5539
|
+
};
|
|
5540
|
+
/**
|
|
5541
|
+
* Split a BCP-47 tag into a lowercase language subtag and, when present, a
|
|
5542
|
+
* two-letter region subtag. Script and variant subtags are ignored.
|
|
5543
|
+
*/
|
|
5544
|
+
function parseTag(locale) {
|
|
5545
|
+
const tag = (locale || "").toLowerCase().replace(/_/g, "-");
|
|
5546
|
+
const parts = tag.split("-").filter(Boolean);
|
|
5547
|
+
const language = parts[0] || "";
|
|
5548
|
+
const region = parts.slice(1).find((part) => /^[a-z]{2}$/.test(part));
|
|
5549
|
+
return { language, region };
|
|
5550
|
+
}
|
|
5551
|
+
/**
|
|
5552
|
+
* Resolve the segment order for a locale. Falls back to day-month-year, which
|
|
5553
|
+
* is what most of the world writes.
|
|
5554
|
+
*/
|
|
5555
|
+
function getDateOrder(locale) {
|
|
5556
|
+
const { language, region } = parseTag(locale);
|
|
5557
|
+
if (region && MONTH_FIRST_REGIONS.has(region)) {
|
|
5558
|
+
return ["month", "day", "year"];
|
|
5559
|
+
}
|
|
5560
|
+
if (region && DAY_FIRST_TAGS.has(`${language}-${region}`)) {
|
|
5561
|
+
return ["day", "month", "year"];
|
|
5562
|
+
}
|
|
5563
|
+
if (YEAR_FIRST_LANGUAGES.has(language)) {
|
|
5564
|
+
return ["year", "month", "day"];
|
|
5565
|
+
}
|
|
5566
|
+
// A bare "en" carries no region — treat it the way Intl does and assume
|
|
5567
|
+
// en-US. Regional English (en-gb, en-au, …) falls through to day-first.
|
|
5568
|
+
if (language === "en" && !region) {
|
|
5569
|
+
return ["month", "day", "year"];
|
|
5570
|
+
}
|
|
5571
|
+
return ["day", "month", "year"];
|
|
5572
|
+
}
|
|
5573
|
+
/**
|
|
5574
|
+
* Parse an explicit format string from the component config, e.g.
|
|
5575
|
+
* "DD/MM/YYYY" or "YYYY-MM-DD". Returns null when the string does not name
|
|
5576
|
+
* all three segments.
|
|
5577
|
+
*/
|
|
5578
|
+
function parseFormat(format) {
|
|
5579
|
+
const matches = format.match(/y+|m+|d+/gi);
|
|
5580
|
+
if (!matches)
|
|
5581
|
+
return null;
|
|
5582
|
+
const order = [];
|
|
5583
|
+
const tokens = { ...DEFAULT_TOKENS };
|
|
5584
|
+
for (const match of matches) {
|
|
5585
|
+
const initial = match[0].toLowerCase();
|
|
5586
|
+
const segment = initial === "y" ? "year" : initial === "m" ? "month" : "day";
|
|
5587
|
+
if (order.includes(segment))
|
|
5588
|
+
continue;
|
|
5589
|
+
order.push(segment);
|
|
5590
|
+
// A "YY" format still collects a four-digit year — the segment holds four
|
|
5591
|
+
// characters and `toIsoDate` rejects anything shorter, with a two-digit
|
|
5592
|
+
// entry expanded on blur. Showing "YY" as the placeholder would promise an
|
|
5593
|
+
// input the field does not accept, so short year tokens are widened.
|
|
5594
|
+
tokens[segment] =
|
|
5595
|
+
segment === "year" && match.length < 4 ? DEFAULT_TOKENS.year : match;
|
|
5596
|
+
}
|
|
5597
|
+
if (order.length !== 3)
|
|
5598
|
+
return null;
|
|
5599
|
+
const separator = format.match(/[^a-z\s]/i)?.[0] ?? "/";
|
|
5600
|
+
return { order, tokens, separator };
|
|
5601
|
+
}
|
|
5602
|
+
/**
|
|
5603
|
+
* Resolve the full layout for a DATE field. An explicit `config.format` wins;
|
|
5604
|
+
* otherwise the locale decides the order.
|
|
5605
|
+
*/
|
|
5606
|
+
function getDateLayout(format, locale) {
|
|
5607
|
+
if (format) {
|
|
5608
|
+
const parsed = parseFormat(format);
|
|
5609
|
+
if (parsed)
|
|
5610
|
+
return parsed;
|
|
5611
|
+
}
|
|
5612
|
+
const order = getDateOrder(locale);
|
|
5613
|
+
return {
|
|
5614
|
+
order,
|
|
5615
|
+
tokens: DEFAULT_TOKENS,
|
|
5616
|
+
separator: order[0] === "year" ? "-" : "/",
|
|
5617
|
+
};
|
|
5618
|
+
}
|
|
5619
|
+
/** Number of days in a given month, honouring leap years. */
|
|
5620
|
+
function daysInMonth(year, month) {
|
|
5621
|
+
if (month < 1 || month > 12)
|
|
5622
|
+
return 0;
|
|
5623
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
5624
|
+
}
|
|
5625
|
+
/** Split an ISO "YYYY-MM-DD" value into its segments. */
|
|
5626
|
+
function parseIsoDate(value) {
|
|
5627
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec((value || "").trim());
|
|
5628
|
+
if (!match)
|
|
5629
|
+
return null;
|
|
5630
|
+
return { year: match[1], month: match[2], day: match[3] };
|
|
5631
|
+
}
|
|
5632
|
+
/**
|
|
5633
|
+
* Build an ISO "YYYY-MM-DD" string from segment values. Returns "" unless all
|
|
5634
|
+
* three segments are complete and form a real calendar date — a half-filled or
|
|
5635
|
+
* impossible date (31 February) is not a value worth submitting.
|
|
5636
|
+
*/
|
|
5637
|
+
function toIsoDate(segments) {
|
|
5638
|
+
const { year, month, day } = segments;
|
|
5639
|
+
if (year.length !== 4 || month.length === 0 || day.length === 0)
|
|
5640
|
+
return "";
|
|
5641
|
+
const y = Number(year);
|
|
5642
|
+
const m = Number(month);
|
|
5643
|
+
const d = Number(day);
|
|
5644
|
+
if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) {
|
|
5645
|
+
return "";
|
|
5646
|
+
}
|
|
5647
|
+
if (m < 1 || m > 12)
|
|
5648
|
+
return "";
|
|
5649
|
+
if (d < 1 || d > daysInMonth(y, m))
|
|
5650
|
+
return "";
|
|
5651
|
+
return `${String(y).padStart(4, "0")}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
|
5652
|
+
}
|
|
5653
|
+
/**
|
|
5654
|
+
* Expand a two-digit year into the most recent matching year at or before
|
|
5655
|
+
* `anchorYear` — "85" becomes 1985, "05" becomes 2005. Anything that is not
|
|
5656
|
+
* exactly two digits is returned unchanged, so a fully typed year is never
|
|
5657
|
+
* second-guessed.
|
|
5658
|
+
*/
|
|
5659
|
+
function expandTwoDigitYear(year, anchorYear) {
|
|
5660
|
+
if (!/^\d{2}$/.test(year))
|
|
5661
|
+
return year;
|
|
5662
|
+
const century = Math.floor(anchorYear / 100) * 100;
|
|
5663
|
+
const candidate = century + Number(year);
|
|
5664
|
+
return String(candidate > anchorYear ? candidate - 100 : candidate);
|
|
5665
|
+
}
|
|
5666
|
+
/**
|
|
5667
|
+
* The latest year a two-digit entry may expand to. `config.max` pins it when
|
|
5668
|
+
* the field has an upper bound; otherwise we assume a date in the past (the
|
|
5669
|
+
* birthdate case) and anchor on the current year.
|
|
5670
|
+
*/
|
|
5671
|
+
function resolveYearAnchor(max, today) {
|
|
5672
|
+
const parsed = parseIsoDate(max);
|
|
5673
|
+
if (parsed)
|
|
5674
|
+
return Number(parsed.year);
|
|
5675
|
+
return today.getUTCFullYear();
|
|
5676
|
+
}
|
|
5677
|
+
|
|
5678
|
+
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)}`;
|
|
5502
5679
|
|
|
5503
5680
|
class AuthheroNode {
|
|
5504
5681
|
constructor(hostRef) {
|
|
@@ -5516,6 +5693,12 @@ class AuthheroNode {
|
|
|
5516
5693
|
* Whether the component is disabled.
|
|
5517
5694
|
*/
|
|
5518
5695
|
disabled = false;
|
|
5696
|
+
/**
|
|
5697
|
+
* BCP-47 locale used for locale-dependent field layout (currently the
|
|
5698
|
+
* segment order of DATE fields). Resolved server-side so SSR and hydration
|
|
5699
|
+
* agree; falls back to day-month-year when absent.
|
|
5700
|
+
*/
|
|
5701
|
+
locale;
|
|
5519
5702
|
/**
|
|
5520
5703
|
* Whether the password field is visible.
|
|
5521
5704
|
*/
|
|
@@ -5537,6 +5720,15 @@ class AuthheroNode {
|
|
|
5537
5720
|
* When true, the value is emitted as-is without dial code prefix.
|
|
5538
5721
|
*/
|
|
5539
5722
|
telEmailMode = false;
|
|
5723
|
+
/**
|
|
5724
|
+
* Segment values for the DATE input, each held as typed (unpadded while the
|
|
5725
|
+
* user is mid-entry) and normalised on blur.
|
|
5726
|
+
*/
|
|
5727
|
+
dateSegments = {
|
|
5728
|
+
year: "",
|
|
5729
|
+
month: "",
|
|
5730
|
+
day: "",
|
|
5731
|
+
};
|
|
5540
5732
|
/**
|
|
5541
5733
|
* Emitted when a field value changes.
|
|
5542
5734
|
*/
|
|
@@ -5548,13 +5740,16 @@ class AuthheroNode {
|
|
|
5548
5740
|
componentChanged() {
|
|
5549
5741
|
this.initCountryFromConfig();
|
|
5550
5742
|
this.initTelValue();
|
|
5743
|
+
this.initDateValue();
|
|
5551
5744
|
}
|
|
5552
5745
|
valueChanged() {
|
|
5553
5746
|
this.initTelValue();
|
|
5747
|
+
this.initDateValue();
|
|
5554
5748
|
}
|
|
5555
5749
|
componentWillLoad() {
|
|
5556
5750
|
this.initCountryFromConfig();
|
|
5557
5751
|
this.initTelValue();
|
|
5752
|
+
this.initDateValue();
|
|
5558
5753
|
}
|
|
5559
5754
|
componentDidLoad() {
|
|
5560
5755
|
// Auto-focus a code field so the caret starts in the first (leftmost) box.
|
|
@@ -5580,6 +5775,20 @@ class AuthheroNode {
|
|
|
5580
5775
|
}
|
|
5581
5776
|
}
|
|
5582
5777
|
}
|
|
5778
|
+
/**
|
|
5779
|
+
* The last value this field emitted for a TEL component. The widget mirrors
|
|
5780
|
+
* every emitted value back onto the `value` prop, and that echo must not be
|
|
5781
|
+
* re-parsed: the wire format carries only a dial code, so a country picked
|
|
5782
|
+
* from the list would be replaced by the first entry sharing that code — a
|
|
5783
|
+
* user who chose Canada would be silently moved to the US on their next
|
|
5784
|
+
* keystroke.
|
|
5785
|
+
*/
|
|
5786
|
+
lastEmittedTel;
|
|
5787
|
+
/** Emit a TEL value and remember it, so the echo can be recognised. */
|
|
5788
|
+
emitTelValue(value) {
|
|
5789
|
+
this.lastEmittedTel = value;
|
|
5790
|
+
this.fieldChange.emit({ id: this.component.id, value });
|
|
5791
|
+
}
|
|
5583
5792
|
/**
|
|
5584
5793
|
* Hydrate localPhoneNumber (and selectedCountry) from the effective value
|
|
5585
5794
|
* for TEL fields. The full value is stored as `{dialCode}{localNumber}`,
|
|
@@ -5588,6 +5797,10 @@ class AuthheroNode {
|
|
|
5588
5797
|
initTelValue() {
|
|
5589
5798
|
if (this.component?.type !== "TEL")
|
|
5590
5799
|
return;
|
|
5800
|
+
if (this.lastEmittedTel !== undefined &&
|
|
5801
|
+
this.getEffectiveValue() === this.lastEmittedTel) {
|
|
5802
|
+
return;
|
|
5803
|
+
}
|
|
5591
5804
|
const config = this.component.config;
|
|
5592
5805
|
const allowEmail = config?.allow_email === true;
|
|
5593
5806
|
const fullValue = this.getEffectiveValue();
|
|
@@ -5622,11 +5835,16 @@ class AuthheroNode {
|
|
|
5622
5835
|
handleCountryChange = (e) => {
|
|
5623
5836
|
const target = e.target;
|
|
5624
5837
|
this.selectedCountry = getCountryByCode(target.value);
|
|
5838
|
+
// A half-typed "+xx" prefix is replaced by the picked country, not kept
|
|
5839
|
+
// as a local number (it would emit "+46+4").
|
|
5840
|
+
if (this.localPhoneNumber.startsWith("+")) {
|
|
5841
|
+
this.localPhoneNumber = "";
|
|
5842
|
+
}
|
|
5625
5843
|
// Re-emit the full phone number with new dial code
|
|
5626
5844
|
const fullNumber = this.localPhoneNumber
|
|
5627
5845
|
? `${this.selectedCountry.dialCode}${this.localPhoneNumber}`
|
|
5628
5846
|
: "";
|
|
5629
|
-
this.
|
|
5847
|
+
this.emitTelValue(fullNumber);
|
|
5630
5848
|
};
|
|
5631
5849
|
/**
|
|
5632
5850
|
* Try to detect a dial code prefix in the raw input (e.g. "+46", "0046")
|
|
@@ -5664,7 +5882,21 @@ class AuthheroNode {
|
|
|
5664
5882
|
target.value = cleanedLocal;
|
|
5665
5883
|
this.localPhoneNumber = cleanedLocal;
|
|
5666
5884
|
const fullNumber = `${this.selectedCountry.dialCode}${cleanedLocal}`;
|
|
5667
|
-
this.
|
|
5885
|
+
this.emitTelValue(fullNumber);
|
|
5886
|
+
}
|
|
5887
|
+
else if (value.startsWith("+")) {
|
|
5888
|
+
// A leading "+" is an international prefix the user is still typing.
|
|
5889
|
+
// Keep it in the field so the dial code can build up until it matches
|
|
5890
|
+
// a country — stripping it per keystroke means "+46" never resolves to
|
|
5891
|
+
// Sweden, since each character is discarded before the next arrives.
|
|
5892
|
+
const cleaned = `+${value.slice(1).replace(/[^\d\s\-()]/g, "")}`;
|
|
5893
|
+
if (cleaned !== value) {
|
|
5894
|
+
target.value = cleaned;
|
|
5895
|
+
}
|
|
5896
|
+
this.localPhoneNumber = cleaned;
|
|
5897
|
+
// Emit the partial prefix as-is; prefixing it with the (not yet chosen)
|
|
5898
|
+
// country's dial code would produce nonsense like "+1+4".
|
|
5899
|
+
this.emitTelValue(cleaned);
|
|
5668
5900
|
}
|
|
5669
5901
|
else {
|
|
5670
5902
|
const cleaned = allowPlus
|
|
@@ -5677,7 +5909,7 @@ class AuthheroNode {
|
|
|
5677
5909
|
const fullNumber = cleaned
|
|
5678
5910
|
? `${this.selectedCountry.dialCode}${cleaned}`
|
|
5679
5911
|
: "";
|
|
5680
|
-
this.
|
|
5912
|
+
this.emitTelValue(fullNumber);
|
|
5681
5913
|
}
|
|
5682
5914
|
}
|
|
5683
5915
|
handlePhoneInput = (e) => {
|
|
@@ -5697,7 +5929,7 @@ class AuthheroNode {
|
|
|
5697
5929
|
else {
|
|
5698
5930
|
// Email or text — emit as-is
|
|
5699
5931
|
this.localPhoneNumber = value;
|
|
5700
|
-
this.
|
|
5932
|
+
this.emitTelValue(value);
|
|
5701
5933
|
}
|
|
5702
5934
|
return;
|
|
5703
5935
|
}
|
|
@@ -5708,6 +5940,196 @@ class AuthheroNode {
|
|
|
5708
5940
|
const target = e.target;
|
|
5709
5941
|
this.fieldChange.emit({ id: this.component.id, value: target.value });
|
|
5710
5942
|
};
|
|
5943
|
+
// ===========================================================================
|
|
5944
|
+
// DATE segment handling
|
|
5945
|
+
// ===========================================================================
|
|
5946
|
+
/**
|
|
5947
|
+
* The last ISO value this field emitted. The widget mirrors every emitted
|
|
5948
|
+
* value back onto the `value` prop, and that echo must not be treated as an
|
|
5949
|
+
* external change: it would re-format the segments under the caret while
|
|
5950
|
+
* the user is still typing (a day of "3" comes back as "03", swallowing the
|
|
5951
|
+
* next digit).
|
|
5952
|
+
*/
|
|
5953
|
+
lastEmittedDate;
|
|
5954
|
+
/** Hydrate the day/month/year segments from the effective ISO value. */
|
|
5955
|
+
initDateValue() {
|
|
5956
|
+
if (this.component?.type !== "DATE")
|
|
5957
|
+
return;
|
|
5958
|
+
if (this.lastEmittedDate !== undefined &&
|
|
5959
|
+
this.getEffectiveValue() === this.lastEmittedDate) {
|
|
5960
|
+
return;
|
|
5961
|
+
}
|
|
5962
|
+
const parsed = parseIsoDate(this.getEffectiveValue());
|
|
5963
|
+
if (parsed) {
|
|
5964
|
+
this.dateSegments = parsed;
|
|
5965
|
+
return;
|
|
5966
|
+
}
|
|
5967
|
+
// No parseable value. Only clear when we currently hold a complete date,
|
|
5968
|
+
// i.e. this is a genuine external reset: a half-typed date emits "" and
|
|
5969
|
+
// the parent echoes that straight back as the value prop, which would
|
|
5970
|
+
// otherwise wipe the segment the user just typed into.
|
|
5971
|
+
if (toIsoDate(this.dateSegments) !== "") {
|
|
5972
|
+
this.dateSegments = { year: "", month: "", day: "" };
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
5975
|
+
dateConfig() {
|
|
5976
|
+
return this.component.config;
|
|
5977
|
+
}
|
|
5978
|
+
getDateLayoutForField() {
|
|
5979
|
+
const config = this.dateConfig();
|
|
5980
|
+
return getDateLayout(config?.format, this.locale);
|
|
5981
|
+
}
|
|
5982
|
+
static SEGMENT_LENGTH = {
|
|
5983
|
+
day: 2,
|
|
5984
|
+
month: 2,
|
|
5985
|
+
year: 4,
|
|
5986
|
+
};
|
|
5987
|
+
/** Birthdate autofill tokens — the common case for a typed date. */
|
|
5988
|
+
static SEGMENT_AUTOCOMPLETE = {
|
|
5989
|
+
day: "bday-day",
|
|
5990
|
+
month: "bday-month",
|
|
5991
|
+
year: "bday-year",
|
|
5992
|
+
};
|
|
5993
|
+
/**
|
|
5994
|
+
* The submittable value for the current segments: an ISO date, or "" when
|
|
5995
|
+
* it is incomplete, impossible, or outside the field's `min`/`max`. ISO
|
|
5996
|
+
* strings compare chronologically, so the bounds need no parsing.
|
|
5997
|
+
*/
|
|
5998
|
+
dateIsoValue(segments) {
|
|
5999
|
+
const iso = toIsoDate(segments);
|
|
6000
|
+
if (!iso)
|
|
6001
|
+
return "";
|
|
6002
|
+
const config = this.dateConfig();
|
|
6003
|
+
const min = config?.min;
|
|
6004
|
+
const max = config?.max;
|
|
6005
|
+
if (typeof min === "string" && parseIsoDate(min) && iso < min)
|
|
6006
|
+
return "";
|
|
6007
|
+
if (typeof max === "string" && parseIsoDate(max) && iso > max)
|
|
6008
|
+
return "";
|
|
6009
|
+
return iso;
|
|
6010
|
+
}
|
|
6011
|
+
setDateSegments(segments) {
|
|
6012
|
+
this.dateSegments = segments;
|
|
6013
|
+
this.lastEmittedDate = this.dateIsoValue(segments);
|
|
6014
|
+
this.fieldChange.emit({
|
|
6015
|
+
id: this.component.id,
|
|
6016
|
+
value: this.lastEmittedDate,
|
|
6017
|
+
});
|
|
6018
|
+
}
|
|
6019
|
+
focusDateSegment(segment) {
|
|
6020
|
+
const input = this.host.shadowRoot?.querySelector(`input[data-date-segment="${segment}"]`);
|
|
6021
|
+
input?.focus();
|
|
6022
|
+
input?.select();
|
|
6023
|
+
}
|
|
6024
|
+
/**
|
|
6025
|
+
* Whether typing this digit completes the segment. A segment is complete at
|
|
6026
|
+
* its full width, and also as soon as a further digit could not produce a
|
|
6027
|
+
* valid number — no day starts with 4 and no month starts with 2.
|
|
6028
|
+
*/
|
|
6029
|
+
isDateSegmentComplete(segment, value) {
|
|
6030
|
+
if (value.length >= AuthheroNode.SEGMENT_LENGTH[segment])
|
|
6031
|
+
return true;
|
|
6032
|
+
if (value.length !== 1)
|
|
6033
|
+
return false;
|
|
6034
|
+
if (segment === "day")
|
|
6035
|
+
return Number(value) > 3;
|
|
6036
|
+
if (segment === "month")
|
|
6037
|
+
return Number(value) > 1;
|
|
6038
|
+
return false;
|
|
6039
|
+
}
|
|
6040
|
+
handleDateSegmentInput = (segment) => (e) => {
|
|
6041
|
+
const target = e.target;
|
|
6042
|
+
const digits = target.value
|
|
6043
|
+
.replace(/\D/g, "")
|
|
6044
|
+
.slice(0, AuthheroNode.SEGMENT_LENGTH[segment]);
|
|
6045
|
+
if (digits !== target.value) {
|
|
6046
|
+
target.value = digits;
|
|
6047
|
+
}
|
|
6048
|
+
this.setDateSegments({ ...this.dateSegments, [segment]: digits });
|
|
6049
|
+
if (this.isDateSegmentComplete(segment, digits)) {
|
|
6050
|
+
const order = this.getDateLayoutForField().order;
|
|
6051
|
+
const next = order[order.indexOf(segment) + 1];
|
|
6052
|
+
if (next)
|
|
6053
|
+
this.focusDateSegment(next);
|
|
6054
|
+
}
|
|
6055
|
+
};
|
|
6056
|
+
/**
|
|
6057
|
+
* Pad a single digit ("5" -> "05") and expand a two-digit year against the
|
|
6058
|
+
* field's upper bound ("85" -> 1985). Shared by blur and paste so a pasted
|
|
6059
|
+
* date is read the same way as a typed one.
|
|
6060
|
+
*/
|
|
6061
|
+
normalizeDateSegment(segment, value) {
|
|
6062
|
+
if (segment !== "year")
|
|
6063
|
+
return value.padStart(2, "0");
|
|
6064
|
+
return expandTwoDigitYear(value, resolveYearAnchor(this.dateConfig()?.max, new Date()));
|
|
6065
|
+
}
|
|
6066
|
+
/**
|
|
6067
|
+
* Normalise on blur: pad a single digit ("5" -> "05") and expand a two-digit
|
|
6068
|
+
* year against the field's upper bound ("85" -> 1985).
|
|
6069
|
+
*/
|
|
6070
|
+
handleDateSegmentBlur = (segment) => (e) => {
|
|
6071
|
+
const target = e.target;
|
|
6072
|
+
const value = target.value.replace(/\D/g, "");
|
|
6073
|
+
if (!value)
|
|
6074
|
+
return;
|
|
6075
|
+
const normalized = this.normalizeDateSegment(segment, value);
|
|
6076
|
+
if (normalized === value)
|
|
6077
|
+
return;
|
|
6078
|
+
target.value = normalized;
|
|
6079
|
+
this.setDateSegments({ ...this.dateSegments, [segment]: normalized });
|
|
6080
|
+
};
|
|
6081
|
+
/**
|
|
6082
|
+
* Backspace at the start of an empty segment moves to the previous one, so
|
|
6083
|
+
* the whole date can be cleared without reaching for the mouse.
|
|
6084
|
+
*/
|
|
6085
|
+
handleDateSegmentKeyDown = (segment) => (e) => {
|
|
6086
|
+
if (e.key === "Enter") {
|
|
6087
|
+
e.preventDefault();
|
|
6088
|
+
this.buttonClick.emit({ id: "submit", type: "submit", value: "next" });
|
|
6089
|
+
return;
|
|
6090
|
+
}
|
|
6091
|
+
if (e.key !== "Backspace")
|
|
6092
|
+
return;
|
|
6093
|
+
const target = e.target;
|
|
6094
|
+
if (target.value.length > 0 || target.selectionStart !== 0)
|
|
6095
|
+
return;
|
|
6096
|
+
const order = this.getDateLayoutForField().order;
|
|
6097
|
+
const previous = order[order.indexOf(segment) - 1];
|
|
6098
|
+
if (previous) {
|
|
6099
|
+
e.preventDefault();
|
|
6100
|
+
this.focusDateSegment(previous);
|
|
6101
|
+
}
|
|
6102
|
+
};
|
|
6103
|
+
/**
|
|
6104
|
+
* Pasting a whole date into any segment fills all three. An ISO value is
|
|
6105
|
+
* read year-first; anything else follows the field's displayed order. A
|
|
6106
|
+
* two-digit year is expanded the same way a typed one is, so "15/03/85"
|
|
6107
|
+
* pastes as 1985 rather than falling through to the browser's own paste.
|
|
6108
|
+
*/
|
|
6109
|
+
handleDatePaste = (e) => {
|
|
6110
|
+
const text = e.clipboardData?.getData("text") ?? "";
|
|
6111
|
+
const groups = text.match(/\d+/g);
|
|
6112
|
+
if (!groups || groups.length < 3)
|
|
6113
|
+
return;
|
|
6114
|
+
const [first, second, third] = groups;
|
|
6115
|
+
const isoLike = first.length === 4;
|
|
6116
|
+
const order = isoLike
|
|
6117
|
+
? ["year", "month", "day"]
|
|
6118
|
+
: this.getDateLayoutForField().order;
|
|
6119
|
+
const segments = {
|
|
6120
|
+
year: "",
|
|
6121
|
+
month: "",
|
|
6122
|
+
day: "",
|
|
6123
|
+
};
|
|
6124
|
+
[first, second, third].forEach((group, index) => {
|
|
6125
|
+
const segment = order[index];
|
|
6126
|
+
segments[segment] = this.normalizeDateSegment(segment, group.slice(0, AuthheroNode.SEGMENT_LENGTH[segment]));
|
|
6127
|
+
});
|
|
6128
|
+
if (!toIsoDate(segments))
|
|
6129
|
+
return;
|
|
6130
|
+
e.preventDefault();
|
|
6131
|
+
this.setDateSegments(segments);
|
|
6132
|
+
};
|
|
5711
6133
|
handleKeyDown = (e) => {
|
|
5712
6134
|
if (e.key === "Enter") {
|
|
5713
6135
|
e.preventDefault();
|
|
@@ -5720,7 +6142,7 @@ class AuthheroNode {
|
|
|
5720
6142
|
if (config?.allow_email === true && target.value.length === 0) {
|
|
5721
6143
|
this.telEmailMode = true;
|
|
5722
6144
|
this.localPhoneNumber = "";
|
|
5723
|
-
this.
|
|
6145
|
+
this.emitTelValue("");
|
|
5724
6146
|
}
|
|
5725
6147
|
}
|
|
5726
6148
|
};
|
|
@@ -5995,17 +6417,40 @@ class AuthheroNode {
|
|
|
5995
6417
|
const effectiveValue = this.getEffectiveValue();
|
|
5996
6418
|
return (hAsync("div", { class: "input-wrapper", part: "input-wrapper" }, this.renderLabel(component.label, inputId, component.required), hAsync("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)));
|
|
5997
6419
|
}
|
|
6420
|
+
/**
|
|
6421
|
+
* DATE is rendered as three numeric segments rather than `input type=date`.
|
|
6422
|
+
* A native date input renders differently in every browser (Safari adds a
|
|
6423
|
+
* stepper), keeps its dd/mm/yyyy hint visible while half-filled, and asks
|
|
6424
|
+
* for a calendar gesture to reach a year decades back — all wrong for a
|
|
6425
|
+
* date typed from memory, which is what these fields collect.
|
|
6426
|
+
*/
|
|
5998
6427
|
renderDateField(component) {
|
|
5999
|
-
const inputId = `input-${component.id}`;
|
|
6000
6428
|
const errors = this.getErrors();
|
|
6001
|
-
const {
|
|
6002
|
-
|
|
6003
|
-
const
|
|
6004
|
-
|
|
6005
|
-
|
|
6429
|
+
const { format } = component.config ?? {};
|
|
6430
|
+
const layout = getDateLayout(format, this.locale);
|
|
6431
|
+
const isoValue = this.dateIsoValue(this.dateSegments);
|
|
6432
|
+
// Every segment filled but no ISO value means the date does not exist
|
|
6433
|
+
// (31 February) or falls outside min/max. Flag it rather than silently
|
|
6434
|
+
// submitting nothing — the widget posts JSON, so the browser's own
|
|
6435
|
+
// constraint validation never runs.
|
|
6436
|
+
const isIncomplete = layout.order.some((segment) => this.dateSegments[segment].length === 0);
|
|
6437
|
+
const hasInvalidDate = !isoValue && !isIncomplete;
|
|
6438
|
+
const hasError = errors.length > 0 || hasInvalidDate;
|
|
6439
|
+
const firstInputId = `input-${component.id}`;
|
|
6440
|
+
const segmentInput = (segment) => (hAsync("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 }));
|
|
6441
|
+
return (hAsync("div", { class: "input-wrapper", part: "input-wrapper" }, hAsync("div", { class: "input-container" }, hAsync("div", { class: hasError ? "date-input has-error" : "date-input", part: "date-input", role: "group", "aria-label": component.label }, layout.order.map((segment, index) => [
|
|
6442
|
+
index > 0 && (hAsync("span", { key: `sep-${segment}`, class: "date-separator", part: "date-separator", "aria-hidden": "true" }, layout.separator)),
|
|
6443
|
+
segmentInput(segment),
|
|
6444
|
+
])), this.renderFloatingLabel(component.label, firstInputId, component.required, true)), hAsync("input", { type: "hidden", name: component.id, "data-input-name": component.id, value: isoValue }), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
|
|
6006
6445
|
}
|
|
6007
6446
|
renderBooleanField(component) {
|
|
6008
|
-
return (hAsync("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, hAsync("input", { type: "checkbox", part: "checkbox", name: component.id, checked:
|
|
6447
|
+
return (hAsync("label", { class: "checkbox-wrapper", part: "checkbox-wrapper" }, hAsync("input", { type: "checkbox", part: "checkbox", name: component.id, checked:
|
|
6448
|
+
// Once the field has a value it is authoritative — falling back to
|
|
6449
|
+
// the default here would keep a ticked-by-default box ticked after
|
|
6450
|
+
// the user unticks it.
|
|
6451
|
+
this.value === undefined
|
|
6452
|
+
? component.config?.default_value === true
|
|
6453
|
+
: this.value === "true", required: component.required, disabled: this.disabled, onChange: this.handleCheckbox }), hAsync("span", { class: "checkbox-label", part: "checkbox-label" }, component.label)));
|
|
6009
6454
|
}
|
|
6010
6455
|
renderLegalField(component) {
|
|
6011
6456
|
const text = component.config?.text ?? component.label ?? "";
|
|
@@ -6185,11 +6630,13 @@ class AuthheroNode {
|
|
|
6185
6630
|
"component": [16],
|
|
6186
6631
|
"value": [1],
|
|
6187
6632
|
"disabled": [4],
|
|
6633
|
+
"locale": [1],
|
|
6188
6634
|
"passwordVisible": [32],
|
|
6189
6635
|
"selectedCountry": [32],
|
|
6190
6636
|
"localPhoneNumber": [32],
|
|
6191
6637
|
"countryDropdownOpen": [32],
|
|
6192
|
-
"telEmailMode": [32]
|
|
6638
|
+
"telEmailMode": [32],
|
|
6639
|
+
"dateSegments": [32]
|
|
6193
6640
|
},
|
|
6194
6641
|
"$listeners$": undefined,
|
|
6195
6642
|
"$lazyBundleId$": "-",
|
|
@@ -6720,6 +7167,27 @@ const authheroWidgetCss = () => `:host{display:block;font-family:var(--ah-font-f
|
|
|
6720
7167
|
* buttons. Used by long pick-one screens such as tenant selection.
|
|
6721
7168
|
*/
|
|
6722
7169
|
const CHOICE_LIST_SEARCH_THRESHOLD = 5;
|
|
7170
|
+
/**
|
|
7171
|
+
* Component types the widget renders as something the user can actually fill
|
|
7172
|
+
* in. Only these gate the primary action button: types that render nothing
|
|
7173
|
+
* (CARDS, FILE, RECAPTCHA, …) can carry `required` in the schema too, and
|
|
7174
|
+
* gating on them would leave Continue disabled with no way to satisfy it.
|
|
7175
|
+
*/
|
|
7176
|
+
const FILLABLE_FIELD_TYPES = new Set([
|
|
7177
|
+
"TEXT",
|
|
7178
|
+
"EMAIL",
|
|
7179
|
+
"CODE",
|
|
7180
|
+
"PASSWORD",
|
|
7181
|
+
"NUMBER",
|
|
7182
|
+
"TEL",
|
|
7183
|
+
"URL",
|
|
7184
|
+
"DATE",
|
|
7185
|
+
"BOOLEAN",
|
|
7186
|
+
"LEGAL",
|
|
7187
|
+
"COUNTRY",
|
|
7188
|
+
"DROPDOWN",
|
|
7189
|
+
"CHOICE",
|
|
7190
|
+
]);
|
|
6723
7191
|
class AuthheroWidget {
|
|
6724
7192
|
constructor(hostRef) {
|
|
6725
7193
|
registerInstance(this, hostRef);
|
|
@@ -6812,6 +7280,13 @@ class AuthheroWidget {
|
|
|
6812
7280
|
* @default false (same as autoSubmit when not specified)
|
|
6813
7281
|
*/
|
|
6814
7282
|
autoNavigate;
|
|
7283
|
+
/**
|
|
7284
|
+
* BCP-47 locale for locale-dependent field layout, e.g. whether a DATE
|
|
7285
|
+
* field reads DD/MM/YYYY, MM/DD/YYYY or YYYY-MM-DD. Resolve it server-side
|
|
7286
|
+
* and pass it in so the server-rendered markup and the hydrated one agree;
|
|
7287
|
+
* screen text itself is already localized by the server.
|
|
7288
|
+
*/
|
|
7289
|
+
locale;
|
|
6815
7290
|
/**
|
|
6816
7291
|
* Internal parsed screen state.
|
|
6817
7292
|
*/
|
|
@@ -6912,14 +7387,19 @@ class AuthheroWidget {
|
|
|
6912
7387
|
initFormDataFromDefaults(screen) {
|
|
6913
7388
|
const defaults = {};
|
|
6914
7389
|
for (const comp of screen.components || []) {
|
|
6915
|
-
if ("config" in comp
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
7390
|
+
if (!("config" in comp) || !comp.config)
|
|
7391
|
+
continue;
|
|
7392
|
+
if (!("default_value" in comp.config))
|
|
7393
|
+
continue;
|
|
7394
|
+
const val = comp.config.default_value;
|
|
7395
|
+
if (typeof val === "string" && val !== "") {
|
|
7396
|
+
defaults[comp.id] = val;
|
|
7397
|
+
}
|
|
7398
|
+
else if (typeof val === "boolean") {
|
|
7399
|
+
// A BOOLEAN's default decides whether the checkbox renders ticked, so
|
|
7400
|
+
// seed the matching value — otherwise a field the user never touches
|
|
7401
|
+
// submits nothing and the screen's state is lost.
|
|
7402
|
+
defaults[comp.id] = val ? "true" : "false";
|
|
6923
7403
|
}
|
|
6924
7404
|
}
|
|
6925
7405
|
if (Object.keys(defaults).length > 0) {
|
|
@@ -7920,6 +8400,11 @@ class AuthheroWidget {
|
|
|
7920
8400
|
handleButtonClick = (detail) => {
|
|
7921
8401
|
// If this is a submit button click, trigger form submission
|
|
7922
8402
|
if (detail.type === "submit") {
|
|
8403
|
+
// Enter in a text field submits too, so the required-field gate has to
|
|
8404
|
+
// live here and not only on the button's disabled state.
|
|
8405
|
+
if (this._screen && this.hasUnfilledRequiredFields(this._screen)) {
|
|
8406
|
+
return;
|
|
8407
|
+
}
|
|
7923
8408
|
// For GET screens (or missing method), navigate directly — no form submission needed
|
|
7924
8409
|
if ((!this._screen?.method ||
|
|
7925
8410
|
this._screen.method.toUpperCase() === "GET") &&
|
|
@@ -8108,6 +8593,32 @@ class AuthheroWidget {
|
|
|
8108
8593
|
isDividerComponent(component) {
|
|
8109
8594
|
return component.type === "DIVIDER";
|
|
8110
8595
|
}
|
|
8596
|
+
/**
|
|
8597
|
+
* Whether a required component holds a value the user has supplied.
|
|
8598
|
+
*/
|
|
8599
|
+
isRequiredFieldFilled(component) {
|
|
8600
|
+
const value = this.formData[component.id];
|
|
8601
|
+
// Checkboxes are only "filled" when ticked. A BOOLEAN's default_value is
|
|
8602
|
+
// seeded into formData, so it needs no separate handling here.
|
|
8603
|
+
if (component.type === "BOOLEAN" || component.type === "LEGAL") {
|
|
8604
|
+
return value === "true";
|
|
8605
|
+
}
|
|
8606
|
+
return typeof value === "string" && value.trim() !== "";
|
|
8607
|
+
}
|
|
8608
|
+
/**
|
|
8609
|
+
* Whether the screen still has required fields the user has not filled in.
|
|
8610
|
+
* Used to hold the primary action button disabled: the widget submits via
|
|
8611
|
+
* its own handler rather than a native form submit, so the browser's
|
|
8612
|
+
* constraint validation never runs and an empty required field would
|
|
8613
|
+
* otherwise only surface as a server error after a round trip.
|
|
8614
|
+
*/
|
|
8615
|
+
hasUnfilledRequiredFields(screen) {
|
|
8616
|
+
return (screen.components ?? []).some((component) => component.visible !== false &&
|
|
8617
|
+
"required" in component &&
|
|
8618
|
+
component.required === true &&
|
|
8619
|
+
FILLABLE_FIELD_TYPES.has(component.type) &&
|
|
8620
|
+
!this.isRequiredFieldFilled(component));
|
|
8621
|
+
}
|
|
8111
8622
|
/**
|
|
8112
8623
|
* Visible label of a choice button, used to filter searchable choice lists.
|
|
8113
8624
|
*/
|
|
@@ -8143,6 +8654,9 @@ class AuthheroWidget {
|
|
|
8143
8654
|
const choiceButtons = fieldComponents.filter((c) => c.type === "NEXT_BUTTON");
|
|
8144
8655
|
const isSearchableChoiceList = choiceButtons.length === fieldComponents.length &&
|
|
8145
8656
|
choiceButtons.length > CHOICE_LIST_SEARCH_THRESHOLD;
|
|
8657
|
+
// Hold the primary action button disabled until every required field has
|
|
8658
|
+
// a value.
|
|
8659
|
+
const requiredFieldsMissing = this.hasUnfilledRequiredFields(screen);
|
|
8146
8660
|
const filterQuery = this.listFilter.trim().toLowerCase();
|
|
8147
8661
|
const visibleChoiceButtons = isSearchableChoiceList && filterQuery
|
|
8148
8662
|
? choiceButtons.filter((c) => this.getChoiceButtonText(c).toLowerCase().includes(filterQuery))
|
|
@@ -8177,7 +8691,7 @@ class AuthheroWidget {
|
|
|
8177
8691
|
};
|
|
8178
8692
|
// Get logo URL from theme.widget (takes precedence) or branding
|
|
8179
8693
|
const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
|
|
8180
|
-
return (hAsync("div", { class: "widget-container", part: "container", "data-authstack-container": true }, hAsync("header", { class: "widget-header", part: "header" }, logoUrl && (hAsync("div", { class: "logo-wrapper", part: "logo-wrapper" }, hAsync("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (hAsync("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (hAsync("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), hAsync("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (hAsync("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (hAsync("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), hAsync("form", { onSubmit: this.handleSubmit, action: screen.action, method: screen.method || "POST", part: "form" }, hiddenComponents.map((c) => (hAsync("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), hAsync("div", { class: "form-content" }, socialComponents.length > 0 && (hAsync("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (hAsync("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
|
|
8694
|
+
return (hAsync("div", { class: "widget-container", part: "container", "data-authstack-container": true }, hAsync("header", { class: "widget-header", part: "header" }, logoUrl && (hAsync("div", { class: "logo-wrapper", part: "logo-wrapper" }, hAsync("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (hAsync("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (hAsync("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), hAsync("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (hAsync("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (hAsync("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), hAsync("form", { onSubmit: this.handleSubmit, action: screen.action, method: screen.method || "POST", part: "form" }, hiddenComponents.map((c) => (hAsync("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), hAsync("div", { class: "form-content" }, socialComponents.length > 0 && (hAsync("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (hAsync("authhero-node", { key: component.id, component: component, locale: this.locale, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
|
|
8181
8695
|
fieldComponents.length > 0 &&
|
|
8182
8696
|
hasDivider && (hAsync("div", { class: "divider", part: "divider" }, hAsync("span", { class: "divider-text" }, dividerText))), hAsync("div", { class: "fields-section", part: "fields-section" }, isSearchableChoiceList && (hAsync("input", { type: "text", class: "choice-list-search", part: "choice-list-search", placeholder: "Search", "aria-label": "Search", autocomplete: "off", value: this.listFilter, onInput: (e) => (this.listFilter = e.target.value), onKeyDown: (e) => {
|
|
8183
8697
|
// The search box lives inside the form; Enter would
|
|
@@ -8190,7 +8704,9 @@ class AuthheroWidget {
|
|
|
8190
8704
|
? "fields-list fields-list-scroll"
|
|
8191
8705
|
: "fields-list" }, (isSearchableChoiceList
|
|
8192
8706
|
? visibleChoiceButtons
|
|
8193
|
-
: fieldComponents).map((component) => (hAsync("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading
|
|
8707
|
+
: fieldComponents).map((component) => (hAsync("authhero-node", { key: component.id, component: component, locale: this.locale, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading ||
|
|
8708
|
+
(component.type === "NEXT_BUTTON" &&
|
|
8709
|
+
requiredFieldsMissing) }))), isSearchableChoiceList &&
|
|
8194
8710
|
visibleChoiceButtons.length === 0 && (hAsync("div", { class: "choice-list-empty", part: "choice-list-empty" }, "No matches")))))), screen.links && screen.links.length > 0 && (hAsync("div", { class: "links", part: "links" }, screen.links.map((link) => (hAsync("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (hAsync("span", null, link.text, " ", hAsync("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
|
|
8195
8711
|
id: link.id,
|
|
8196
8712
|
href: link.href,
|
|
@@ -8236,6 +8752,7 @@ class AuthheroWidget {
|
|
|
8236
8752
|
"loading": [1028],
|
|
8237
8753
|
"autoSubmit": [4, "auto-submit"],
|
|
8238
8754
|
"autoNavigate": [4, "auto-navigate"],
|
|
8755
|
+
"locale": [1],
|
|
8239
8756
|
"_screen": [32],
|
|
8240
8757
|
"_authParams": [32],
|
|
8241
8758
|
"_branding": [32],
|