@devmansam/forms
Version:
Professional, customizable web form components for contact and inquiry forms
1,560 lines (1,329 loc) • 70 kB
JavaScript
class WebInquiryForm extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.currentStep = 0;
this.totalSteps = 5; // Steps 0, 1, 2, 3, 4
this.completedSteps = new Set();
this.googleFontLoaded = false;
}
static get observedAttributes() {
return [
"theme",
"primary-color",
"background-color",
"text-color",
"border-color",
"border-radius",
"font-family",
"font-size",
"google-font",
"api-url",
"dark-primary-color",
"dark-background-color",
"dark-text-color",
"dark-border-color",
"form-title",
"input-background-color",
"input-text-color",
"input-border-color",
"fieldset-background-color",
"success-color",
"error-color",
"progress-color",
"dark-input-background-color",
"dark-input-text-color",
"dark-input-border-color",
"dark-fieldset-background-color",
"dark-success-color",
"dark-error-color",
"dark-progress-color",
"button-color",
"button-text-color",
"dark-button-color",
"dark-button-text-color",
"heading-color",
"dark-heading-color",
"back-button-color",
"back-button-text-color",
"dark-back-button-color",
"dark-back-button-text-color",
"asterisk-color",
"dark-asterisk-color",
];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (name === "theme") {
this.updateTheme();
} else if (name === "google-font") {
// When google-font changes, reset the loaded flag and re-load/update
this.googleFontLoaded = false;
this.loadGoogleFont().then(() => {
this.updateStyles();
});
} else {
// Re-render styles for other attribute changes
this.updateStyles();
}
}
connectedCallback() {
// Render the initial structure immediately
this.render();
// Load Google Font and then update styles
this.loadGoogleFont().then(() => {
this.updateStyles();
});
this.initializeEvents();
this.updateTheme();
this.setupThemeWatchers();
this.updateProgress();
}
disconnectedCallback() {
if (this.themeMediaQuery) {
this.themeMediaQuery.removeEventListener(
"change",
this.handleSystemThemeChange
);
}
if (this.themeObserver) {
this.themeObserver.disconnect();
}
}
loadGoogleFont() {
return new Promise((resolve) => {
const googleFont = this.getAttribute("google-font");
if (!googleFont) {
this.googleFontLoaded = false;
resolve();
return;
}
const existingLink = document.head.querySelector(
`link[href*="fonts.googleapis.com"][href*="${googleFont.replace(
/\s+/g,
"+"
)}"]`
);
if (existingLink) {
this.googleFontLoaded = true;
resolve();
return;
}
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = `https://fonts.googleapis.com/css2?family=${googleFont.replace(
/\s+/g,
"+"
)}:wght@400;500;600;700&display=swap`;
link.onload = () => {
this.googleFontLoaded = true;
resolve();
};
link.onerror = () => {
console.warn(`Failed to load Google Font: ${googleFont}`);
this.googleFontLoaded = false;
resolve();
};
document.head.appendChild(link);
});
}
setupThemeWatchers() {
this.themeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
this.handleSystemThemeChange = () => {
if (!this.getAttribute("theme")) {
this.updateTheme();
}
};
this.themeMediaQuery.addEventListener(
"change",
this.handleSystemThemeChange
);
this.themeObserver = new MutationObserver(() => {
if (!this.getAttribute("theme")) {
this.updateTheme();
}
});
this.themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme", "class"],
});
this.themeObserver.observe(document.body, {
attributes: true,
attributeFilter: ["data-theme", "class"],
});
}
updateTheme() {
const container = this.shadowRoot.querySelector(".form-container");
if (!container) return;
const explicitTheme = this.getAttribute("theme");
let isDark = false;
if (explicitTheme) {
isDark = explicitTheme === "dark";
} else {
isDark = this.detectDarkMode();
}
if (isDark) {
container.classList.add("dark-mode");
} else {
container.classList.remove("dark-mode");
}
}
detectDarkMode() {
const html = document.documentElement;
const body = document.body;
const dataTheme =
html.getAttribute("data-theme") || body.getAttribute("data-theme");
if (dataTheme === "dark") return true;
if (dataTheme === "light") return false;
if (html.classList.contains("dark") || body.classList.contains("dark"))
return true;
if (
html.classList.contains("dark-mode") ||
body.classList.contains("dark-mode")
)
return true;
if (
html.classList.contains("theme-dark") ||
body.classList.contains("theme-dark")
)
return true;
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
getFontFamily() {
const googleFont = this.getAttribute("google-font");
const customFontFamily = this.getAttribute("font-family");
if (googleFont && this.googleFontLoaded) {
return `"${googleFont}", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
} else if (customFontFamily) {
return customFontFamily;
} else {
return '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
}
}
get styles() {
const primaryColor = this.getAttribute("primary-color") || "#3498db";
const backgroundColor = this.getAttribute("background-color") || "#ffffff";
const textColor = this.getAttribute("text-color") || "#333333";
const borderColor = this.getAttribute("border-color") || "#aaaaaa";
const borderRadius = this.getAttribute("border-radius") || "6px";
const fontFamily = this.getFontFamily();
const fontSize = this.getAttribute("font-size") || "16px";
// Enhanced input styling
const inputBackgroundColor =
this.getAttribute("input-background-color") || backgroundColor;
const inputTextColor = this.getAttribute("input-text-color") || textColor;
const inputBorderColor =
this.getAttribute("input-border-color") || borderColor;
// Enhanced fieldset styling
const fieldsetBackgroundColor =
this.getAttribute("fieldset-background-color") || backgroundColor;
// Enhanced status colors
const successColor = this.getAttribute("success-color") || "#4caf50";
const errorColor = this.getAttribute("error-color") || "#d32f2f";
const progressColor = this.getAttribute("progress-color") || primaryColor;
// Button colors
const buttonColor = this.getAttribute("button-color") || primaryColor;
const buttonTextColor = this.getAttribute("button-text-color") || "#ffffff";
// Back button colors
const backButtonColor = this.getAttribute("back-button-color") || primaryColor;
const backButtonTextColor = this.getAttribute("back-button-text-color") || buttonTextColor;
// Heading colors
const headingColor = this.getAttribute("heading-color") || textColor;
// Asterisk colors
const asteriskColor = this.getAttribute("asterisk-color") || "#ef4444";
// Dark mode variants
const darkPrimaryColor =
this.getAttribute("dark-primary-color") || "#60a5fa";
const darkBackgroundColor =
this.getAttribute("dark-background-color") || "#1e2026";
const darkTextColor = this.getAttribute("dark-text-color") || "#e9ecef";
const darkBorderColor = this.getAttribute("dark-border-color") || "#495057";
const darkInputBackgroundColor =
this.getAttribute("dark-input-background-color") || darkBackgroundColor;
const darkInputTextColor =
this.getAttribute("dark-input-text-color") || darkTextColor;
const darkInputBorderColor =
this.getAttribute("dark-input-border-color") || darkBorderColor;
const darkFieldsetBackgroundColor =
this.getAttribute("dark-fieldset-background-color") ||
darkBackgroundColor;
const darkSuccessColor =
this.getAttribute("dark-success-color") || "#4ade80";
const darkErrorColor = this.getAttribute("dark-error-color") || "#f87171";
const darkProgressColor =
this.getAttribute("dark-progress-color") || darkPrimaryColor;
// Dark mode button colors
const darkButtonColor = this.getAttribute("dark-button-color") || darkPrimaryColor;
const darkButtonTextColor = this.getAttribute("dark-button-text-color") || "#ffffff";
// Dark mode back button colors
const darkBackButtonColor = this.getAttribute("dark-back-button-color") || darkPrimaryColor;
const darkBackButtonTextColor = this.getAttribute("dark-back-button-text-color") || darkButtonTextColor;
// Dark mode heading colors
const darkHeadingColor = this.getAttribute("dark-heading-color") || darkTextColor;
// Dark mode asterisk color
const darkAsteriskColor = this.getAttribute("dark-asterisk-color") || "#f87171";
return `
:host {
display: block;
font-family: ${fontFamily};
line-height: 1.6;
color: ${textColor};
max-width: 600px;
margin: 0 auto;
padding: 16px;
font-size: ${fontSize};
letter-spacing: 1px;
}
.form-container {
background-color: ${backgroundColor};
padding: 24px;
border-radius: ${borderRadius};
box-shadow: 0 3.2px 6.4px rgba(0, 0, 0, 0.1), 0 1.6px 3.2px rgba(0, 0, 0, 0.08), 0 0.8px 1.6px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
font-family: ${fontFamily};
}
.form-header {
color: ${textColor};
margin-bottom: 24px;
text-align: center;
}
.form-header h1 {
font-size: calc(${fontSize} * 1.5);
line-height: 1;
margin-bottom: -6px;
font-family: ${fontFamily};
letter-spacing: 2px;
color: ${headingColor};
}
.form-header p {
font-size: calc(${fontSize} * 0.875);
opacity: 0.8;
font-family: ${fontFamily};
}
.progress-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid ${borderColor};
}
.progress-bar {
width: 100%;
height: 5px;
background: rgba(${this.hexToRgb(progressColor)}, 0.1);
border-radius: calc(${borderRadius} / 2);
overflow: hidden;
margin-bottom: 12px;
}
.progress-fill {
height: 100%;
background: ${progressColor};
border-radius: calc(${borderRadius} / 2);
transition: width 0.3s ease;
width: 0%;
}
.step-indicators {
display: flex;
justify-content: space-between;
font-size: calc(${fontSize} * 0.75);
padding: 0 4px;
font-family: ${fontFamily};
letter-spacing: 0.7px;
}
.step-indicator {
display: flex;
align-items: center;
color: ${textColor};
opacity: 0.6;
}
.step-indicator.active {
color: ${progressColor};
font-weight: 600;
opacity: 1;
}
.step-indicator.completed {
color: ${successColor};
opacity: 1;
}
.step-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: ${borderColor};
color: ${textColor};
margin-right: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: calc(${fontSize} * 0.6875);
font-weight: bold;
font-family: ${fontFamily};
}
.step-indicator.active .step-dot {
background: ${progressColor};
color: ${buttonTextColor};
}
.step-indicator.completed .step-dot {
background: ${successColor};
color: ${buttonTextColor};
}
.section {
display: none;
}
.section.active {
display: block;
}
.section-content {
border: none;
border-radius: 0;
padding: 0;
margin-bottom: 0;
background-color: transparent;
position: relative;
}
fieldset {
border: 1px solid ${borderColor};
border-radius: ${borderRadius};
padding: 16px;
margin-bottom: 20px;
background-color: ${fieldsetBackgroundColor};
filter: brightness(0.98);
}
legend {
font-size: calc(${fontSize} * 1.125);
font-weight: bold;
color: ${headingColor};
letter-spacing: 2px;
padding: 0 8px;
font-family: ${fontFamily};
}
.section-subtitle {
color: ${textColor};
opacity: 0.7;
font-size: ${fontSize};
text-align: center;
margin-top: -8px;
font-family: ${fontFamily};
}
.form-group {
margin: 0 0 16px 0;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 600;
font-size: ${fontSize};
color: ${textColor};
font-family: ${fontFamily};
}
.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="tel"],
.form-group textarea,
.form-group select {
width: 100%;
padding: 8px;
border: 2px solid ${inputBorderColor};
border-radius: ${borderRadius};
box-sizing: border-box;
transition: border-color 0.3s, background-color 0.3s;
background-color: ${inputBackgroundColor};
color: ${inputTextColor};
font-size: ${fontSize};
font-family: ${fontFamily};
}
.form-group textarea {
resize: vertical;
min-height: 64px;
}
.form-group input[type="text"]:focus,
.form-group input[type="email"]:focus,
.form-group input[type="tel"]:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: ${primaryColor};
box-shadow: 0 0 2px rgba(52, 152, 219, 0.3);
}
.required::after {
content: "*";
color: ${asteriskColor};
margin-left: 3px;
font-weight: bold;
}
.radio-group {
margin-top: 8px;
}
.radio-option {
display: flex;
align-items: center;
margin-bottom: 8px;
width: 100%;
}
.radio-option input[type="radio"] {
width: auto;
margin-right: 6px;
margin-bottom: 0;
flex-shrink: 0;
}
.radio-option label {
margin: 0;
font-weight: normal;
cursor: pointer;
width: auto;
flex: none;
color: ${textColor};
font-family: ${fontFamily};
}
.extension-option {
margin-top: 12px;
padding: 12px;
}
.checkbox-wrapper {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.checkbox-wrapper input[type="checkbox"] {
width: auto;
margin-right: 6px;
}
.checkbox-wrapper label {
margin: 0;
font-weight: normal;
color: ${textColor};
font-family: ${fontFamily};
}
.conditional-field {
display: none;
margin-top: 8px;
}
.conditional-field.show {
display: block;
}
.address-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
}
.address-row .form-group {
margin-bottom: 0;
}
.navigation {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid ${borderColor};
}
.btn {
padding: 10px 16px;
border: none;
border-radius: ${borderRadius};
font-size: ${fontSize};
font-family: ${fontFamily};
cursor: pointer;
transition: background-color 0.3s, opacity 0.2s ease;
min-width: 96px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-secondary {
background-color: ${backButtonColor};
color: ${backButtonTextColor};
}
.btn-secondary:hover:not(:disabled) {
opacity: 0.9;
}
.btn-primary {
background-color: ${buttonColor};
color: ${buttonTextColor};
}
.btn-primary:hover:not(:disabled) {
opacity: 0.9;
}
[role="alert"] {
color: ${errorColor};
font-size: calc(${fontSize} * 0.75);
margin-top: 4px;
font-weight: 500;
font-family: ${fontFamily};
display: none;
}
[role="alert"]:not(:empty) {
display: block;
}
.error-message {
color: ${errorColor};
font-size: calc(${fontSize} * 0.75);
margin-top: 4px;
font-weight: 500;
font-family: ${fontFamily};
}
.invalid {
border: 2px solid ${errorColor} !important;
background-color: rgba(${this.hexToRgb(errorColor)}, 0.05);
}
.radio-option input[type="radio"].invalid {
border: 2px solid ${errorColor};
box-shadow: 0 0 3px rgba(${this.hexToRgb(errorColor)}, 0.3);
}
.valid {
border-color: ${successColor} !important;
background-color: rgba(${this.hexToRgb(successColor)}, 0.05);
}
.toast {
position: fixed;
bottom: 280px;
right: 16px;
background: ${successColor};
color: white;
padding: 12px 16px;
border-radius: ${borderRadius};
box-shadow: 0 5px 13px rgba(0, 0, 0, 0.2);
z-index: 1000;
transform: translateX(100%);
transition: transform 0.3s ease, opacity 0.3s ease;
opacity: 1;
font-family: ${fontFamily};
font-size: calc(${fontSize} * 0.875);
font-weight: 500;
min-width: 160px;
max-width: 320px;
word-wrap: break-word;
display: block;
}
.toast.show {
transform: translateX(0);
}
.toast.hide {
opacity: 0;
transform: translateX(100%);
}
.toast.error {
background: ${errorColor};
}
/* Review Section */
.review-container {
display: grid;
gap: 16px;
}
.review-section {
background: ${fieldsetBackgroundColor};
filter: brightness(0.98);
border-radius: ${borderRadius};
padding: 16px;
border: 1px solid ${borderColor};
}
.review-section h3 {
color: ${textColor};
font-size: ${fontSize};
font-weight: 600;
margin-bottom: 12px;
border-bottom: 1px solid ${borderColor};
padding-bottom: 6px;
font-family: ${fontFamily};
}
.review-item {
display: grid;
grid-template-columns: 128px 1fr;
gap: 10px;
margin-bottom: 6px;
align-items: start;
}
.review-label {
font-weight: 500;
color: ${textColor};
opacity: 0.8;
font-size: ${fontSize};
font-family: ${fontFamily};
}
.review-value {
color: ${textColor};
word-break: break-word;
font-size: ${fontSize};
font-family: ${fontFamily};
}
.review-value.empty {
color: ${textColor};
opacity: 0.5;
font-style: italic;
}
.edit-step-btn {
background: none;
border: 1px solid ${primaryColor};
color: ${primaryColor};
padding: 5px 10px;
border-radius: ${borderRadius};
font-size: calc(${fontSize} * 0.875);
font-family: ${fontFamily};
cursor: pointer;
transition: all 0.2s ease;
margin-top: 8px;
}
.edit-step-btn:hover {
background: ${primaryColor};
color: white;
}
/* Dark Mode */
.form-container.dark-mode {
background-color: ${darkBackgroundColor};
color: ${darkTextColor};
}
.dark-mode fieldset {
background-color: ${darkFieldsetBackgroundColor};
filter: brightness(1.1);
border-color: ${darkBorderColor};
}
.dark-mode legend {
color: ${darkHeadingColor};
}
.dark-mode .progress-section {
border-bottom-color: ${darkBorderColor};
}
.dark-mode .form-header {
color: ${darkTextColor};
}
.dark-mode .form-header h1 {
color: ${darkHeadingColor};
}
.dark-mode .section-subtitle {
color: ${darkTextColor};
}
.dark-mode .form-group label {
color: ${darkTextColor};
}
.dark-mode .step-indicator {
color: ${darkTextColor};
}
.dark-mode .step-indicator.active {
color: ${darkProgressColor};
}
.dark-mode .step-indicator.completed {
color: ${darkSuccessColor};
}
.dark-mode .step-dot {
background: ${darkBorderColor};
color: ${darkTextColor};
}
.dark-mode .step-indicator.active .step-dot {
background: ${darkProgressColor};
color: ${darkButtonTextColor};
}
.dark-mode .step-indicator.completed .step-dot {
background: ${darkSuccessColor};
color: ${darkButtonTextColor};
}
.dark-mode .progress-fill {
background: ${darkProgressColor};
}
.dark-mode .progress-bar {
background: rgba(${this.hexToRgb(darkProgressColor)}, 0.1);
}
.dark-mode .form-group input[type="text"],
.dark-mode .form-group input[type="email"],
.dark-mode .form-group input[type="tel"],
.dark-mode .form-group textarea,
.dark-mode .form-group select {
background-color: ${darkInputBackgroundColor};
filter: brightness(1.1);
color: ${darkInputTextColor};
border-color: ${darkInputBorderColor};
}
.dark-mode .form-group input[type="text"]:focus,
.dark-mode .form-group input[type="email"]:focus,
.dark-mode .form-group input[type="tel"]:focus,
.dark-mode .form-group textarea:focus,
.dark-mode .form-group select:focus {
border-color: ${darkPrimaryColor};
box-shadow: 0 0 3px rgba(96, 165, 250, 0.3);
}
.dark-mode .radio-option label {
color: ${darkTextColor};
}
.dark-mode .checkbox-wrapper label {
color: ${darkTextColor};
}
.dark-mode .required::after {
color: ${darkAsteriskColor};
}
.dark-mode .navigation {
border-top-color: ${darkBorderColor};
}
.dark-mode .btn-primary {
background-color: ${darkButtonColor};
color: ${darkButtonTextColor};
}
.dark-mode .btn-secondary {
background-color: ${darkBackButtonColor};
color: ${darkBackButtonTextColor};
}
.dark-mode .review-section {
background-color: ${darkFieldsetBackgroundColor};
filter: brightness(1.1);
border-color: ${darkBorderColor};
}
.dark-mode .review-section h3 {
color: ${darkTextColor};
border-bottom-color: ${darkBorderColor};
}
.dark-mode .review-label {
color: ${darkTextColor};
}
.dark-mode .review-value {
color: ${darkTextColor};
}
.dark-mode .review-value.empty {
color: ${darkTextColor};
opacity: 0.5;
}
.dark-mode .edit-step-btn {
border-color: ${darkPrimaryColor};
color: ${darkPrimaryColor};
}
.dark-mode .edit-step-btn:hover {
background: ${darkPrimaryColor};
color: white;
}
.dark-mode .toast {
background: ${darkSuccessColor};
}
.dark-mode .toast.error {
background: ${darkErrorColor};
}
.dark-mode [role="alert"] {
color: ${darkErrorColor};
}
.dark-mode .error-message {
color: ${darkErrorColor};
}
.dark-mode .invalid {
border-color: ${darkErrorColor} !important;
background-color: rgba(${this.hexToRgb(darkErrorColor)}, 0.05);
}
.dark-mode .radio-option input[type="radio"].invalid {
border: 2px solid ${darkErrorColor};
box-shadow: 0 0 3px rgba(${this.hexToRgb(darkErrorColor)}, 0.3);
}
.dark-mode .valid {
border-color: ${darkSuccessColor} !important;
background-color: rgba(${this.hexToRgb(successColor)}, 0.05);
}
/* Responsive */
@media (max-width: 768px) {
:host {
padding: 8px;
max-width: 480px;
}
.form-container {
padding: 16px;
}
.form-header h1 {
font-size: calc(${fontSize} * 1.25);
}
fieldset {
padding: 12px;
}
legend {
font-size: ${fontSize};
}
.form-group label {
font-size: calc(${fontSize} * 0.875);
}
.step-indicators {
font-size: calc(${fontSize} * 0.75);
}
.step-indicator span {
display: none;
}
.review-item {
grid-template-columns: 1fr;
gap: 3px;
}
.review-label {
font-weight: 600;
}
.review-section h3 {
font-size: ${fontSize};
}
.review-label {
font-size: calc(${fontSize} * 0.875);
}
.review-value {
font-size: calc(${fontSize} * 0.875);
}
}
@media (max-width: 480px) {
:host {
padding: 4px;
max-width: 480px;
}
.form-container {
padding: 12px;
}
.form-header h1 {
font-size: calc(${fontSize} * 1.125);
}
.form-header p {
font-size: calc(${fontSize} * 0.75);
}
fieldset {
padding: 8px;
}
legend {
font-size: calc(${fontSize} * 0.875);
}
.section-subtitle {
font-size: calc(${fontSize} * 0.75);
}
.form-group label {
font-size: calc(${fontSize} * 0.75);
}
.address-row {
grid-template-columns: 1fr;
}
.step-indicators {
font-size: calc(${fontSize} * 0.625);
}
.btn {
font-size: calc(${fontSize} * 0.875);
padding: 8px 13px;
min-width: 80px;
}
.review-section h3 {
font-size: calc(${fontSize} * 0.875);
}
.review-label {
font-size: calc(${fontSize} * 0.75);
}
.review-value {
font-size: calc(${fontSize} * 0.75);
}
.edit-step-btn {
font-size: calc(${fontSize} * 0.75);
padding: 3px 6px;
}
}
`;
}
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
result[3],
16
)}`
: "0, 0, 0";
}
updateStyles() {
const styleTag = this.shadowRoot.querySelector("style");
if (styleTag) {
styleTag.textContent = this.styles;
}
}
render() {
if (!this.shadowRoot.querySelector("style")) {
const styleTag = document.createElement("style");
this.shadowRoot.appendChild(styleTag);
}
this.shadowRoot.innerHTML = `
${this.shadowRoot.querySelector("style").outerHTML}
<div class="form-container">
<div class="form-header">
<h1 id="form-title">${
this.getAttribute("form-title") || "Web Development Inquiry"
}</h1>
</div>
<div class="progress-section" role="navigation" aria-label="Form progress">
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="Form completion progress">
<div class="progress-fill"></div>
</div>
<div class="step-indicators">
<div class="step-indicator active" data-step="0" aria-current="step">
<div class="step-dot" aria-hidden="true">1</div>
<span>Personal</span>
</div>
<div class="step-indicator" data-step="1">
<div class="step-dot" aria-hidden="true">2</div>
<span>Business</span>
</div>
<div class="step-indicator" data-step="2">
<div class="step-dot" aria-hidden="true">3</div>
<span>Address</span>
</div>
<div class="step-indicator" data-step="3">
<div class="step-dot" aria-hidden="true">4</div>
<span>Service</span>
</div>
<div class="step-indicator" data-step="4">
<div class="step-dot" aria-hidden="true">5</div>
<span>Review</span>
</div>
</div>
</div>
<form id="inquiry-form" role="form" aria-labelledby="form-title">
<!-- Step 1: Personal Information -->
<div class="section active" data-step="0" role="group" aria-labelledby="step1-legend">
<fieldset>
<legend id="step1-legend">Personal Information</legend>
<p class="section-subtitle">Tell us about yourself!</p>
<div class="address-row">
<div class="form-group">
<label for="firstName" id="firstName-label" class="required">First Name</label>
<input type="text" id="firstName" name="firstName" required aria-required="true" aria-invalid="false" aria-describedby="firstName-error" />
<div id="firstName-error" role="alert" aria-live="assertive"></div>
</div>
<div class="form-group">
<label for="lastName" id="lastName-label" class="required">Last Name</label>
<input type="text" id="lastName" name="lastName" required aria-required="true" aria-invalid="false" aria-describedby="lastName-error" />
<div id="lastName-error" role="alert" aria-live="assertive"></div>
</div>
</div>
<div class="form-group">
<label for="email" id="email-label" class="required">Email Address</label>
<input type="email" id="email" name="email" required aria-required="true" aria-invalid="false" aria-describedby="email-error" />
<div id="email-error" role="alert" aria-live="assertive"></div>
</div>
<div class="form-group">
<label for="phone" id="phone-label" class="required">Phone Number</label>
<input type="tel" id="phone" name="phone" required aria-required="true" aria-invalid="false" aria-describedby="phone-error" />
<div id="phone-error" role="alert" aria-live="assertive"></div>
<div class="extension-option">
<div class="checkbox-wrapper">
<input type="checkbox" id="phoneExtCheck" name="phoneExtCheck" aria-label="Add phone extension" />
<label for="phoneExtCheck">Add Extension</label>
</div>
<div class="conditional-field" id="phoneExtField" aria-hidden="true">
<div class="form-group">
<label for="phoneExt" id="phoneExt-label">Extension</label>
<input type="text" id="phoneExt" name="phoneExt" aria-required="false" aria-invalid="false" aria-describedby="phoneExt-error" />
<div id="phoneExt-error" role="alert" aria-live="assertive"></div>
</div>
</div>
</div>
</div>
</fieldset>
</div>
<!-- Step 2: Business Information (ADDED) -->
<div class="section" data-step="1" role="group" aria-labelledby="step2-legend">
<fieldset>
<legend id="step2-legend">Business Information</legend>
<p class="section-subtitle">Tell us about your business.</p>
<div class="form-group">
<label for="businessName" id="businessName-label">Business Name</label>
<input type="text" id="businessName" name="businessName" aria-required="false" aria-invalid="false" aria-describedby="businessName-error" />
<div id="businessName-error" role="alert" aria-live="assertive"></div>
</div>
<div class="form-group">
<label for="businessPhone" id="businessPhone-label">Business Phone Number</label>
<input type="tel" id="businessPhone" name="businessPhone" aria-required="false" aria-invalid="false" aria-describedby="businessPhone-error" />
<div id="businessPhone-error" role="alert" aria-live="assertive"></div>
<div class="extension-option">
<div class="checkbox-wrapper">
<input type="checkbox" id="businessPhoneExtCheck" name="businessPhoneExtCheck" aria-label="Add business phone extension" />
<label for="businessPhoneExtCheck">Add Extension</label>
</div>
<div class="conditional-field" id="businessPhoneExtField" aria-hidden="true">
<div class="form-group">
<label for="businessPhoneExt" id="businessPhoneExt-label">Extension</label>
<input type="text" id="businessPhoneExt" name="businessPhoneExt" aria-required="false" aria-invalid="false" aria-describedby="businessPhoneExt-error" />
<div id="businessPhoneExt-error" role="alert" aria-live="assertive"></div>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="businessEmail" id="businessEmail-label">Business Email</label>
<input type="email" id="businessEmail" name="businessEmail" aria-required="false" aria-invalid="false" aria-describedby="businessEmail-error" />
<div id="businessEmail-error" role="alert" aria-live="assertive"></div>
</div>
<div class="form-group">
<label for="businessServices" id="businessServices-label">Type of Business/Services</label>
<textarea id="businessServices" name="businessServices" placeholder="e.g., Web Design, Marketing, Consulting" aria-required="false" aria-invalid="false" aria-describedby="businessServices-error"></textarea>
<div id="businessServices-error" role="alert" aria-live="assertive"></div>
</div>
</fieldset>
</div>
<!-- Step 3: Mailing Address -->
<div class="section" data-step="2" role="group" aria-labelledby="step3-legend">
<fieldset>
<legend id="step3-legend">Mailing Address</legend>
<p class="section-subtitle">What is your mailing address?</p>
<div class="form-group">
<label for="billingStreet" id="billingStreet-label" class="required">Street Address</label>
<input type="text" id="billingStreet" name="billingStreet" required aria-required="true" aria-invalid="false" aria-describedby="billingStreet-error" />
<div id="billingStreet-error" role="alert" aria-live="assertive"></div>
</div>
<div class="address-row">
<div class="form-group">
<label for="billingAptUnit" id="billingAptUnit-label">Apt/Unit</label>
<input type="text" id="billingAptUnit" name="billingAptUnit" aria-required="false" aria-invalid="false" aria-describedby="billingAptUnit-error" />
<div id="billingAptUnit-error" role="alert" aria-live="assertive"></div>
</div>
<div class="form-group">
<label for="billingCity" id="billingCity-label" class="required">City</label>
<input type="text" id="billingCity" name="billingCity" required aria-required="true" aria-invalid="false" aria-describedby="billingCity-error" />
<div id="billingCity-error" role="alert" aria-live="assertive"></div>
</div>
</div>
<div class="address-row">
<div class="form-group">
<label for="billingState" id="billingState-label" class="required">State/Province</label>
<input type="text" id="billingState" name="billingState" required aria-required="true" aria-invalid="false" aria-describedby="billingState-error" />
<div id="billingState-error" role="alert" aria-live="assertive"></div>
</div>
<div class="form-group">
<label for="billingZipCode" id="billingZipCode-label" class="required">ZIP/Postal Code</label>
<input type="text" id="billingZipCode" name="billingZipCode" required aria-required="true" aria-invalid="false" aria-describedby="billingZipCode-error" />
<div id="billingZipCode-error" role="alert" aria-live="assertive"></div>
</div>
</div>
<div class="form-group">
<label for="billingCountry" id="billingCountry-label" class="required">Country</label>
<input type="text" id="billingCountry" name="billingCountry" value="USA" required aria-required="true" aria-invalid="false" aria-describedby="billingCountry-error" />
<div id="billingCountry-error" role="alert" aria-live="assertive"></div>
</div>
</fieldset>
</div>
<!-- Step 4: Service Details -->
<div class="section" data-step="3" role="group" aria-labelledby="step4-legend">
<fieldset>
<legend id="step4-legend">Service Details</legend>
<p class="section-subtitle">What can we help you with?</p>
<div class="form-group">
<label id="preferredContact-label" class="required">Preferred Contact Method</label>
<div class="radio-group" role="radiogroup" aria-labelledby="preferredContact-label" aria-required="true" aria-describedby="preferredContact-error">
<div class="radio-option">
<input type="radio" id="contactPhone" name="preferredContact" value="phone" required aria-required="true" />
<label for="contactPhone">Phone</label>
</div>
<div class="radio-option">
<input type="radio" id="contactEmail" name="preferredContact" value="email" />
<label for="contactEmail">Email</label>
</div>
<div class="radio-option">
<input type="radio" id="contactText" name="preferredContact" value="text" />
<label for="contactText">Text</label>
</div>
<div class="radio-option">
<input type="radio" id="contactBusinessPhone" name="preferredContact" value="businessPhone" />
<label for="contactBusinessPhone">Business Phone</label>
</div>
<div class="radio-option">
<input type="radio" id="contactBusinessEmail" name="preferredContact" value="businessEmail" />
<label for="contactBusinessEmail">Business Email</label>
</div>
</div>
<div id="preferredContact-error" role="alert" aria-live="assertive"></div>
</div>
<div class="form-group">
<label id="serviceDesired-label" class="required">Service Desired</label>
<div class="radio-group" role="radiogroup" aria-labelledby="serviceDesired-label" aria-required="true" aria-describedby="serviceDesired-error">
<div class="radio-option">
<input type="radio" id="serviceWebsite" name="serviceDesired" value="Web Development" required aria-required="true" />
<label for="serviceWebsite">Website</label>
</div>
<div class="radio-option">
<input type="radio" id="serviceApp" name="serviceDesired" value="App Development" />
<label for="serviceApp">App Development</label>
</div>
</div>
<div id="serviceDesired-error" role="alert" aria-live="assertive"></div>
</div>
<div class="form-group">
<label id="hasWebsite-label">Do you currently have a website?</label>
<div class="radio-group" role="radiogroup" aria-labelledby="hasWebsite-label" aria-required="false" aria-describedby="hasWebsite-error">
<div class="radio-option">
<input type="radio" id="websiteYes" name="hasWebsite" value="yes" />
<label for="websiteYes">Yes</label>
</div>
<div class="radio-option">
<input type="radio" id="websiteNo" name="hasWebsite" value="no" />
<label for="websiteNo">No</label>
</div>
</div>
<div id="hasWebsite-error" role="alert" aria-live="assertive"></div>
<div class="conditional-field" id="websiteAddressField" aria-hidden="true">
<div class="form-group">
<label for="websiteAddress" id="websiteAddress-label">Website Address</label>
<input type="text" id="websiteAddress" name="websiteAddress" placeholder="example.com" aria-required="false" aria-invalid="false" aria-describedby="websiteAddress-error" />
<div id="websiteAddress-error" role="alert" aria-live="assertive"></div>
</div>
</div>
</div>
<div class="form-group">
<label for="message" id="message-label">Message</label>
<textarea id="message" name="message" placeholder="Your message..." aria-required="false" aria-invalid="false" aria-describedby="message-error"></textarea>
<div id="message-error" role="alert" aria-live="assertive"></div>
</div>
</fieldset>
</div>
<!-- Step 5: Review -->
<div class="section" data-step="4" role="group" aria-labelledby="step5-legend">
<fieldset>
<legend id="step5-legend">Review Your Information</legend>
<p class="section-subtitle">Please review your details before submitting</p>
<div class="review-container" id="reviewContainer" role="region" aria-live="polite">
<!-- Review content will be populated here -->
</div>
</fieldset>
</div>
<div class="navigation" role="navigation" aria-label="Form navigation">
<button type="button" class="btn btn-secondary" id="prevBtn" style="visibility: hidden;" aria-label="Go to previous step">Previous</button>
<button type="button" class="btn btn-primary" id="nextBtn" aria-label="Go to next step">Next</button>
<button type="submit" class="btn btn-primary" id="submitBtn" style="display: none;" aria-label="Submit inquiry form">Submit Inquiry</button>
</div>
</form>
</div>
`;
this.updateStyles();
}
initializeEvents() {
const form = this.shadowRoot.getElementById("inquiry-form");
const nextBtn = this.shadowRoot.getElementById("nextBtn");
const prevBtn = this.shadowRoot.getElementById("prevBtn");
const submitBtn = this.shadowRoot.getElementById("submitBtn");
// Initialize phone formatting
this.loadCleavejs().then(() => {
this.initializePhoneFormatting();
});
// Initialize conditional fields
this.initializeConditionalFields();
// Initialize validation
this.initializeValidation();
// Navigation events
nextBtn.addEventListener("click", () => this.nextStep());
prevBtn.addEventListener("click", () => this.prevStep());
// Prevent premature form submission on Enter key
form.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
// If we're on the last step (review), allow submission
if (this.currentStep === this.totalSteps - 1) {
// Call handleFormSubmit directly - NO untrusted events
this.handleFormSubmit(e);
} else {
// Otherwise, try to go to next step
this.nextStep();
}
}
});
// Form submission - IMPORTANT: Remove the form submit event listener entirely for Firefox
const isFirefox = navigator.userAgent.toLowerCase().includes("firefox");
console.log(
"Browser detected:",
isFirefox ? "Firefox" : "Other",
navigator.userAgent
);
if (!isFirefox) {
// Only add form submit listener for non-Firefox browsers
form.addEventListener("submit", this.handleFormSubmit.bind(this));
}
// Submit button - always call directly
submitBtn.addEventListener("click", (e) => {
e.preventDefault();
console.log("Submit button clicked, calling handleFormSubmit directly");
this.handleFormSubmit(e);
});
// Validate current step on input
form.addEventListener("input", (e) => {
// Clear validation errors as user types
if (e.target.classList.contains("invalid")) {
this.removeError(e.target);
}
this.validateCurrentStep();
});
form.addEventListener("change", (e) => {
// Clear validation errors when radio buttons are selected
if (e.target.type === "radio" && e.target.classList.contains("invalid")) {
const radioName = e.target.name;
const allRadiosInGroup = form.querySelectorAll(`input[name="${radioName}"]`);
// Clear invalid state from all radios in group
allRadiosInGroup.forEach(radio => {
radio.classList.remove("invalid");
radio.setAttribute('aria-invalid', 'false');
});
// Clear the error message for this radio group
const errorId = `${radioName}-error`;
const errorDiv = this.shadowRoot.getElementById(errorId);
if (errorDiv) {
errorDiv.textContent = '';
}
}
this.validateCurrentStep();
});
}
loadCleavejs() {
return new Promise((resolve, reject) => {
if (window.Cleave) {
resolve();
return;
}
const script = document.createElement("script");
script.src =
"https://cdn.jsdelivr.net/npm/cleave.js@1.6.0/dist/cleave.min.js";
script.onload = () => resolve();
script.onerror = () => reject(new Error("Failed to load Cleave.js"));
document.head.appendChild(script);
});
}
initializePhoneFormatting() {
const phoneInputs = this.shadowRoot.querySelectorAll('input[type="tel"]');
if (window.Cleave) {
phoneInputs.forEach((input) => {
new window.Cleave(input, {
numericOnly: true,
blocks: [3, 3, 4],
delimiters: ["-", "-"],
});
});
}
}
initializeConditionalFields() {
// Phone extension
const phoneExtCheck = this.shadowRoot.getElementById("phoneExtCheck");
const phoneExtField = this.shadowRoot.getElementById("phoneExtField");
phoneExtCheck.addEventListener("change", function () {
if (this.checked) {
phoneExtField.classList.add("show");
phoneExtField.setAttribute('aria-hidden', 'false');
} else {
phoneExtField.classList.remove("show");
phoneExtField.setAttribute('aria-hidden', 'true');
phoneExtField.querySelector("input").value = "";
}
});
// Business phone extension
const businessPhoneExtCheck = this.shadowRoot.getElementById(
"businessPhoneExtCheck"
);
const businessPhoneExtField = this.shadowRoot.getElementById(
"businessPhoneExtField"
);
businessPhoneExtCheck.addEventListener("change", function () {
if (this.checked) {
businessPhoneExtField.classList.add("show");
businessPhoneExtField.setAttribute('aria-hidden', 'false');
} else {
businessPhoneExtField.classList.remove("show");
businessPhoneExtField.setAttribute('aria-hidden', 'true');
businessPhoneExtField.querySelector("input").value = "";
}
});
// Website address
const websiteYes = this.shadowRoot.getElementById("websiteYes");
const websiteNo = this.shadowRoot.getElementById("websiteNo");
const websiteAddressField = this.shadowRoot.getElementById(
"websiteAddressField"
);
websiteYes.addEventListener("change", function () {
if (this.checked) {
websiteAddressField.classList.add("show");
websiteAddressField.setAttribute('aria-hidden', 'false');
}
});
websiteNo.addEventListener("change", function () {
if (this.checked) {
websiteAddressField.classLis