(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";
}
});
})();