PageSourceSearch

https://www.m2j.co.jp/assets/js/top/hp-top.js?1790384371

js m2j.co.jp collected 2026-09-27 20:56:16 UTC 14,309 bytes, 379 lines download raw bytes

1// Hero banner carousel:
2//  • Desktop (≥761px): manual carousel with prev/next arrows.
3//  • SP (≤760px): continuous "conveyor belt" — autoplay (delay:0 + freeMode)
4//    scrolls it like a ticker, but touch/drag stays enabled so it can still
5//    be flicked by hand; autoplay resumes after the drag.
6// Rebuilt on breakpoint change so rotating / resizing switches modes.
7document.addEventListener("DOMContentLoaded", function () {
8  var el = document.querySelector(".hero-swiper");
9  if (!el || typeof Swiper === "undefined") return;
10  var mq = window.matchMedia("(min-width: 761px)");
11  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
12
13  function buildHeroCarousel() {
14    if (el.swiper) el.swiper.destroy(true, true);
15    if (mq.matches) {
16      new Swiper(el, {
17        slidesPerView: "auto",
18        spaceBetween: 13,
19        loop: true,
20        speed: 500,
21        watchOverflow: false,
22        allowTouchMove: true,
23        navigation: { prevEl: "#heroCarPrev", nextEl: "#heroCarNext" },
24      });
25    } else {
26      new Swiper(el, {
27        slidesPerView: "auto",
28        spaceBetween: 8,
29        loop: true,
30        // 2026/08/06 SP:左端寄せではなく中央にスライドを配置(左右に隣が覗く)
31        centeredSlides: true,
32        allowTouchMove: true,
33        grabCursor: true,
34        speed: reduce ? 600 : 4000,
35        freeMode: { enabled: true, momentum: true },
36        autoplay: reduce
37          ? false
38          : { delay: 0, disableOnInteraction: false, pauseOnMouseEnter: false },
39      });
40    }
41  }
42  buildHeroCarousel();
43  if (mq.addEventListener) mq.addEventListener("change", buildHeroCarousel);
44  else if (mq.addListener) mq.addListener(buildHeroCarousel);
45});
46
47// Merit 03: airplane-window photo carousel — autoplays and loops.
48// ~4 slides show so the next window peeks in and fades out via the
49// right-edge mask. Continuous "conveyor" autoplay (delay:0 + freeMode).
50document.addEventListener("DOMContentLoaded", function () {
51  var el = document.querySelector(".service-windows-swiper");
52  if (!el || typeof Swiper === "undefined") return;
53  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
54  new Swiper(el, {
55    slidesPerView: 1.6,
56    spaceBetween: 12,
57    loop: true,
58    grabCursor: true,
59    speed: reduce ? 800 : 4500,
60    freeMode: { enabled: true, momentum: false },
61    autoplay: reduce
62      ? false
63      : { delay: 0, disableOnInteraction: false, pauseOnMouseEnter: true },
64    breakpoints: {
65      // tablet (768–991): show ~3 larger windows instead of 4 cramped ones
66      768: { slidesPerView: 3, spaceBetween: 16 },
67      992: { slidesPerView: 4, spaceBetween: 20 },
68    },
69  });
70});
71
72// QA: accordion toggle (flips chevron / aria-expanded)
73(function () {
74  document.querySelectorAll(".qa-trigger").forEach(function (btn) {
75    btn.addEventListener("click", function () {
76      var expanded = btn.getAttribute("aria-expanded") === "true";
77      btn.setAttribute("aria-expanded", String(!expanded));
78    });
79  });
80})();
81
82// 口座開設までの流れ: draw the decorative line fan when it scrolls into view
83(function () {
84  var lines = document.querySelector(".jal-lines");
85  var card = document.querySelector(".jal-card");
86  if (!lines || !card) return;
87  if (!("IntersectionObserver" in window)) {
88    lines.classList.add("is-drawn");
89    return;
90  }
91  var io = new IntersectionObserver(
92    function (entries) {
93      entries.forEach(function (e) {
94        if (e.isIntersecting) {
95          lines.classList.add("is-drawn");
96          io.disconnect();
97        }
98      });
99    },
100    { threshold: 0.25 },
101  );
102  io.observe(card);
103})();
104
105// 2026/08/06 イントロ動画を最後まで再生してからフェードアウトし hero を表示する。
106// タイマーではなく "ended" 起点のため、読み込みが遅くても途中で切れない。
107(function () {
108  var hero = document.querySelector(".hero");
109  if (!hero) return;
110  var video = hero.querySelector(".hero-intro");
111  var revealed = false;
112  var safetyTimer;
113  function reveal() {
114    if (revealed) return;
115    revealed = true;
116    if (safetyTimer) clearTimeout(safetyTimer);
117    hero.classList.add("is-revealed");
118    if (video) {
119      video.classList.add("is-done");
120      video.addEventListener("transitionend", function () {
121        if (video && video.parentNode) video.parentNode.removeChild(video);
122      });
123    }
124  }
125
126  if (!video) {
127    reveal();
128    return;
129  }
130
131  // 2026/08/06 reduced-motion 設定時はイントロも動画の読み込みもスキップ
132  if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
133    reveal();
134    return;
135  }
136
137  // 2026/08/06 WebKit は画面外の動画を自動再生せず、再試行もしない
138  if (hero.getBoundingClientRect().bottom <= 0) {
139    reveal();
140    return;
141  }
142
143  // 2026/08/06 動画の背後の hero を隠すカバーを有効化。JS 無効時に hero が隠れないようJS側で付与
144  hero.classList.add("is-intro");
145
146  // 2026/08/06 Safari は muted を属性でなくプロパティでのみ判定。無音の音声トラックがあると自動再生が弾かれる
147  video.muted = true;
148  video.defaultMuted = true;
149  // 2026/08/06 HTML ではなくここで指定。reduced-motion 時に読み込みを走らせないため
150  video.autoplay = true;
151  video.preload = "auto";
152
153  // 2026/08/06 ブレークポイントは CSS の .hero-intro と揃えること
154  var mqSP = window.matchMedia ? window.matchMedia("(max-width: 767.98px)") : null;
155  function wantedSrc() {
156    return mqSP && mqSP.matches ? "/assets/video/SP_KV.mp4" : "/assets/video/PC_KV.mp4";
157  }
158  video.src = wantedSrc();
159
160  // 2026/08/06 保険:再生開始まで30秒、開始後は6秒進捗なしでハングとみなす
161  function armSafety(ms) {
162    if (revealed) return;
163    if (safetyTimer) clearTimeout(safetyTimer);
164    safetyTimer = window.setTimeout(reveal, ms || 30000);
165  }
166  armSafety();
167
168  video.addEventListener("timeupdate", function () {
169    if (video.duration && video.currentTime >= video.duration) {
170      reveal();
171      return;
172    }
173    armSafety(6000); // 2026/08/06 再生が進んでいるので期限を延ばす
174  });
175  video.addEventListener("ended", reveal);
176  video.addEventListener("error", reveal); // 2026/08/06 動画が壊れていてもページを塞がない
177
178  var reloading = false; // 2026/08/06 自前の再読み込みで再生が中断される間だけ true
179  var retried = false;
180
181  // 2026/08/06 WebKit が自動再生を勝手に中断すると "ended" が発火しない
182  video.addEventListener("pause", function () {
183    if (revealed || reloading || video.ended) return;
184    armSafety(1200);
185  });
186
187  function play() {
188    var playPromise;
189    try {
190      playPromise = video.play();
191    } catch (e) {}
192    if (playPromise && typeof playPromise.catch === "function") {
193      playPromise.catch(function (err) {
194        // 2026/08/06 自前の再読み込み。後続の play() が引き継ぐ
195        if (reloading || (err && err.name === "AbortError")) return;
196        // 2026/08/06 preload="none" だとデコード前に最初の play() が失敗しうる
197        if (!retried && video.readyState < 2) {
198          retried = true;
199          var onCanPlay = function () {
200            video.removeEventListener("canplay", onCanPlay);
201            play();
202          };
203          video.addEventListener("canplay", onCanPlay);
204          return;
205        }
206        reveal(); // 2026/08/06 本当に再生不可(低電力モード等)。サイトを表示する
207      });
208    }
209  }
210
211  // 2026/08/06 <video> はリサイズで source を選び直さないため手動で差し替える
212  if (mqSP) {
213    var onBreakpointChange = function () {
214      if (revealed || !video.parentNode) return;
215      var next = wantedSrc();
216      if (video.getAttribute("src") === next) return;
217      // 2026/08/06 src 変更で実行中の play() が AbortError で reject される
218      reloading = true;
219      retried = false;
220      video.src = next;
221      video.load();
222      video.muted = true;
223      armSafety();
224      play();
225      window.setTimeout(function () {
226        reloading = false;
227      }, 0);
228    };
229    if (mqSP.addEventListener) {
230      mqSP.addEventListener("change", onBreakpointChange);
231    } else if (mqSP.addListener) {
232      mqSP.addListener(onBreakpointChange);
233    }
234  }
235
236  play();
237})();
238
239// Circle arrows: measure each outer line so the hover "draw" animation is smooth/even
240(function () {
241  function measureArrowLines() {
242    document.querySelectorAll(".spin-arrow__ring path").forEach(function (p) {
243      try {
244        p.style.setProperty("--ring-len", p.getTotalLength());
245      } catch (e) {}
246    });
247  }
248  document.addEventListener("DOMContentLoaded", measureArrowLines);
249  window.addEventListener("load", measureArrowLines);
250})();
251
252// SP carousels (Other-initiatives / Benefit Merit 02): arrows move the row one card at a time
253(function () {
254  function initCarousel(navSel, trackSel, cardSel) {
255    var nav = document.querySelector(navSel);
256    var track = document.querySelector(trackSel);
257    if (!nav || !track) return;
258    // 2026/08/07 NodeList のまま forEach できない環境向けに配列化(.service-tab と同じ扱い)
259    var cards = Array.prototype.slice.call(track.querySelectorAll(cardSel));
260    if (!cards.length) return;
261
262    // 2026/08/07 offsetLeft は offsetParent 基準でスクロール領域の原点と一致しないため実測する
263    function offsetOf(card) {
264      return card.getBoundingClientRect().left - track.getBoundingClientRect().left + track.scrollLeft;
265    }
266    // 2026/08/07 scrollBy は端を越えると iOS 側のスナップと競合するため、停止位置を自前で丸める。
267    // 左パディングぶん手前で止めて、先頭カードのガターが潰れないようにする
268    function targetOf(card) {
269      var padLeft = parseFloat(window.getComputedStyle(track).paddingLeft) || 0;
270      var max = track.scrollWidth - track.clientWidth;
271      return Math.min(Math.max(offsetOf(card) - padLeft, 0), max);
272    }
273    // 2026/08/07 指スワイプでも位置が動くので、カウンタを持たず毎回スクロール位置から現在地を求める
274    function currentIndex() {
275      var idx = 0;
276      var best = Infinity;
277      cards.forEach(function (card, i) {
278        var d = Math.abs(targetOf(card) - track.scrollLeft);
279        if (d < best) {
280          best = d;
281          idx = i;
282        }
283      });
284      return idx;
285    }
286
287    nav.addEventListener("click", function (e) {
288      var btn = e.target.closest("button[data-dir]");
289      if (!btn) return;
290      var idx = currentIndex() + (btn.dataset.dir === "prev" ? -1 : 1);
291      idx = Math.min(Math.max(idx, 0), cards.length - 1);
292      track.scrollTo({ left: targetOf(cards[idx]), behavior: "smooth" });
293    });
294  }
295
296  initCarousel(".brand-cards-nav", ".brand-cards", ".brand-card");
297  initCarousel(".service-charts-nav", ".service-charts", ".service-chartcard");
298})();
299
300// Benefit Merit 01 tabs: sliding active pill + panel switch + prev/next arrows
301(function () {
302  document.querySelectorAll(".service-block--app").forEach(function (block) {
303    var list = block.querySelector(".service-tablist");
304    var pill = block.querySelector(".service-tab-pill");
305    var tabs = Array.prototype.slice.call(block.querySelectorAll(".service-tab"));
306    var panels = block.querySelectorAll(".service-feature[data-panel]");
307    var notes = block.querySelector(".service-notes");
308    var prevBtn = block.querySelector(".service-tabnav--prev");
309    var nextBtn = block.querySelector(".service-tabnav--next");
310    if (!tabs.length) return;
311
312    // hide the prev arrow on the first tab and the next arrow on the last tab;
313    // also drop the right-edge fade once the last tab is active (nothing more to scroll)
314    function updateArrows(idx) {
315      if (prevBtn) prevBtn.classList.toggle("is-hidden", idx <= 0);
316      if (nextBtn) nextBtn.classList.toggle("is-hidden", idx >= tabs.length - 1);
317      if (list) list.classList.toggle("is-end", idx >= tabs.length - 1);
318    }
319
320    function activeIndex() {
321      for (var i = 0; i < tabs.length; i++) {
322        if (tabs[i].classList.contains("is-active")) return i;
323      }
324      return 0;
325    }
326    function movePill(tab) {
327      if (!pill || !tab) return;
328      pill.style.width = tab.offsetWidth + "px";
329      pill.style.transform = "translateX(" + tab.offsetLeft + "px)";
330    }
331    function activate(idx, scroll) {
332      var tab = tabs[idx];
333      if (!tab) return;
334      var name = tab.dataset.tab;
335      tabs.forEach(function (t, i) {
336        var on = i === idx;
337        t.classList.toggle("is-active", on);
338        t.setAttribute("aria-selected", on ? "true" : "false");
339      });
340      panels.forEach(function (p) {
341        p.hidden = p.dataset.panel !== name;
342      });
343      if (notes) notes.hidden = name !== "app"; // footnotes belong to the app tab
344      movePill(tab);
345      updateArrows(idx);
346      if (scroll && list) {
347        var l = tab.offsetLeft;
348        var r = l + tab.offsetWidth;
349        if (l < list.scrollLeft) list.scrollTo({ left: l - 16, behavior: "smooth" });
350        else if (r > list.scrollLeft + list.clientWidth)
351          list.scrollTo({ left: r - list.clientWidth + 16, behavior: "smooth" });
352      }
353    }
354    // click a tab -> activate it
355    tabs.forEach(function (t, i) {
356      t.addEventListener("click", function () {
357        activate(i, true);
358      });
359    });
360    // SP prev/next arrows -> move the active tab one step (clamped)
361    block.querySelectorAll(".service-tabnav").forEach(function (btn) {
362      btn.addEventListener("click", function () {
363        var dir = btn.classList.contains("service-tabnav--prev") ? -1 : 1;
364        var idx = activeIndex() + dir;
365        if (idx < 0) idx = 0;
366        if (idx > tabs.length - 1) idx = tabs.length - 1;
367        activate(idx, true);
368      });
369    });
370    // position the pill under the initial active tab (re-sync after fonts/layout/resize)
371    function sync() {
372      movePill(tabs[activeIndex()]);
373      updateArrows(activeIndex());
374    }
375    requestAnimationFrame(sync);
376    window.addEventListener("load", sync);
377    window.addEventListener("resize", sync);
378  });
379})();

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.