UNPKG

@schukai/monster

Version:

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

1,327 lines (1,170 loc) 36.4 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 { assembleMethodSymbol, CustomElement, getSlottedElements, registerCustomElement, } from "../../dom/customelement.mjs"; import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs"; import { getLocaleOfDocument } from "../../dom/locale.mjs"; import { fireCustomEvent } from "../../dom/events.mjs"; import { isFunction, isObject } from "../../types/is.mjs"; import { Observer } from "../../types/observer.mjs"; import { WizardStyleSheet } from "./stylesheet/wizard.mjs"; import "../navigation/wizard-navigation.mjs"; import "./button-bar.mjs"; import "./message-state-button.mjs"; export { Wizard }; const controlElementSymbol = Symbol("controlElement"); const navigationElementSymbol = Symbol("navigationElement"); const slotElementSymbol = Symbol("slotElement"); const previousButtonElementSymbol = Symbol("previousButtonElement"); const nextButtonElementSymbol = Symbol("nextButtonElement"); const submitButtonElementSymbol = Symbol("submitButtonElement"); const statusElementSymbol = Symbol("statusElement"); const panelRecordsSymbol = Symbol("panelRecords"); const groupsSymbol = Symbol("groups"); const currentPanelIndexSymbol = Symbol("currentPanelIndex"); const optionObserverSymbol = Symbol("optionObserver"); const idsSymbol = Symbol("ids"); const finalRecordSymbol = Symbol("finalRecord"); /** * A reusable wizard container that combines content panels with `monster-wizard-navigation`. * * Every direct child panel that defines `data-monster-option-navigation-label` becomes a wizard panel. * Panels sharing the same label are grouped into one main step. Optional sub labels can be defined with * `data-monster-option-navigation-sub-label`. * * @fragments /fragments/components/form/wizard/ * @example /examples/components/form/wizard-basic * @since 4.126.0 * @summary A generic wizard control for checkout, creation and multi-step forms. * @fires monster-wizard-refresh * @fires monster-wizard-change-request * @fires monster-wizard-validation * @fires monster-wizard-panel-change * @fires monster-wizard-invalid * @fires monster-wizard-complete * @fires monster-wizard-final-panel-show */ class Wizard extends CustomElement { static get [instanceSymbol]() { return Symbol.for("@schukai/monster/components/form/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 Text labels * @property {string} labels.previous Previous button label * @property {string} labels.next Next button label * @property {string} labels.submit Submit button label * @property {string} labels.stepStatus Step status pattern with `{current}` and `{total}` * @property {Object} features Feature toggles * @property {boolean} features.allowBackNavigation Allows navigation to previous panels * @property {boolean} features.allowForwardNavigation Allows navigation to next panels * @property {boolean} features.allowDirectNavigation Allows skipping ahead using the navigation * @property {boolean} features.showActions Shows the action buttons row * @property {boolean} features.hideSubmittedActions Hides buttons after completion * @property {boolean} features.autoFocus Focuses the first focusable element of the active panel * @property {boolean} features.keepCompletedNavigationState Marks visited items as completed in the navigation * @property {Object} validation Validation settings * @property {boolean} validation.native Runs `reportValidity()` on native forms and form controls before moving forward * @property {string} validation.selector Selector for standalone controls that should run `reportValidity()` * @property {function|null} validation.callback Global validation callback receiving a transition context * @property {Object<string,function>} validation.panels Panel validation callbacks, keyed by panel id * @property {Object<string,function>} validation.groups Group validation callbacks, keyed by group id * @property {Object} actions Callback actions * @property {function|null} actions.beforeStepChange Called before a panel change. Return `false` to cancel. * @property {function|null} actions.afterStepChange Called after a panel change. * @property {function|null} actions.invalid Called when validation blocks navigation. * @property {function|null} actions.complete Called when submit is triggered on the last panel. */ get defaults() { return Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, labels: getTranslations(), features: { mutationObserver: true, allowBackNavigation: true, allowForwardNavigation: true, allowDirectNavigation: true, showActions: true, showStatus: false, hideSubmittedActions: false, autoFocus: false, keepCompletedNavigationState: true, }, validation: { native: true, selector: "input,select,textarea,button,monster-select,monster-password,monster-toggle-switch,monster-digits", callback: null, panels: {}, groups: {}, }, actions: { beforeStepChange: null, afterStepChange: null, invalid: null, complete: null, }, }); } static getTag() { return "monster-wizard"; } static getCSSStyleSheet() { return [WizardStyleSheet]; } [assembleMethodSymbol]() { super[assembleMethodSymbol](); this[currentPanelIndexSymbol] = -1; this[idsSymbol] = { panel: 0, group: 0, }; initControlReferences.call(this); initEventHandler.call(this); initOptionObserver.call(this); ensureLabels.call(this); queueMicrotask(() => { this.refresh(); }); return this; } /** * Rebuild the internal navigation structure from the slotted panels. * * @return {Wizard} */ refresh() { const records = collectPanels.call(this); const groups = buildGroups.call(this, records); this[panelRecordsSymbol] = records; this[groupsSymbol] = groups; renderNavigation.call(this); syncActionLabels.call(this); if (!records.length) { syncStatus.call(this); return this; } const currentIndex = this[currentPanelIndexSymbol] >= 0 && this[currentPanelIndexSymbol] < records.length ? this[currentPanelIndexSymbol] : 0; syncNavigationInteraction.call(this); setActivePanel.call(this, currentIndex, { force: true }); fireCustomEvent(this, "monster-wizard-refresh", { panelCount: records.length, groupCount: groups.length, }); return this; } /** * Returns the current panel index. * * @return {number} */ getCurrentIndex() { return this[currentPanelIndexSymbol]; } /** * Returns the current panel element or `null`. * * @return {HTMLElement|null} */ getCurrentPanel() { const record = this[panelRecordsSymbol]?.[this[currentPanelIndexSymbol]]; return record?.element || null; } /** * Navigate to the next panel. * * @return {Promise<boolean>} */ async next() { return this.goTo(this[currentPanelIndexSymbol] + 1); } /** * Navigate to the previous panel. * * @return {Promise<boolean>} */ async previous() { return this.goTo(this[currentPanelIndexSymbol] - 1); } /** * Navigate to the requested panel index. * * @param {number} targetIndex * @param {Object} [options] * @param {boolean} [options.force=false] * @param {string} [options.reason="api"] * @return {Promise<boolean>} */ async goTo(targetIndex, options = {}) { if (typeof targetIndex !== "number" || Number.isNaN(targetIndex)) { return false; } const records = this[panelRecordsSymbol] || []; if (!records.length || targetIndex < 0 || targetIndex >= records.length) { return false; } const currentIndex = this[currentPanelIndexSymbol]; if (targetIndex === currentIndex) { return true; } const { force = false, reason = "api" } = options; const direction = currentIndex === -1 ? "initial" : targetIndex > currentIndex ? "forward" : "backward"; const context = createTransitionContext.call( this, currentIndex, targetIndex, direction, reason, ); if (!force) { const allowed = await allowTransition.call(this, context); if (!allowed) { return false; } } setActivePanel.call(this, targetIndex, options); return true; } /** * Validates the current panel without changing the current position. * * @return {Promise<boolean>} */ async validateCurrent() { const index = this[currentPanelIndexSymbol]; if (index < 0) { return true; } const context = createTransitionContext.call( this, index, Math.min(index + 1, (this[panelRecordsSymbol] || []).length - 1), "forward", "validate", ); return runValidation.call(this, context); } /** * Trigger the completion flow on the last panel. * * @return {Promise<boolean>} */ async submit() { const index = this[currentPanelIndexSymbol]; const records = this[panelRecordsSymbol] || []; if (index < 0 || index !== records.length - 1) { return false; } const context = createTransitionContext.call( this, index, index, "submit", "submit", ); const valid = await runValidation.call(this, context); if (!valid) { return false; } const callback = this.getOption("actions.complete"); if (isFunction(callback)) { const result = await Promise.resolve(callback.call(this, context)); if (result === false) { return false; } } fireCustomEvent(this, "monster-wizard-complete", context); this[navigationElementSymbol]?.completeAll?.(); if (this[finalRecordSymbol]) { showFinalPanel.call(this); } if (this.getOption("features.hideSubmittedActions") === true) { this[controlElementSymbol]?.setAttribute( "data-monster-state", "completed", ); } this[submitButtonElementSymbol]?.removeState?.(); this[submitButtonElementSymbol]?.clearMessage?.(); this[submitButtonElementSymbol]?.setState?.("successful", 2000); return true; } } function initControlReferences() { this[controlElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}="control"]`, ); this[navigationElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}="navigation"]`, ); this[slotElementSymbol] = this.shadowRoot.querySelector("slot"); this[previousButtonElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}="previous"]`, ); this[nextButtonElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}="next"]`, ); this[submitButtonElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}="submit"]`, ); this[statusElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}="status"]`, ); } function initEventHandler() { this[slotElementSymbol]?.addEventListener("slotchange", () => { this.refresh(); }); this[navigationElementSymbol]?.addEventListener( "monster-wizard-step-changed", (event) => { const mainIndex = event?.detail?.newIndex; if (typeof mainIndex !== "number") { return; } const targetRecord = this[groupsSymbol]?.[mainIndex]?.panels?.[0]; if ( !targetRecord || targetRecord.index === this[currentPanelIndexSymbol] ) { return; } setActivePanel.call(this, targetRecord.index, { force: true, reason: "navigation", }); }, ); this[navigationElementSymbol]?.addEventListener( "monster-wizard-substep-changed", (event) => { const mainIndex = event?.detail?.mainIndex; const subIndex = event?.detail?.subIndex; if (typeof mainIndex !== "number" || typeof subIndex !== "number") { return; } const targetRecord = this[groupsSymbol]?.[mainIndex]?.panels?.[subIndex]; if ( !targetRecord || targetRecord.index === this[currentPanelIndexSymbol] ) { return; } setActivePanel.call(this, targetRecord.index, { force: true, reason: "navigation", }); }, ); this[previousButtonElementSymbol]?.addEventListener("click", () => { this.previous(); }); this[nextButtonElementSymbol]?.addEventListener("click", () => { this.next(); }); this[submitButtonElementSymbol]?.addEventListener("click", () => { this.submit(); }); } function initOptionObserver() { if (this[optionObserverSymbol]) { return; } this[optionObserverSymbol] = true; this.attachObserver( new Observer(() => { ensureLabels.call(this); syncActionLabels.call(this); syncNavigationInteraction.call(this); syncActionState.call(this); syncStatus.call(this); }), ); } function syncActionLabels() { const labels = this.getOption("labels"); if (this[previousButtonElementSymbol]) { this[previousButtonElementSymbol].setOption( "labels.button", labels.previous || "Back", ); } if (this[nextButtonElementSymbol]) { this[nextButtonElementSymbol].setOption( "labels.button", labels.next || "Next", ); } if (this[submitButtonElementSymbol]) { this[submitButtonElementSymbol].setOption( "labels.button", labels.submit || "Finish", ); } } function syncNavigationInteraction() { const navigation = this[navigationElementSymbol]; if (!(navigation instanceof HTMLElement)) { return; } const allowDirect = this.getOption("features.allowDirectNavigation"); navigation.style.pointerEvents = allowDirect ? "" : "none"; navigation.toggleAttribute("data-monster-direct-navigation", allowDirect); } function syncActionState() { const records = this[panelRecordsSymbol] || []; const currentIndex = this[currentPanelIndexSymbol]; const hasPanels = records.length > 0; const atFirst = currentIndex <= 0; const atLast = hasPanels && currentIndex === records.length - 1; const finalVisible = this[finalRecordSymbol]?.element?.hasAttribute?.("data-monster-active") === true; const allowBack = this.getOption("features.allowBackNavigation"); const allowForward = this.getOption("features.allowForwardNavigation"); const showActions = this.getOption("features.showActions"); const showPrevious = showActions && !finalVisible && allowBack === true && !atFirst; this[controlElementSymbol]?.toggleAttribute( "data-monster-show-actions", showActions, ); if (this[previousButtonElementSymbol]) { this[previousButtonElementSymbol].hidden = !showPrevious; this[previousButtonElementSymbol].style.display = showPrevious ? "" : "none"; this[previousButtonElementSymbol].disabled = !hasPanels || atFirst || allowBack !== true; } if (this[nextButtonElementSymbol]) { this[nextButtonElementSymbol].hidden = !showActions || atLast || finalVisible; this[nextButtonElementSymbol].style.display = !showActions || atLast || finalVisible ? "none" : ""; this[nextButtonElementSymbol].disabled = !hasPanels || atLast || allowForward !== true; } if (this[submitButtonElementSymbol]) { this[submitButtonElementSymbol].hidden = !showActions || !atLast || finalVisible; this[submitButtonElementSymbol].style.display = !showActions || !atLast || finalVisible ? "none" : ""; this[submitButtonElementSymbol].disabled = !hasPanels || !atLast; } } function syncStatus() { const statusElement = this[statusElementSymbol]; if (!(statusElement instanceof HTMLElement)) { return; } if (this.getOption("features.showStatus") !== true) { statusElement.hidden = true; statusElement.textContent = ""; return; } statusElement.hidden = false; const total = this[panelRecordsSymbol]?.length || 0; const finalVisible = this[finalRecordSymbol]?.element?.hasAttribute?.("data-monster-active") === true; if (total === 0 || this[currentPanelIndexSymbol] < 0 || finalVisible) { statusElement.textContent = ""; return; } const template = this.getOption("labels.stepStatus") || "Step {current} of {total}"; statusElement.textContent = template .replace("{current}", String(this[currentPanelIndexSymbol] + 1)) .replace("{total}", String(total)); } function ensureLabels() { const current = this.getOption("labels"); const fallback = getTranslations(); if (!isObject(current)) { this.setOption("labels", fallback); return; } this.setOption("labels", Object.assign({}, fallback, current)); } function collectPanels() { const elements = Array.from(getSlottedElements.call(this) || []).filter( (element) => element instanceof HTMLElement, ); const finalIndex = elements.length - 1; const hasImplicitFinal = finalIndex >= 0 && elements.length > 1 && elements[finalIndex] instanceof HTMLElement && elements[finalIndex].hasAttribute( "data-monster-option-navigation-label", ) === false; this[finalRecordSymbol] = null; const stepElements = elements.filter((element, index) => { if (hasImplicitFinal && index === finalIndex) { return false; } return element.hasAttribute("data-monster-option-navigation-label"); }); const records = stepElements.map((element, index) => { const mainLabel = element.getAttribute("data-monster-option-navigation-label")?.trim() || `Step ${index + 1}`; const subLabel = element .getAttribute("data-monster-option-navigation-sub-label") ?.trim() || null; const panelId = element.getAttribute("data-monster-option-navigation-id")?.trim() || slugify( subLabel ? `${mainLabel}-${subLabel}-${index + 1}` : `${mainLabel}-${index + 1}`, ) || `panel-${++this[idsSymbol].panel}`; const groupBase = element.getAttribute("data-monster-option-navigation-group")?.trim() || mainLabel; const groupId = slugify(groupBase) || `group-${++this[idsSymbol].group}`; element.setAttribute("data-monster-wizard-panel-id", panelId); element.setAttribute("data-monster-wizard-group-id", groupId); element.setAttribute("data-monster-role", "panel"); normalizePanelHeading(element); return { element, index, panelId, groupId, mainLabel, subLabel, mainIndex: -1, subIndex: -1, }; }); const finalElement = elements[finalIndex]; if (hasImplicitFinal && finalElement instanceof HTMLElement) { finalElement.setAttribute("data-monster-role", "panel"); normalizePanelHeading(finalElement); finalElement.hidden = true; finalElement.removeAttribute("data-monster-active"); this[finalRecordSymbol] = { element: finalElement, panelId: finalElement .getAttribute("data-monster-option-navigation-id") ?.trim() || "final", }; } for (const element of elements) { applyEmbeddedFieldSetContext(element); } return records; } function applyEmbeddedFieldSetContext(element) { if (!(element instanceof HTMLElement)) { return; } for (const fieldSet of element.querySelectorAll("monster-field-set")) { fieldSet.setAttribute("data-monster-context", "wizard"); } } function buildGroups(records) { const groups = []; const map = new Map(); for (const record of records) { let group = map.get(record.groupId); if (!group) { group = { id: record.groupId, label: record.mainLabel, mainIndex: groups.length, panels: [], }; map.set(record.groupId, group); groups.push(group); } record.mainIndex = group.mainIndex; record.subIndex = group.panels.length; group.panels.push(record); } return groups; } function renderNavigation() { const navigation = this[navigationElementSymbol]; if (!(navigation instanceof HTMLElement)) { return; } navigation.innerHTML = ""; const list = document.createElement("ol"); list.className = "wizard-steps"; for (const group of this[groupsSymbol] || []) { const step = document.createElement("li"); step.className = "step"; const label = document.createElement("span"); label.setAttribute("data-monster-role", "step-label"); label.textContent = group.label; step.appendChild(label); if (group.panels.length > 1 || group.panels[0]?.subLabel) { const subList = document.createElement("ul"); for (const panel of group.panels) { const item = document.createElement("li"); item.textContent = panel.subLabel || `Step ${panel.subIndex + 1}`; subList.appendChild(item); } step.appendChild(subList); } list.appendChild(step); } navigation.appendChild(list); navigation.setOption("actions.beforeStepChange", (fromIndex, toIndex) => { return handleNavigationRequest.call(this, fromIndex, toIndex); }); queueMicrotask(() => { const currentIndex = this[currentPanelIndexSymbol] >= 0 ? this[currentPanelIndexSymbol] : 0; const record = this[panelRecordsSymbol]?.[currentIndex]; if (record) { navigation.activate(record.mainIndex, record.subIndex, { force: true }); } }); } async function handleNavigationRequest(fromIndex, toIndex) { if (this.getOption("features.allowDirectNavigation") !== true) { return false; } const targetPanel = this[groupsSymbol]?.[toIndex]?.panels?.[0]; if (!targetPanel) { return false; } const currentIndex = this[currentPanelIndexSymbol]; if (currentIndex === -1) { return true; } const targetIndex = targetPanel.index; if (targetIndex === currentIndex) { return false; } const direction = targetIndex > currentIndex ? "forward" : "backward"; const context = createTransitionContext.call( this, currentIndex, targetIndex, direction, "navigation", ); return allowTransition.call(this, context); } function setActivePanel(targetIndex, options = {}) { const records = this[panelRecordsSymbol] || []; const previousIndex = this[currentPanelIndexSymbol]; const record = records[targetIndex]; if (!record) { return; } for (const panelRecord of records) { const active = panelRecord.index === targetIndex; panelRecord.element.hidden = !active; panelRecord.element.toggleAttribute("data-monster-active", active); panelRecord.element.toggleAttribute( "data-monster-completed", this.getOption("features.keepCompletedNavigationState") === true && panelRecord.index < targetIndex, ); } if (this[finalRecordSymbol]?.element) { this[finalRecordSymbol].element.hidden = true; this[finalRecordSymbol].element.removeAttribute("data-monster-active"); } this[currentPanelIndexSymbol] = targetIndex; if (this.getOption("features.keepCompletedNavigationState") === true) { this[navigationElementSymbol]?.activate(record.mainIndex, record.subIndex, { force: true, }); } else { this[navigationElementSymbol]?.activate(record.mainIndex, record.subIndex, { force: true, }); } syncActionState.call(this); syncStatus.call(this); clearActionFeedback.call(this); if (this.getOption("features.autoFocus") === true) { queueMicrotask(() => { focusFirstControl(record.element); }); } fireCustomEvent(this, "monster-wizard-panel-change", { panelIndex: targetIndex, previousPanelIndex: previousIndex, panelId: record.panelId, groupId: record.groupId, panel: record.element, options, }); const callback = this.getOption("actions.afterStepChange"); if (isFunction(callback)) { callback.call( this, createTransitionContext.call( this, previousIndex, targetIndex, targetIndex > previousIndex ? "forward" : "backward", options.reason || "internal", ), ); } } function showFinalPanel() { const finalRecord = this[finalRecordSymbol]; if (!finalRecord?.element) { return; } for (const panelRecord of this[panelRecordsSymbol] || []) { panelRecord.element.hidden = true; panelRecord.element.removeAttribute("data-monster-active"); } finalRecord.element.hidden = false; finalRecord.element.setAttribute("data-monster-active", ""); this[currentPanelIndexSymbol] = -1; syncActionState.call(this); syncStatus.call(this); clearActionFeedback.call(this); fireCustomEvent(this, "monster-wizard-final-panel-show", { panelId: finalRecord.panelId, panel: finalRecord.element, }); } async function allowTransition(context) { const currentIndex = this[currentPanelIndexSymbol]; fireCustomEvent(this, "monster-wizard-change-request", context); if (context.direction === "backward") { if (this.getOption("features.allowBackNavigation") !== true) { return false; } } else if (context.direction === "forward") { if (this.getOption("features.allowForwardNavigation") !== true) { return false; } if ( this.getOption("features.allowDirectNavigation") !== true && context.toIndex > currentIndex + 1 ) { return false; } const valid = await runValidation.call(this, context); if (!valid) { return false; } } const beforeStepChange = this.getOption("actions.beforeStepChange"); if (isFunction(beforeStepChange)) { const result = await Promise.resolve(beforeStepChange.call(this, context)); if (result === false) { return false; } } return true; } async function runValidation(context) { let valid = true; let message = null; if (this.getOption("validation.native") === true) { valid = validatePanelElement.call(this, context.fromPanel) && valid; } const groupCallback = this.getOption( `validation.groups.${context.fromGroup?.id || ""}`, ); if (isFunction(groupCallback)) { const result = await Promise.resolve(groupCallback.call(this, context)); valid = normalizeValidationResult(result, valid); message = extractValidationMessage(result) || message; } const panelCallback = this.getOption( `validation.panels.${context.fromPanelId || ""}`, ); if (isFunction(panelCallback)) { const result = await Promise.resolve(panelCallback.call(this, context)); valid = normalizeValidationResult(result, valid); message = extractValidationMessage(result) || message; } const globalCallback = this.getOption("validation.callback"); if (isFunction(globalCallback)) { const result = await Promise.resolve(globalCallback.call(this, context)); valid = normalizeValidationResult(result, valid); message = extractValidationMessage(result) || message; } if (!valid) { const actionButton = getActionButtonForContext.call(this, context); const fallbackMessage = this.getOption("labels.validationFailed"); showActionError.call( this, actionButton, message || fallbackMessage || "Please check the current step before continuing.", ); fireCustomEvent(this, "monster-wizard-invalid", context); const invalid = this.getOption("actions.invalid"); if (isFunction(invalid)) { invalid.call(this, Object.assign({}, context, { message })); } } fireCustomEvent(this, "monster-wizard-validation", { ...context, valid, message, }); return valid; } function validatePanelElement(panel) { if (!(panel instanceof HTMLElement)) { return true; } let valid = true; const formContainers = panel.querySelectorAll("form, monster-form"); for (const element of formContainers) { if (typeof element.reportValidity === "function") { valid = element.reportValidity() !== false && valid; } } const selector = this.getOption("validation.selector"); if (typeof selector !== "string" || selector.trim() === "") { return valid; } const controls = panel.querySelectorAll(selector); for (const element of controls) { if (element.closest("form, monster-form")) { continue; } if (typeof element.reportValidity === "function") { valid = element.reportValidity() !== false && valid; } } return valid; } function createTransitionContext(fromIndex, toIndex, direction, reason) { const records = this[panelRecordsSymbol] || []; const groups = this[groupsSymbol] || []; const fromRecord = fromIndex >= 0 && fromIndex < records.length ? records[fromIndex] : null; const toRecord = toIndex >= 0 && toIndex < records.length ? records[toIndex] : null; return { wizard: this, reason, direction, fromIndex, toIndex, fromPanelId: fromRecord?.panelId || null, toPanelId: toRecord?.panelId || null, fromPanel: fromRecord?.element || null, toPanel: toRecord?.element || null, fromGroup: fromRecord ? groups[fromRecord.mainIndex] : null, toGroup: toRecord ? groups[toRecord.mainIndex] : null, actionButton: getActionButtonForDirection.call(this, direction), }; } function getActionButtonForContext(context) { if (context.direction === "submit") { return this[submitButtonElementSymbol] || null; } return this[nextButtonElementSymbol] || null; } function getActionButtonForDirection(direction) { if (direction === "submit") { return this[submitButtonElementSymbol] || null; } if (direction === "backward") { return this[previousButtonElementSymbol] || null; } return this[nextButtonElementSymbol] || null; } function clearActionFeedback() { for (const button of [ this[previousButtonElementSymbol], this[nextButtonElementSymbol], this[submitButtonElementSymbol], ]) { button?.removeState?.(); button?.hideMessage?.(); button?.clearMessage?.(); } } function showActionError(button, message) { if (!button) { return; } button.removeState?.(); button.hideMessage?.(); button.clearMessage?.(); button.setState?.("failed", 2500); button.setMessage?.(message); button.showMessage?.(3500); } function normalizeValidationResult(result, valid) { if (result === false) { return false; } if (typeof result === "string") { return false; } return valid; } function extractValidationMessage(result) { if (typeof result === "string" && result.trim() !== "") { return result.trim(); } if ( result && typeof result === "object" && typeof result.message === "string" && result.message.trim() !== "" ) { return result.message.trim(); } return null; } function focusFirstControl(panel) { if (!(panel instanceof HTMLElement)) { return; } const target = panel.querySelector( 'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])', ); target?.focus?.(); } function normalizePanelHeading(panel) { if (!(panel instanceof HTMLElement)) { return; } const heading = panel.querySelector("h1, h2, h3, h4, h5, h6"); if (!(heading instanceof HTMLElement)) { return; } heading.style.marginTop = "0"; heading.style.paddingTop = "0"; } function getTranslations() { const locale = getLocaleOfDocument(); switch (locale.language) { case "de": return { previous: "Zurueck", next: "Weiter", submit: "Abschliessen", stepStatus: "Schritt {current} von {total}", validationFailed: "Bitte pruefen Sie den aktuellen Schritt.", }; case "fr": return { previous: "Retour", next: "Suivant", submit: "Terminer", stepStatus: "Etape {current} sur {total}", validationFailed: "Veuillez verifier l'etape actuelle.", }; case "es": return { previous: "Atras", next: "Siguiente", submit: "Finalizar", stepStatus: "Paso {current} de {total}", validationFailed: "Revise el paso actual.", }; case "zh": return { previous: "返回", next: "下一步", submit: "完成", stepStatus: "第 {current} / {total} 步", validationFailed: "请检查当前步骤。", }; case "hi": return { previous: "वापस", next: "आगे", submit: "पूरा करें", stepStatus: "चरण {current} / {total}", validationFailed: "कृपया वर्तमान चरण जांचें।", }; case "bn": return { previous: "পেছনে", next: "পরবর্তী", submit: "সমাপ্ত", stepStatus: "ধাপ {current} / {total}", validationFailed: "অনুগ্রহ করে বর্তমান ধাপটি পরীক্ষা করুন।", }; case "pt": return { previous: "Voltar", next: "Avancar", submit: "Concluir", stepStatus: "Passo {current} de {total}", validationFailed: "Verifique a etapa atual.", }; case "ru": return { previous: "Назад", next: "Далее", submit: "Завершить", stepStatus: "Шаг {current} из {total}", validationFailed: "Проверьте текущий шаг.", }; case "ja": return { previous: "戻る", next: "次へ", submit: "完了", stepStatus: "{total} 件中 {current} ステップ", validationFailed: "現在のステップを確認してください。", }; case "pa": return { previous: "ਵਾਪਸ", next: "ਅੱਗੇ", submit: "ਪੂਰਾ ਕਰੋ", stepStatus: "ਕਦਮ {current} / {total}", validationFailed: "ਕਿਰਪਾ ਕਰਕੇ ਮੌਜੂਦਾ ਕਦਮ ਦੀ ਜਾਂਚ ਕਰੋ।", }; case "mr": return { previous: "मागे", next: "पुढे", submit: "पूर्ण करा", stepStatus: "पायरी {current} / {total}", validationFailed: "कृपया सद्य पायरी तपासा.", }; case "it": return { previous: "Indietro", next: "Avanti", submit: "Concludi", stepStatus: "Passo {current} di {total}", validationFailed: "Controlla il passaggio corrente.", }; case "nl": return { previous: "Terug", next: "Volgende", submit: "Voltooien", stepStatus: "Stap {current} van {total}", validationFailed: "Controleer de huidige stap.", }; case "sv": return { previous: "Tillbaka", next: "Nasta", submit: "Slutfor", stepStatus: "Steg {current} av {total}", validationFailed: "Kontrollera det aktuella steget.", }; case "pl": return { previous: "Wstecz", next: "Dalej", submit: "Zakoncz", stepStatus: "Krok {current} z {total}", validationFailed: "Sprawdz biezacy krok.", }; case "da": return { previous: "Tilbage", next: "Naeste", submit: "Afslut", stepStatus: "Trin {current} af {total}", validationFailed: "Kontroller det aktuelle trin.", }; case "fi": return { previous: "Takaisin", next: "Seuraava", submit: "Valmis", stepStatus: "Vaihe {current} / {total}", validationFailed: "Tarkista nykyinen vaihe.", }; case "no": return { previous: "Tilbake", next: "Neste", submit: "Fullfor", stepStatus: "Trinn {current} av {total}", validationFailed: "Kontroller gjeldende trinn.", }; case "cs": return { previous: "Zpet", next: "Dalsi", submit: "Dokoncit", stepStatus: "Krok {current} z {total}", validationFailed: "Zkontrolujte aktualni krok.", }; default: return { previous: "Back", next: "Next", submit: "Finish", stepStatus: "Step {current} of {total}", validationFailed: "Please check the current step before continuing.", }; } } function slugify(value) { return String(value || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } function getTemplate() { return ` <div data-monster-role="control" part="control"> <div data-monster-role="body" part="body"> <monster-wizard-navigation data-monster-role="navigation" part="navigation"></monster-wizard-navigation> <div data-monster-role="content" part="content"> <div data-monster-role="status" part="status" aria-live="polite"></div> <div data-monster-role="panel-slot" part="panel-slot"> <slot></slot> </div> <monster-button-bar data-monster-role="actions" part="actions"> <monster-message-state-button data-monster-role="previous" part="previous"></monster-message-state-button> <monster-message-state-button data-monster-role="next" part="next"></monster-message-state-button> <monster-message-state-button data-monster-role="submit" part="submit"></monster-message-state-button> </monster-button-bar> </div> </div> </div>`; } registerCustomElement(Wizard);