(function () { // 防止重复初始化 if (window.contactusFormInitialized) { return; } window.contactusFormInitialized = true; // 国家选择器样式(注入一次) if (!document.getElementById("cp-style")) { const st = document.createElement("style"); st.id = "cp-style"; st.textContent = [ ".country-picker{position:relative;font-size:14px}", ".country-picker .cp-display{display:flex;align-items:center;gap:6px;padding:10px;border:1px solid #d1d5db;border-radius:6px;background:#fff;color:#333;cursor:pointer}", ".country-picker .cp-search{width:100%;box-sizing:border-box;margin-top:4px;padding:8px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:14px;outline:none}", ".country-picker .cp-list{display:none;position:absolute;z-index:99;top:100%;left:0;width:260px;max-height:220px;overflow-y:auto;margin-top:4px;background:#fff;border:1px solid #d1d5db;border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.12)}", ".country-picker .cp-list.cp-open{display:block}", ".country-picker .cp-opt{display:flex;align-items:center;gap:8px;padding:7px 10px;cursor:pointer}", ".country-picker .cp-opt:hover{background:#f3f4f6}", ".cp-caret{color:#9ca3af;font-size:12px}" ].join(""); (document.head || document.documentElement).appendChild(st); } const forms = document.querySelectorAll(".contactus-form"); if (!forms.length) { return; } const DEFAULT_MESSAGES = { pending: "Sending your request…", success: "Thank you! Our engineering team will contact you within 24 hours.", error: "Send failed, please try again later." }; const ensureStatusPlacement = (form, node) => { const submitButton = form.querySelector("[type='submit']"); if (submitButton) { if (submitButton.nextElementSibling !== node) { submitButton.insertAdjacentElement("afterend", node); } return; } if (!node.parentElement) { form.appendChild(node); } }; const getStatusNode = (form) => { let node = form.querySelector(".contactus-status"); if (!node) { node = document.createElement("p"); node.className = "contactus-status"; node.setAttribute("role", "status"); node.setAttribute("aria-live", "polite"); } ensureStatusPlacement(form, node); return node; }; const setStatus = (node, state, message) => { if (!node) return; node.dataset.state = state; node.textContent = message || DEFAULT_MESSAGES[state] || ""; }; const extractMessage = (payload, fallback) => { if (!payload || typeof payload !== "object") { return fallback; } return payload.msg || payload.message || fallback; }; // 全世界国家电话代码 + ISO 国旗代码(默认 +1 美国) const COUNTRY_CODES = [ ["+93", "Afghanistan", "af"], ["+355", "Albania", "al"], ["+213", "Algeria", "dz"], ["+376", "Andorra", "ad"], ["+244", "Angola", "ao"], ["+54", "Argentina", "ar"], ["+374", "Armenia", "am"], ["+61", "Australia", "au"], ["+43", "Austria", "at"], ["+994", "Azerbaijan", "az"], ["+973", "Bahrain", "bh"], ["+880", "Bangladesh", "bd"], ["+375", "Belarus", "by"], ["+32", "Belgium", "be"], ["+501", "Belize", "bz"], ["+229", "Benin", "bj"], ["+975", "Bhutan", "bt"], ["+591", "Bolivia", "bo"], ["+387", "Bosnia and Herzegovina", "ba"], ["+267", "Botswana", "bw"], ["+55", "Brazil", "br"], ["+673", "Brunei", "bn"], ["+359", "Bulgaria", "bg"], ["+226", "Burkina Faso", "bf"], ["+257", "Burundi", "bi"], ["+855", "Cambodia", "kh"], ["+237", "Cameroon", "cm"], ["+1", "Canada", "ca"], ["+238", "Cape Verde", "cv"], ["+236", "Central African Republic", "cf"], ["+235", "Chad", "td"], ["+56", "Chile", "cl"], ["+86", "China", "cn"], ["+57", "Colombia", "co"], ["+269", "Comoros", "km"], ["+242", "Congo", "cg"], ["+243", "Congo (DRC)", "cd"], ["+506", "Costa Rica", "cr"], ["+225", "Cote d'Ivoire", "ci"], ["+385", "Croatia", "hr"], ["+53", "Cuba", "cu"], ["+357", "Cyprus", "cy"], ["+420", "Czech Republic", "cz"], ["+45", "Denmark", "dk"], ["+253", "Djibouti", "dj"], ["+593", "Ecuador", "ec"], ["+20", "Egypt", "eg"], ["+503", "El Salvador", "sv"], ["+240", "Equatorial Guinea", "gq"], ["+291", "Eritrea", "er"], ["+372", "Estonia", "ee"], ["+268", "Eswatini", "sz"], ["+251", "Ethiopia", "et"], ["+679", "Fiji", "fj"], ["+358", "Finland", "fi"], ["+33", "France", "fr"], ["+241", "Gabon", "ga"], ["+220", "Gambia", "gm"], ["+995", "Georgia", "ge"], ["+49", "Germany", "de"], ["+233", "Ghana", "gh"], ["+30", "Greece", "gr"], ["+502", "Guatemala", "gt"], ["+224", "Guinea", "gn"], ["+245", "Guinea-Bissau", "gw"], ["+592", "Guyana", "gy"], ["+509", "Haiti", "ht"], ["+504", "Honduras", "hn"], ["+852", "Hong Kong", "hk"], ["+36", "Hungary", "hu"], ["+354", "Iceland", "is"], ["+91", "India", "in"], ["+62", "Indonesia", "id"], ["+98", "Iran", "ir"], ["+964", "Iraq", "iq"], ["+353", "Ireland", "ie"], ["+972", "Israel", "il"], ["+39", "Italy", "it"], ["+81", "Japan", "jp"], ["+962", "Jordan", "jo"], ["+7", "Kazakhstan", "kz"], ["+254", "Kenya", "ke"], ["+686", "Kiribati", "ki"], ["+850", "Korea (North)", "kp"], ["+82", "Korea (South)", "kr"], ["+965", "Kuwait", "kw"], ["+996", "Kyrgyzstan", "kg"], ["+856", "Laos", "la"], ["+371", "Latvia", "lv"], ["+961", "Lebanon", "lb"], ["+266", "Lesotho", "ls"], ["+231", "Liberia", "lr"], ["+218", "Libya", "ly"], ["+423", "Liechtenstein", "li"], ["+370", "Lithuania", "lt"], ["+352", "Luxembourg", "lu"], ["+853", "Macau", "mo"], ["+261", "Madagascar", "mg"], ["+265", "Malawi", "mw"], ["+60", "Malaysia", "my"], ["+960", "Maldives", "mv"], ["+223", "Mali", "ml"], ["+356", "Malta", "mt"], ["+692", "Marshall Islands", "mh"], ["+222", "Mauritania", "mr"], ["+230", "Mauritius", "mu"], ["+52", "Mexico", "mx"], ["+691", "Micronesia", "fm"], ["+373", "Moldova", "md"], ["+377", "Monaco", "mc"], ["+976", "Mongolia", "mn"], ["+382", "Montenegro", "me"], ["+212", "Morocco", "ma"], ["+258", "Mozambique", "mz"], ["+95", "Myanmar", "mm"], ["+264", "Namibia", "na"], ["+674", "Nauru", "nr"], ["+977", "Nepal", "np"], ["+31", "Netherlands", "nl"], ["+64", "New Zealand", "nz"], ["+505", "Nicaragua", "ni"], ["+227", "Niger", "ne"], ["+234", "Nigeria", "ng"], ["+389", "North Macedonia", "mk"], ["+47", "Norway", "no"], ["+968", "Oman", "om"], ["+92", "Pakistan", "pk"], ["+680", "Palau", "pw"], ["+507", "Panama", "pa"], ["+675", "Papua New Guinea", "pg"], ["+595", "Paraguay", "py"], ["+51", "Peru", "pe"], ["+63", "Philippines", "ph"], ["+48", "Poland", "pl"], ["+351", "Portugal", "pt"], ["+974", "Qatar", "qa"], ["+40", "Romania", "ro"], ["+7", "Russia", "ru"], ["+250", "Rwanda", "rw"], ["+685", "Samoa", "ws"], ["+378", "San Marino", "sm"], ["+239", "Sao Tome and Principe", "st"], ["+966", "Saudi Arabia", "sa"], ["+221", "Senegal", "sn"], ["+381", "Serbia", "rs"], ["+248", "Seychelles", "sc"], ["+232", "Sierra Leone", "sl"], ["+65", "Singapore", "sg"], ["+421", "Slovakia", "sk"], ["+386", "Slovenia", "si"], ["+677", "Solomon Islands", "sb"], ["+252", "Somalia", "so"], ["+27", "South Africa", "za"], ["+211", "South Sudan", "ss"], ["+34", "Spain", "es"], ["+94", "Sri Lanka", "lk"], ["+249", "Sudan", "sd"], ["+597", "Suriname", "sr"], ["+46", "Sweden", "se"], ["+41", "Switzerland", "ch"], ["+963", "Syria", "sy"], ["+886", "Taiwan", "tw"], ["+992", "Tajikistan", "tj"], ["+255", "Tanzania", "tz"], ["+66", "Thailand", "th"], ["+670", "Timor-Leste", "tl"], ["+228", "Togo", "tg"], ["+676", "Tonga", "to"], ["+1", "Trinidad and Tobago", "tt"], ["+216", "Tunisia", "tn"], ["+90", "Turkey", "tr"], ["+993", "Turkmenistan", "tm"], ["+256", "Uganda", "ug"], ["+380", "Ukraine", "ua"], ["+971", "United Arab Emirates", "ae"], ["+44", "United Kingdom", "gb"], ["+1", "United States", "us"], ["+598", "Uruguay", "uy"], ["+998", "Uzbekistan", "uz"], ["+678", "Vanuatu", "vu"], ["+58", "Venezuela", "ve"], ["+84", "Vietnam", "vn"], ["+967", "Yemen", "ye"], ["+260", "Zambia", "zm"], ["+263", "Zimbabwe", "zw"] ]; // 可搜索国家选择器:搜索框 + 国旗 + 代码,选中后写入隐藏 input[name=phone_code] const buildCountryPicker = (form) => { const host = form.querySelector(".country-picker[data-name='phone_code']"); if (!host || host.dataset.built) return; host.dataset.built = "1"; const hidden = document.createElement("input"); hidden.type = "hidden"; hidden.name = "phone_code"; hidden.value = "+1"; host.appendChild(hidden); const display = document.createElement("div"); display.className = "cp-display"; display.innerHTML = ' +1 '; host.appendChild(display); const search = document.createElement("input"); search.type = "text"; search.className = "cp-search"; search.placeholder = "Search country / code"; search.autocomplete = "off"; host.appendChild(search); const list = document.createElement("div"); list.className = "cp-list"; host.appendChild(list); const renderList = (q) => { q = (q || "").toLowerCase().replace(/^\+/, ""); list.innerHTML = ""; COUNTRY_CODES.forEach(([code, name, a2]) => { if (q && name.toLowerCase().indexOf(q) === -1 && code.replace("+", "").indexOf(q) === -1) return; const row = document.createElement("div"); row.className = "cp-opt"; row.innerHTML = ' ' + name + ' ' + code + ''; row.addEventListener("mousedown", (ev) => { ev.preventDefault(); hidden.value = code; display.querySelector(".cp-code").textContent = code; display.querySelector("img").src = "https://flagcdn.com/w20/" + a2 + ".png"; list.classList.remove("cp-open"); search.value = ""; search.blur(); }); list.appendChild(row); }); }; display.addEventListener("click", () => { list.classList.toggle("cp-open"); if (list.classList.contains("cp-open")) { search.focus(); renderList(""); } }); search.addEventListener("input", () => renderList(search.value)); search.addEventListener("focus", () => { list.classList.add("cp-open"); renderList(search.value); }); document.addEventListener("click", (e) => { if (!host.contains(e.target)) list.classList.remove("cp-open"); }); renderList(""); }; const flagEmailIfNeeded = (form, message) => { if (!message) return; const normalized = message.toLowerCase(); if (!normalized.includes("email")) { return; } const emailField = form.querySelector("input[name='email']"); if (!emailField) { return; } emailField.setAttribute("aria-invalid", "true"); emailField.focus(); const clearInvalid = () => { emailField.removeAttribute("aria-invalid"); emailField.removeEventListener("input", clearInvalid); }; emailField.addEventListener("input", clearInvalid); }; const toggleButton = (button, disabled) => { if (!button) return; if (disabled) { // 保存原始文本(如果还没有保存) if (!button.dataset.originalText) { button.dataset.originalText = button.textContent; } // 也保存当前文本作为 previousText(用于兼容) button.dataset.previousText = button.textContent; // 从按钮的 data-submitting 属性读取多语言文本,如果没有则使用默认值 const submittingText = button.dataset.submitting || "Submitting..."; button.textContent = submittingText; button.setAttribute("disabled", "disabled"); } else { // 优先使用 previousText,如果没有则使用 originalText const restoreText = button.dataset.previousText || button.dataset.originalText; if (restoreText) { button.textContent = restoreText; } button.removeAttribute("disabled"); // 清除 previousText,但保留 originalText 以备下次使用 delete button.dataset.previousText; } }; const buildRequest = (form) => { const target = form.getAttribute("action") || "/Contactus"; const formData = new FormData(form); if (!formData.get("source") && form.dataset.contactusSource) { formData.set("source", form.dataset.contactusSource); } // 自动采集 page_url: 当前页面路径(含 query) if (!formData.get("page_url")) { formData.set("page_url", window.location.pathname + window.location.search); } // form_position: 优先 data-form-position,回退到 source if (!formData.get("form_position")) { formData.set("form_position", form.dataset.formPosition || formData.get("source") || "unknown"); } return { target, body: formData }; }; const handleSubmit = async (event) => { event.preventDefault(); event.stopPropagation(); // 防止事件冒泡导致重复提交 const form = event.currentTarget; const submitButton = form.querySelector("[type='submit']"); const statusNode = getStatusNode(form); // 如果按钮已经被禁用,说明正在提交中,直接返回 if (submitButton && submitButton.disabled) { return; } // ── 前端校验:电话(国家代码+号码,必填)与数量(必选)── const phoneCode = (form.querySelector("input[name='phone_code']") || {}).value || ""; const phoneNum = ((form.querySelector("input[name='phone']") || {}).value || "").trim(); if (!/^\+\d{1,4}$/.test(phoneCode)) { setStatus(statusNode, "error", "Please select your country code"); const picker = form.querySelector(".country-picker"); if (picker) picker.scrollIntoView({ block: "center" }); return; } if (!/^[\d\s\-()]{4,20}$/.test(phoneNum)) { setStatus(statusNode, "error", "Please enter a valid phone number"); const inp = form.querySelector("input[name='phone']"); if (inp) inp.focus(); return; } const qty = (form.querySelector("select[name='quantity']") || {}).value || ""; if (qty === "") { setStatus(statusNode, "error", "Please select the estimated order quantity"); const qs = form.querySelector("select[name='quantity']"); if (qs) qs.focus(); return; } // 保存按钮的原始文本 if (!submitButton.dataset.originalText) { submitButton.dataset.originalText = submitButton.textContent; } setStatus(statusNode, "pending"); toggleButton(submitButton, true); const { target, body } = buildRequest(form); try { const response = await fetch(target, { method: "POST", headers: { "X-Requested-With": "XMLHttpRequest" }, body }); let payload = null; try { payload = await response.json(); } catch (jsonError) { // 如果响应不是有效的 JSON,payload 保持为 null console.warn("Response is not valid JSON:", jsonError); } const serverMessage = extractMessage(payload, DEFAULT_MESSAGES.error); if (!response.ok || !payload) { throw new Error(serverMessage || "Unexpected server response."); } if (Number(payload.code) === 200) { setStatus(statusNode, "success", serverMessage || payload.msg); form.reset(); form.querySelectorAll("[aria-invalid='true']").forEach((input) => { input.removeAttribute("aria-invalid"); }); // Reset file name display if exists const fileInput = form.querySelector('input[type="file"]'); if (fileInput) { const fileNameDisplay = fileInput.closest('.form-file-upload')?.querySelector('.form-file-name') || fileInput.closest('.contact-modal__field')?.querySelector('.form-file-name'); if (fileNameDisplay) { const placeholder = fileInput.dataset.placeholder || fileNameDisplay.textContent; fileNameDisplay.textContent = placeholder; } } // GA4 转化追踪 if (typeof window.gtag === 'function') { window.gtag('event', 'generate_lead', { event_category: 'Inquiry', event_label: 'Form Submission Success' }); } // 跳转到 Thank You 页面 setTimeout(function () { window.location.href = '/thank-you'; }, 800); } else { setStatus(statusNode, "error", serverMessage); flagEmailIfNeeded(form, serverMessage); } } catch (error) { const fallbackMessage = error && error.message ? error.message : DEFAULT_MESSAGES.error; setStatus(statusNode, "error", fallbackMessage); flagEmailIfNeeded(form, fallbackMessage); } finally { // 确保在所有情况下都恢复按钮状态 toggleButton(submitButton, false); } }; // 为每个表单绑定事件监听器,但先移除可能存在的旧监听器 forms.forEach((form) => { buildCountryPicker(form); // 移除可能存在的旧事件监听器(通过克隆节点来移除所有事件监听器) // 或者使用 once 选项,但更好的方法是检查是否已经绑定 if (!form.dataset.contactusBound) { form.addEventListener("submit", handleSubmit); form.dataset.contactusBound = "true"; } }); })();