UNPKG

@devmansam/forms

Version:

Professional, customizable web form components for contact and inquiry forms

1,560 lines (1,329 loc) 70 kB
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