@adrii_/wizard-js
Version:
A lightweight wizard UI component that supports accessibility and HTML5 in vanilla JavaScript.
632 lines (610 loc) • 22.5 kB
JavaScript
import "./scss/main.scss";
/**
* A lightweight wizard UI component that supports accessibility and HTML5 in Vanilla JavaScript.
*
* @link https://github.com/AdrianVillamayor/Wizard-JS
* @author Adrian
*
* @class Wizard
*/
class Wizard {
constructor(args = {}) {
const defaults = {
wz_class: ".wizard",
wz_nav: ".wizard-nav",
wz_ori: ".horizontal",
wz_nav_style: "dots",
wz_content: ".wizard-content",
wz_buttons: ".wizard-buttons",
wz_button: ".wizard-btn",
wz_button_style: ".btn",
wz_step: ".wizard-step",
wz_form: ".wizard-form",
wz_next: ".next",
wz_prev: ".prev",
wz_finish: ".finish",
wz_highlight: ".highlight-error",
bubbles: true,
nav: true,
buttons: true,
highlight: true,
current_step: 0,
steps: 0,
highlight_time: 1000,
navigation: "all",
next: "Next",
prev: "Prev",
finish: "Submit",
highlight_type: {
error: "error",
warning: "warning",
success: "success",
info: "info"
},
i18n: {
empty_wz: "No item has been found with which to generate the Wizard.",
empty_nav: "Nav does not exist or is empty.",
empty_content: "Content does not exist or is empty.",
empty_html: "Undefined or null content cannot be added.",
empty_update: "Nothing to update.",
no_nav: "Both the nav and the buttons are disabled, there is no navigation system.",
form_validation: "One or more of the form fields are invalid.",
diff_steps: "Discordance between the steps of nav and content.",
random: "There has been a problem, check the configuration and use of the wizard.",
already_defined: "This item is already defined",
title: "Step"
}
};
this.options = {
...defaults,
...args
};
const {
wz_class,
wz_nav,
wz_ori,
wz_nav_style,
wz_content,
wz_buttons,
wz_button,
wz_button_style,
wz_step,
wz_form,
wz_next,
wz_prev,
wz_finish,
wz_highlight,
nav,
buttons,
highlight,
highlight_time,
highlight_type,
current_step,
steps,
navigation,
prev,
next,
finish,
i18n,
bubbles
} = this.options;
Object.assign(this, {
wz_class,
wz_nav,
wz_ori,
wz_nav_style,
wz_content,
wz_buttons,
wz_button,
wz_button_style,
wz_step,
wz_form,
wz_next,
wz_prev,
wz_finish,
wz_highlight,
nav,
buttons,
highlight,
highlight_time,
highlight_type,
current_step,
steps,
navigation,
prev,
next,
finish,
i18n,
last_step: current_step,
form: false,
locked: false,
locked_step: null,
bubbles
});
}
/**
* Initializes the wizard
*/
init() {
try {
const wz_check = document.querySelector(this.wz_class);
if (!wz_check) throw new Error(this.i18n.empty_wz);
if (wz_check.getAttribute("data-wz-load") === "true") {
console.warn(`${this.wz_class} : ${this.i18n.already_defined}`);
return;
}
const wz = wz_check;
if (!this.buttons && !this.nav) {
console.warn(this.i18n.no_nav);
}
wz.classList.add(this.wz_ori.replace(".", ""));
if (wz.tagName === "FORM") {
this.form = true;
}
this.checkAndPrepare(wz);
switch (this.navigation) {
case "all":
case "nav":
this.setNavEvent();
this.setBtnEvent();
break;
case "buttons":
this.setBtnEvent();
break;
}
wz.style.display = wz.classList.contains("vertical") ? "flex" : "block";
wz.setAttribute("data-wz-load", "true");
document.dispatchEvent(new CustomEvent("wz.ready", {
bubbles: this.bubbles,
detail: {
target: this.wz_class,
elem: wz
}
}));
} catch (error) {
console.error(error);
}
}
/**
* Check and update each section of the wizard.
*/
update() {
const wz = document.querySelector(this.wz_class);
if (!wz) throw new Error(this.i18n.empty_wz);
if (wz.getAttribute("data-wz-load") !== "true") {
throw new Error(this.i18n.empty_wz);
}
this.checkAndPrepare(wz);
this.content_update = false;
wz.dispatchEvent(new CustomEvent("wz.update", {
bubbles: this.bubbles,
detail: {
target: this.wz_class,
elem: wz
}
}));
}
/**
* Restart the wizard
*/
reset() {
this.setCurrentStep(0);
const wz = document.querySelector(this.wz_class);
const nav = wz.querySelector(this.wz_nav);
const content = wz.querySelector(this.wz_content);
if (this.buttons) {
const buttons = wz.querySelector(this.wz_buttons);
const next = buttons.querySelector(`${this.wz_button}${this.wz_next}`);
const prev = buttons.querySelector(`${this.wz_button}${this.wz_prev}`);
const finish = buttons.querySelector(`${this.wz_button}${this.wz_finish}`);
this.checkButtons(next, prev, finish);
}
if (this.nav) {
const wz_nav_steps = nav.querySelectorAll(this.wz_step);
wz_nav_steps.forEach(el => el.classList.remove("active"));
nav.querySelector(`${this.wz_step}[data-wz-step="${this.getCurrentStep()}"]`).classList.add("active");
}
const wz_content_steps = content.querySelectorAll(this.wz_step);
wz_content_steps.forEach(el => el.classList.remove("active"));
content.querySelector(`${this.wz_step}[data-wz-step="${this.getCurrentStep()}"]`).classList.add("active");
wz.dispatchEvent(new Event("wz.reset", {
bubbles: this.bubbles
}));
}
/**
* Locks the wizard in the active step
*/
lock() {
this.locked = true;
this.locked_step = this.getCurrentStep();
}
/**
* Unlock wizard
*/
unlock() {
this.locked = false;
this.locked_step = null;
document.querySelector(this.wz_class).dispatchEvent(new Event("wz.unlock", {
bubbles: this.bubbles
}));
}
/**
* Generate the steps and define a standard for each step.
*/
prefabSteps(wz_content_steps, wz_nav, wz_nav_steps) {
const active_index = this.getCurrentStep();
wz_content_steps.forEach((step, i) => {
step.setAttribute("data-wz-step", i);
if (this.nav) wz_nav_steps[i].setAttribute("data-wz-step", i);
});
if (this.nav) {
wz_nav_steps.forEach(el => el.classList.remove("active"));
wz_nav_steps[active_index].classList.add("active");
wz_nav.classList.add(this.wz_nav_style);
}
wz_content_steps.forEach(el => el.classList.remove("active"));
wz_content_steps[active_index].classList.add("active");
this.setButtons();
}
/**
* Adds the form tag and converts the wizard into a <form>
*/
updateToForm() {
const wz = document.querySelector(this.wz_class);
const wz_content = wz.querySelector(this.wz_content);
if (wz_content.tagName !== "FORM") {
const wz_content_class = wz_content.getAttribute("class");
const wz_content_content = wz_content.innerHTML;
wz_content.remove();
const form = document.createElement("form");
form.setAttribute("method", "POST");
form.setAttribute("class", `${wz_content_class} ${this.wz_form.replace(".", "")}`);
form.innerHTML = wz_content_content;
wz.appendChild(form);
}
}
/**
* Checks and validates each input/select/textarea of the active step.
*/
checkForm() {
const wz = document.querySelector(this.wz_class);
const wz_content = wz.querySelector(this.wz_content);
const steps = wz_content.querySelectorAll(this.wz_step);
const target = steps[this.getCurrentStep()];
const inputs = target.querySelectorAll("input, textarea, select");
if (inputs.length > 0) {
return this.formValidator(wz_content, inputs);
}
return {
error: false
};
}
/**
* Generating, styling and shaping the Nav
*/
setNav(wz) {
let wz_nav = wz.querySelector(this.wz_nav);
if (wz_nav && this.nav) {
wz_nav.remove();
wz_nav = null;
}
if (!wz_nav && this.nav) {
const wz_content = wz.querySelector(this.wz_content);
const steps = wz_content.querySelectorAll(this.wz_step);
const nav = document.createElement("aside");
nav.classList.add(this.wz_nav.replace(".", ""));
steps.forEach((step, i) => {
const nav_step = document.createElement("div");
const title = step.getAttribute("data-wz-title") || `${this.i18n.title} ${i}`;
nav_step.classList.add(this.wz_step.replace(".", ""));
if (this.navigation === "buttons") nav_step.classList.add("nav-buttons");
const dot = document.createElement("span");
dot.classList.add("dot");
nav_step.appendChild(dot);
const span = document.createElement("span");
span.innerHTML = title;
nav_step.appendChild(span);
nav.appendChild(nav_step);
});
wz.prepend(nav);
}
}
/**
* Generating, styling and shaping Buttons
*/
setButtons() {
const wz = document.querySelector(this.wz_class);
let wz_btns = wz.querySelector(this.wz_buttons);
if (wz_btns && this.buttons) {
wz_btns.remove();
wz_btns = null;
}
if (!wz_btns && this.buttons) {
const buttons = document.createElement("aside");
buttons.classList.add(this.wz_buttons.replace(".", ""));
const btn_style = this.wz_button_style.replace(/\./g, "").split(" ");
const prev = document.createElement("button");
prev.innerHTML = this.prev;
prev.classList.add(this.wz_button.replace(".", ""), ...btn_style, this.wz_prev.replace(".", ""));
if (this.navigation === "nav") prev.style.display = "none";
buttons.appendChild(prev);
const next = document.createElement("button");
next.innerHTML = this.next;
next.classList.add(this.wz_button.replace(".", ""), ...btn_style, this.wz_next.replace(".", ""));
if (this.navigation === "nav") next.style.display = "none";
buttons.appendChild(next);
const finish = document.createElement("button");
finish.innerHTML = this.finish;
finish.classList.add(this.wz_button.replace(".", ""), ...btn_style, this.wz_finish.replace(".", ""));
buttons.appendChild(finish);
this.checkButtons(next, prev, finish);
wz.appendChild(buttons);
}
}
/**
* Generating, styling and shaping Buttons
*/
checkButtons(next, prev, finish) {
const current_step = this.getCurrentStep();
const n_steps = this.steps - 1;
if (current_step === 0) {
prev.setAttribute("disabled", "true");
} else {
prev.removeAttribute("disabled");
}
if (current_step === n_steps) {
next.setAttribute("disabled", "true");
finish.style.display = "block";
} else {
finish.style.display = "none";
next.removeAttribute("disabled");
}
}
/**
* Common function for wizard checks and prefab.
*/
checkAndPrepare(wz) {
this.setNav(wz);
const wz_content = wz.querySelector(this.wz_content);
if (!wz_content) throw new Error(this.i18n.empty_content);
const wz_content_steps = wz_content.querySelectorAll(this.wz_step);
if (!wz_content_steps.length) throw new Error(this.i18n.empty_content);
let wz_nav, wz_nav_steps;
if (this.nav) {
wz_nav = wz.querySelector(this.wz_nav);
if (!wz_nav) throw new Error(this.i18n.empty_nav);
wz_nav_steps = wz_nav.querySelectorAll(this.wz_step);
if (!wz_nav_steps.length) throw new Error(this.i18n.empty_nav);
if (wz_nav_steps.length !== wz_content_steps.length) {
throw new Error(this.i18n.diff_steps);
}
}
this.steps = wz_content_steps.length;
this.prefabSteps(wz_content_steps, wz_nav, wz_nav_steps);
}
/**
* Click event handler for Buttons and Nav.
*/
onClick(element) {
const wz = document.querySelector(this.wz_class);
if (this.locked && this.locked_step === this.getCurrentStep()) {
wz.dispatchEvent(new Event("wz.lock", {
bubbles: this.bubbles
}));
return;
}
const parent = element.closest(this.wz_class);
const nav = parent.querySelector(this.wz_nav);
const content = parent.querySelector(this.wz_content);
const is_btn = element.classList.contains(this.wz_button.replace(".", ""));
const is_nav = element.classList.contains(this.wz_step.replace(".", ""));
let step = element.getAttribute("data-wz-step");
step = step !== null ? parseInt(step) : this.getCurrentStep();
if (is_btn) {
if (element.classList.contains(this.wz_prev.replace(".", ""))) {
step -= 1;
wz.dispatchEvent(new Event("wz.btn.prev", {
bubbles: this.bubbles
}));
} else if (element.classList.contains(this.wz_next.replace(".", ""))) {
step += 1;
wz.dispatchEvent(new Event("wz.btn.next", {
bubbles: this.bubbles
}));
}
}
const step_action = step > this.getCurrentStep();
if (is_nav) {
if (step_action) {
wz.dispatchEvent(new Event("wz.nav.forward", {
bubbles: this.bubbles
}));
} else if (step < this.getCurrentStep()) {
wz.dispatchEvent(new Event("wz.nav.backward", {
bubbles: this.bubbles
}));
}
}
if (this.form && this.navigation !== "buttons") {
if (step_action) {
if (step !== this.getCurrentStep() + 1) {
step = step >= this.last_step ? this.last_step : this.getCurrentStep() + 1;
}
}
}
if (this.form) {
const check_form = this.checkForm();
if (check_form.error) {
if (step_action) {
wz.dispatchEvent(new CustomEvent("wz.error", {
bubbles: this.bubbles,
detail: {
id: "form_validation",
msg: this.i18n.form_validation,
target: check_form.target
}
}));
}
this.last_step = this.getCurrentStep();
if (this.getCurrentStep() < step) {
return;
}
}
}
if (step !== null && step !== undefined) {
this.setCurrentStep(step);
}
if (this.buttons) {
const buttons = parent.querySelector(this.wz_buttons);
const next = buttons.querySelector(`${this.wz_button}${this.wz_next}`);
const prev = buttons.querySelector(`${this.wz_button}${this.wz_prev}`);
const finish = buttons.querySelector(`${this.wz_button}${this.wz_finish}`);
this.checkButtons(next, prev, finish);
}
if (this.nav) {
const wz_nav_steps = nav.querySelectorAll(this.wz_step);
wz_nav_steps.forEach(el => el.classList.remove("active"));
nav.querySelector(`${this.wz_step}[data-wz-step="${this.getCurrentStep()}"]`).classList.add("active");
}
const wz_content_steps = content.querySelectorAll(this.wz_step);
wz_content_steps.forEach(el => el.classList.remove("active"));
content.querySelector(`${this.wz_step}[data-wz-step="${this.getCurrentStep()}"]`).classList.add("active");
}
/**
* Notifies that the wizard has been completed.
*/
onClickFinish() {
if (this.form) {
const check_form = this.checkForm();
if (!check_form.error) {
document.querySelector(this.wz_class).dispatchEvent(new Event("wz.form.submit", {
bubbles: this.bubbles
}));
}
} else {
document.querySelector(this.wz_class).dispatchEvent(new Event("wz.end", {
bubbles: this.bubbles
}));
}
}
/**
* Set the active step
*/
setCurrentStep(step) {
this.current_step = this.setStep(step);
}
/**
* Return the active step
*/
getCurrentStep() {
return this.current_step;
}
/**
* Check and match the steps of the wizard.
*/
setStep(step) {
const parent = document.querySelector(this.wz_class);
const content = parent.querySelector(this.wz_content);
const check_content = content.querySelector(`${this.wz_step}[data-wz-step="${step}"]`);
if (!check_content) {
const content_length = content.querySelectorAll(this.wz_step).length - 1;
step = Math.min(content_length, step);
}
this.last_step = Math.max(step, this.last_step);
return parseInt(step, 10);
}
/**
* Set Nav events
*/
setNavEvent() {
const wz = document.querySelector(this.wz_class);
wz.addEventListener("click", event => {
const target = event.target.closest(`${this.wz_nav} ${this.wz_step}`);
if (target) {
event.preventDefault();
this.onClick(target);
}
});
}
/**
* Set Button events
*/
setBtnEvent() {
const wz = document.querySelector(this.wz_class);
wz.addEventListener("click", event => {
const target = event.target.closest(`${this.wz_buttons} ${this.wz_button}`);
if (target) {
event.preventDefault();
if (target.classList.contains(this.wz_finish.replace(".", ""))) {
this.onClickFinish();
} else {
this.onClick(target);
}
}
});
}
/**
* Checks the fields of the active step, in case there is an error it generates a highlight.
*/
formValidator(wz_content, formData) {
let error = false;
const target = [];
formData.forEach(e => {
let isRequired = e.required || e.classList.contains("required");
const isOnActiveRequired = e.classList.contains("on-active-required");
// Check for data-require-if attribute
const requireIf = e.getAttribute("data-require-if");
if (requireIf) {
const [dependencyId, requiredValue] = requireIf.split(":");
const dependencyField = wz_content.querySelector(`#${dependencyId}`);
if (dependencyField) {
const dependencyValue = dependencyField.type === "checkbox" || dependencyField.type === "radio" ? dependencyField.checked : dependencyField.value;
if (dependencyValue === requiredValue) {
isRequired = true;
}
}
}
let valid = true;
if (isRequired || isOnActiveRequired) {
valid = this.validateField(e);
if (!valid) {
error = true;
target.push(e);
if (this.highlight) {
this.highlightElement(e, this.highlight_type.error);
}
}
}
});
return {
error,
target
};
}
validateField(e) {
switch (e.tagName) {
case "INPUT":
if (e.type === "checkbox" || e.type === "radio") {
return e.checked;
} else {
return e.value.trim() !== "";
}
case "SELECT":
return e.value.trim() !== "";
case "TEXTAREA":
return e.value.trim() !== "";
default:
return true;
}
}
/**
* Highlights an element to indicate validation errors.
*/
highlightElement(element, type) {
element.classList.add(this.wz_highlight.replace(".", ""), type);
setTimeout(() => {
element.classList.remove(this.wz_highlight.replace(".", ""), type);
}, this.highlight_time);
}
}
export default Wizard;