UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

1,818 lines (1,713 loc) 113 kB
/** * 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"