PageSourceSearch

https://nakanoshimagate.jp/js/form-validation-helpers.js

js nakanoshimagate.jp collected 2026-10-04 22:45:32 UTC 10,642 bytes, 276 lines download raw bytes

1/**
2 * 予約フォーム系の共通バリデーションヘルパー。
3 * marina/chotto-yacht.com, hokkomarina.com, boatpark.yokohama の
4 * entry/form.blade.php 等、各フォームのインラインscriptから呼び出して使う。
5 *
6 * 必須フィールドの一覧やフォーム固有の追加ロジック(カテゴリ別分岐、特定日ブロック等)は
7 * この共通ファイルには含めず、各フォーム側のインラインscriptに残す。
8 */
9
10function showFormError(input, message) {
11  const container = input.closest('.w60, .block, .mb1, .table_td, td');
12  if (!container) return;
13  let errorEl = container.querySelector(':scope > .js-error');
14  if (!errorEl) {
15    errorEl = document.createElement('div');
16    errorEl.className = 'pink js-error';
17    container.appendChild(errorEl);
18  }
19  errorEl.textContent = message;
20  errorEl.style.display = 'block';
21  const block = input.closest('.table_block, tr');
22  if (block) block.classList.add('has-error');
23}
24
25function clearFormError(input) {
26  const container = input.closest('.w60, .block, .mb1, .table_td, td');
27  if (container) {
28    const errorEl = container.querySelector(':scope > .js-error');
29    if (errorEl) {
30      errorEl.textContent = '';
31      errorEl.style.display = 'none';
32    }
33  }
34  const block = input.closest('.table_block, tr');
35  if (block) {
36    const hasRemainingError = Array.from(block.querySelectorAll('div.pink'))
37      .filter((el) => !el.classList.contains('js-error'))
38      .some((el) => el.textContent.trim() && el.style.display !== 'none');
39    if (!hasRemainingError) block.classList.remove('has-error');
40  }
41}
42
43function scrollToFirstFormError(errorTarget) {
44  if (errorTarget) {
45    const block = errorTarget.closest('.table_block, tr') || errorTarget;
46    block.scrollIntoView({ behavior: 'smooth', block: 'center' });
47  }
48}
49
50/**
51 * サーバーバリデーションエラー行のハイライトと先頭へのスクロール。
52 * ページロード時(confirm/complete失敗でフォームに戻された時)に呼ぶ。
53 */
54function highlightServerFormErrors() {
55  document.querySelectorAll('.table_block div.pink, table tr div.pink').forEach((el) => {
56    if (el.textContent.trim() && el.style.display !== 'none') {
57      const block = el.closest('.table_block, tr');
58      if (block) block.classList.add('has-error');
59    }
60  });
61  const firstError = document.querySelector('.table_block.has-error, tr.has-error');
62  if (firstError) {
63    setTimeout(() => {
64      firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
65    }, 100);
66  }
67}
68
69/**
70 * post.js (YubinBango) が郵便番号検索で書き込んだ値を
71 * #address_field の1フィールドに結合する。
72 */
73function bindAddressAutofill() {
74  const regionEl = document.querySelector('.p-region');
75  const localityEl = document.querySelector('.p-locality');
76  const streetEl = document.querySelector('.p-street-address');
77  const af = document.getElementById('address_field');
78  const aaF = document.getElementById('address_auto');
79
80  if (!regionEl || !af) return;
81
82  let lastAutoFilled = aaF ? aaF.value : '';
83  let updateTimer = null;
84
85  function applyAutoAddress() {
86    const region = regionEl.textContent || '';
87    const locality = localityEl ? localityEl.textContent : '';
88    const street = streetEl ? streetEl.textContent : '';
89    const auto = (region + locality + street).trim();
90
91    if (!auto) return;
92
93    const current = af.value.trim();
94    let suffix = '';
95    if (lastAutoFilled !== '' && current.startsWith(lastAutoFilled)) {
96      suffix = current.slice(lastAutoFilled.length);
97    }
98
99    const newVal = auto + suffix;
100    if (aaF) aaF.value = auto;
101    lastAutoFilled = auto;
102    af.value = newVal;
103    af.focus();
104    af.setSelectionRange(newVal.length, newVal.length);
105
106    clearFormError(af);
107  }
108
109  function scheduleUpdate() {
110    clearTimeout(updateTimer);
111    updateTimer = setTimeout(applyAutoAddress, 0);
112  }
113
114  const observeConfig = { childList: true, characterData: true, subtree: true };
115  new MutationObserver(scheduleUpdate).observe(regionEl, observeConfig);
116  if (localityEl) new MutationObserver(scheduleUpdate).observe(localityEl, observeConfig);
117  if (streetEl) new MutationObserver(scheduleUpdate).observe(streetEl, observeConfig);
118}
119
120/**
121 * 必須フィールドの一括チェック。
122 * requiredFields: [{ name: 'name', message: '氏名を入力してください。' }, ...]
123 */
124function validateRequiredFields(form, requiredFields) {
125  let hasError = false;
126  let firstErrorTarget = null;
127  requiredFields.forEach((field) => {
128    const input = form.querySelector(`[name="${field.name}"]`);
129    if (!input) return;
130    clearFormError(input);
131    if (!input.value.trim()) {
132      showFormError(input, field.message);
133      hasError = true;
134      if (!firstErrorTarget) firstErrorTarget = input;
135    }
136  });
137  return { hasError, firstErrorTarget };
138}
139
140/**
141 * 希望日の過去日チェック。
142 * name="date" の値が YYYY-MM-DD 形式であれば、input[type=date]でもflatpickrのtext inputでも動作する。
143 */
144function validatePastDate(form, message) {
145  const dateEl = form.querySelector('[name="date"]');
146  if (dateEl && /^\d{4}-\d{2}-\d{2}$/.test(dateEl.value)) {
147    const today = new Date();
148    today.setHours(0, 0, 0, 0);
149    if (new Date(dateEl.value) < today) {
150      showFormError(dateEl, message || '過去の日付は選択できません。');
151      return { hasError: true, firstErrorTarget: dateEl };
152    }
153  }
154  return { hasError: false, firstErrorTarget: null };
155}
156
157/** 電話番号形式チェック(ハイフン許容、10〜11桁の数字) */
158function validateTelFormat(form, message) {
159  const telInput = form.querySelector('[name="tel"]');
160  if (telInput && telInput.value.trim()) {
161    const telValue = telInput.value.replace(/[-ー\s]/g, '');
162    if (!/^[0-9]{10,11}$/.test(telValue)) {
163      showFormError(telInput, message || '電話番号は10〜11桁の数字で入力してください。');
164      return { hasError: true, firstErrorTarget: telInput };
165    }
166  }
167  return { hasError: false, firstErrorTarget: null };
168}
169
170/** メールアドレス形式チェック */
171function validateMailFormat(form, message) {
172  const mailInput = form.querySelector('[name="mail"]');
173  if (mailInput && mailInput.value.trim()) {
174    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(mailInput.value.trim())) {
175      showFormError(mailInput, message || 'メールアドレスの形式をご確認ください。');
176      return { hasError: true, firstErrorTarget: mailInput };
177    }
178  }
179  return { hasError: false, firstErrorTarget: null };
180}
181
182/** 性別チェック(name="sex" のラジオが存在する場合のみ対象) */
183function validateSexSelected(form, message) {
184  const sexRadio = form.querySelector('[name="sex"]');
185  if (sexRadio && !form.querySelector('[name="sex"]:checked')) {
186    showFormError(sexRadio, message || '性別を選択してください。');
187    return { hasError: true, firstErrorTarget: sexRadio };
188  }
189  return { hasError: false, firstErrorTarget: null };
190}
191
192/** 任意のname属性のラジオボタングループが選択されているかチェックする汎用版 */
193function validateRadioSelected(form, name, message) {
194  const radio = form.querySelector(`[name="${name}"]`);
195  if (radio && !form.querySelector(`[name="${name}"]:checked`)) {
196    showFormError(radio, message || '選択してください。');
197    return { hasError: true, firstErrorTarget: radio };
198  }
199  return { hasError: false, firstErrorTarget: null };
200}
201
202/** 生年月日チェック(name="age_month"/"age_day" が存在する場合のみ対象) */
203function validateBirthday(form, message) {
204  const ageMonth = form.querySelector('[name="age_month"]');
205  const ageDay = form.querySelector('[name="age_day"]');
206  const agePivot = ageMonth || ageDay;
207  if (agePivot) {
208    clearFormError(agePivot);
209    const monthInvalid = ageMonth && (ageMonth.value === '0' || !ageMonth.value);
210    const dayInvalid = ageDay && (ageDay.value === '0' || !ageDay.value);
211    if (monthInvalid || dayInvalid) {
212      const target = monthInvalid ? ageMonth : ageDay;
213      showFormError(target, message || '生年月日を正しく選択してください。');
214      return { hasError: true, firstErrorTarget: target };
215    }
216  }
217  return { hasError: false, firstErrorTarget: null };
218}
219
220/**
221 * 規約同意チェック。存在するチェックボックスのみ対象にする。
222 * names: 省略時は ['agree', 'agree2', 'agree3']
223 */
224function validateAgreements(form, names, message) {
225  let hasError = false;
226  let firstErrorTarget = null;
227  (names || ['agree', 'agree2', 'agree3']).forEach((name) => {
228    const checkbox = form.querySelector(`[name="${name}"]`);
229    if (checkbox && !checkbox.checked) {
230      showFormError(checkbox, message || '同意チェックを入れてください。');
231      hasError = true;
232      if (!firstErrorTarget) firstErrorTarget = checkbox;
233    }
234  });
235  return { hasError, firstErrorTarget };
236}
237
238/**
239 * 住所番地チェック。
240 * #address_field が郵便番号オートフィルの値のままだったり、番地の数字を含んでいなければエラーにする。
241 */
242function validateAddressDetail(message) {
243  const af = document.getElementById('address_field');
244  const aaF = document.getElementById('address_auto');
245  if (af && af.value.trim()) {
246    const addressVal = af.value.trim();
247    const autoVal = aaF ? aaF.value.trim() : '';
248    if ((autoVal && addressVal === autoVal) || !/[0-90-9]/.test(addressVal)) {
249      showFormError(af, message || '番地・建物名を入力してください。');
250      return { hasError: true, firstErrorTarget: af };
251    }
252  }
253  return { hasError: false, firstErrorTarget: null };
254}
255
256/** 送信ボタンをdisableにして二重送信を防止する */
257function disableSubmitButton(form, submittingText) {
258  const submitBtn = form.querySelector('input[type="submit"], button[type="submit"]');
259  if (!submitBtn) return;
260  submitBtn.disabled = true;
261  // button要素は内部にspan等の装飾構造を持つことがあるため、
262  // テキストの書き換えはinput[type=submit]の場合のみ行う。
263  if (submitBtn.tagName === 'INPUT') {
264    submitBtn.value = submittingText || '送信中...';
265  }
266}
267
268/** 入力・変更のたびに、そのフィールドのエラー表示をクリアするイベントを登録する */
269function bindClearErrorOnChange(form) {
270  form.addEventListener('input', (e) => {
271    if (e.target) clearFormError(e.target);
272  });
273  form.addEventListener('change', (e) => {
274    if (e.target) clearFormError(e.target);
275  });
276}

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.