PageSourceSearch

https://manaviism.com/wp/wp-content/themes/manaviism_v4/public/js/mform.js?ver=20260930193054

js manaviism.com collected 2026-09-30 19:31:20 UTC 20,240 bytes, 497 lines download raw bytes

1/*!
2 * mform.js — マナビズム ステップ式お問い合わせフォーム
3 * 既存 contactForm.js とは独立に動作(CF7のフォーム内で .mform クラスがあれば起動)
4 */
5(function () {
6  'use strict';
7
8  document.addEventListener('DOMContentLoaded', function () {
9    document.querySelectorAll('.mform').forEach(function (el) {
10      new MForm(el);
11    });
12
13    // ステージング環境 + ?debug=1 のときだけデバッグ用ブロック (.mform__hidden) を可視化
14    // 通常のステージングプレビューでは本番と同じ見た目になる
15    if (window.location.hostname === 'connectsyuro.xsrv.jp'
16        && window.location.search.indexOf('debug=1') !== -1) {
17      document.body.classList.add('mform-staging');
18    }
19  });
20
21  // 校舎別メール分岐(your-kousya → radio-1/2/3 連動)は
22  // public/js/main.js L28以降「202504共通フォームの校舎選択処理上書き処理」が担当。
23  // 重複処理を避けるため mform.js 側にマッピングを持たない。
24
25  function MForm(el) {
26    this.el = el;
27    this.form = el.closest('form') || el;
28    this.steps = Array.prototype.slice.call(el.querySelectorAll('.mform__step'));
29    this.indicators = Array.prototype.slice.call(el.querySelectorAll('.mform__indicator li'));
30    this.stepsContainer = el.querySelector('.mform__steps');
31    this.currentStep = 0;
32    this.requiresAddress = false;
33
34    this.bindEvents();
35    this.updateView();
36
37    // ウィンドウリサイズ時にも高さ再計算
38    var self = this;
39    window.addEventListener('resize', function () {
40      self.updateStepsHeight();
41    });
42
43    // 画像読み込み完了後にも再計算(LINEバナー等のSVGがロード後に高さ変動)
44    window.addEventListener('load', function () {
45      self.updateStepsHeight();
46    });
47    el.querySelectorAll('img').forEach(function (img) {
48      if (!img.complete) {
49        img.addEventListener('load', function () { self.updateStepsHeight(); });
50      }
51    });
52
53    // ResizeObserver でアクティブstepの内部高さ変化を監視
54    if (typeof ResizeObserver !== 'undefined') {
55      var ro = new ResizeObserver(function () { self.updateStepsHeight(); });
56      this.steps.forEach(function (step) { ro.observe(step); });
57    }
58  }
59
60  MForm.prototype.bindEvents = function () {
61    var self = this;
62
63    // ステップ遷移ボタン
64    this.el.addEventListener('click', function (e) {
65      var actionEl = e.target.closest('[data-action]');
66      if (!actionEl) return;
67      var action = actionEl.dataset.action;
68      if (action === 'next') {
69        e.preventDefault();
70        self.next();
71      } else if (action === 'prev') {
72        e.preventDefault();
73        self.prev();
74      } else if (action === 'submit') {
75        e.preventDefault();
76        self.submit();
77      }
78    });
79
80    // Step1: お問い合わせ内容チェックボックス変更
81    this.el.querySelectorAll('input[name="your-contact[]"]').forEach(function (cb) {
82      cb.addEventListener('change', function () { self.onContentChange(); });
83    });
84
85    // Step2: 入力欄のバリデーション & 自動次へ
86    var nameStep = this.el.querySelector('.mform__step[data-name="name"]');
87    if (nameStep) {
88      nameStep.querySelectorAll('.mform__input').forEach(function (input) {
89        input.addEventListener('input', function () { self.onNameInput(input); });
90        input.addEventListener('blur', function () {
91          self.validateField(input);
92          self.updateStep2NextButton();
93        });
94      });
95    }
96
97    // Step2: 学年ラジオ変更でエラークリア & 次へボタン再判定
98    this.el.querySelectorAll('input[name="school-year"]').forEach(function (r) {
99      r.addEventListener('change', function () {
100        var err = self.el.querySelector('.mform__error-msg[data-for="school-year"]');
101        if (err) err.textContent = '';
102        self.updateStep2NextButton();
103      });
104    });
105
106    // Step3: 校舎選択のバリデーション(メール分岐連動はmain.jsが担当)
107    this.el.querySelectorAll('input[name="your-kousya"]').forEach(function (r) {
108      r.addEventListener('change', function () {
109        self.validateField(r);
110      });
111    });
112
113    // 住所欄バリデーション
114    var addrStep = this.el.querySelector('.mform__step[data-name="address"]');
115    if (addrStep) {
116      addrStep.querySelectorAll('.mform__input, .mform__select').forEach(function (input) {
117        input.addEventListener('input', function () {
118          self.validateField(input);
119          self.updateStep4SubmitButton();
120        });
121        input.addEventListener('change', function () {
122          self.validateField(input);
123          self.updateStep4SubmitButton();
124        });
125        input.addEventListener('blur', function () {
126          self.validateField(input);
127          self.updateStep4SubmitButton();
128        });
129      });
130    }
131  };
132
133  MForm.prototype.updateStep4SubmitButton = function () {
134    var step = this.el.querySelector('.mform__step[data-name="address"]');
135    if (!step) return;
136    var submitBtn = step.querySelector('[data-action="submit"]');
137    if (!submitBtn) return;
138
139    // 資料請求未選択ならStep4は表示されないので判定不要
140    if (!this.requiresAddress) {
141      submitBtn.disabled = true;
142      return;
143    }
144
145    var inputs = step.querySelectorAll('.mform__input, .mform__select');
146    var allValid = true;
147    inputs.forEach(function (i) {
148      if (!i.value.trim()) { allValid = false; return; }
149      if (!this.validateField(i, /*silent*/ true)) allValid = false;
150    }, this);
151    submitBtn.disabled = !allValid;
152  };
153
154  MForm.prototype.onContentChange = function () {
155    var checked = this.el.querySelectorAll('input[name="your-contact[]"]:checked');
156    var btn = this.steps[0].querySelector('[data-action="next"]');
157    if (btn) btn.disabled = checked.length === 0;
158
159    // チェックがついたらエラーメッセージをクリア
160    var contactErr = this.steps[0].querySelector('.mform__error-msg[data-for="your-contact"]');
161    if (contactErr && checked.length > 0) contactErr.textContent = '';
162
163    // 資料請求が含まれているかで Step4 の要否を決定
164    this.requiresAddress = false;
165    checked.forEach(function (cb) {
166      if (cb.value === '資料請求') this.requiresAddress = true;
167    }, this);
168
169    // group group-address の clear_on_hide 制御:
170    // 資料請求が選択されていない場合、住所欄は無効化(required抑制)
171    this.toggleAddressRequired(this.requiresAddress);
172
173    this.updateIndicator();
174    this.updateStep3Buttons();
175  };
176
177  MForm.prototype.toggleAddressRequired = function (required) {
178    // CF7のフォーム本文では住所フィールドを [text * なし] にしているため初期で required も aria-required も付かない。
179    // 資料請求時のみ Step4 を表示し、その際は全フィールドを必須にする。
180    // 非表示時は [group clear_on_hide] 相当として値とエラー表示もクリアする。
181    var addrStep = this.el.querySelector('.mform__step[data-name="address"]');
182    if (!addrStep) return;
183    addrStep.querySelectorAll('.mform__input, .mform__select').forEach(function (input) {
184      if (required) {
185        input.setAttribute('required', 'required');
186      } else {
187        input.removeAttribute('required');
188        // clear_on_hide 相当: 値をクリア
189        if (input.tagName === 'SELECT') {
190          input.selectedIndex = 0;
191        } else {
192          input.value = '';
193        }
194        // CF7のwpcf7-not-valid等の付帯クラスもクリア
195        input.classList.remove('wpcf7-not-valid');
196        input.removeAttribute('aria-invalid');
197        // change/input イベントを発火してYubinbango等の関連処理にも知らせる
198        input.dispatchEvent(new Event('input', { bubbles: true }));
199        input.dispatchEvent(new Event('change', { bubbles: true }));
200      }
201    });
202    if (!required) {
203      // エラーメッセージ表示もクリア
204      addrStep.querySelectorAll('.mform__error-msg').forEach(function (el) {
205        el.textContent = '';
206      });
207    }
208  };
209
210  MForm.prototype.onNameInput = function (input) {
211    this.validateField(input);
212    this.updateStep2NextButton();
213    // 自動ステップ移動は廃止(ユーザーが意図せず遷移するため)
214  };
215
216  MForm.prototype.updateStep2NextButton = function () {
217    var step = this.steps[1];
218    if (!step) return;
219    var nextBtn = step.querySelector('[data-action="next"]');
220    if (!nextBtn) return;
221
222    var inputs = step.querySelectorAll('.mform__input');
223    var allValid = true;
224    inputs.forEach(function (i) {
225      if (!i.value.trim()) { allValid = false; return; }
226      if (!this.validateField(i, /*silent*/ true)) allValid = false;
227    }, this);
228
229    // 学年ラジオ(必須・default無し)
230    if (step.querySelector('input[name="school-year"]')
231        && !step.querySelector('input[name="school-year"]:checked')) {
232      allValid = false;
233    }
234
235    nextBtn.disabled = !allValid;
236  };
237
238  MForm.prototype.validateField = function (input, silent) {
239    var valid = true;
240    var msg = '';
241    var value = (input.value || '').trim();
242    // CF7は [text*] を required属性ではなく aria-required="true" でレンダリングするため両方判定
243    var isRequired = input.hasAttribute('required') || input.getAttribute('aria-required') === 'true';
244
245    if (isRequired && !value) {
246      valid = false;
247      msg = '必須項目です';
248    } else if (input.type === 'email' && value) {
249      valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
250      if (!valid) {
251        msg = '正しいメールアドレスを入力してください';
252      } else if (/@(outlook\.com|hotmail\.com)$/i.test(value)) {
253        // functions.php のスパム対策と連携: outlook.com / hotmail.com ブロック
254        valid = false;
255        msg = '恐れ入りますが、別のメールアドレスをご利用ください。';
256      }
257    } else if (input.type === 'tel' && value) {
258      // 数字+ハイフン10桁以上(緩めに)
259      valid = /^[\d\-+()\s]{10,}$/.test(value);
260      if (!valid) msg = '正しい電話番号を入力してください';
261    } else if (input.name === 'your-zipcode' && value) {
262      valid = /^\d{3}-?\d{4}$/.test(value);
263      if (!valid) msg = '正しい郵便番号を入力してください';
264    } else if (['your-name', 'sei', 'mei'].indexOf(input.name) !== -1 && value) {
265      // functions.php のスパム対策と連携: 氏名に漢字 or 全角カタカナを含むこと
266      if (!/[一-龥ァ-ヶ]/.test(value)) {
267        valid = false;
268        msg = (input.name === 'sei') ? '姓は漢字で入力してください。'
269            : (input.name === 'mei') ? '名は漢字で入力してください。'
270            : 'お名前は漢字で入力してください。';
271      }
272    }
273
274    if (!silent) {
275      input.classList.toggle('is-error', !valid);
276      var errorEl = this._findErrorEl(input);
277      if (errorEl) errorEl.textContent = valid ? '' : msg;
278    }
279    return valid;
280  };
281
282  MForm.prototype._findErrorEl = function (input) {
283    // CF7が input を <span class="wpcf7-form-control-wrap"> で包むため、
284    // 同じ .mform__field 内の .mform__error-msg を辿る
285    var field = input.closest('.mform__field');
286    if (field) {
287      var err = field.querySelector('.mform__error-msg');
288      if (err) return err;
289    }
290    // フォールバック: 親方向を順に探索
291    var p = input.parentElement;
292    while (p && !p.classList.contains('mform__step') && p !== document.body) {
293      var next = p.nextElementSibling;
294      while (next) {
295        if (next.classList && next.classList.contains('mform__error-msg')) return next;
296        next = next.nextElementSibling;
297      }
298      var inside = p.querySelector(':scope > .mform__error-msg');
299      if (inside) return inside;
300      p = p.parentElement;
301    }
302    return null;
303  };
304
305  MForm.prototype.next = function () {
306    var curr = this.steps[this.currentStep];
307    if (!this.validateStep(curr)) return;
308
309    // Step3 (kousya) で資料請求なしの場合は送信
310    if (this.currentStep === 2 && !this.requiresAddress) {
311      this.submit();
312      return;
313    }
314
315    this.currentStep = Math.min(this.currentStep + 1, this.steps.length - 1);
316    this.updateView('forward');
317  };
318
319  MForm.prototype.prev = function () {
320    this.currentStep = Math.max(0, this.currentStep - 1);
321    this.updateView('backward');
322  };
323
324  MForm.prototype.submit = function () {
325    var curr = this.steps[this.currentStep];
326    if (!this.validateStep(curr)) return;
327
328    // 隠しの本物 submit ボタンをクリックして送信
329    var submitBtn = this.form.querySelector('.mform__submit-anchor input[type="submit"], .mform__submit-anchor button[type="submit"]');
330    if (!submitBtn) {
331      // フォールバック: form 内最初の submit
332      submitBtn = this.form.querySelector('input[type="submit"], button[type="submit"]');
333    }
334    if (submitBtn) submitBtn.click();
335  };
336
337  MForm.prototype.validateStep = function (stepEl) {
338    if (!stepEl) return true;
339    var allValid = true;
340
341    // テキスト/select系
342    stepEl.querySelectorAll('.mform__input, .mform__select').forEach(function (i) {
343      if (!this.validateField(i)) allValid = false;
344    }, this);
345
346    // Step1: お問い合わせ内容チェックボックス(最低1つ必須)
347    var contactCheckboxes = stepEl.querySelectorAll('input[name="your-contact[]"]');
348    if (contactCheckboxes.length > 0) {
349      var checkedCount = stepEl.querySelectorAll('input[name="your-contact[]"]:checked').length;
350      var contactErr = stepEl.querySelector('.mform__error-msg[data-for="your-contact"]');
351      if (checkedCount === 0) {
352        allValid = false;
353        if (contactErr) contactErr.textContent = '1つ以上選択してください';
354      } else if (contactErr) {
355        contactErr.textContent = '';
356      }
357    }
358
359    // Step2: 学年ラジオ必須(default無しのため未選択チェック)
360    var schoolRadios = stepEl.querySelectorAll('input[name="school-year"]');
361    if (schoolRadios.length > 0) {
362      var schoolErr = stepEl.querySelector('.mform__error-msg[data-for="school-year"]');
363      if (!stepEl.querySelector('input[name="school-year"]:checked')) {
364        allValid = false;
365        if (schoolErr) schoolErr.textContent = '学年を選択してください';
366      } else if (schoolErr) {
367        schoolErr.textContent = '';
368      }
369    }
370
371    // Step3: 校舎ラジオ必須(your-kousya は default:1 で初期選択されている想定だが念のため)
372    var kousyaRadios = stepEl.querySelectorAll('input[name="your-kousya"]');
373    if (kousyaRadios.length > 0) {
374      var checked = stepEl.querySelector('input[name="your-kousya"]:checked');
375      if (!checked) allValid = false;
376    }
377
378    return allValid;
379  };
380
381  MForm.prototype.updateView = function (direction) {
382    var self = this;
383    // スライド遷移: 既存アクティブステップを退場、新ステップを入場
384    var prevActive = this.steps.find(function (s) { return s.classList.contains('is-active'); });
385    var nextActive = this.steps[this.currentStep];
386
387    if (prevActive && nextActive && prevActive !== nextActive && direction) {
388      var exitClass = direction === 'forward' ? 'is-leaving-left' : 'is-leaving-right';
389      var enterClass = direction === 'forward' ? 'is-entering-right' : 'is-entering-left';
390
391      prevActive.classList.add(exitClass);
392      nextActive.classList.add('is-active', enterClass);
393
394      // 次フレームで遷移クラスを外してアニメ完了状態に
395      requestAnimationFrame(function () {
396        requestAnimationFrame(function () {
397          nextActive.classList.remove(enterClass);
398        });
399      });
400
401      // 退場アニメ完了後にクラス削除
402      var onEnd = function () {
403        prevActive.classList.remove('is-active', exitClass);
404        prevActive.removeEventListener('transitionend', onEnd);
405      };
406      prevActive.addEventListener('transitionend', onEnd);
407      // フォールバック: transitionend が発火しなかった場合
408      setTimeout(onEnd, 500);
409    } else {
410      // 初回表示など direction 無し
411      this.steps.forEach(function (s, i) {
412        s.classList.toggle('is-active', i === self.currentStep);
413        s.classList.remove('is-leaving-left', 'is-leaving-right', 'is-entering-left', 'is-entering-right');
414      });
415    }
416
417    this.updateIndicator();
418    this.updateStep2NextButton();
419    this.updateStep3Buttons();
420    this.updateStep4SubmitButton();
421    this.updateStepsHeight();
422
423    // direction が指定された遷移(次へ/戻る)のみスクロール、初期表示はしない
424    if (direction) {
425      this._scrollToTop();
426    }
427  };
428
429  MForm.prototype.updateStepsHeight = function () {
430    if (!this.stepsContainer) return;
431    var activeStep = this.steps[this.currentStep];
432    if (!activeStep) return;
433    // getBoundingClientRect で実レンダリング高さを取得
434    var h = activeStep.getBoundingClientRect().height;
435    if (h > 0) this.stepsContainer.style.height = h + 'px';
436  };
437
438  MForm.prototype._scrollToTop = function () {
439    try {
440      var target = this.el.closest('.mform-page') || this.el;
441      var rect = target.getBoundingClientRect();
442      var top = rect.top + window.pageYOffset;
443
444      // 固定ヘッダーとの重なり防止:
445      // /contact/ は .mform-page の padding-top で固定ヘッダー分の余白が確保済みだが、
446      // フッター文脈(.mform-page--footer は padding-top:0)では担保が無いため、
447      // ターゲットの padding-top で不足する分だけヘッダー高さを差し引く
448      var header = document.querySelector('.l-header');
449      if (header) {
450        var hs = window.getComputedStyle(header);
451        if (hs.position === 'fixed' || hs.position === 'sticky') {
452          var padTop = parseFloat(window.getComputedStyle(target).paddingTop) || 0;
453          top -= Math.max(0, header.offsetHeight + 16 - padTop);
454        }
455      }
456
457      // フッター文脈の止め位置微調整(SP: 30px先まで=フォームを上げる / PC: 30px手前=下げる)
458      if (target.classList.contains('mform-page--footer')) {
459        top += window.matchMedia('(max-width: 767px)').matches ? 30 : -30;
460      }
461
462      window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
463    } catch (_) { /* noop */ }
464  };
465
466  MForm.prototype.updateIndicator = function () {
467    var container = this.el.querySelector('.mform__indicator');
468    if (container) {
469      // HTML側で hidden 属性付与済み(FOUC防止)。JS が制御を引き継ぐ
470      container.hidden = (this.currentStep === 0);
471    }
472    this.indicators.forEach(function (li) {
473      var stepNum = parseInt(li.dataset.step, 10);
474      // 資料請求未選択時は送付先(step=3)を非表示
475      var hidden = (stepNum === 3 && !this.requiresAddress);
476      li.classList.toggle('is-hidden', hidden);
477      li.classList.toggle('is-active', stepNum === this.currentStep);
478      li.classList.toggle('is-done', stepNum < this.currentStep);
479    }, this);
480  };
481
482  MForm.prototype.updateStep3Buttons = function () {
483    // Step3 (kousya) のボタン表示制御:
484    //   資料請求あり → 「次へ」表示、送信は隠す、プライバシーポリシーも隠す(Step4で表示)
485    //   資料請求なし → 「同意して送信」表示、次へ隠す、プライバシーポリシーも表示(Step3が最終)
486    var step3 = this.el.querySelector('.mform__step[data-name="kousya"]');
487    if (!step3) return;
488    var nextBtn = step3.querySelector('[data-action="next"][data-cond="address"]');
489    var submitBtn = step3.querySelector('[data-action="submit"]');
490    var privacy = step3.querySelector('.mform__privacy[data-show-when="last"]');
491    if (nextBtn) nextBtn.hidden = !this.requiresAddress;
492    if (submitBtn) submitBtn.hidden = this.requiresAddress;
493    if (privacy) privacy.hidden = this.requiresAddress;
494  };
495
496  // onKousyaChangeはmain.jsに統合済みのため削除
497})();

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.