PageSourceSearch

https://haiphen.io/components/contact-form/contact-form.js?v=2

js haiphen.io collected 2026-10-01 12:02:07 UTC 5,461 bytes, 185 lines download raw bytes

1(function () {
2  const API_ORIGIN = "https://contact.haiphen.io";
3  const ENDPOINT = `${API_ORIGIN}/api/contact`;
4
5  const LOG_PREFIX = "[contact]";
6  const WIRE_TIMEOUT_MS = 15000;
7
8  function qsIn(root, id) {
9    return (root || document).querySelector(`#${CSS.escape(id)}`);
10  }
11
12  function setStatus(root, msg) {
13    const el = qsIn(root, "contactStatus");
14    if (el) el.textContent = msg || "";
15  }
16
17  function disableForm(root, disabled) {
18    const btn = qsIn(root, "contactSubmit");
19    if (btn) btn.disabled = !!disabled;
20
21    const form = qsIn(root, "contactForm");
22    if (!form) return;
23
24    [...form.elements].forEach((e) => {
25      if (e && e.tagName && e.tagName !== "BUTTON") e.disabled = !!disabled;
26    });
27  }
28
29  async function postJson(url, body) {
30    console.debug(LOG_PREFIX, "POST", url, body);
31    const resp = await fetch(url, {
32      method: "POST",
33      headers: { "Content-Type": "application/json" },
34      body: JSON.stringify(body),
35    });
36
37    const text = await resp.text();
38    let data = null;
39    try { data = text ? JSON.parse(text) : null; } catch (_) {}
40
41    if (!resp.ok) {
42      const msg = data?.error || `Request failed (${resp.status})`;
43      throw new Error(msg);
44    }
45    return data;
46  }
47
48  function showReceipt(root, data) {
49    const form = qsIn(root, "contactForm");
50    const receipt = qsIn(root, "contactReceipt");
51    if (!form || !receipt) return;
52
53    form.hidden = true;
54    receipt.hidden = false;
55
56    qsIn(root, "ticketId").textContent = data.ticketId || "—";
57    qsIn(root, "ticketEmail").textContent = data.email || "—";
58    qsIn(root, "ticketMeta").textContent =
59      `Queue position: ${data.queuePosition ?? "—"} • Received: ${data.receivedAt || "—"}`;
60  }
61
62  function showForm(root) {
63    const form = qsIn(root, "contactForm");
64    const receipt = qsIn(root, "contactReceipt");
65    if (!form || !receipt) return;
66
67    form.hidden = false;
68    receipt.hidden = true;
69    form.reset();
70    setStatus(root, "");
71  }
72
73  function attachOnce(root) {
74    const form = qsIn(root, "contactForm");
75    if (!form) return false;
76
77    // Prevent double-wiring even if you re-inject
78    if (form.dataset.hpWired === "1") return true;
79    form.dataset.hpWired = "1";
80
81    console.info(LOG_PREFIX, "wiring submit handler");
82
83    form.addEventListener("submit", async (e) => {
84      e.preventDefault();
85      setStatus(root, "");
86
87      const fd = new FormData(form);
88      const payload = {
89        name: String(fd.get("name") || "").trim(),
90        email: String(fd.get("email") || "").trim(),
91        phone: String(fd.get("phone") || "").trim(),
92        message: String(fd.get("message") || "").trim(),
93        website: String(fd.get("website") || "").trim(), // honeypot
94        token: "", // optional
95        pageUrl: window.location.href,
96        userAgent: navigator.userAgent,
97      };
98
99      if (!payload.name || !payload.email || payload.message.length < 10) {
100        setStatus(root, "Please fill name, email, and a longer message.");
101        return;
102      }
103
104      disableForm(root, true);
105      setStatus(root, "Submitting…");
106
107      try {
108        const data = await postJson(ENDPOINT, payload);
109        setStatus(root, "");
110        showReceipt(root, data);
111      } catch (err) {
112        console.warn(LOG_PREFIX, "submit failed", err);
113        setStatus(root, err?.message || "Submit failed. Try again.");
114      } finally {
115        disableForm(root, false);
116      }
117    });
118
119    const another = qsIn(root, "contactAnother");
120    if (another && another.dataset.hpWired !== "1") {
121      another.dataset.hpWired = "1";
122      another.addEventListener("click", () => showForm(root));
123    }
124
125    return true;
126  }
127
128  function applyPrefill(root) {
129    const prefill = window.HAIPHEN?.prefillContact;
130    if (!prefill?.message) return;
131    const form = qsIn(root, "contactForm");
132    if (!form) return;
133    const msgEl = form.querySelector('[name="message"]');
134    if (msgEl) {
135      msgEl.value = prefill.message;
136      delete window.HAIPHEN.prefillContact;
137    }
138  }
139
140  function wireReveal(root) {
141    var revealEls = (root || document).querySelectorAll('.hp-contact-reveal');
142    if (!revealEls.length) return;
143    var io = new IntersectionObserver(function(entries) {
144      entries.forEach(function(e) {
145        if (e.isIntersecting) { e.target.classList.add('is-visible'); io.unobserve(e.target); }
146      });
147    }, { threshold: 0.15 });
148    revealEls.forEach(function(el) { io.observe(el); });
149  }
150
151  // ✅ PUBLIC INIT (no auto observer unless you call it)
152  function init(rootEl) {
153    const root = rootEl || document;
154
155    // Fast-path: if the form exists now, wire immediately.
156    if (attachOnce(root)) {
157      applyPrefill(root);
158      wireReveal(root);
159      return true;
160    }
161
162    // Optional: if you *do* want a wait mode, do it only when Contact is being opened.
163    const startedAt = Date.now();
164    const obs = new MutationObserver(() => {
165      if (attachOnce(root)) {
166        obs.disconnect();
167        applyPrefill(root);
168        wireReveal(root);
169      } else if (Date.now() - startedAt > WIRE_TIMEOUT_MS) {
170        obs.disconnect();
171        console.error(LOG_PREFIX, "timed out waiting for #contactForm to appear");
172      }
173    });
174
175    obs.observe(root === document ? document.documentElement : root, {
176      childList: true,
177      subtree: true,
178    });
179
180    return false;
181  }
182
183  window.HAIPHEN = window.HAIPHEN || {};
184  window.HAIPHEN.ContactForm = { init };
185})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.