PageSourceSearch

https://x-crossing.com/common/js/contact-form.js

js x-crossing.com collected 2026-10-08 01:05:35 UTC 7,915 bytes, 273 lines download raw bytes

1
2
3
4
5
6(function () {
7  "use strict";
8
9  var MIN_BODY = 10;
10  var MAX_BODY = 4000;
11  var MIN_DWELL_MS = 3000;
12  var TIMEOUT_MS = 20000;   
13  var SLOW_MS = 5000;       
14  var KINDS = ["番組への感想・リクエスト", "特定の回について", "その他"];
15
16  function epFromSearch(search) {
17    var raw = new URLSearchParams(search || "").get("ep") || "";
18    return /^\d+$/.test(raw) ? raw : "";
19  }
20
21  function shareValue(checked) {
22    return checked ? "はい" : "いいえ";
23  }
24
25  function problems(v) {
26    var out = [];
27    var body = (v.body || "").trim();
28    if (body.length < MIN_BODY || body.length > MAX_BODY) out.push("body");
29    
30    
31    if (KINDS.indexOf(v.kind) < 0) out.push("kind");
32    if (v.kind === "特定の回について" && !(v.episode || "")) out.push("episode");
33    var mail = (v.email || "").trim();
34    if (mail && mail.indexOf("@") < 0) out.push("email");
35    return out;
36  }
37
38  function passedTrap(v) {
39    if ((v.website || "") !== "") return false;
40    return (v.elapsedMs || 0) >= MIN_DWELL_MS;
41  }
42
43  
44  function receiptFor(state, prev) {
45    if (prev && prev.state === state) return prev;
46    var id = "";
47    for (var i = 0; i < 12; i++) {
48      id += Math.floor(Math.random() * 16).toString(16);
49    }
50    return {state: state, id: id};
51  }
52
53  var api = { epFromSearch: epFromSearch, shareValue: shareValue, problems: problems,
54              passedTrap: passedTrap, receiptFor: receiptFor, KINDS: KINDS };
55  if (typeof module !== "undefined" && module.exports) module.exports = api;
56  if (typeof document === "undefined") return;
57
58  
59  var openedAt = Date.now();
60  var receipt = null;
61
62  function boot() {
63    var form = document.getElementById("contact-form");
64    if (!form) return;
65    var endpoint = form.getAttribute("data-endpoint") || "";
66    var group = document.getElementById("contact-episode-group");
67    var select = document.getElementById("contact-episode");
68    var body = document.getElementById("contact-body");
69    var share = document.getElementById("contact-share");
70    var name = document.getElementById("contact-name");
71    var email = document.getElementById("contact-email");
72    var trap = document.getElementById("contact-website");
73    var submit = document.getElementById("contact-submit");
74    var status = document.getElementById("contact-status");
75    
76    
77    
78    var label = submit.textContent;
79
80    function kindNow() {
81      var on = form.querySelector('input[name="kind"]:checked');
82      return on ? on.value : "";
83    }
84
85    function alarm(which, on) {
86      var el = document.getElementById("contact-" + which + "-alarm");
87      if (el) el.hidden = !on;
88    }
89
90    
91    
92    function syncEpisode() {
93      var wanted = kindNow() === "特定の回について";
94      group.hidden = !wanted;
95      select.required = wanted;   
96      if (!wanted) alarm("episode", false);
97    }
98
99    function clearAlarms() {
100      ["episode", "body", "email"].forEach(function (w) { alarm(w, false); });
101    }
102
103    form.addEventListener("change", function (e) {
104      if (e.target.name === "kind") syncEpisode();
105    });
106
107    var ep = epFromSearch(location.search);
108    if (ep && select.querySelector('option[value="' + ep + '"]')) {
109      Array.prototype.forEach.call(form.querySelectorAll('input[name="kind"]'), function (r) {
110        r.checked = r.value === "特定の回について";
111      });
112      select.value = ep;
113    }
114    syncEpisode();
115
116    form.addEventListener("submit", function (e) {
117      e.preventDefault();
118      if (!endpoint) return;
119      clearAlarms();
120
121      var values = {kind: kindNow(), episode: select.value, body: body.value,
122                    email: email.value};
123      var bad = problems(values);
124      if (bad.length) {
125        bad.forEach(function (which) { alarm(which, true); });
126        var first = document.getElementById("contact-" + bad[0]);
127        if (first) first.focus();
128        return;
129      }
130
131      if (!passedTrap({website: trap.value, elapsedMs: Date.now() - openedAt})) {
132        
133        
134        showDone(form, body.value, true);
135        return;
136      }
137
138      
139      
140      
141      
142      receipt = receiptFor(JSON.stringify([values.kind, values.episode, values.body,
143                                           name.value, values.email, share.checked]),
144                           receipt);
145
146      
147      
148      
149      
150      submit.disabled = true;
151      submit.textContent = "送っています";
152      status.hidden = false;
153      status.classList.add("contact-form__status--sending");
154      status.textContent = "送っています。届いたかどうかを確かめています。";
155
156      
157      
158      var slow = setTimeout(function () {
159        status.textContent = "まだ確かめています。ページを閉じずにお待ちください。";
160      }, SLOW_MS);
161
162      
163      
164      
165      
166      
167      
168      var settled = false;
169      var stop = typeof AbortController === "function" ? new AbortController() : null;
170      var timer = setTimeout(function () {
171        if (stop) stop.abort();
172        finish(false);
173      }, TIMEOUT_MS);
174
175      
176      function finish(ok) {
177        if (settled) return;
178        settled = true;
179        clearTimeout(timer);
180        clearTimeout(slow);
181        submit.disabled = false;
182        submit.textContent = label;
183        status.hidden = true;
184        status.classList.remove("contact-form__status--sending");
185        showDone(form, body.value, ok);
186      }
187
188      fetch(endpoint, {
189        method: "POST",
190        signal: stop ? stop.signal : undefined,
191        headers: {"Content-Type": "text/plain;charset=utf-8"},
192        body: JSON.stringify({
193          kind: values.kind, episode: values.episode, body: body.value,
194          name: name.value, email: email.value, share: shareValue(share.checked),
195          website: trap.value, receipt: receipt.id
196        })
197      })
198        .then(function (res) {
199          
200          
201          if (!res.ok) throw new Error("http " + res.status);
202          return res.json();
203        })
204        .then(function (data) { finish(!!(data && data.ok)); })
205        .catch(function () {
206          
207          
208          
209          finish(false);
210        });
211    });
212  }
213
214  function showDone(form, copy, ok) {
215    var box = document.getElementById("contact-done");
216    box.textContent = "";
217
218    var h = document.createElement("h2");
219    h.textContent = ok ? "お便りありがとうございました" : "送れませんでした";
220
221    var p = document.createElement("p");
222    p.textContent = ok
223      ? "3人で読ませていただきます。お書きになった内容を、控えとして下に残しています。"
224      : "うまく送れませんでした。お書きになった内容は下に残してあります。"
225        + "もう一度お試しいただくか、X の DM へお送りください。";
226
227    var pre = document.createElement("pre");
228    pre.className = "contact-done__copy";
229    pre.textContent = copy;
230
231    var note = document.createElement("p");
232    note.appendChild(document.createTextNode("番組へのご連絡は "));
233    var a = document.createElement("a");
234    a.href = "https://x.com/x_crossing_";
235    a.target = "_blank";
236    a.rel = "noopener";
237    a.textContent = "X の DM(@x_crossing_)";
238    note.appendChild(a);
239    note.appendChild(document.createTextNode(" からもどうぞ。"));
240
241    box.appendChild(h);
242    box.appendChild(p);
243    box.appendChild(pre);
244    box.appendChild(note);
245    box.hidden = false;
246
247    
248    
249    
250    if (typeof box.focus === "function") box.focus();
251
252    
253    
254    
255    
256    
257    
258    
259    
260    if (ok) {
261      form.hidden = true;
262      var intro = document.getElementById("contact-intro");
263      if (intro) intro.hidden = true;
264    }
265    box.scrollIntoView({block: "start"});
266  }
267
268  if (document.readyState === "loading") {
269    document.addEventListener("DOMContentLoaded", boot);
270  } else {
271    boot();
272  }
273})();

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.