@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
1,818 lines (1,713 loc) • 113 kB
JavaScript
/**
* Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact Volker Schukai.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import { instanceSymbol } from "../../constants.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";
import {
assembleMethodSymbol,
registerCustomElement,
CustomElement,
} from "../../dom/customelement.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { Pathfinder } from "../../data/pathfinder.mjs";
import { isFunction, isObject, isString } from "../../types/is.mjs";
import { RegisterWizardStyleSheet } from "./stylesheet/register-wizard.mjs";
import "../datatable/datasource/rest.mjs";
import "./field-set.mjs";
import "./password.mjs";
import "./button.mjs";
import "./message-state-button.mjs";
import "../navigation/wizard-navigation.mjs";
export { RegisterWizard };
const wizardElementSymbol = Symbol("wizardElement");
const panelElementsSymbol = Symbol("panelElements");
const currentStepSymbol = Symbol("currentStep");
const availabilityDatasourceSymbol = Symbol("availabilityDatasource");
const registerDatasourceSymbol = Symbol("registerDatasource");
const consentsDatasourceSymbol = Symbol("consentsDatasource");
const availabilityStateSymbol = Symbol("availabilityState");
const availabilityTimerSymbol = Symbol("availabilityTimer");
const completedSymbol = Symbol("completed");
const fieldSetElementsSymbol = Symbol("fieldSetElements");
const emailInputSymbol = Symbol("emailInput");
const passwordInputSymbol = Symbol("passwordInput");
const passwordConfirmInputSymbol = Symbol("passwordConfirmInput");
const passwordConfirmLabelSymbol = Symbol("passwordConfirmLabel");
const profileInputsSymbol = Symbol("profileInputs");
const birthDateLabelSymbol = Symbol("birthDateLabel");
const nationalityLabelSymbol = Symbol("nationalityLabel");
const addressInputsSymbol = Symbol("addressInputs");
const availabilityMessageSymbol = Symbol("availabilityMessage");
const nextButtonsSymbol = Symbol("nextButtons");
const backButtonsSymbol = Symbol("backButtons");
const availabilityNextButtonSymbol = Symbol("availabilityNextButton");
const submitButtonSymbol = Symbol("submitButton");
/**
* Registration wizard control using wizard navigation and multi-step forms.
*
* @fragments /fragments/components/form/register-wizard/
*
* @example /examples/components/form/register-wizard-basic Basic register wizard
*
* @since 4.38.0
* @summary A multi-step registration wizard with availability checks and register submit.
* @fires monster-register-wizard-step-change
* @fires monster-register-wizard-availability-check
* @fires monster-register-wizard-availability-result
* @fires monster-register-wizard-register
* @fires monster-register-wizard-success
* @fires monster-register-wizard-error
* @fires monster-register-wizard-invalid
*/
class RegisterWizard extends CustomElement {
static get [instanceSymbol]() {
return Symbol.for(
"@schukai/monster/components/form/register-wizard@@instance",
);
}
/**
* To set the options via the HTML Tag, the attribute `data-monster-options` must be used.
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
*
* @property {Object} templates Template definitions
* @property {string} templates.main Main template
* @property {Object} labels Labels
* @property {Object} features Feature toggles
* @property {Object} requirements Field requirement flags
* @property {Object} tenant Tenant configuration
* @property {Object} availability Availability check config
* @property {Object} register Register submit config
* @property {Object} consents Consent topics config
* @property {Object} actions Callback actions
*/
get defaults() {
return Object.assign({}, super.defaults, {
templates: {
main: getTemplate(),
},
labels: getTranslations(),
features: {
autoCheckAvailability: false,
requireAvailability: true,
confirmPassword: false,
showAvailabilityMessage: true,
showBirthDate: false,
showNationality: false,
},
autocomplete: {
email: "email",
password: "new-password",
passwordConfirm: "new-password",
firstName: "given-name",
lastName: "family-name",
birthDate: "bday",
nationality: "country-name",
street: "address-line1",
zip: "postal-code",
city: "address-level2",
country: "country",
},
requirements: {
email: true,
password: true,
passwordConfirm: false,
firstName: true,
lastName: true,
birthDate: false,
nationality: false,
address: {
street: true,
zip: true,
city: true,
country: true,
},
},
tenant: {
id: null,
},
availability: {
url: null,
fetch: {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
},
payload: {
emailPath: "email",
tenantIdPath: "tenantId",
},
mapping: {
selector: "*",
availablePath: "available",
existsPath: "exists",
messagePath: null,
},
},
register: {
url: null,
fetch: {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
},
payload: {
tenantIdPath: "tenantId",
emailPath: "email",
passwordPath: "password",
firstNamePath: "person.firstName",
lastNamePath: "person.lastName",
birthDatePath: "person.birthDate",
nationalityPath: "person.nationality",
streetPath: "person.address.street",
zipPath: "person.address.zip",
cityPath: "person.address.city",
countryPath: "person.address.country",
consentsPath: "consents",
},
mapping: {
selector: "*",
},
},
consents: {
url: null,
fetch: {
method: "GET",
headers: {
Accept: "application/json",
},
},
mapping: {
selector: "*",
topicsPath: "topics",
topicPath: "topic",
labelPath: "label",
descriptionPath: "description",
requiredPath: "required",
},
topics: [
{
topic: "terms_and_conditions",
label: "Terms and conditions",
required: true,
},
{
topic: "privacy",
label: "Privacy policy",
required: true,
},
{
topic: "marketing",
label: "Marketing",
required: false,
},
],
grantedValue: "granted",
revokedValue: "revoked",
},
actions: {
onstepchange: null,
onavailabilitycheck: null,
onavailabilityresult: null,
onregister: null,
onsuccess: null,
onerror: null,
oninvalid: null,
},
});
}
static getTag() {
return "monster-register-wizard";
}
static getCSSStyleSheet() {
return [RegisterWizardStyleSheet];
}
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
this[currentStepSymbol] = 0;
this[completedSymbol] = false;
this[availabilityStateSymbol] = {
checked: false,
available: null,
email: null,
};
initControlReferences.call(this);
initEventHandler.call(this);
applyRequirements.call(this);
applyPasswordConfirmVisibility.call(this);
applyProfileFeatureVisibility.call(this);
applyAddressLayout.call(this);
ensureLabels.call(this);
applyFieldLabels.call(this);
updateWizardLabels.call(this);
updateStepVisibility.call(this);
loadConsentTopics.call(this);
goToStep.call(this, 0, true);
return this;
}
refresh() {
applyRequirements.call(this);
applyPasswordConfirmVisibility.call(this);
applyProfileFeatureVisibility.call(this);
applyAddressLayout.call(this);
ensureLabels.call(this);
applyFieldLabels.call(this);
updateWizardLabels.call(this);
updateStepVisibility.call(this);
loadConsentTopics.call(this);
return this;
}
}
function initControlReferences() {
this[wizardElementSymbol] = this.shadowRoot.querySelector(
"monster-wizard-navigation",
);
this[panelElementsSymbol] = {
email: this.shadowRoot.querySelector('[data-step="email"]'),
password: this.shadowRoot.querySelector('[data-step="password"]'),
profile: this.shadowRoot.querySelector('[data-step="profile"]'),
address: this.shadowRoot.querySelector('[data-step="address"]'),
consents: this.shadowRoot.querySelector('[data-step="consents"]'),
};
this[fieldSetElementsSymbol] = {
email: this.shadowRoot.querySelector(
'[data-monster-role="field-set-email"]',
),
password: this.shadowRoot.querySelector(
'[data-monster-role="field-set-password"]',
),
profile: this.shadowRoot.querySelector(
'[data-monster-role="field-set-profile"]',
),
address: this.shadowRoot.querySelector(
'[data-monster-role="field-set-address"]',
),
consents: this.shadowRoot.querySelector(
'[data-monster-role="field-set-consents"]',
),
};
this[emailInputSymbol] = this.shadowRoot.querySelector(
'[data-monster-role="email-input"]',
);
this[passwordInputSymbol] = this.shadowRoot.querySelector(
'[data-monster-role="password-input"]',
);
this[passwordConfirmInputSymbol] = this.shadowRoot.querySelector(
'[data-monster-role="password-confirm-input"]',
);
this[passwordConfirmLabelSymbol] = this.shadowRoot.querySelector(
'[data-monster-role~="password-confirm-label"]',
);
this[profileInputsSymbol] = {
firstName: this.shadowRoot.querySelector(
'[data-monster-role="first-name-input"]',
),
lastName: this.shadowRoot.querySelector(
'[data-monster-role="last-name-input"]',
),
birthDate: this.shadowRoot.querySelector(
'[data-monster-role="birth-date-input"]',
),
nationality: this.shadowRoot.querySelector(
'[data-monster-role="nationality-input"]',
),
};
this[birthDateLabelSymbol] = this.shadowRoot.querySelector(
'[data-monster-role~="birth-date-label"]',
);
this[nationalityLabelSymbol] = this.shadowRoot.querySelector(
'[data-monster-role~="nationality-label"]',
);
this[addressInputsSymbol] = {
street: this.shadowRoot.querySelector('[data-monster-role="street-input"]'),
zip: this.shadowRoot.querySelector('[data-monster-role="zip-input"]'),
city: this.shadowRoot.querySelector('[data-monster-role="city-input"]'),
country: this.shadowRoot.querySelector(
'[data-monster-role="country-input"]',
),
};
this[availabilityMessageSymbol] = this.shadowRoot.querySelector(
'[data-monster-role="availability-message"]',
);
this[nextButtonsSymbol] = Array.from(
this.shadowRoot.querySelectorAll('[data-monster-role="next-button"]'),
);
this[backButtonsSymbol] = Array.from(
this.shadowRoot.querySelectorAll('[data-monster-role="back-button"]'),
);
this[availabilityNextButtonSymbol] = this.shadowRoot.querySelector(
'[data-step="email"] [data-monster-role="next-button"]',
);
this[submitButtonSymbol] = this.shadowRoot.querySelector(
'[data-monster-role="submit-button"]',
);
this[availabilityDatasourceSymbol] = this.shadowRoot.querySelector(
'[data-monster-role="availability-datasource"]',
);
this[registerDatasourceSymbol] = this.shadowRoot.querySelector(
'[data-monster-role="register-datasource"]',
);
this[consentsDatasourceSymbol] = this.shadowRoot.querySelector(
'[data-monster-role="consents-datasource"]',
);
}
function initEventHandler() {
const wizard = this[wizardElementSymbol];
if (wizard) {
wizard.setOption("actions.beforeStepChange", (fromIndex, toIndex) => {
return handleStepChangeRequest.call(this, fromIndex, toIndex);
});
wizard.addEventListener("monster-wizard-step-changed", (event) => {
const mainIndex = event?.detail?.newIndex ?? 0;
const stepIndex = mapWizardStepToIndex(mainIndex);
if (stepIndex !== null && stepIndex !== this[currentStepSymbol]) {
goToStep.call(this, stepIndex);
}
});
wizard.addEventListener("monster-wizard-substep-changed", (event) => {
const mainIndex = event?.detail?.mainIndex ?? 0;
const subIndex = event?.detail?.subIndex ?? 0;
const stepIndex = mapWizardSubStepToIndex(mainIndex, subIndex);
if (stepIndex !== null && stepIndex !== this[currentStepSymbol]) {
goToStep.call(this, stepIndex);
}
});
}
if (this[emailInputSymbol]) {
this[emailInputSymbol].addEventListener("input", () => {
resetAvailabilityState.call(this);
if (this.getOption("features.autoCheckAvailability") === true) {
debounceAvailabilityCheck.call(this);
}
});
}
this[nextButtonsSymbol].forEach((button) => {
button.setOption("actions.click", () => {
handleNextClick.call(this);
});
});
this[backButtonsSymbol].forEach((button) => {
button.setOption("actions.click", () => {
handleBackClick.call(this);
});
});
if (this[submitButtonSymbol]) {
this[submitButtonSymbol].setOption("actions.click", () => {
handleSubmit.call(this);
});
}
}
function updateWizardLabels() {
const wizard = this[wizardElementSymbol];
if (!wizard) {
return;
}
const list = wizard.querySelector("ol.wizard-steps");
if (!list) {
return;
}
const labels = this.getOption("labels");
const items = list.querySelectorAll("li.step");
const entries = [
{
main: labels.stepAccount,
subs: [labels.subEmail, labels.subPassword],
},
{
main: labels.stepProfileGroup,
subs: [labels.subProfile, labels.subAddress],
},
{
main: labels.stepConsentsGroup,
subs: [labels.subConsents],
},
];
items.forEach((item, index) => {
const entry = entries[index];
if (!entry) {
return;
}
const label = item.querySelector('[data-monster-role="step-label"]');
if (label && entry.main) {
label.textContent = entry.main;
}
const subItems = item.querySelectorAll("ul li");
subItems.forEach((subItem, subIndex) => {
const subLabel = entry.subs?.[subIndex];
if (subLabel) {
subItem.textContent = subLabel;
}
});
});
}
function updateStepVisibility() {
if (this[completedSymbol]) {
Object.values(this[panelElementsSymbol] || {}).forEach((panel) => {
if (panel) {
panel.hidden = true;
panel.classList.remove("is-active");
}
});
const successPanel = this.shadowRoot.querySelector('[data-step="success"]');
if (successPanel) {
successPanel.hidden = false;
successPanel.classList.add("is-active");
}
return;
}
const steps = getStepOrder();
steps.forEach((stepId, index) => {
const panel = this[panelElementsSymbol]?.[stepId];
if (!panel) {
return;
}
panel.hidden = index !== this[currentStepSymbol];
panel.classList.toggle("is-active", index === this[currentStepSymbol]);
});
}
function handleBackClick() {
const nextIndex = Math.max(0, this[currentStepSymbol] - 1);
goToStep.call(this, nextIndex);
}
function handleNextClick() {
if (this[currentStepSymbol] === 0) {
handleAvailabilityAndNext.call(this);
return;
}
const canProceed = validateStep.call(this, this[currentStepSymbol]);
if (!canProceed) {
fireInvalid.call(this, this[currentStepSymbol]);
return;
}
goToStep.call(this, this[currentStepSymbol] + 1);
}
function handleStepChangeRequest(fromIndex, toIndex) {
if (this[completedSymbol]) {
return false;
}
const currentStepIndex = this[currentStepSymbol];
const mapping = mapStepToWizardIndices(currentStepIndex);
const currentMain = mapping ? mapping.main : currentStepIndex;
if (toIndex <= currentMain) {
return true;
}
const canProceed = validateStep.call(this, currentStepIndex);
if (!canProceed) {
fireInvalid.call(this, currentStepIndex);
return false;
}
if (
currentStepIndex === 0 &&
this.getOption("features.requireAvailability") === true
) {
const state = this[availabilityStateSymbol];
if (!state?.checked || state?.available !== true) {
setInlineMessage.call(
this,
this.getOption("labels.messageAvailabilityRequired"),
);
fireInvalid.call(this, currentStepIndex);
return false;
}
}
return true;
}
function goToStep(index, force = false) {
if (this[completedSymbol]) {
return;
}
const steps = getStepOrder();
const bounded = Math.min(Math.max(index, 0), steps.length - 1);
this[currentStepSymbol] = bounded;
updateStepVisibility.call(this);
const wizard = this[wizardElementSymbol];
if (wizard) {
const mapping = mapStepToWizardIndices(bounded);
if (mapping) {
wizard.activate(mapping.main, mapping.sub, { force });
} else {
wizard.goToStep(bounded, { force });
}
}
fireCustomEvent(this, "monster-register-wizard-step-change", {
index: bounded,
step: steps[bounded],
});
const action = this.getOption("actions.onstepchange");
if (isFunction(action)) {
action.call(this, bounded, steps[bounded]);
}
}
function validateStep(index) {
clearInlineMessage.call(this);
const steps = getStepOrder();
const stepId = steps[index];
if (!stepId) {
return true;
}
if (stepId === "email") {
return validateEmailStep.call(this);
}
if (stepId === "password") {
return validatePasswordStep.call(this);
}
if (stepId === "profile") {
return validateProfileStep.call(this);
}
if (stepId === "address") {
return validateAddressStep.call(this);
}
if (stepId === "consents") {
return validateConsentsStep.call(this);
}
return true;
}
function validateEmailStep() {
const input = this[emailInputSymbol];
if (!input) {
return false;
}
if (this.getOption("requirements.email") === false) {
return true;
}
const value = getValue(input).trim();
if (value === "") {
setInlineMessage.call(this, this.getOption("labels.messageRequiredEmail"));
return false;
}
if (!isValidEmail(value)) {
setInlineMessage.call(this, this.getOption("labels.messageInvalidEmail"));
return false;
}
return true;
}
function validatePasswordStep() {
const password = this[passwordInputSymbol];
const confirm = this[passwordConfirmInputSymbol];
let valid = true;
if (this.getOption("requirements.password") !== false) {
const pwdValue = getValue(password);
if (!pwdValue) {
setInlineMessage.call(
this,
this.getOption("labels.messageRequiredPassword"),
);
valid = false;
}
}
if (!valid) {
return false;
}
if (
this.getOption("features.confirmPassword") === true &&
this.getOption("requirements.passwordConfirm") !== false
) {
const pwdValue = getValue(password);
if (confirm) {
const confirmValue = getValue(confirm);
if (!confirmValue) {
setInlineMessage.call(
this,
this.getOption("labels.messageRequiredPasswordConfirm"),
);
return false;
}
if (pwdValue !== confirmValue) {
setInlineMessage.call(
this,
this.getOption("labels.messagePasswordMismatch"),
);
valid = false;
}
}
}
return valid;
}
function validateProfileStep() {
const inputs = this[profileInputsSymbol] || {};
let valid = true;
if (this.getOption("requirements.firstName") !== false && inputs.firstName) {
valid = isFilled(inputs.firstName);
}
if (
valid &&
this.getOption("requirements.lastName") !== false &&
inputs.lastName
) {
valid = isFilled(inputs.lastName);
}
if (
valid &&
this.getOption("requirements.birthDate") !== false &&
this.getOption("features.showBirthDate") === true &&
inputs.birthDate
) {
valid = isFilled(inputs.birthDate);
}
if (
valid &&
this.getOption("requirements.nationality") !== false &&
this.getOption("features.showNationality") === true &&
inputs.nationality
) {
valid = isFilled(inputs.nationality);
}
if (!valid) {
setInlineMessage.call(this, this.getOption("labels.messageRequiredField"));
}
return valid;
}
function validateAddressStep() {
const inputs = this[addressInputsSymbol] || {};
const req = this.getOption("requirements.address") || {};
let valid = true;
if (req.street !== false && inputs.street) {
valid = isFilled(inputs.street);
}
if (valid && req.zip !== false && inputs.zip) {
valid = isFilled(inputs.zip);
}
if (valid && req.city !== false && inputs.city) {
valid = isFilled(inputs.city);
}
if (valid && req.country !== false && inputs.country) {
valid = isFilled(inputs.country);
}
if (!valid) {
setInlineMessage.call(this, this.getOption("labels.messageRequiredField"));
}
return valid;
}
function validateConsentsStep() {
const container = this.shadowRoot.querySelector(
'[data-monster-role="consents-list"]',
);
if (!container) {
return true;
}
const required = container.querySelectorAll(
'input[type="checkbox"][data-required="true"]',
);
for (const checkbox of required) {
if (!checkbox.checked) {
setInlineMessage.call(
this,
this.getOption("labels.messageRequiredConsents"),
);
return false;
}
}
return true;
}
function handleAvailabilityAndNext() {
const valid = validateEmailStep.call(this);
if (!valid) {
fireInvalid.call(this, 0);
return;
}
const url = this.getOption("availability.url");
const requireAvailability = this.getOption("features.requireAvailability");
if ((!isString(url) || url === "") && requireAvailability === false) {
goToStep.call(this, 1);
return;
}
checkAvailability.call(this).then((available) => {
if (available) {
goToStep.call(this, 1);
}
});
}
function resetAvailabilityState() {
this[availabilityStateSymbol] = {
checked: false,
available: null,
email: getValue(this[emailInputSymbol]) || null,
};
setAvailabilityMessage.call(this, "");
}
function debounceAvailabilityCheck() {
if (this[availabilityTimerSymbol]) {
clearTimeout(this[availabilityTimerSymbol]);
}
this[availabilityTimerSymbol] = setTimeout(() => {
const email = getValue(this[emailInputSymbol]) || "";
if (!isValidEmail(email)) {
resetAvailabilityState.call(this);
return;
}
checkAvailability.call(this);
}, 450);
}
async function checkAvailability() {
const url = this.getOption("availability.url");
const email = getValue(this[emailInputSymbol]) || "";
const requireAvailability = this.getOption("features.requireAvailability");
if (!isValidEmail(email)) {
resetAvailabilityState.call(this);
return false;
}
if (!isString(url) || url === "") {
if (requireAvailability) {
handleMessageButtonError.call(
this,
this[availabilityNextButtonSymbol],
this.getOption("labels.messageAvailabilityNotConfigured"),
);
}
return false;
}
const payload = buildAvailabilityPayload.call(this, email);
const action = this.getOption("actions.onavailabilitycheck");
fireCustomEvent(this, "monster-register-wizard-availability-check", {
email,
payload,
});
if (isFunction(action)) {
action.call(this, email, payload);
}
try {
setAvailabilityMessage.call(this, this.getOption("labels.messageChecking"));
setButtonState.call(this, this[availabilityNextButtonSymbol], "activity");
const { data } = await writeWithDatasource.call(
this,
this[availabilityDatasourceSymbol],
{
url,
fetch: this.getOption("availability.fetch"),
},
payload,
);
const result = parseAvailabilityResponse.call(this, data);
this[availabilityStateSymbol] = {
checked: true,
available: result.available,
email,
};
fireCustomEvent(
this,
"monster-register-wizard-availability-result",
result,
);
const resultAction = this.getOption("actions.onavailabilityresult");
if (isFunction(resultAction)) {
resultAction.call(this, result);
}
if (result.available) {
setAvailabilityMessage.call(
this,
this.getOption("labels.messageEmailAvailable"),
);
setButtonState.call(
this,
this[availabilityNextButtonSymbol],
"successful",
900,
);
return true;
}
setAvailabilityMessage.call(
this,
this.getOption("labels.messageEmailUnavailable"),
);
handleMessageButtonError.call(
this,
this[availabilityNextButtonSymbol],
this.getOption("labels.messageEmailUnavailable"),
);
return false;
} catch (e) {
const msg =
extractAvailabilityMessage.call(this, e) ||
this.getOption("labels.messageAvailabilityFailed");
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`);
setAvailabilityMessage.call(this, msg);
handleMessageButtonError.call(
this,
this[availabilityNextButtonSymbol],
msg,
);
return false;
}
}
function parseAvailabilityResponse(data) {
const mapping = this.getOption("availability.mapping") || {};
const selector = mapping.selector || "*";
let source = data;
if (selector && selector !== "*") {
try {
source = new Pathfinder(data).getVia(selector);
} catch (_e) {
source = data;
}
}
const available = getPathValue(source, mapping.availablePath);
const exists = getPathValue(source, mapping.existsPath);
let resolved = available;
if (resolved === null || resolved === undefined) {
if (exists !== null && exists !== undefined) {
resolved = !Boolean(exists);
}
}
return {
available: Boolean(resolved),
exists: exists === null || exists === undefined ? null : Boolean(exists),
data: source,
};
}
function handleSubmit() {
const steps = getStepOrder();
for (let i = 0; i < steps.length; i += 1) {
if (!validateStep.call(this, i)) {
fireInvalid.call(this, i);
goToStep.call(this, i);
return;
}
}
const payload = buildRegisterPayload.call(this);
const url = this.getOption("register.url");
if (!isString(url) || url === "") {
handleMessageButtonError.call(
this,
this[submitButtonSymbol],
this.getOption("labels.messageRegisterNotConfigured"),
);
return;
}
fireCustomEvent(this, "monster-register-wizard-register", { payload });
const action = this.getOption("actions.onregister");
if (isFunction(action)) {
action.call(this, payload);
}
setButtonState.call(this, this[submitButtonSymbol], "activity");
writeWithDatasource
.call(
this,
this[registerDatasourceSymbol],
{
url,
fetch: this.getOption("register.fetch"),
},
payload,
)
.then(({ data }) => {
setButtonState.call(this, this[submitButtonSymbol], "successful", 1500);
const wizard = this[wizardElementSymbol];
if (wizard) {
wizard.completeAll();
}
this[completedSymbol] = true;
updateStepVisibility.call(this);
fireCustomEvent(this, "monster-register-wizard-success", { data });
const success = this.getOption("actions.onsuccess");
if (isFunction(success)) {
success.call(this, data);
}
})
.catch((e) => {
const msg =
extractRegisterMessage.call(this, e) ||
this.getOption("labels.messageRegisterFailed");
handleMessageButtonError.call(this, this[submitButtonSymbol], msg);
fireCustomEvent(this, "monster-register-wizard-error", { error: e });
const errorAction = this.getOption("actions.onerror");
if (isFunction(errorAction)) {
errorAction.call(this, e);
}
});
}
function buildAvailabilityPayload(email) {
const payload = {};
const mapping = this.getOption("availability.payload") || {};
const tenantId = this.getOption("tenant.id");
setPathValue(payload, mapping.emailPath || "email", email);
if (tenantId) {
setPathValue(payload, mapping.tenantIdPath || "tenantId", tenantId);
}
return payload;
}
function buildRegisterPayload() {
const payload = {};
const mapping = this.getOption("register.payload") || {};
const data = collectFormData.call(this);
setPathValue(payload, mapping.tenantIdPath || "tenantId", data.tenantId);
setPathValue(payload, mapping.emailPath || "email", data.email);
setPathValue(payload, mapping.passwordPath || "password", data.password);
if (data.displayName) {
setPathValue(
payload,
mapping.displayNamePath || "displayName",
data.displayName,
);
}
setPathValue(
payload,
mapping.firstNamePath || "person.firstName",
data.firstName,
);
setPathValue(
payload,
mapping.lastNamePath || "person.lastName",
data.lastName,
);
if (data.birthDate) {
setPathValue(
payload,
mapping.birthDatePath || "person.birthDate",
data.birthDate,
);
}
if (data.nationality) {
setPathValue(
payload,
mapping.nationalityPath || "person.nationality",
data.nationality,
);
}
setPathValue(
payload,
mapping.streetPath || "person.address.street",
data.street,
);
setPathValue(payload, mapping.zipPath || "person.address.zip", data.zip);
setPathValue(payload, mapping.cityPath || "person.address.city", data.city);
setPathValue(
payload,
mapping.countryPath || "person.address.country",
data.country,
);
setPathValue(payload, mapping.consentsPath || "consents", data.consents);
return payload;
}
function collectFormData() {
const tenantId = this.getOption("tenant.id");
const email = getValue(this[emailInputSymbol]) || "";
const password = getValue(this[passwordInputSymbol]) || "";
const firstName = getValue(this[profileInputsSymbol]?.firstName) || "";
const lastName = getValue(this[profileInputsSymbol]?.lastName) || "";
const birthDate =
this.getOption("features.showBirthDate") === true
? getValue(this[profileInputsSymbol]?.birthDate) || null
: null;
const nationality =
this.getOption("features.showNationality") === true
? getValue(this[profileInputsSymbol]?.nationality) || null
: null;
const street = getValue(this[addressInputsSymbol]?.street) || "";
const zip = getValue(this[addressInputsSymbol]?.zip) || "";
const city = getValue(this[addressInputsSymbol]?.city) || "";
const country = getValue(this[addressInputsSymbol]?.country) || "";
const displayName = [firstName, lastName].filter(Boolean).join(" ").trim();
return {
tenantId,
email,
password,
displayName,
firstName,
lastName,
birthDate,
nationality,
street,
zip,
city,
country,
consents: collectConsents.call(this),
};
}
function collectConsents() {
const container = this.shadowRoot.querySelector(
'[data-monster-role="consents-list"]',
);
if (!container) {
return [];
}
const grantedValue = this.getOption("consents.grantedValue");
const revokedValue = this.getOption("consents.revokedValue");
const items = [];
container.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {
const topic = checkbox.getAttribute("data-topic");
if (!topic) {
return;
}
items.push({
topic,
status: checkbox.checked ? grantedValue : revokedValue,
});
});
return items;
}
function applyRequirements() {
applyRequiredFlag.call(
this,
this[emailInputSymbol],
this.getOption("requirements.email"),
);
applyRequiredFlag.call(
this,
this[passwordInputSymbol],
this.getOption("requirements.password"),
);
applyRequiredFlag.call(
this,
this[passwordConfirmInputSymbol],
this.getOption("requirements.passwordConfirm"),
);
const profile = this[profileInputsSymbol] || {};
applyRequiredFlag.call(
this,
profile.firstName,
this.getOption("requirements.firstName"),
);
applyRequiredFlag.call(
this,
profile.lastName,
this.getOption("requirements.lastName"),
);
applyRequiredFlag.call(
this,
profile.birthDate,
this.getOption("features.showBirthDate") === true
? this.getOption("requirements.birthDate")
: false,
);
applyRequiredFlag.call(
this,
profile.nationality,
this.getOption("features.showNationality") === true
? this.getOption("requirements.nationality")
: false,
);
const address = this[addressInputsSymbol] || {};
const addr = this.getOption("requirements.address") || {};
applyRequiredFlag.call(this, address.street, addr.street);
applyRequiredFlag.call(this, address.zip, addr.zip);
applyRequiredFlag.call(this, address.city, addr.city);
applyRequiredFlag.call(this, address.country, addr.country);
}
function applyPasswordConfirmVisibility() {
const enabled = this.getOption("features.confirmPassword") !== false;
if (this[passwordConfirmInputSymbol]) {
this[passwordConfirmInputSymbol].hidden = !enabled;
this[passwordConfirmInputSymbol].setAttribute(
"aria-hidden",
enabled ? "false" : "true",
);
}
if (this[passwordConfirmLabelSymbol]) {
this[passwordConfirmLabelSymbol].hidden = !enabled;
this[passwordConfirmLabelSymbol].setAttribute(
"aria-hidden",
enabled ? "false" : "true",
);
}
}
function applyProfileFeatureVisibility() {
const showBirthDate = this.getOption("features.showBirthDate") === true;
const showNationality = this.getOption("features.showNationality") === true;
const birthDateInput = this[profileInputsSymbol]?.birthDate;
const nationalityInput = this[profileInputsSymbol]?.nationality;
if (birthDateInput) {
birthDateInput.hidden = !showBirthDate;
birthDateInput.setAttribute(
"aria-hidden",
showBirthDate ? "false" : "true",
);
}
if (this[birthDateLabelSymbol]) {
this[birthDateLabelSymbol].hidden = !showBirthDate;
this[birthDateLabelSymbol].setAttribute(
"aria-hidden",
showBirthDate ? "false" : "true",
);
}
if (nationalityInput) {
nationalityInput.hidden = !showNationality;
nationalityInput.setAttribute(
"aria-hidden",
showNationality ? "false" : "true",
);
}
if (this[nationalityLabelSymbol]) {
this[nationalityLabelSymbol].hidden = !showNationality;
this[nationalityLabelSymbol].setAttribute(
"aria-hidden",
showNationality ? "false" : "true",
);
}
}
function applyAddressLayout() {
const fieldSet = this[fieldSetElementsSymbol]?.address;
if (!fieldSet || typeof fieldSet.setOption !== "function") {
return;
}
fieldSet.setOption("features.multipleColumns", true);
}
function applyRequiredFlag(element, required) {
if (!element) {
return;
}
if (typeof element.setOption === "function") {
element.setOption("required", false);
}
if (required === false) {
element.removeAttribute("required");
element.setAttribute("aria-required", "false");
return;
}
element.setAttribute("aria-required", "true");
}
function ensureLabels() {
const current = this.getOption("labels");
const fallback = getTranslations();
if (!isObject(current)) {
this.setOption("labels", fallback);
return;
}
const merged = Object.assign({}, fallback, current);
this.setOption("labels", merged);
}
function applyFieldLabels() {
const labels = this.getOption("labels") || {};
setFieldSetTitle.call(this, "email", labels.stepEmail);
setFieldSetTitle.call(this, "password", labels.stepPassword);
setFieldSetTitle.call(this, "profile", labels.stepProfile);
setFieldSetTitle.call(this, "address", labels.stepAddress);
setFieldSetTitle.call(this, "consents", labels.stepConsents);
setLabelText.call(this, '[data-monster-role~="email-label"]', labels.email);
setLabelText.call(
this,
'[data-monster-role~="password-label"]',
labels.password,
);
setLabelText.call(
this,
'[data-monster-role~="password-confirm-label"]',
labels.passwordConfirm,
);
setLabelText.call(
this,
'[data-monster-role~="first-name-label"]',
labels.firstName,
);
setLabelText.call(
this,
'[data-monster-role~="last-name-label"]',
labels.lastName,
);
setLabelText.call(
this,
'[data-monster-role~="birth-date-label"]',
labels.birthDate,
);
setLabelText.call(
this,
'[data-monster-role~="nationality-label"]',
labels.nationality,
);
setLabelText.call(this, '[data-monster-role~="street-label"]', labels.street);
setLabelText.call(this, '[data-monster-role~="zip-label"]', labels.zip);
setLabelText.call(this, '[data-monster-role~="city-label"]', labels.city);
setLabelText.call(
this,
'[data-monster-role~="country-label"]',
labels.country,
);
setInputMeta.call(
this,
this[emailInputSymbol],
labels.emailPlaceholder,
labels.emailAria,
);
setInputMeta.call(
this,
this[passwordConfirmInputSymbol],
labels.passwordConfirmPlaceholder,
labels.passwordConfirmAria,
);
setInputMeta.call(
this,
this[profileInputsSymbol]?.firstName,
labels.firstNamePlaceholder,
labels.firstNameAria,
);
setInputMeta.call(
this,
this[profileInputsSymbol]?.lastName,
labels.lastNamePlaceholder,
labels.lastNameAria,
);
setInputMeta.call(
this,
this[profileInputsSymbol]?.birthDate,
null,
labels.birthDateAria,
);
setInputMeta.call(
this,
this[profileInputsSymbol]?.nationality,
labels.nationalityPlaceholder,
labels.nationalityAria,
);
setInputMeta.call(
this,
this[addressInputsSymbol]?.street,
labels.streetPlaceholder,
labels.streetAria,
);
setInputMeta.call(
this,
this[addressInputsSymbol]?.zip,
labels.zipPlaceholder,
labels.zipAria,
);
setInputMeta.call(
this,
this[addressInputsSymbol]?.city,
labels.cityPlaceholder,
labels.cityAria,
);
setInputMeta.call(
this,
this[addressInputsSymbol]?.country,
labels.countryPlaceholder,
labels.countryAria,
);
}
function setFieldSetTitle(key, text) {
const fieldSet = this[fieldSetElementsSymbol]?.[key];
if (!fieldSet || typeof fieldSet.setOption !== "function") {
return;
}
fieldSet.setOption("labels.title", text || "");
}
function setLabelText(selector, text) {
const element = this.shadowRoot.querySelector(selector);
if (!element) {
return;
}
element.textContent = text || "";
}
function setInputMeta(element, placeholder, aria) {
if (!element) {
return;
}
if (placeholder !== null && placeholder !== undefined) {
element.setAttribute("placeholder", placeholder);
}
if (aria) {
element.setAttribute("aria-label", aria);
}
}
async function loadConsentTopics() {
const url = this.getOption("consents.url");
const container = this.shadowRoot.querySelector(
'[data-monster-role="consents-list"]',
);
if (!container) {
return;
}
if (isString(url) && url !== "") {
try {
const { data } = await readWithDatasource.call(
this,
this[consentsDatasourceSymbol],
{
url,
fetch: this.getOption("consents.fetch"),
},
);
const topics = mapConsentTopics.call(this, data);
renderConsentTopics.call(this, topics);
return;
} catch (e) {
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`);
}
}
const fallbackTopics = this.getOption("consents.topics") || [];
renderConsentTopics.call(this, fallbackTopics);
}
function mapConsentTopics(data) {
const mapping = this.getOption("consents.mapping") || {};
const selector = mapping.selector || "*";
let source = data;
if (selector && selector !== "*") {
try {
source = new Pathfinder(data).getVia(selector);
} catch (_e) {
source = data;
}
}
const topicsPath = mapping.topicsPath;
let list = Array.isArray(source) ? source : [];
if (topicsPath) {
try {
list = new Pathfinder(source).getVia(topicsPath);
} catch (_e) {
list = [];
}
}
if (!Array.isArray(list)) {
return [];
}
return list
.map((entry) => ({
topic: getPathValue(entry, mapping.topicPath) ?? entry?.topic,
label: getPathValue(entry, mapping.labelPath) ?? entry?.label,
description:
getPathValue(entry, mapping.descriptionPath) ?? entry?.description,
required: Boolean(
getPathValue(entry, mapping.requiredPath) ?? entry?.required,
),
}))
.filter((entry) => isString(entry.topic) && entry.topic !== "");
}
function renderConsentTopics(topics) {
const container = this.shadowRoot.querySelector(
'[data-monster-role="consents-list"]',
);
if (!container) {
return;
}
container.innerHTML = "";
topics.forEach((topic) => {
const row = document.createElement("label");
row.setAttribute("data-monster-role", "consent-row");
row.setAttribute("part", "consent-row");
const input = document.createElement("input");
input.type = "checkbox";
input.setAttribute("data-topic", topic.topic);
input.setAttribute("part", "consent-input");
if (topic.required) {
input.setAttribute("data-required", "true");
input.setAttribute("required", "");
}
const text = document.createElement("span");
text.setAttribute("data-monster-role", "consent-label");
text.setAttribute("part", "consent-label");
text.textContent = topic.label || topic.topic;
row.appendChild(input);
row.appendChild(text);
if (topic.description) {
const description = document.createElement("span");
description.setAttribute("data-monster-role", "consent-description");
description.setAttribute("part", "consent-description");
description.textContent = topic.description;
row.appendChild(description);
}
container.appendChild(row);
});
}
async function writeWithDatasource(datasource, options, payload) {
if (!datasource) {
throw new Error("datasource not available");
}
const responseHolder = { data: null };
datasource.setOption("write.url", options.url);
datasource.setOption("write.init", options.fetch || {});
datasource.setOption("write.parameters", payload);
datasource.setOption("write.responseCallback", (obj) => {
responseHolder.data = obj;
});
datasource.data = payload;
const response = await datasource.write();
return { response, data: responseHolder.data };
}
async function readWithDatasource(datasource, options) {
if (!datasource) {
throw new Error("datasource not available");
}
const responseHolder = { data: null };
datasource.setOption("read.url", options.url);
datasource.setOption("read.init", options.fetch || {});
datasource.setOption("read.parameters", {});
datasource.setOption("read.responseCallback", (obj) => {
responseHolder.data = obj;
});
const response = await datasource.read();
return { response, data: responseHolder.data };
}
function handleMessageButtonError(button, message) {
if (!button || !message) {
return;
}
try {
button.setMessage(message);
button.showMessage(3000);
button.setState("failed", 1500);
} catch (_e) {}
}
function setButtonState(button, state, timeout) {
if (!button || !state) {
return;
}
try {
button.setState(state, timeout);
} catch (_e) {}
}
function setAvailabilityMessage(message) {
if (this.getOption("features.showAvailabilityMessage") === false) {
return;
}
const button = this[availabilityNextButtonSymbol];
if (button && typeof button.setMessage === "function") {
if (!message) {
if (typeof button.clearMessage === "function") {
button.clearMessage();
}
if (typeof button.hideMessage === "function") {
button.hideMessage();
}
return;
}
try {
button.setMessage(message);
button.showMessage(3500);
} catch (_e) {}
return;
}
if (!this[availabilityMessageSymbol]) {
return;
}
this[availabilityMessageSymbol].textContent = message || "";
}
function setInlineMessage(message) {
if (!message) {
return;
}
const button = getActiveStepActionButton.call(this);
if (!button) {
return;
}
handleMessageButtonError.call(this, button, message);
}
function clearInlineMessage() {
const buttons = [];
if (Array.isArray(this[nextButtonsSymbol])) {
buttons.push(...this[nextButtonsSymbol]);
}
if (this[submitButtonSymbol]) {
buttons.push(this[submitButtonSymbol]);
}
for (const button of buttons) {
if (!button) {
continue;
}
if (typeof button.clearMessage === "function") {
button.clearMessage();
}
if (typeof button.hideMessage === "function") {
button.hideMessage();
}
}
}
function getActiveStepActionButton() {
const steps = getStepOrder();
const stepId = steps[this[currentStepSymbol]];
if (!stepId) {
return null;
}
if (stepId === "consents") {
return this[submitButtonSymbol] || null;
}
const panel = this[panelElementsSymbol]?.[stepId];
if (!panel) {
return null;
}
return panel.querySelector('[data-monster-role="next-button"]') || null;
}
function fireInvalid(stepIndex) {
fireCustomEvent(this, "monster-register-wizard-invalid", {
stepIndex,
});
const action = this.getOption("actions.oninvalid");
if (isFunction(action)) {
action.call(this, stepIndex);
}
}
function extractAvailabilityMessage(error) {
if (!error) {
return null;
}
const response = error?.response;
const raw =
response?.[
Symbol.for("@schukai/monster/data/datasource/server/restapi/rawdata")
];
const messagePath = this.getOption("availability.mapping.messagePath");
if (raw && messagePath) {
return getPathValue(raw, messagePath);
}
return null;
}
function extractRegisterMessage(error) {
if (!error) {
return null;
}
const response = error?.response;
const raw =
response?.[
Symbol.for("@schukai/monster/data/datasource/server/restapi/rawdata")
];
const messagePath = this.getOption("register.mapping.messagePath");
if (raw && messagePath) {
return getPathValue(raw, messagePath);
}
return null;
}
function getStepOrder() {
return ["email", "password", "profile", "address", "consents"];
}
function mapStepToWizardIndices(stepIndex) {
switch (stepIndex) {
case 0:
return { main: 0, sub: 0 };
case 1:
return { main: 0, sub: 1 };
case 2:
return { main: 1, sub: 0 };
case 3:
return { main: 1, sub: 1 };
case 4:
return { main: 2, sub: 0 };
default:
return null;
}
}
function mapWizardStepToIndex(mainIndex) {
switch (mainIndex) {
case 0:
return 0;
case 1:
return 2;
case 2:
return 4;
default:
return null;
}
}
function mapWizardSubStepToIndex(mainIndex, subIndex) {
if (mainIndex === 0) {
return subIndex === 1 ? 1 : 0;
}
if (mainIndex === 1) {
return subIndex === 1 ? 3 : 2;
}
if (mainIndex === 2) {
return 4;
}
return null;
}
function getValue(element) {
if (!element) {
return "";
}
if (typeof element.value === "string" || typeof element.value === "number") {
return element.value;
}
if (typeof element.getOption === "function") {
return element.getOption("value");
}
return "";
}
function isFilled(element) {
const value = getValue(element);
return String(value ?? "").trim() !== "";
}
function isValidEmail(value) {
if (!isString(value)) {
return false;
}
const trimmed = value.trim();
if (trimmed === "") {
return false;
}
return /^[^\s@]+@[^\s@]+(\.[^\s@]+)*$/.test(trimmed);
}
function setPathValue(target, path, value) {
if (!isString(path) || path === "") {
return;
}
try {
new Pathfinder(target).setVia(path, value);
} catch (_e) {}
}
function getPathValue(source, path) {
if (!isString(path) || path === "") {
return null;
}
try {
return new Pathfinder(source).getVia(path);
} catch (_e) {
return null;
}
}
function getTemplate() {
// language=HTML
return `
<div data-monster-role="control" part="control">
<div data-monster-role="header" part="header">
<slot name="title"></slot>
</div>
<div data-monster-role="body" part="body">
<div data-monster-role="nav" part="nav">
<monster-wizard-navigation part="nav-control">
<ol class="wizard-steps">
<li class="step">
<span data-monster-role="step-label" part="step-label"
data-monster-replace="path:labels.stepAccount"></span>
<ul>
<li data-monster-replace="path:labels.subEmail"></li>
<li data-monster-replace="path:labels.subPassword"></li>
</ul>
</li>
<li class="step">
<span data-monster-role="step-label" part="step-label"
data-monster-replace="path:labels.stepProfileGroup"></span>
<ul>
<li data-monster-replace="path:labels.subProfile"></li>
<li data-monster-replace="path:labels.subAddress"></li>
</ul>
</li>
<li class="step">
<span data-monster-role="step-label" part="step-label"
data-monster-replace="path:labels.stepConsentsGroup"></span>
<ul>
<li data-monster-replace="path:labels.subConsents"></li>
</ul>
</li>
</ol>
</monster-wizard-navigation>
</div>
<div data-monster-role="content" part="content">
<section data-monster-role="panel" data-step="email" part="panel panel-email">
<monster-field-set data-monster-role="field-set-email" part="field-set field-set-email">
<label data-monster-role="field-label email-label" part="field-label email-label"
data-monster-replace="path:labels.email"></label>
<input data-monster-role="email-input" part="input input-email"
type="text" name="email"
data-monster-attributes="placeholder path:labels.emailPlaceholder, autocomplete path:autocomplete.email, aria-label path:labels.emailAria" />
<slot name="email-fields"></slot>
</monster-field-set>
<slot name="email-info" part="slot slot-email"></slot>
<div data-monster-role="availability-message" part="availability-message"></div>
<div data-monster-role="step-message" part="step-message"></div>
<div data-monster-role="actions" part="actions">
<monster-message-state-button data-monster-role="next-button" part="next-button"
data-monster-replace="path:labels.next"></monster-message-state-button>
</div>
</section>
<section data-monster-role="panel" data-step="password" part="panel panel-password"