PageSourceSearch

https://saikatei-jidaiya.jp/jidaiyaroom/js/main.js

js saikatei-jidaiya.jp collected 2026-10-02 17:17:39 UTC 36,343 bytes, 1,132 lines download raw bytes

1//===============================================================
2// メニュー制御用の関数とイベント設定(※バージョン2026-4|オーバーレイ対応版)
3//===============================================================
4$(function(){
5  //-------------------------------------------------
6  // 変数の宣言
7  //-------------------------------------------------
8  const $menubar = $('#menubar');
9  const $menubarHdr = $('#menubar_hdr');
10  const $overlay = $('#menubar-overlay');
11  const breakPoint = 900;	// ここがブレイクポイント指定箇所です
12
13  // ▼ここを切り替えるだけで 2パターンを使い分け!
14  //   false → “従来どおり”
15  //   true  → “ハンバーガーが非表示の間は #menubar も非表示”
16  const HIDE_MENUBAR_IF_HDR_HIDDEN = false;
17
18  // タッチデバイスかどうかの判定
19  const isTouchDevice = ('ontouchstart' in window) ||
20                       (navigator.maxTouchPoints > 0) ||
21                       (navigator.msMaxTouchPoints > 0);
22
23  //-------------------------------------------------
24  // debounce(処理の呼び出し頻度を抑制) 関数
25  //-------------------------------------------------
26  function debounce(fn, wait) {
27    let timerId;
28    return function(...args) {
29      if (timerId) {
30        clearTimeout(timerId);
31      }
32      timerId = setTimeout(() => {
33        fn.apply(this, args);
34      }, wait);
35    };
36  }
37
38  //-------------------------------------------------
39  // メニューを閉じる共通関数
40  // ※ハンバーガー解除・メニュー非表示・オーバーレイ非表示・
41  //   noscroll解除・ドロップダウン閉じ を一括で行う
42  //-------------------------------------------------
43  function closeMenu() {
44    $menubarHdr.removeClass('ham');
45    $menubar.hide();
46    $overlay.hide();
47    $menubar.find('.ddmenu_parent ul').hide();
48    $('body').removeClass('noscroll');
49  }
50
51  //-------------------------------------------------
52  // メニューを開く共通関数
53  //-------------------------------------------------
54  function openMenu() {
55    $menubarHdr.addClass('ham');
56    $menubar.show();
57    $overlay.show();
58    $menubar.find('.ddmenu_parent ul').hide();
59    if ($(window).width() < breakPoint) {
60      $('body').addClass('noscroll');
61    }
62  }
63
64  //-------------------------------------------------
65  // ドロップダウン用の初期化関数
66  //-------------------------------------------------
67  function initDropdown($menu, isTouch) {
68    // ドロップダウンメニューが存在するliにクラス追加
69    $menu.find('ul li').each(function() {
70      if ($(this).find('ul').length) {
71        $(this).addClass('ddmenu_parent');
72        $(this).children('a').addClass('ddmenu');
73      }
74    });
75
76    // 子メニューは初期状態で閉じる(ちらつき防止)
77    $menu.find('.ddmenu_parent ul').hide();
78
79    // 万一の再初期化に備えてイベントを解除(多重バインド防止)
80    $menu.find('.ddmenu').off('click.ddmenu');
81    $menu.find('.ddmenu_parent').off('mouseenter.ddmenu mouseleave.ddmenu');
82
83    //---------------------------------------------
84    // ▼ブレイクポイント未満(開閉メニュー時)は
85    //   PCでも「クリックで開閉」に統一(hover無効)
86    //---------------------------------------------
87    $menu.find('.ddmenu').on('click.ddmenu', function(e) {
88      if (!isTouch && $(window).width() >= breakPoint) return; // PC大画面はhover運用
89
90      e.preventDefault();
91      e.stopPropagation();
92
93      const $dropdownMenu = $(this).siblings('ul');
94      if ($dropdownMenu.is(':visible')) {
95        $dropdownMenu.hide();
96      } else {
97        $menu.find('.ddmenu_parent ul').hide(); // 他を閉じる
98        $dropdownMenu.show();
99      }
100    });
101
102    //---------------------------------------------
103    // ▼PC大画面(breakPoint以上)のみ hover で開閉
104    //---------------------------------------------
105    $menu.find('.ddmenu_parent').on('mouseenter.ddmenu', function() {
106      if (isTouch) return;
107      if ($(window).width() < breakPoint) return; // 開閉メニュー時はhover無効
108      $(this).children('ul').show();
109    }).on('mouseleave.ddmenu', function() {
110      if (isTouch) return;
111      if ($(window).width() < breakPoint) return; // 開閉メニュー時はhover無効
112      $(this).children('ul').hide();
113    });
114  }
115
116  //-------------------------------------------------
117  // ハンバーガーメニューでの開閉制御関数
118  //-------------------------------------------------
119  function initHamburger($hamburger) {
120    let isAnimating = false;	// 連打防止用フラグ
121    $hamburger.on('click', function() {
122      if (isAnimating) return;	// アニメーション中は何もしない
123      isAnimating = true;
124
125      if ($(this).hasClass('ham')) {
126        // 開いている → 閉じる
127        closeMenu();
128      } else {
129        // 閉じている → 開く
130        openMenu();
131      }
132
133      // メニューのCSSアニメーション(0.2s)完了後にロック解除
134      setTimeout(function() { isAnimating = false; }, 300);
135    });
136  }
137
138  //-------------------------------------------------
139  // オーバーレイクリックでメニューを閉じる
140  //-------------------------------------------------
141  $overlay.on('click', function() {
142    closeMenu();
143  });
144
145  //-------------------------------------------------
146  // レスポンシブ時の表示制御 (リサイズ時)
147  //-------------------------------------------------
148  const handleResize = debounce(function() {
149    const windowWidth = $(window).width();
150
151    // bodyクラスの制御 (small-screen / large-screen)
152    if (windowWidth < breakPoint) {
153      $('body').removeClass('large-screen').addClass('small-screen');
154    } else {
155      $('body').removeClass('small-screen').addClass('large-screen');
156      // PC表示になったら、ハンバーガー解除 + メニュー・オーバーレイを閉じる
157      $menubarHdr.removeClass('ham');
158      $menubar.find('.ddmenu_parent ul').hide();
159      $overlay.hide();
160      $('body').removeClass('noscroll');
161
162      // ▼ #menubar を表示するか/しないかの切り替え
163      if (HIDE_MENUBAR_IF_HDR_HIDDEN) {
164        $menubarHdr.hide();
165        $menubar.hide();
166      } else {
167        $menubarHdr.hide();
168        $menubar.show();
169      }
170    }
171
172    // スマホ(ブレイクポイント未満)のとき
173    if (windowWidth < breakPoint) {
174      $menubarHdr.show();
175      if (!$menubarHdr.hasClass('ham')) {
176        $menubar.hide();
177        $overlay.hide();
178        $('body').removeClass('noscroll');
179      }
180    }
181  }, 200);
182
183  //-------------------------------------------------
184  // 初期化
185  //-------------------------------------------------
186  // 1) ドロップダウン初期化 (#menubar)
187  initDropdown($menubar, isTouchDevice);
188
189  // 2) ハンバーガーメニュー初期化 (#menubar_hdr)
190  initHamburger($menubarHdr);
191
192  // 3) レスポンシブ表示の初期処理 & リサイズイベント
193  handleResize();
194  $(window).on('resize', handleResize);
195
196  //-------------------------------------------------
197  // アンカーリンク(#)のクリックイベント
198  //-------------------------------------------------
199  $menubar.find('a[href^="#"]').on('click', function() {
200    // ドロップダウンメニューの親(a.ddmenu)のリンクはメニューを閉じない
201    if ($(this).hasClass('ddmenu')) return;
202
203    // スマホ表示&ハンバーガーが開いている状態なら閉じる
204    if ($menubarHdr.is(':visible') && $menubarHdr.hasClass('ham')) {
205      closeMenu();
206    }
207  });
208
209  //-------------------------------------------------
210  // 「header nav」など別メニューにドロップダウンだけ適用したい場合
211  //-------------------------------------------------
212  // 例:header nav へドロップダウンだけ適用(ハンバーガー連動なし)
213  //initDropdown($('header nav'), isTouchDevice);
214});
215
216
217//===============================================================
218// スムーススクロール(※バージョン2025-3)
219// 通常タイプ / fixedヘッダー対応 切り替え版
220//===============================================================
221$(function() {
222
223    //===========================================================
224    // 設定
225    //===========================================================
226    // 'normal' = 通常タイプ(固定ヘッダーなし)
227    // 'fixed' = fixedヘッダー対応
228    var scrollType = 'normal';
229
230    // fixedヘッダー時に位置計算に使う要素(※fixed版を使う際は必ずチェック。画面上部に貼り付くブロックを指定する。)
231    // 例:'header' / '#header' / '.site-header'
232    var fixedHeaderSelector = '#menubar';
233
234    // ページ上部へ戻るボタンのセレクター
235    var topButton = $('.pagetop');
236
237    // ページトップボタン表示用のクラス名
238    var scrollShow = 'pagetop-show';
239
240
241    //===========================================================
242    // fixedヘッダーぶんの補正値を取得
243    //===========================================================
244    function getHeaderOffset() {
245
246        // 通常タイプなら補正なし
247        if(scrollType !== 'fixed') {
248            return 0;
249        }
250
251        // 指定要素を取得
252        var $header = $(fixedHeaderSelector);
253
254        // 要素がなければ補正なし
255        if(!$header.length) {
256            return 0;
257        }
258
259        // 画面上でのヘッダー下端位置を取得
260        // 高さ + 上部の余白(topやmarginで見た目上ずれている分)も含めて見られる
261        var rect = $header.get(0).getBoundingClientRect();
262
263        // 念のためマイナスは0にする
264        return Math.max(0, rect.bottom);
265    }
266
267
268    //===========================================================
269    // スムーススクロール本体
270    //===========================================================
271    function smoothScroll(target) {
272
273        var scrollTo = 0;
274
275        // '#' の場合はページ最上部へ
276        if(target === '#') {
277            scrollTo = 0;
278
279        } else {
280
281            // スクロール先の要素を取得
282            var $target = $(target);
283
284            // 対象が存在しない場合は何もしない
285            if(!$target.length) {
286                return;
287            }
288
289            // 通常位置から、fixedヘッダー分を引く
290            scrollTo = $target.offset().top - getHeaderOffset();
291
292            // 0未満にならないように補正
293            if(scrollTo < 0) {
294                scrollTo = 0;
295            }
296        }
297
298        // アニメーションでスムーススクロール
299        $('html, body').animate({scrollTop: scrollTo}, 500);
300    }
301
302	//===========================================================
303	// ページ内リンク / ページトップボタン
304	//===========================================================
305	$('a[href^="#"], .pagetop').click(function(e) {
306
307		// hrefが無い.pagtopでも '#' 扱いにする
308		var id = $(this).attr('href') || '#';
309
310		// .pagetop 以外の href="#" は無視(その場に止める)
311		if(id === '#' && !$(this).hasClass('pagetop')) {
312			e.preventDefault();
313			return;
314		}
315
316		e.preventDefault();
317		smoothScroll(id);
318	});
319
320    //===========================================================
321    // ページトップボタンの表示切り替え
322    //===========================================================
323    $(topButton).hide();
324
325    $(window).scroll(function() {
326        if($(this).scrollTop() >= 300) {
327            $(topButton).fadeIn().addClass(scrollShow);
328        } else {
329            $(topButton).fadeOut().removeClass(scrollShow);
330        }
331    });
332
333
334    //===========================================================
335    // ハッシュ付きURLで開いた時
336    //===========================================================
337    if(window.location.hash) {
338        $('html, body').scrollTop(0);
339
340        setTimeout(function() {
341            smoothScroll(window.location.hash);
342        }, 500);
343    }
344
345});
346
347
348//===============================================================
349// スライドショー(ズーム)
350//===============================================================
351$(function() {
352  $('.mainimg').each(function() {
353    var $root = $(this);
354    var slides = $root.find('.slide');
355    var slideCount = slides.length;
356    var currentIndex = 0;
357
358    // CSSの transition から "opacity" の時間(ms)を取得(delayも加算)
359    function cssTimeToMs(v) {
360      v = (v || '').toString().trim();
361      if (!v) return 0;
362      if (v.indexOf('ms') > -1) return Math.round(parseFloat(v));
363      if (v.indexOf('s') > -1)  return Math.round(parseFloat(v) * 1000);
364      var n = parseFloat(v);
365      return isNaN(n) ? 0 : Math.round(n);
366    }
367
368    function getTransitionMs(el, propName) {
369      var st = window.getComputedStyle(el);
370      var props = (st.transitionProperty || '').split(',');
371      var durs  = (st.transitionDuration || '').split(',');
372      var dels  = (st.transitionDelay || '').split(',');
373
374      function pick(list, i) {
375        if (!list.length) return '0s';
376        return (list[i] !== undefined ? list[i] : list[list.length - 1]).trim();
377      }
378
379      for (var i = 0; i < props.length; i++) {
380        var p = props[i].trim();
381        if (p === propName || p === 'all') {
382          return cssTimeToMs(pick(durs, i)) + cssTimeToMs(pick(dels, i));
383        }
384      }
385      return 0;
386    }
387
388    var INTERVAL = 5000; // 自動切替の間隔(ms)
389
390    // CSS側の transition: opacity ... を自動で拾う(取れなã
390‘れば1000ms)
391    var FADE_MS = getTransitionMs(slides.get(0), 'opacity');
392    if (!FADE_MS) FADE_MS = 1000;
393
394    // 切替完了まで拡大を続ける
395    var ZOOM_MS = INTERVAL + FADE_MS;
396
397    var autoTimer = null;
398    var isAnimating = false;
399
400    // インジケータ作成
401    var $indicators = $root.find('.slide-indicators').empty();
402    for (var i = 0; i < slideCount; i++) {
403      $indicators.append('<span class="indicator" data-index="' + i + '"></span>');
404    }
405    var $dots = $indicators.find('.indicator');
406
407    // 拡大(Ken Burns)用:拡大時間をJS側で揃える
408    slides.find('img').css('transition', 'transform ' + ZOOM_MS + 'ms linear');
409
410    function resetZoom($slide) {
411      var $img = $slide.find('img');
412      $img.css('transition', 'none');         // リセットは瞬時に(見えない所で戻す)
413      $slide.removeClass('zoom');             // CSS側の scale(1) に戻す
414      if ($img[0]) { $img[0].offsetHeight; }  // reflow
415      $img.css('transition', 'transform ' + ZOOM_MS + 'ms linear');
416    }
417
418    function startZoom($slide) {
419      window.requestAnimationFrame(function() {
420        $slide.addClass('zoom');
421      });
422    }
423
424    // 初期表示
425    slides.css('opacity', 0).removeClass('active');
426    slides.each(function() { resetZoom($(this)); });
427
428    slides.eq(0).css('opacity', 1).addClass('active');
429    $dots.removeClass('active').eq(0).addClass('active');
430    startZoom(slides.eq(0));
431
432    function setActive(nextIndex) {
433      if (nextIndex === currentIndex) return;
434      isAnimating = true;
435
436      var $current = slides.eq(currentIndex);
437      var $next = slides.eq(nextIndex);
438
439      // 次スライドは必ずscale(1)から開始
440      resetZoom($next);
441
442      // フェード(退場側のzoomはフェード完了まで維持)
443      $current.css('opacity', 0).removeClass('active');
444      $next.css('opacity', 1).addClass('active');
445
446      // 次スライドの拡大を開始(フェード中も拡大)
447      startZoom($next);
448
449      // インジケータ
450      $dots.eq(currentIndex).removeClass('active');
451      $dots.eq(nextIndex).addClass('active');
452
453      var prevIndex = currentIndex;
454      currentIndex = nextIndex;
455
456      // フェード完了後に退場側だけリセット(見えないので違和感が出ない)
457      setTimeout(function() {
458        resetZoom(slides.eq(prevIndex));
459        isAnimating = false;
460      }, FADE_MS);
461    }
462
463    function next() {
464      var n = (currentIndex + 1) % slideCount;
465      setActive(n);
466      restartTimer();
467    }
468
469    function restartTimer() {
470      clearTimeout(autoTimer);
471      autoTimer = setTimeout(next, INTERVAL);
472    }
473
474    // クリックで移動 → タイマーをリセット
475    $dots.on('click', function() {
476      var to = $(this).data('index');
477      if (isAnimating) return;
478      if (to === currentIndex) {
479        return restartTimer();
480      }
481      setActive(to);
482      restartTimer();
483    });
484
485    // 自動再生開始
486    restartTimer();
487  });
488});
489
490
491//===============================================================
492// サムネイルの横スライドショー(複数設置対応)
493//===============================================================
494$(function() {
495
496	var slideDuration = 1000;      // アニメーション時間(ミリ秒)
497	var autoSlideInterval = 4000;  // 自動スライドの間隔(ミリ秒)
498
499	//----------------------------
500	// スライダー1個を初期化
501	//----------------------------
502	function initOneSlider($slider) {
503
504		// ★追加:.list1.list-autoモード時のDOM準備
505		//   (1) inviewはスライドショーと相性が悪いので、関連クラス&イベントを除去
506		//       (inviewが付いた要素はopacity:0で待機し、画面外に出ると非表示が継続して
507		//         スライドの2周目以降が消えてしまう為)
508		$slider.find('.list').each(function() {
509			$(this)
510				.off('inview')
511				.removeClass('inview up upstyle down downstyle transform1 transform1style transform2 transform2style transform3 transform3style blur blurstyle');
512		});
513		//   (2) .imgラッパーが無ければ自動挿入
514		if ($slider.children('.img').length === 0) {
515			$slider.children('.list').wrapAll('<div class="img"></div>');
516		}
517		//   (3) .slide-indicators-partsが無ければ追加
518		if ($slider.children('.slide-indicators-parts').length === 0) {
519			$slider.append('<div class="slide-indicators-parts"></div>');
520		}
521
522		var imagesPerView, slideBy;
523		var isAnimating = false;
524		var currentImageIndex = 0;
525		var pendingIndex = null;	// アニメ中クリックの「予約」
526
527		var $imgParts = $slider.find('.img');
528		var $indicatorsArea = $slider.find('.slide-indicators-parts');
529
530		// 既存タイマー停止(このスライダーだけ)
531		stopAutoSlide();
532
533		// 既存のクローンとインジケータ削除(このスライダーだけ)
534		$imgParts.find('.clone').remove();
535		$indicatorsArea.empty();
536
537		// クローン削除後に元要素を取り直す
538		var $divs = $imgParts.children('div').not('.clone');
539		var totalImages = $divs.length;
540
541		// 画像が1個以下なら何もしない
542		if (totalImages <= 1) {
543			$imgParts.css({
544				'transition': 'none',
545				'transform': 'translateX(0)'
546			});
547			return;
548		}
549
550		var windowWidth = $(window).width();
551
552		if (windowWidth >= 801) {
553			imagesPerView = 4;
554			slideBy = 2;
555		} else {
556			imagesPerView = 2;
557			slideBy = 1;
558		}
559
560		// 表示枚数以下ならスライド不要(インジケータも作らない)
561		if (totalImages <= imagesPerView) {
562			$imgParts.css({
563				'transition': 'none',
564				'transform': 'translateX(0)'
565			});
566			return;
567		}
568
569		// 無限ループ用クローン追加
570		$divs.clone().addClass('clone').appendTo($imgParts);
571
572		// インジケータ生成
573		var totalSlides = Math.ceil(totalImages / slideBy);
574		for (var i = 0; i < totalSlides; i++) {
575			$indicatorsArea.append('<span class="indicator" data-index="' + (i * slideBy) + '"></span>');
576		}
577		var $indicatorItems = $indicatorsArea.find('.indicator');
578
579		function updateIndicators() {
580			var activeIndex = Math.floor(currentImageIndex / slideBy) % totalSlides;
581			$indicatorItems.removeClass('active');
582			$indicatorItems.eq(activeIndex).addClass('active');
583		}
584
585		function slideTo(index) {
586
587			// アニメ中は動かさない(クリックは予約で処理)
588			if (isAnimating) return;
589
590			isAnimating = true;
591			currentImageIndex = index;
592
593			$imgParts.css({
594				'transition': 'transform ' + (slideDuration / 1000) + 's ease',
595				'transform': 'translateX(' + (-currentImageIndex * (100 / imagesPerView)) + '%)'
596			});
597
598			updateIndicators();
599
600			setTimeout(function() {
601
602				// ループ処理
603				if (currentImageIndex >= totalImages) {
604
605					$imgParts.css('transition', 'none');
606					$imgParts.css('transform', 'translateX(0)');
607
608					currentImageIndex = 0;
609					updateIndicators();
610
611					// 再描画を強制
612					$imgParts[0].offsetHeight;
613
614					$imgParts.css('transition', 'transform ' + (slideDuration / 1000) + 's ease');
615				}
616
617				isAnimating = false;
618
619				// ★アニメ中にクリックされた予約があれば、ここで実行
620				if (pendingIndex !== null) {
621					var next = pendingIndex;
622					pendingIndex = null;
623
624					slideTo(next);
625
626					// 自動再生を再開(startは内部で必ずstopするので多重にならない)
627					startAutoSlide();
628				}
629
630			}, slideDuration);
631		}
632
633		function startAutoSlide() {
634
635			// ★速度が上がる原因(タイマー多重)を根本的に潰す
636			stopAutoSlide();
637
638			var interval = setInterval(function() {
639				slideTo(currentImageIndex + slideBy);
640			}, autoSlideInterval);
641
642			$slider.data('interval', interval);
643		}
644
645		function stopAutoSlide() {
646			var interval = $slider.data('interval');
647			if (interval) {
648				clearInterval(interval);
649				$slider.removeData('interval');
650			}
651		}
652
653		// 初期位置
654		$imgParts.css({
655			'transition': 'none',
656			'transform': 'translateX(0)'
657		});
658		updateIndicators();
659
660		// 自動再生開始
661		startAutoSlide();
662
663		// hover停止(この機能のイベントだけ外せるように名前空間)
664		$slider.off('.auto1')
665			.on('mouseenter.auto1', function() {
666				stopAutoSlide();
667			})
668			.on('mouseleave.auto1', function() {
669				startAutoSlide();
670			});
671
672		// インジケータクリック
673		$indicatorItems.off('.auto1').on('click.auto1', function() {
674
675			var index = $(this).data('index');
676
677			// まず止める(多重タイマー予防)
678			stopAutoSlide();
679
680			// アニメ中なら「予約」して終わり(終わった瞬間に実行される)
681			if (isAnimating) {
682				pendingIndex = index;
683				return;
684			}
685
686			slideTo(index);
687			startAutoSlide();
688
689		});
690
691	}
692
693	//----------------------------
694	// 全スライダーを初期化
695	//----------------------------
696	function initAllSliders() {
697		$('.list-auto').each(function() {
698			initOneSlider($(this));
699		});
700	}
701
702	// 初期化
703	initAllSliders();
704
705	// リサイズで再初期化
706	var resizeTimer;
707	$(window).off('.auto1resize').on('resize.auto1resize', function() {
708		clearTimeout(resizeTimer);
709		resizeTimer = setTimeout(function() {
710			initAllSliders();
711		}, 250);
712	});
713
714});
715
716
717//===============================================================
718// form-simple
719//===============================================================
720(function() {
721    "use strict";
722
723    function initAjaxForm(form) {
724        var actionUrl = form.getAttribute("action") || "form.php";
725        var submitButton = form.querySelector('button[type="submit"]');
726        var globalErrorBox = findSiblingBox(form, ".form-global-error");
727        var successBox = findSiblingBox(form, ".form-success-box");
728        var privacyRow = form.querySelector(".js-privacy-row");
729        var attachmentRow = form.querySelector(".js-attachment-row");
730        var attachmentInput = attachmentRow ? attachmentRow.querySelector('input[type="file"]') : null;
731        var csrfInput = form.querySelector('input[name="csrf_token"]');
732
733        if (!submitButton || !csrfInput) {
734            return;
735        }
736
737        initDateSelectGroups(form);
738
739        function applyPreviewFallback() {
740            var previewPrivacy = form.getAttribute("data-preview-privacy") === "true";
741            var previewAttachment = form.getAttribute("data-preview-attachment") === "true";
742
743            togglePrivacyRow(previewPrivacy);
744            toggleAttachmentRow(previewAttachment);
745        }
746
747        fetchInitData();
748
749        form.addEventListener("submit", function(event) {
750            event.preventDefault();
751
752            /* ローカルの通常プレビュー(file://)では送信しない */
753            if (window.location.protocol === "file:") {
754                return;
755            }
756
757            clearErrors();
758            hideGlobalError();
759
760            submitButton.disabled = true;
761
762            var hadSelectedFile = attachmentInput && attachmentInput.files && attachmentInput.files.length > 0;
763            var formData = new FormData(form);
764
765            fetch(actionUrl, {
766                method: "POST",
767                body: formData,
768                headers: {
769                    "X-Requested-With": "XMLHttpRequest"
770                }
771            })
772            .then(function(response) {
773                return response.json();
774            })
775            .then(function(json) {
776                if (json && json.csrfToken) {
777                    csrfInput.value = json.csrfToken;
778                }
779
780                if (json && json.ok) {
781                    showSuccess();
782                    return;
783                }
784
785                resetAttachmentOnError(hadSelectedFile);
786
787                if (json && json.fieldErrors) {
788                    applyFieldErrors(json.fieldErrors);
789                }
790
791                if (json && json.formError) {
792                    showGlobalError(json.formError);
793                }
794            })
795            .catch(function() {
796                resetAttachmentOnError(false);
797                showGlobalError("送信に失敗しました。時間をおいて再度お試しください。");
798            })
799            .finally(function() {
800                submitButton.disabled = false;
801            });
802        });
803
804        function fetchInitData() {
805            /* ローカルの通常プレビュー(file://)では初期化通信を行わず、HTML側のプレビュー設定を使う */
806            if (window.location.protocol === "file:") {
807                applyPreviewFallback();
808                return;
809            }
810
811            fetch(actionUrl + "?action=init", {
812                method: "GET",
813                headers: {
814                    "X-Requested-With": "XMLHttpRequest"
815                }
816            })
817            .then(function(response) {
818                /* form.php が存在しない(404等)場合はプレビューモードにフォールバック */
819                if (!response.ok) {
820                    applyPreviewFallback();
821                    return;
822                }
823                return response.json();
824            })
825            .then(function(json) {
826                if (!json) {
827                    return;
828                }
829
830                if (!json.ok) {
831                    showGlobalError("フォームの初期化に失敗しました。ページを再読み込みしてください。", false);
832                    return;
833                }
834
835                if (json.csrfToken) {
836                    csrfInput.value = json.csrfToken;
837                }
838
839                togglePrivacyRow(json.privacyConsentEnabled !== false);
840                toggleAttachmentRow(json.attachmentEnabled !== false);
841            })
842            .catch(function() {
843                /* ネットワークエラーやJSONパース失敗時もプレビューモードにフォールバック */
844                applyPreviewFallback();
845            });
846        }
847
848        function togglePrivacyRow(isEnabled) {
849            if (!privacyRow) {
850                return;
851            }
852
853            privacyRow.hidden = !isEnabled;
854
855            var privacyInput = privacyRow.querySelector('input[name="privacy_consent"]');
856            if (!privacyInput) {
857                return;
858            }
859
860            if (!isEnabled) {
861                privacyInput.checked = false;
862            }
863
864            privacyInput.disabled = !isEnabled;
865        }
866
867        function toggleAttachmentRow(isEnabled) {
868            if (!attachmentRow) {
869                return;
870            }
871
872            attachmentRow.hidden = !isEnabled;
873
874            if (!attachmentInput) {
875                return;
876            }
877
878            if (!isEnabled) {
879                attachmentInput.value = "";
880            }
881
882            attachmentInput.disabled = !isEnabled;
883        }
884
885        function clearErrors() {
886            var rows = form.querySelectorAll(".form-row");
887            rows.forEach(function(row) {
888                row.classList.remove("is-error");
889
890                row.querySelectorAll(".form-error-text, .form-file-reset-text").forEach(function(message) {
891                    message.remove();
892                });
893            });
894        }
895
896        function applyFieldErrors(fieldErrors) {
897            var firstRow = null;
898            var firstInput = null;
899
900            Object.keys(fieldErrors).forEach(function(fieldName) {
901                var input = form.querySelector('[name="' + cssEscape(fieldName) + '"]') ||
902                            form.querySelector('[name="' + cssEscape(fieldName) + '[]"]');
903                var row = input ? input.closest(".form-row") : null;
904
905                if (!row) {
906                    row = form.querySelector('.js-date-select-group[data-field-name="' + cssEscape(fieldName) + '"]');
907                    if (row) {
908                        input = row.querySelector("select, input, textarea");
909                    }
910                }
911
912                if (!row) {
913                    return;
914                }
915
916                row.classList.add("is-error");
917
918                var error = document.createElement("p");
919                error.className = "form-error-text";
920                error.textContent = fieldErrors[fieldName];
921                row.appendChild(error);
922
923                if (!firstRow) {
924                    firstRow = row;
925                    firstInput = input || null;
926                }
927            });
928
929            if (firstInput && typeof firstInput.focus === "function") {
930                try {
931                    firstInput.focus({ preventScroll: true });
932                } catch (error) {
933                    firstInput.focus();
934                }
935            }
936
937            if (firstRow) {
938                firstRow.scrollIntoView({
939                    behavior: "smooth",
940                    block: "center"
941                });
942            }
943        }
944
945        function resetAttachmentOnError(hadSelectedFile) {
946            if (!attachmentRow || !attachmentInput) {
947                return;
948            }
949
950            if (attachmentInput.value !== "") {
951                attachmentInput.value = "";
952            }
953
954            if (!hadSelectedFile) {
955                return;
956            }
957
958            var note = document.createElement("p");
959            note.className = "form-file-reset-text";
960            note.textContent = "安全のため、添付ファイルは再選択してください。";
961            attachmentRow.appendChild(note);
962        }
963
964        function showGlobalError(message, shouldScroll) {
965            if (!globalErrorBox) {
966                return;
967            }
968
969            if (typeof shouldScroll === "undefined") {
970                shouldScroll = true;
971            }
972
973            globalErrorBox.textContent = message;
974            globalErrorBox.hidden = false;
975
976            if (shouldScroll) {
977                globalErrorBox.scrollIntoView({
978                    behavior: "smooth",
979                    block: "center"
980                });
981            }
982        }
983
984        function hideGlobalError() {
985            if (!globalErrorBox) {
986                return;
987            }
988
989            globalErrorBox.hidden = true;
990            globalErrorBox.textContent = "";
991        }
992
993        function showSuccess() {
994            form.hidden = true;
995            hideGlobalError();
996
997            if (successBox) {
998                successBox.hidden = false;
999                successBox.scrollIntoView({
1000                    behavior: "smooth",
1001                    block: "center"
1002                });
1003            }
1004        }
1005    }
1006
1007    function initDateSelectGroups(scope) {
1008        var groups = scope.querySelectorAll(".js-date-select-group");
1009
1010        groups.forEach(function(group) {
1011            var mode = group.getAttribute("data-date-mode");
1012            var yearSelect = group.querySelector('[data-date-part="year"]');
1013            var monthSelect = group.querySelector('[data-date-part="month"]');
1014            var daySelect = group.querySelector('[data-date-part="day"]');
1015            var startYear = parseInt(group.getAttribute("data-year-start"), 10);
1016            var endYear = parseInt(group.getAttribute("data-year-end"), 10);
1017
1018            if (monthSelect) {
1019                populateNumberSelect(monthSelect, 1, 12, "月を選択", "月");
1020            }
1021
1022            if (yearSelect) {
1023                if (isNaN(startYear)) {
1024                    startYear = 1900;
1025                }
1026                if (isNaN(endYear)) {
1027                    endYear = new Date().getFullYear();
1028                }
1029
1030                populateNumberSelect(yearSelect, startYear, endYear, "年を選択", "年", true);
1031            }
1032
1033            if (daySelect) {
1034                updateDayOptions();
1035            }
1036
1037            if (monthSelect) {
1038                monthSelect.addEventListener("change", updateDayOptions);
1039            }
1040
1041            if (yearSelect && mode === "ymd") {
1042                yearSelect.addEventListener("change", updateDayOptions);
1043            }
1044
1045            function updateDayOptions() {
1046                if (!daySelect) {
1047                    return;
1048                }
1049
1050                var maxDay = 31;
1051                var month = monthSelect ? parseInt(monthSelect.value, 10) : NaN;
1052                var year = yearSelect ? parseInt(yearSelect.value, 10) : new Date().getFullYear();
1053
1054                if (!isNaN(month)) {
1055                    if (isNaN(year)) {
1056                        year = new Date().getFullYear();
1057                    }
1058                    maxDay = new Date(year, month, 0).getDate();
1059                }
1060
1061                populateNumberSelect(daySelect, 1, maxDay, "日を選択", "日");
1062            }
1063        });
1064    }
1065
1066    function populateNumberSelect(select, start, end, placeholder, suffix, descending) {
1067        if (!select) {
1068            return;
1069        }
1070
1071        var currentValue = select.value;
1072        var numbers = [];
1073        var i;
1074
1075        if (descending) {
1076            for (i = end; i >= start; i -= 1) {
1077                numbers.push(i);
1078            }
1079        } else {
1080            for (i = start; i <= end; i += 1) {
1081                numbers.push(i);
1082            }
1083        }
1084
1085        select.innerHTML = "";
1086
1087        var placeholderOption = document.createElement("option");
1088        placeholderOption.value = "";
1089        placeholderOption.textContent = placeholder;
1090        select.appendChild(placeholderOption);
1091
1092        numbers.forEach(function(number) {
1093            var option = document.createElement("option");
1094            option.value = String(number);
1095            option.textContent = String(number) + suffix;
1096            select.appendChild(option);
1097        });
1098
1099        if (currentValue !== "" && hasOptionValue(select, currentValue)) {
1100            select.value = currentValue;
1101        } else {
1102            select.value = "";
1103        }
1104    }
1105
1106    function hasOptionValue(select, value) {
1107        var options = Array.prototype.slice.call(select.options);
1108        return options.some(function(option) {
1109            return option.value === value;
1110        });
1111    }
1112
1113    function findSiblingBox(form, selector) {
1114        var parent = form.parentElement;
1115        if (!parent) {
1116            return null;
1117        }
1118        return parent.querySelector(selector);
1119    }
1120
1121    function cssEscape(value) {
1122        if (window.CSS && typeof window.CSS.escape === "function") {
1123            return window.CSS.escape(value);
1124        }
1125        return String(value).replace(/"/g, '\\"');
1126    }
1127
1128    document.addEventListener("DOMContentLoaded", function() {
1129        var forms = document.querySelectorAll(".js-ajax-form");
1130        forms.forEach(initAjaxForm);
1131    });
1132})();

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.