PageSourceSearch

https://www.takakyu.co.jp/securewp/wp-content/plugins/contact-form…/contact-form-7-confirm-modal.js?ver=0.5.1

js takakyu.co.jp collected 2026-10-06 07:42:20 UTC 21,751 bytes, 588 lines download raw bytes

1/**
2 * Contact Form 7 確認画面モーダル
3 *
4 * 送信前に確認モーダルを表示する
5 */
6
7class ContactFormConfirm {
8  /**
9   * @param {HTMLFormElement} form - 対象のCF7フォーム要素
10   */
11  constructor(form) {
12    this.form = form;
13    this.modal = null;
14    this.isConfirming = false;
15    this.formId = null;
16    this.settings = {};
17    this.submitButton = null;
18    this.init();
19  }
20
21  init() {
22    // Contact Form 7が初期化されているか確認
23    if (!this.form.wpcf7) {
24      // まだ初期化されていない場合は少し待つ
25      setTimeout(() => this.init(), 100);
26      return;
27    }
28
29    // フォームIDを取得
30    this.formId = this.form.wpcf7.id;
31
32    // 設定を読み込む
33    this.loadSettings();
34
35    // このフォームで確認モーダルが無効化されている場合は何もしない
36    if (!this.settings.enabled) {
37      return;
38    }
39
40    this.createModal();
41    this.attachEventListeners();
42    this.attachCF7EventListeners();
43  }
44
45  /**
46   * 設定を読み込む
47   */
48  loadSettings() {
49    const defaultSettings = window.cf7ConfirmDefaultSettings || {
50      enabled: false,
51      modal_title: '入力内容の確認',
52      back_button_text: '戻る',
53      submit_button_text: '送信する',
54      primary_color: '#0d6efd',
55      empty_text: '(未入力)',
56      show_empty_fields: false
57    };
58
59    const formSettings = window.cf7ConfirmFormSettings || {};
60    const currentFormSettings = formSettings[this.formId] || {};
61
62    // 設定をマージ(フォーム設定が優先、空の場合はデフォルト値を使用)
63    this.settings = {
64      enabled: currentFormSettings.enabled !== undefined ? currentFormSettings.enabled : defaultSettings.enabled,
65      modal_title: currentFormSettings.modal_title || defaultSettings.modal_title,
66      back_button_text: currentFormSettings.back_button_text || defaultSettings.back_button_text,
67      submit_button_text: currentFormSettings.submit_button_text || defaultSettings.submit_button_text,
68      primary_color: currentFormSettings.primary_color || defaultSettings.primary_color,
69      empty_text: currentFormSettings.empty_text || defaultSettings.empty_text,
70      show_empty_fields: currentFormSettings.show_empty_fields !== undefined ? currentFormSettings.show_empty_fields : defaultSettings.show_empty_fields
71    };
72  }
73
74  /**
75   * 確認モーダルのHTMLを作成
76   *
77   * 同一ページに複数のCF7フォームが存在する場合にそれぞれが独立したモーダルを
78   * 持てるよう、IDは使用せずクラスベースで要素を作成し、参照を直接保持します。
79   */
80  createModal() {
81    const modalHTML = `
82      <div class="cf7-confirm-modal">
83        <div class="cf7-confirm-modal__overlay"></div>
84        <div class="cf7-confirm-modal__content">
85          <div class="cf7-confirm-modal__header">
86            <h2 class="cf7-confirm-modal__title">${this.escapeHtml(this.settings.modal_title)}</h2>
87            <button type="button" class="cf7-confirm-modal__close" aria-label="閉じる">
88              <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
89                <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
90              </svg>
91            </button>
92          </div>
93          <div class="cf7-confirm-modal__body">
94            <div class="cf7-confirm-modal__data"></div>
95          </div>
96          <div class="cf7-confirm-modal__footer">
97            <button type="button" class="cf7-confirm-modal__button cf7-confirm-modal__button--back">${this.escapeHtml(this.settings.back_button_text)}</button>
98            <button type="button" class="cf7-confirm-modal__button cf7-confirm-modal__button--submit">${this.escapeHtml(this.settings.submit_button_text)}</button>
99          </div>
100        </div>
101      </div>
102    `;
103
104    const template = document.createElement('template');
105    template.innerHTML = modalHTML.trim();
106    this.modal = template.content.firstElementChild;
107    document.body.appendChild(this.modal);
108
109    // プライマリカラーをCSS変数として設定
110    this.modal.style.setProperty('--cf7-confirm-modal-primary-color', this.settings.primary_color);
111  }
112
113  /**
114   * イベントリスナーを設定
115   */
116  attachEventListeners() {
117    // フォームのsubmitイベントをリッスン
118    this.form.addEventListener('submit', (e) => {
119      if (!this.isConfirming) {
120        // 確認前の送信は中断
121        e.preventDefault();
122        e.stopPropagation();
123        e.stopImmediatePropagation();
124
125        // CF7必須フィールドのバリデーション
126        if (!this.validateRequiredFields()) {
127          // バリデーションエラーの場合はCF7に処理を委ねる
128          this.isConfirming = true;
129          wpcf7.submit(this.form);
130          setTimeout(() => { this.isConfirming = false; }, 2000);
131          return false;
132        }
133
134        // バリデーション成功時はモーダルを表示
135        this.displayConfirmData();
136        this.openModal();
137
138        return false;
139      }
140    }, true);
141
142    // モーダルを閉じる
143    const closeBtn = this.modal.querySelector('.cf7-confirm-modal__close');
144    const backBtn = this.modal.querySelector('.cf7-confirm-modal__button--back');
145    const overlay = this.modal.querySelector('.cf7-confirm-modal__overlay');
146
147    closeBtn.addEventListener('click', () => this.closeModal());
148    backBtn.addEventListener('click', () => this.closeModal());
149    overlay.addEventListener('click', () => this.closeModal());
150
151    // 確認モーダルから送信
152    const submitBtn = this.modal.querySelector('.cf7-confirm-modal__button--submit');
153    submitBtn.addEventListener('click', () => this.confirmSubmit());
154
155    // ESCキーでモーダルを閉じる
156    document.addEventListener('keydown', (e) => {
157      if (e.key === 'Escape' && this.modal.classList.contains('is-open')) {
158        this.closeModal();
159      }
160    });
161
162    // 送信ボタンの非活性制御
163    this.submitButton = this.form.querySelector('[type="submit"]');
164    if (this.submitButton) {
165      this.updateSubmitButtonState();
166      this.form.addEventListener('input', () => this.updateSubmitButtonState());
167      this.form.addEventListener('change', () => this.updateSubmitButtonState());
168
169      // CF7 Conditional Fieldsのグループ表示切替を監視
170      this.observeConditionalGroups();
171    }
172  }
173
174  /**
175   * CF7 Conditional Fieldsのグループ表示切替を監視
176   * wpcf7cf-hiddenクラスの追加・削除を検知して送信ボタンの状態を更新
177   */
178  observeConditionalGroups() {
179    const observer = new MutationObserver(() => this.updateSubmitButtonState());
180    observer.observe(this.form, { subtree: true, attributes: true, attributeFilter: ['class'] });
181  }
182
183  /**
184   * 送信ボタンの活性/非活性を更新
185   */
186  updateSubmitButtonState() {
187    const isValid = this.validateRequiredFields();
188    this.submitButton.disabled = !isValid;
189  }
190
191  /**
192   * フォームデータを確認モーダルに表示する
193   *
194   * このメソッドは、Contact Form 7フォームの入力値を
195   * 確認モーダルのテーブル形式で表示します。
196   *
197   * @returns {void}
198   *
199   * @description
200   * - フォーム内のすべての入力フィールドを処理します
201   * - チェックボックス、ラジオボタン、セレクトボックス、テキストエリアを適切に処理します
202   * - 空のフィールドは設定に従って表示/非表示を決定します
203   * - 各フィールドのラベルは getFieldLabel メソッドで取得されます(エスケープ済みHTML)
204   * - 入力値は HTML エスケープしてセキュリティを確保します
205   * - テーブル形式でデータを表示し、デザイン用のCSSクラスを適切に適用します
206   *
207   * @example
208   * // 確認モーダルのデータを更新する場合
209   * this.displayConfirmData();
210   */
211  displayConfirmData() {
212    const dataContainer = this.modal.querySelector('.cf7-confirm-modal__data');
213    const formData = new FormData(this.form);
214    let html = '<table class="cf7-confirm-table">';
215
216    // フォームの各フィールドを取得
217    const fields = this.form.querySelectorAll('input, select, textarea');
218
219    fields.forEach((field) => {
220      // 非表示フィールドやボタン、CF7 Conditional Fieldsで非表示のグループ内のフィールドはスキップ
221      if (
222        field.type === 'hidden' ||
223        field.type === 'submit' ||
224        field.name.startsWith('_wpcf7') ||
225        !field.name ||
226        this.isFieldInHiddenGroup(field)
227      ) {
228        return;
229      }
230
231      // getFieldLabel() はエスケープ済みのHTML文字列を返す(data-label-suffix を含む)
232      const labelHtml = this.getFieldLabel(field);
233      let value = '';
234
235      // チェックボックス
236      if (field.type === 'checkbox') {
237        // acceptance系のチェックボックスは表示しない
238        if (field.closest('.wpcf7-acceptance')) {
239          return;
240        }
241        value = field.checked ? 'チェック済み' : '';
242      }
243      // ラジオボタン
244      else if (field.type === 'radio') {
245        if (field.checked) {
246          value = field.nextElementSibling ? field.nextElementSibling.textContent : field.value;
247        } else {
248          return; // 選択されていないラジオボタンはスキップ
249        }
250      }
251      // セレクトボックス
252      else if (field.tagName === 'SELECT') {
253        const selectedOption = field.options[field.selectedIndex];
254        if (selectedOption) {
255          value = selectedOption.value; // まずvalueを取得
256          // valueが空の場合はスキップ
257          if (!value || value === '') {
258            return;
259          }
260          // 表示用にtextを使用
261          value = selectedOption.text;
262        }
263      }
264      // テキストエリア・テキストボックス
265      else {
266        value = field.value;
267      }
268
269      // 空の値の処理
270      if (!value || value.trim() === '') {
271        // フォームごとの設定に基づいて空の値を表示するかどうか決定
272        if (!this.settings.show_empty_fields) {
273          return; // 空の値は表示しない
274        }
275
276        // 空の値を表示する場合のテキスト(フォームごとの設定を使用)
277        value = this.settings.empty_text;
278      }
279
280      // テキストエリアの場合は改行を保持
281      const isTextarea = field.tagName === 'TEXTAREA';
282      const displayValue = this.escapeHtml(value);
283
284      html += `
285        <tr>
286          <th>${labelHtml}</th>
287          <td${isTextarea ? ' class="cf7-confirm-textarea"' : ''}>${displayValue}</td>
288        </tr>
289      `;
290    });
291
292    html += '</table>';
293    dataContainer.innerHTML = html;
294  }
295
296  /**
297   * フィールドのラベル(HTML文字列)を取得
298   *
299   * findLabelText() でベースのラベルテキストを取得した後、
300   * HTMLエスケープを行い、data-label-suffix(HTML可)を末尾に結合して返します。
301   *
302   * @param {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement} field - ラベルを取得するフォームフィールド
303   * @returns {string} HTMLとして安全に挿入できるラベル文字列
304   *
305   * @description
306   * - ベースとなるラベルテキストはエスケープされるため、XSSを防止できます
307   * - data-label-suffix の値はHTMLとしてそのまま結合されるため、`<span>`等で装飾可能です
308   * - data-label-suffix は信頼できる管理者のみが設定する前提です
309   *
310   * @example
311   * // フィールドのラベルを取得(HTML文字列が返る)
312   * const labelHtml = this.getFieldLabel(field);
313   */
314  getFieldLabel(field) {
315    const baseText = this.findLabelText(field);
316    const escapedBase = this.escapeHtml(baseText);
317
318    // data-label-suffix(祖先要素の属性)があれば末尾にHTMLとして結合
319    const suffixAncestor = field.closest('[data-label-suffix]');
320    const suffix = suffixAncestor ? (suffixAncestor.getAttribute('data-label-suffix') || '') : '';
321
322    return escapedBase + suffix;
323  }
324
325  /**
326   * フィールドに紐づくラベルのテキスト(プレーンテキスト)を取得
327   *
328   * フォームの構造に応じて複数の方法でラベルテキストを検索し、
329   * 優先順位に基づいて最初に見つかったテキストを返します。
330   *
331   * @param {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement} field - ラベルを取得するフォームフィールド
332   * @returns {string} フィールドに関連付けられたラベルテキスト(未エスケープのプレーンテキスト)
333   *
334   * @description
335   * 優先順位:
336   * - 0. 祖先要素の data-label 属性(最優先)
337   * - 1. for属性で関連付けられたlabelタグ
338   * - 2. 親要素内のlabelタグ(新しい構造)
339   * - 3. 親のlabel要素(古い構造)
340   * - 4. 直前のlabelタグ
341   * - 5. wpcf7-form-control-wrapの直前要素
342   * - 6. プレースホルダー
343   * - 7. name属性(最終手段)
344   */
345  findLabelText(field) {
346    // 0. 祖先要素の data-label 属性から取得(最優先)
347    const ancestor = field.closest('[data-label]');
348    if (ancestor) {
349      return ancestor.getAttribute('data-label') || '';
350    }
351
352    // 1. for属性で関連付けられたlabelタグから取得
353    const labelElement = this.form.querySelector(`label[for="${field.id}"]`);
354    if (labelElement) {
355      const clone = labelElement.cloneNode(true);
356      // 不要な要素を削除
357      clone.querySelectorAll('input, select, textarea, span.required, .wpcf7-form-control-wrap, [data-label-required]').forEach(el => el.remove());
358      let labelText = clone.textContent.trim();
359      if (labelText) {
360        return labelText;
361      }
362    }
363
364    // 2. 親のdivまたはpタグ内のlabelから取得(新しい構造に対応)
365    const parentContainer = field.closest('div, p');
366    if (parentContainer) {
367      const containerLabel = parentContainer.querySelector('label');
368      if (containerLabel) {
369        // labelのテキストのみを取得(子要素のテキストは除く)
370        const clone = containerLabel.cloneNode(true);
371        // 不要な要素を削除
372        clone.querySelectorAll('input, select, textarea, span.required, .wpcf7-form-control-wrap, [data-label-required]').forEach(el => el.remove());
373        let labelText = clone.textContent.trim();
374        if (labelText) {
375          return labelText;
376        }
377      }
378    }
379
380    // 3. 親のlabelから取得(古い構造の場合)
381    const parentLabel = field.closest('label');
382    if (parentLabel) {
383      const clone = parentLabel.cloneNode(true);
384      clone.querySelectorAll('input, select, textarea, span.required, .wpcf7-form-control-wrap, [data-label-required]').forEach(el => el.remove());
385      let labelText = clone.textContent.trim();
386      if (labelText) {
387        return labelText;
388      }
389    }
390
391    // 4. 直前のlabelタグから取得(親要素がない場合)
392    let previousElement = field.previousElementSibling;
393    while (previousElement) {
394      if (previousElement.tagName === 'LABEL') {
395        const clone = previousElement.cloneNode(true);
396        clone.querySelectorAll('input, select, textarea, span.required, .wpcf7-form-control-wrap, [data-label-required]').forEach(el => el.remove());
397        let labelText = clone.textContent.trim();
398        if (labelText) {
399          return labelText;
400        }
401      }
402      // CF7が挿入する<br>タグなどをスキップ
403      if (previousElement.tagName === 'BR' || previousElement.nodeType === Node.TEXT_NODE) {
404        previousElement = previousElement.previousElementSibling;
405      } else {
406        break;
407      }
408    }
409
410    // 5. wpcf7-form-control-wrapの直前のlabelを取得
411    const wrapper = field.closest('.wpcf7-form-control-wrap');
412    if (wrapper) {
413      let prevElement = wrapper.previousElementSibling;
414      while (prevElement) {
415        if (prevElement.tagName === 'LABEL') {
416          const clone = prevElement.cloneNode(true);
417          clone.querySelectorAll('input, select, textarea, span.required, .wpcf7-form-control-wrap, [data-label-required]').forEach(el => el.remove());
418          let labelText = clone.textContent.trim();
419          if (labelText) {
420            return labelText;
421          }
422        }
423        if (prevElement.tagName === 'BR' || prevElement.nodeType === Node.TEXT_NODE) {
424          prevElement = prevElement.previousElementSibling;
425        } else {
426          break;
427        }
428      }
429    }
430
431    // 6. プレースホルダーから取得
432    if (field.placeholder) {
433      return field.placeholder;
434    }
435
436    // 7. name属性から取得(最終手段)
437    return field.name || '';
438  }
439
440  /**
441   * フィールドがCF7 Conditional Fieldsで非表示のグループ内にあるか判定
442   *
443   * @param {HTMLElement} field - 判定するフォームフィールド
444   * @returns {boolean} 非表示グループ内ならtrue
445   */
446  isFieldInHiddenGroup(field) {
447    return field.closest('.wpcf7cf-hidden') !== null;
448  }
449
450  /**
451   * HTMLエスケープ
452   *
453   * このメソッドは、テキストをHTMLエスケープして
454   * XSS(クロスサイトスクリプティング)攻撃から保護します。
455   *
456   * @param {string} text - エスケープするテキスト
457   * @returns {string} エスケープされたHTML文字列
458   *
459   * @description
460   * - テキストをdiv要素に設定してからinnerHTMLを取得することで、
461   *   クリティカルなHTMLタグをエスケープします
462   * - これにより、ユーザー入力データがHTMLとして実行されるのを防ぎます
463   *
464   * @example
465   * // XSS保護のためのエスケープ
466   * const escaped = this.escapeHtml('<script>alert("XSS")</script>');
467   * // 戻り値: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;
468   */
469  escapeHtml(text) {
470    const div = document.createElement('div');
471    div.textContent = text;
472    return div.innerHTML;
473  }
474
475  /**
476   * CF7必須フィールドのバリデーション
477   *
478   * @returns {boolean} バリデーション成功ならtrue
479   */
480  validateRequiredFields() {
481    // CF7必須フィールド(wpcf7-validates-as-required)の空チェック
482    // CF7 Conditional Fieldsで非表示のグループ内のフィールドはスキップ
483    const requiredFields = this.form.querySelectorAll('.wpcf7-validates-as-required');
484    for (const field of requiredFields) {
485      if (this.isFieldInHiddenGroup(field)) continue;
486      if (field.type === 'checkbox' || field.type === 'radio') {
487        const name = field.name;
488        const checked = this.form.querySelector(`input[name="${name}"]:checked`);
489        if (!checked) return false;
490      } else {
491        if (!field.value || field.value.trim() === '') return false;
492      }
493    }
494
495    // CF7 acceptanceフィールド(未チェックの必須acceptance)
496    const uncheckedAcceptance = this.form.querySelectorAll('.wpcf7-acceptance:not(.optional) input[type="checkbox"]:not(:checked)');
497    if (uncheckedAcceptance.length > 0) return false;
498
499    return true;
500  }
501
502  /**
503   * モーダルを開く
504   */
505  openModal() {
506    this.modal.classList.add('is-open');
507    document.body.style.overflow = 'hidden';
508  }
509
510  /**
511   * モーダルを閉じる
512   */
513  closeModal() {
514    this.modal.classList.remove('is-open');
515    document.body.style.overflow = '';
516  }
517
518  /**
519   * 確認後の送信
520   */
521  confirmSubmit() {
522    this.isConfirming = true;
523
524    // モーダルを閉じる
525    this.closeModal();
526
527    // Contact Form 7の送信を実行
528    if (typeof wpcf7 !== 'undefined' && this.form.wpcf7) {
529      wpcf7.submit(this.form);
530    }
531
532    // フラグをリセット(送信後)
533    setTimeout(() => {
534      this.isConfirming = false;
535    }, 2000);
536  }
537
538  /**
539   * Contact Form 7のイベントリスナーを設定
540   *
541   * CF7のイベントは document に発火し、同一ページの複数フォームで共有されるため、
542   * event.detail.contactFormId で自フォーム宛てのイベントだけを処理する。
543   */
544  attachCF7EventListeners() {
545    const isOwnEvent = (e) => e && e.detail && e.detail.contactFormId === this.formId;
546
547    // 送信成功イベント
548    document.addEventListener('wpcf7mailsent', (e) => {
549      if (!isOwnEvent(e)) return;
550      this.closeModal();
551      this.isConfirming = false;
552    });
553
554    // バリデーションエラーイベント
555    document.addEventListener('wpcf7invalid', (e) => {
556      if (!isOwnEvent(e)) return;
557      this.closeModal();
558      this.isConfirming = false;
559    });
560
561    // スパム判定イベント
562    document.addEventListener('wpcf7spam', (e) => {
563      if (!isOwnEvent(e)) return;
564      this.closeModal();
565      this.isConfirming = false;
566    });
567
568    // 送信失敗イベント
569    document.addEventListener('wpcf7mailfailed', (e) => {
570      if (!isOwnEvent(e)) return;
571      this.closeModal();
572      this.isConfirming = false;
573    });
574
575    // 中断イベント
576    document.addEventListener('wpcf7aborted', (e) => {
577      if (!isOwnEvent(e)) return;
578      this.closeModal();
579      this.isConfirming = false;
580    });
581  }
582}
583
584// 初期化: ページ内のすべてのCF7フォームに対してインスタンスを生成
585document.addEventListener('DOMContentLoaded', () => {
586  const forms = document.querySelectorAll('.wpcf7-form');
587  forms.forEach((form) => new ContactFormConfirm(form));
588});

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.