(function () { 'use strict'; if (window.__DBC_FORM_RUNTIME_V7301__) return; window.__DBC_FORM_RUNTIME_V7301__ = true; const CONTRACT_URL = 'data/form-contract-v7301.json'; const governedSelector = 'form[data-bioten-request-form]'; // Match the shared PHP phone rule after the same trimming as buildFields. const phoneValuePattern = /^\+?[0-9 ()-]{7,32}$/; // Escape punctuation for the native HTML pattern's Unicode-v grammar. const phoneInputPattern = String.raw`\s*(?:\+[0-9 \(\)\-]{7,31}|[0-9 \(\)\-]{7,32})\s*`; const state = new WeakMap(); let contractPromise; function loadContract() { if (!contractPromise) { contractPromise = fetch(CONTRACT_URL, { credentials: 'same-origin', cache: 'no-store' }) .then((response) => { if (!response.ok) throw new Error('contract_http_error'); return response.json(); }) .then((contract) => { if (!contract || contract.release !== 'V7.30.1' || !contract.forms || !contract.fieldRules) { throw new Error('contract_invalid'); } return contract; }); } return contractPromise; } function resolveForm(contract, formId, seen) { const visited = seen || new Set(); const current = contract.forms[formId]; if (!current || visited.has(formId)) throw new Error('form_contract_missing'); visited.add(formId); if (!current.inherits) return { ...current }; const base = resolveForm(contract, current.inherits, visited); return { ...base, ...current, fields: current.fields || base.fields, required: current.required || base.required, fixed: { ...(base.fixed || {}), ...(current.fixed || {}) }, multi: { ...(base.multi || {}), ...(current.multi || {}) }, enums: { ...(base.enums || {}), ...(current.enums || {}) }, labels: { ...(base.labels || {}), ...(current.labels || {}) }, atLeastOne: Object.prototype.hasOwnProperty.call(current, 'atLeastOne') ? current.atLeastOne : base.atLeastOne, conditional: Object.prototype.hasOwnProperty.call(current, 'conditional') ? current.conditional : base.conditional, }; } function language() { return document.documentElement.dataset.lang === 'en' || document.documentElement.lang === 'en' ? 'en' : 'zh'; } function ensureHidden(form, name, value) { let field = form.querySelector(`input[type="hidden"][name="${name}"]`); if (!field) { field = document.createElement('input'); field.type = 'hidden'; field.name = name; form.prepend(field); } field.value = value == null ? '' : String(value); return field; } function statusNode(form) { let node = form.querySelector('[data-request-status]'); if (!node) { node = document.createElement('p'); node.dataset.requestStatus = ''; const actions = form.querySelector('.request-submit, .portal-auth-form-actions, [class*="form-actions"]'); (actions || form).appendChild(node); } node.setAttribute('role', 'status'); node.setAttribute('aria-live', 'polite'); node.setAttribute('aria-atomic', 'true'); return node; } function resultNode(form) { let node = form.parentElement && form.parentElement.querySelector(':scope > [data-request-result]'); if (!node) node = document.querySelector('[data-request-result]'); if (!node) { node = document.createElement('aside'); node.dataset.requestResult = ''; form.insertAdjacentElement('afterend', node); } node.hidden = true; node.setAttribute('role', 'status'); node.setAttribute('aria-live', 'polite'); node.setAttribute('aria-atomic', 'true'); return node; } function decorateForm(form, contract, formId, formContract) { form.dataset.v7301FormGovernance = 'active'; form.setAttribute('novalidate', 'novalidate'); phoneControls(form, contract).forEach((control) => { if (control instanceof HTMLInputElement) control.setAttribute('pattern', phoneInputPattern); }); ensureHidden(form, 'form_id', formId); ensureHidden(form, 'source', formContract.source); ensureHidden(form, 'privacy_notice_version', contract.privacyNoticeVersion); const startedAt = ensureHidden(form, 'form_started_at', form.querySelector('[name="form_started_at"]')?.value || Date.now()); if (!startedAt.value) startedAt.value = String(Date.now()); Object.entries(formContract.fixed || {}).forEach(([key, value]) => ensureHidden(form, key, value)); if (!form.querySelector('[name="fax_number"]')) { const trap = document.createElement('div'); trap.hidden = true; trap.setAttribute('aria-hidden', 'true'); trap.dataset.formHoneypot = ''; trap.innerHTML = ''; form.prepend(trap); } statusNode(form); resultNode(form); form.querySelectorAll('button, input, select, textarea').forEach((control) => { if (control.matches('button, input[type="checkbox"], input[type="radio"], select')) { control.classList.add('v7301-touch-target'); } }); } function clearCustomValidity(form) { form.querySelectorAll('[data-v7301-contract-error]').forEach((control) => { control.setCustomValidity(''); delete control.dataset.v7301ContractError; }); } function phoneControls(form, contract) { return Array.from(form.elements).filter((control) => control && !control.disabled && (control.type === 'tel' || contract.fieldRules[control.name]?.type === 'phone')); } function controlsFor(form, name) { return Array.from(form.elements).filter((control) => control && control.name === name && !control.disabled); } function valuesFor(form, name) { const controls = controlsFor(form, name); if (!controls.length) return []; const checkables = controls.filter((control) => control.type === 'checkbox' || control.type === 'radio'); if (checkables.length) return checkables.filter((control) => control.checked).map((control) => String(control.value).trim()).filter(Boolean); return controls.map((control) => String(control.value || '').trim()).filter(Boolean); } function setContractError(form, name, zh, en, targetControl) { const control = targetControl || controlsFor(form, name)[0] || form.querySelector('[type="submit"]'); if (control && typeof control.setCustomValidity === 'function') { control.setCustomValidity(language() === 'en' ? en : zh); control.dataset.v7301ContractError = 'true'; control.focus({ preventScroll: true }); control.reportValidity(); } const status = statusNode(form); status.textContent = language() === 'en' ? en : zh; status.setAttribute('role', 'alert'); status.setAttribute('aria-live', 'assertive'); return false; } function validateContract(form, contract, formContract) { clearCustomValidity(form); for (const control of phoneControls(form, contract)) { const value = String(control.value || '').trim(); const maxLength = Number(contract.fieldRules[control.name]?.maxLength) || 32; if (value && (value.length > maxLength || !phoneValuePattern.test(value))) { const label = control.labels?.[0]?.textContent?.replace(/\s+/g, ' ').trim() || formContract.labels?.[control.name] || contract.fieldRules[control.name]?.label || control.name; return setContractError(form, control.name, `${label}:电话格式无效,请填写7–32个字符,可含数字、空格、括号、连字符和开头的+。`, `${label}: Enter a valid phone number (7–32 characters; digits, spaces, parentheses and hyphens, with an optional leading +).`, control); } } if (!form.checkValidity()) { form.reportValidity(); return false; } const required = new Set(['form_id', 'source', 'privacy_notice_version', 'form_started_at', 'privacy_consent', ...(formContract.required || [])]); if (formContract.requiresAgreement) required.add('agreement'); for (const name of required) { if (!valuesFor(form, name).length) { const label = formContract.labels?.[name] || contract.fieldRules[name]?.label || name; return setContractError(form, name, `请填写或确认:${label}`, `Complete or confirm the required field: ${label}`); } } for (const name of formContract.atLeastOne || []) { if (!valuesFor(form, name).length) { const label = contract.fieldRules[name]?.label || name; return setContractError(form, name, `请至少选择一项:${label}`, `Select at least one option: ${label}`); } } for (const condition of formContract.conditional || []) { if (valuesFor(form, condition.field).includes(condition.contains) && !valuesFor(form, condition.require).length) { const label = contract.fieldRules[condition.require]?.label || condition.require; return setContractError(form, condition.require, `选择“${condition.contains}”后请填写:${label}`, `After selecting "${condition.contains}", complete: ${label}`); } } if (formContract.combinations && formContract.combinations.length) { const matches = formContract.combinations.some((combination) => Object.entries(combination).every(([name, value]) => valuesFor(form, name)[0] === value)); if (!matches) return setContractError(form, 'sample_selection', '样品对象、样品层级与提供方组合无效,请重新选择。', 'The topic, sample level, and provider combination is invalid. Select the sample again.'); } return true; } function buildFields(form, formContract) { const allowed = new Set(formContract.fields || []); const data = new FormData(form); const fields = {}; for (const name of allowed) { const values = data.getAll(name).map((value) => String(value).trim()).filter(Boolean); if (Object.prototype.hasOwnProperty.call(formContract.multi || {}, name)) fields[name] = values; else fields[name] = values[0] || ''; } return fields; } function setBusy(form, busy) { form.dataset.v7301SubmitState = busy ? 'submitting' : 'idle'; form.querySelectorAll('button[type="submit"], input[type="submit"]').forEach((button) => { button.disabled = busy; if (busy) button.setAttribute('aria-busy', 'true'); else button.removeAttribute('aria-busy'); }); } function renderFailure(form, message) { const status = statusNode(form); const result = resultNode(form); status.textContent = message; status.setAttribute('role', 'alert'); status.setAttribute('aria-live', 'assertive'); result.replaceChildren(); result.className = 'request-result v7301-form-receipt is-error'; result.setAttribute('role', 'alert'); result.setAttribute('aria-live', 'assertive'); const heading = document.createElement('strong'); heading.textContent = language() === 'en' ? 'The form was not sent.' : '本次表单未发送。'; const copy = document.createElement('p'); copy.textContent = message; const link = document.createElement('a'); link.href = 'mailto:Bioten@dbc.cn'; link.textContent = language() === 'en' ? 'Use the published email channel' : '使用页面公布的邮箱联系'; result.append(heading, copy, link); result.hidden = false; } function ensureChallengeStyles() { if (document.getElementById('v7301-form-challenge-styles')) return; const style = document.createElement('style'); style.id = 'v7301-form-challenge-styles'; style.textContent = ` .v7301-form-receipt.is-challenge{display:grid;gap:.85rem;padding:1rem 1.1rem;border:1px solid rgba(34,92,58,.28);border-radius:12px;background:rgba(245,250,246,.96)} .v7301-form-challenge__label{display:grid;gap:.45rem;font-weight:650} .v7301-form-challenge__input{box-sizing:border-box;width:min(12rem,100%);min-height:44px;padding:.65rem .75rem;border:1px solid rgba(23,52,38,.35);border-radius:8px;background:#fff;color:inherit;font:inherit} .v7301-form-challenge__button{width:max-content;min-height:44px;padding:.65rem 1rem;border:0;border-radius:8px;background:#215b3b;color:#fff;font:inherit;font-weight:700;cursor:pointer} .v7301-form-challenge__button:focus-visible,.v7301-form-challenge__input:focus-visible{outline:3px solid rgba(42,126,79,.35);outline-offset:2px} `; document.head.append(style); } function renderChallenge(form, data) { ensureChallengeStyles(); const status = statusNode(form); const result = resultNode(form); const challenge = data && data.challenge ? data.challenge : {}; status.textContent = language() === 'en' ? 'A quick security check is required before this form can be sent.' : '发送前需要完成一次简短的安全验证。'; status.setAttribute('role', 'status'); status.setAttribute('aria-live', 'polite'); result.replaceChildren(); result.className = 'request-result v7301-form-receipt is-challenge'; result.setAttribute('role', 'group'); result.setAttribute('aria-live', 'polite'); const heading = document.createElement('strong'); heading.textContent = language() === 'en' ? 'Security verification' : '安全验证'; const copy = document.createElement('p'); copy.textContent = language() === 'en' ? 'This extra step appears only when a submission needs additional verification.' : '仅当提交需要进一步确认时才会出现此步骤。'; const label = document.createElement('label'); label.className = 'v7301-form-challenge__label'; label.textContent = language() === 'en' ? challenge.questionEn : challenge.questionZh; const input = document.createElement('input'); input.className = 'v7301-form-challenge__input'; input.type = 'text'; input.inputMode = 'numeric'; input.autocomplete = 'off'; input.pattern = '[0-9]+'; input.maxLength = 3; input.required = true; input.dataset.v7301ChallengeAnswer = ''; input.setAttribute('aria-label', language() === 'en' ? 'Security check answer' : '安全验证答案'); label.append(input); const button = document.createElement('button'); button.className = 'v7301-form-challenge__button'; button.type = 'button'; button.textContent = language() === 'en' ? 'Verify and send' : '验证并发送'; button.addEventListener('click', () => { if (!input.value.trim()) { input.focus({ preventScroll: true }); input.reportValidity(); return; } void submitForm(form); }); result.append(heading, copy, label, button); result.hidden = false; input.focus({ preventScroll: true }); } function renderSuccess(form, data, contract) { const status = statusNode(form); const result = resultNode(form); const confirmed = new Date(data.confirmedAt); const confirmedText = confirmed.toLocaleString(language() === 'en' ? 'en-US' : 'zh-CN', { hour12: false }); status.textContent = language() === 'en' ? `Server-confirmed send: ${confirmedText}.` : `服务器已确认发送:${confirmedText}。`; status.setAttribute('role', 'status'); status.setAttribute('aria-live', 'polite'); result.replaceChildren(); result.className = 'request-result v7301-form-receipt is-success'; result.setAttribute('role', 'status'); result.setAttribute('aria-live', 'polite'); const eyebrow = document.createElement('span'); eyebrow.className = 'v7301-form-receipt__eyebrow'; eyebrow.textContent = language() === 'en' ? 'SERVER-CONFIRMED FORM SEND' : '服务器确认回执'; const heading = document.createElement('strong'); heading.textContent = language() === 'en' ? 'The server confirmed and sent this form.' : '服务器已确认并发送本次表单。'; const id = document.createElement('code'); id.className = 'v7301-form-receipt__id'; id.textContent = data.requestId; id.setAttribute('aria-label', language() === 'en' ? `Receipt reference ${data.requestId}` : `回执编号 ${data.requestId}`); const detail = document.createElement('dl'); detail.className = 'v7301-form-receipt__detail'; const rows = language() === 'en' ? [['Server-confirmed type', data.applicationTypeLabel], ['Confirmed at', confirmedText], ['Authoritative route', data.route]] : [['服务器确认类型', data.applicationTypeLabel], ['确认时间', confirmedText], ['权威路由', data.route]]; rows.forEach(([term, value]) => { const dt = document.createElement('dt'); const dd = document.createElement('dd'); dt.textContent = term; dd.textContent = value; detail.append(dt, dd); }); const boundary = document.createElement('p'); boundary.className = 'v7301-form-receipt__boundary'; boundary.textContent = language() === 'en' ? contract.successBoundaryEn : contract.successBoundaryZh; result.append(eyebrow, heading, id, detail, boundary); result.hidden = false; result.focus({ preventScroll: true }); } function isConfirmedResponse(response, data, contract, formId, formContract) { let requestIdValid = false; try { requestIdValid = new RegExp(contract.requestIdPattern).test(String(data.requestId || '')); } catch (_error) { requestIdValid = false; } return response.ok && data && data.ok === true && requestIdValid && data.formId === formId && data.applicationType === formContract.applicationType && data.route === formContract.route && typeof data.applicationTypeLabel === 'string' && data.applicationTypeLabel.trim().length > 0 && typeof data.confirmedAt === 'string' && Number.isFinite(Date.parse(data.confirmedAt)); } async function submitForm(form) { const current = state.get(form) || { pending: false, confirmed: false }; if (current.pending || current.confirmed) { const message = language() === 'en' ? (current.confirmed ? 'This form already has a server-confirmed receipt. Reset the form before a new submission.' : 'This form is already being submitted. Please wait.') : (current.confirmed ? '本表单已获得服务器确认回执;如需再次提交,请先重置表单。' : '本表单正在提交,请稍候。'); statusNode(form).textContent = message; return; } let contract; let formContract; let formId; try { contract = await loadContract(); formId = String(form.querySelector('[name="form_id"]')?.value || '').trim().toLowerCase(); formContract = resolveForm(contract, formId); decorateForm(form, contract, formId, formContract); } catch (_error) { renderFailure(form, language() === 'en' ? 'The form contract could not be loaded. Refresh the page or use the published email channel.' : '表单契约未能加载,请刷新页面或使用公布的邮箱联系。'); return; } if (!validateContract(form, contract, formContract)) return; const challengeAnswer = current.challengeToken ? String(resultNode(form).querySelector('[data-v7301-challenge-answer]')?.value || '').trim() : ''; if (current.challengeToken && !challengeAnswer) { const challengeInput = resultNode(form).querySelector('[data-v7301-challenge-answer]'); if (challengeInput) { challengeInput.focus({ preventScroll: true }); challengeInput.reportValidity(); } return; } const next = { ...current, pending: true, confirmed: false }; state.set(form, next); setBusy(form, true); const status = statusNode(form); status.textContent = language() === 'en' ? 'Sending the form for server confirmation…' : '正在等待服务器确认发送…'; status.setAttribute('role', 'status'); status.setAttribute('aria-live', 'polite'); resultNode(form).hidden = true; let response; let data = {}; try { const requestBody = { fields: buildFields(form, formContract) }; if (current.challengeToken) { requestBody.security = { challengeToken: current.challengeToken, challengeAnswer, }; } response = await fetch(contract.endpoint, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(requestBody), }); data = await response.json().catch(() => ({})); if (response.status === 202 && data && data.challengeRequired === true && data.challenge && typeof data.challenge.token === 'string' && data.challenge.token.trim()) { next.pending = false; next.confirmed = false; next.challengeToken = data.challenge.token.trim(); state.set(form, next); setBusy(form, false); renderChallenge(form, data); return; } if (!isConfirmedResponse(response, data, contract, formId, formContract)) { // A payload that claims `ok: true` but fails any confirmation binding // must never surface its success-like message as user-facing feedback. const contractMismatch = data && data.ok === true; const safeMessage = language() === 'en' ? (!contractMismatch && typeof data.messageEn === 'string' && data.messageEn.trim() ? data.messageEn.trim() : 'The server did not return a valid confirmation. No receipt was created.') : (!contractMismatch && typeof data.message === 'string' && data.message.trim() ? data.message.trim() : '服务器未返回合法确认,本次未生成回执。'); throw new Error(safeMessage); } next.pending = false; next.confirmed = true; delete next.challengeToken; state.set(form, next); form.dataset.v7301SubmitState = 'confirmed'; renderSuccess(form, data, contract); form.querySelectorAll('button[type="submit"], input[type="submit"]').forEach((button) => { button.disabled = true; button.removeAttribute('aria-busy'); }); } catch (error) { next.pending = false; next.confirmed = false; state.set(form, next); setBusy(form, false); renderFailure(form, error instanceof Error && error.message ? error.message : (language() === 'en' ? 'The submission did not complete.' : '本次提交未完成。')); } } document.addEventListener('submit', (event) => { const form = event.target instanceof HTMLFormElement ? event.target : null; if (!form || !form.matches(governedSelector)) return; event.preventDefault(); event.stopImmediatePropagation(); void submitForm(form); }, true); document.addEventListener('input', (event) => { const control = event.target; if (!(control instanceof HTMLElement)) return; const form = control.closest(governedSelector); if (!form) return; clearCustomValidity(form); const status = statusNode(form); if (status.getAttribute('role') === 'alert') { status.setAttribute('role', 'status'); status.setAttribute('aria-live', 'polite'); } }); document.addEventListener('reset', (event) => { const form = event.target instanceof HTMLFormElement ? event.target : null; if (!form || !form.matches(governedSelector)) return; window.setTimeout(async () => { state.set(form, { pending: false, confirmed: false }); setBusy(form, false); form.dataset.v7301SubmitState = 'idle'; const result = resultNode(form); result.hidden = true; result.replaceChildren(); ensureHidden(form, 'form_started_at', Date.now()); try { const contract = await loadContract(); const formId = String(form.querySelector('[name="form_id"]')?.value || '').trim().toLowerCase(); const formContract = resolveForm(contract, formId); decorateForm(form, contract, formId, formContract); } catch (_error) { // A later submit reports the contract loading failure accessibly. } }, 0); }, true); async function initialize() { try { const contract = await loadContract(); document.querySelectorAll(governedSelector).forEach((form) => { const formId = String(form.querySelector('[name="form_id"]')?.value || '').trim().toLowerCase(); if (!contract.forms[formId]) return; const formContract = resolveForm(contract, formId); decorateForm(form, contract, formId, formContract); state.set(form, { pending: false, confirmed: false }); }); } catch (_error) { document.querySelectorAll(governedSelector).forEach((form) => { form.dataset.v7301FormGovernance = 'contract-unavailable'; statusNode(form).textContent = language() === 'en' ? 'The form contract is unavailable. Use the published contact channel.' : '表单契约暂时不可用,请使用页面公布的联系方式。'; }); } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initialize, { once: true }); else void initialize(); }());