249 lines
8.0 KiB
JavaScript
249 lines
8.0 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
const config = window.itstudioJoinOtp;
|
|
if (!config) {
|
|
return;
|
|
}
|
|
|
|
const OTP_COOLDOWN_SECONDS = 60;
|
|
|
|
function getLanguage() {
|
|
return document.documentElement.lang === 'en' ? 'en' : 'cn';
|
|
}
|
|
|
|
function getMessage(payload, fallback) {
|
|
const key = getLanguage() === 'en' ? 'message_en' : 'message_cn';
|
|
return payload && payload[key] ? payload[key] : fallback;
|
|
}
|
|
|
|
function findField(form, fieldId) {
|
|
return form.querySelector('[name="item_meta[' + fieldId + ']"]');
|
|
}
|
|
|
|
function findTargetForm() {
|
|
const candidates = document.querySelectorAll('.join-form-content .frm_forms form');
|
|
for (const form of candidates) {
|
|
const formIdInput = form.querySelector('input[name="form_id"]');
|
|
if (!formIdInput || Number(formIdInput.value) === Number(config.formId)) {
|
|
if (findField(form, config.emailFieldId) && findField(form, config.codeFieldId)) {
|
|
return form;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function setStatus(status, message, tone) {
|
|
if (!message) {
|
|
status.textContent = '';
|
|
status.className = 'itstudio-otp-status';
|
|
status.style.display = 'none';
|
|
return;
|
|
}
|
|
status.style.display = 'flex';
|
|
status.textContent = message;
|
|
status.className = 'itstudio-otp-status ' + (tone === 'error' ? 'frm_error_style' : 'frm_message');
|
|
}
|
|
|
|
function startCooldown(button, seconds) {
|
|
let remaining = Math.max(1, Number(seconds) || OTP_COOLDOWN_SECONDS);
|
|
if (button.itstudioOtpTimer) {
|
|
window.clearInterval(button.itstudioOtpTimer);
|
|
}
|
|
|
|
button.disabled = true;
|
|
button.dataset.otpLocked = '1';
|
|
button.setAttribute('aria-disabled', 'true');
|
|
button.classList.add('is-countdown');
|
|
|
|
const update = function () {
|
|
button.textContent = String(config.labels.resend || '重新发送 (%s秒)').replace('%s', remaining);
|
|
remaining -= 1;
|
|
if (remaining < 0) {
|
|
window.clearInterval(button.itstudioOtpTimer);
|
|
button.itstudioOtpTimer = null;
|
|
button.dataset.otpLocked = '0';
|
|
button.setAttribute('aria-disabled', 'false');
|
|
button.classList.remove('is-countdown');
|
|
button.textContent = config.labels.send || '发送验证码';
|
|
button.disabled = false;
|
|
}
|
|
};
|
|
|
|
update();
|
|
button.itstudioOtpTimer = window.setInterval(update, 1000);
|
|
}
|
|
|
|
async function readAjaxResult(response) {
|
|
const raw = await response.text();
|
|
const normalized = raw.replace(/^\uFEFF/, '').trim();
|
|
if (!normalized) {
|
|
throw new Error('Empty AJAX response');
|
|
}
|
|
|
|
const result = JSON.parse(normalized);
|
|
if (result && typeof result === 'object' && typeof result.success !== 'undefined') {
|
|
return {
|
|
success: result.success === true || result.success === 1 || result.success === '1',
|
|
payload: result.data && typeof result.data === 'object' ? result.data : {}
|
|
};
|
|
}
|
|
|
|
if (result && typeof result === 'object' && (result.message_cn || result.message_en)) {
|
|
return {
|
|
success: true,
|
|
payload: result
|
|
};
|
|
}
|
|
|
|
throw new Error('Unexpected AJAX response');
|
|
}
|
|
|
|
async function sendCode(form, button, status) {
|
|
if (button.dataset.otpLocked === '1' || button.disabled) {
|
|
return;
|
|
}
|
|
|
|
const emailInput = findField(form, config.emailFieldId);
|
|
const email = emailInput ? emailInput.value.trim() : '';
|
|
if (!email) {
|
|
setStatus(status, config.labels.emailRequired || '请先填写邮箱地址。', 'error');
|
|
emailInput?.focus();
|
|
return;
|
|
}
|
|
if (emailInput && !emailInput.checkValidity()) {
|
|
setStatus(status, config.labels.invalidEmail || '请输入有效的邮箱地址。', 'error');
|
|
emailInput.focus();
|
|
return;
|
|
}
|
|
|
|
// Block rapid duplicate clicks while the request is in flight.
|
|
button.disabled = true;
|
|
button.dataset.otpLocked = '1';
|
|
button.setAttribute('aria-disabled', 'true');
|
|
button.classList.add('is-loading');
|
|
button.textContent = config.labels.sending || '发送中…';
|
|
setStatus(status, config.labels.sending || '正在发送验证码…', 'info');
|
|
|
|
try {
|
|
const response = await window.fetch(config.ajaxUrl, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
|
|
body: new URLSearchParams({
|
|
action: 'itstudio_join_otp_send',
|
|
email: email,
|
|
nonce: config.nonce
|
|
}).toString()
|
|
});
|
|
const result = await readAjaxResult(response);
|
|
const payload = result.payload || {};
|
|
button.classList.remove('is-loading');
|
|
|
|
if (!result.success) {
|
|
setStatus(status, getMessage(payload, config.labels.networkError || '发送失败,请稍后重试。'), 'error');
|
|
button.dataset.otpLocked = '0';
|
|
button.setAttribute('aria-disabled', 'false');
|
|
button.textContent = config.labels.send || '发送验证码';
|
|
button.disabled = false;
|
|
return;
|
|
}
|
|
|
|
setStatus(status, getMessage(payload, '验证码已发送,请检查邮箱及垃圾箱。'), 'success');
|
|
startCooldown(button, payload.cooldown || OTP_COOLDOWN_SECONDS);
|
|
} catch (error) {
|
|
button.classList.remove('is-loading');
|
|
setStatus(status, config.labels.networkError || '网络错误,请稍后重试。', 'error');
|
|
button.dataset.otpLocked = '0';
|
|
button.setAttribute('aria-disabled', 'false');
|
|
button.textContent = config.labels.send || '发送验证码';
|
|
button.disabled = false;
|
|
}
|
|
}
|
|
|
|
function mount() {
|
|
const form = findTargetForm();
|
|
if (!form || form.querySelector('.itstudio-otp-wrap')) {
|
|
return;
|
|
}
|
|
|
|
const codeInput = findField(form, config.codeFieldId);
|
|
if (!codeInput) {
|
|
return;
|
|
}
|
|
|
|
codeInput.setAttribute('inputmode', 'numeric');
|
|
codeInput.setAttribute('pattern', '[0-9]{6}');
|
|
codeInput.setAttribute('maxlength', '6');
|
|
codeInput.setAttribute('autocomplete', 'one-time-code');
|
|
if (!codeInput.getAttribute('placeholder')) {
|
|
codeInput.setAttribute('placeholder', '请输入 6 位验证码');
|
|
}
|
|
codeInput.classList.add('itstudio-otp-input');
|
|
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'itstudio-otp-wrap';
|
|
|
|
const group = document.createElement('div');
|
|
group.className = 'itstudio-otp-group';
|
|
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'itstudio-otp-send';
|
|
button.textContent = config.labels.send || '发送验证码';
|
|
button.dataset.otpLocked = '0';
|
|
button.setAttribute('aria-disabled', 'false');
|
|
|
|
const status = document.createElement('div');
|
|
status.className = 'itstudio-otp-status';
|
|
status.setAttribute('role', 'status');
|
|
status.setAttribute('aria-live', 'polite');
|
|
status.style.display = 'none';
|
|
|
|
if (codeInput.parentNode) {
|
|
codeInput.parentNode.insertBefore(wrapper, codeInput);
|
|
group.appendChild(codeInput);
|
|
group.appendChild(button);
|
|
wrapper.appendChild(group);
|
|
wrapper.appendChild(status);
|
|
}
|
|
|
|
button.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
sendCode(form, button, status);
|
|
});
|
|
}
|
|
|
|
let mountQueued = false;
|
|
const scheduleMount = function () {
|
|
if (mountQueued) {
|
|
return;
|
|
}
|
|
|
|
mountQueued = true;
|
|
window.requestAnimationFrame(function () {
|
|
mountQueued = false;
|
|
mount();
|
|
});
|
|
};
|
|
|
|
mount();
|
|
const observerRoot = document.querySelector('.join-form-content') || document.body;
|
|
const observer = new MutationObserver(function (mutations) {
|
|
const formStructureChanged = mutations.some(function (mutation) {
|
|
return Array.from(mutation.addedNodes).concat(Array.from(mutation.removedNodes)).some(function (node) {
|
|
return node.nodeType === 1 && (
|
|
node.matches('.frm_forms, form') ||
|
|
Boolean(node.querySelector && node.querySelector('.frm_forms, form'))
|
|
);
|
|
});
|
|
});
|
|
|
|
if (formStructureChanged) {
|
|
scheduleMount();
|
|
}
|
|
});
|
|
observer.observe(observerRoot, {childList: true, subtree: true});
|
|
})();
|