PageSourceSearch

https://mens-eminal.jp/wp/wp-content/themes/cocoon-master/mens-wp-…?ver=1782267707&fver=20260624022147

js mens-eminal.jp collected 2026-09-30 22:32:25 UTC 17,794 bytes, 400 lines download raw bytes

1/**
2 * wp-components-common.js | 2026リニューアルページ共通JS
3 * ---------------------------------------------------------------------
4 */
5(function () {
6  'use strict';
7
8  /**
9   * FVヒーロー画像を確実に表示する(Safari対策)
10   * ---------------------------------------------------------------------
11   *   ページ最上部の FV 画像(LCP対象)を、Safari でも確実に表示させる。
12   *
13   *   Cocoon の遅延読み込み(lozad)は、画像の src を一旦 data-src に退避させ、
14   *   その画像が画面内に入ったタイミングで src に戻して表示する仕組み。
15   *   ところが FV 画像は PC幅(≥782px)で width:auto のため、src が空のままだと
16   *   幅が 0 になる。Safari は「幅も高さも 0 の要素」を画面内に入ったと見なさず、
17   *   遅延読み込みを発火させない。その結果、src が空のまま(data-src だけが残る)
18   *   で画像が出ない。
19   *   ※ Chrome は 0 サイズでも発火するため、この問題は起きない。
20   *
21   * 【対策】
22   *   FV 画像はそもそも遅延読み込みする必要がない(最初から見える位置にある)ので、
23   *   ここで data-src を src に戻し、lozad の発火を待たずに表示させる。
24   */
25  function revealFvHeroImage() {
26    var imgs = document.querySelectorAll('.fv__bg-img[data-src]');
27    for (var i = 0; i < imgs.length; i++) {
28      var img = imgs[i];
29      var url = img.getAttribute('data-src');
30      if (!url) continue;
31      if (!img.getAttribute('src')) img.setAttribute('src', url);
32      img.removeAttribute('data-src');
33      img.classList.remove('lozad', 'lozad-img');
34    }
35  }
36  if (document.readyState === 'loading') {
37    document.addEventListener('DOMContentLoaded', revealFvHeroImage);
38  } else {
39    revealFvHeroImage();
40  }
41
42  // ヘッダー下に確保する余白(px)
43  var GAP = 12;
44
45  // 直近に検出した固定ヘッダー要素のキャッシュ(全要素走査の回数を最小化)。
46  var _headerEl = null;
47
48  // 要素が「画面最上部の固定ヘッダー」の条件を満たすか判定し、満たせば最下端(px)を返す。満たさなければ -1。
49  function headerBottomOf(el) {
50    if (!el || !document.contains(el)) return -1;
51    var cs = window.getComputedStyle(el);
52    if (cs.position !== 'fixed' && cs.position !== 'sticky') return -1;
53    if (cs.visibility === 'hidden' || cs.display === 'none' || parseFloat(cs.opacity) === 0) return -1;
54    var r = el.getBoundingClientRect();
55    if (r.top <= 2 && r.width >= window.innerWidth * 0.5 && r.height > 4 && r.bottom < window.innerHeight * 0.6) {
56      return r.bottom;
57    }
58    return -1;
59  }
60
61  /**
62   * 画面最上部に固定表示されている要素の最下端(=見出しが隠れる高さ)を実測。
63   * パフォーマンス対策として、検出済み要素をキャッシュし、まずそれを再評価する。
64   * 無効になっていた場合のみ全要素を走査して再検出する(=重い走査は実質ナビ毎に最大1回)。
65   */
66  function fixedHeaderBottom() {
67    var cached = headerBottomOf(_headerEl);
68    if (cached >= 0) return cached;
69
70    var max = 0;
71    _headerEl = null;
72    var nodes = document.body.getElementsByTagName('*');
73    for (var i = 0; i < nodes.length; i++) {
74      var b = headerBottomOf(nodes[i]);
75      if (b > max) { max = b; _headerEl = nodes[i]; }
76    }
77    return max;
78  }
79
80  // ユーザー操作とみなすキー(これらが押されたら補正を中止しユーザー操作を優先)
81  var SCROLL_KEYS = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' ', 'Spacebar'];
82
83  /**
84   * 対象要素へスクロール。
85   * まずスムーズスクロールで移動し、停止してから実測で着地ずれを補正する。
86   * (追従ヘッダーの高さ変動・画像の遅延読み込みによるずれを吸収)
87   *
88   * 重要:
89   *  - 着地(補正不要)したら監視を必ず終了する(戻され続ける不具合の防止)。
90   *  - ユーザーがスクロール操作(ホイール / タッチ / スクロール系キー)をしたら
91   *    その時点で補正を即中止し、ユーザー操作を最優先する。
92   *  - 念のため最大 4 秒で監視を打ち切る。
93   *  - prefers-reduced-motion 指定時は即時スクロール。
94   */
95  function scrollToTarget(target) {
96    var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
97    var behavior = reduce ? 'auto' : 'smooth';
98
99    function targetY() {
100      return Math.max(0, window.pageYOffset + target.getBoundingClientRect().top - fixedHeaderBottom() - GAP);
101    }
102
103    window.scrollTo({ top: targetY(), behavior: behavior });
104
105    var cancelled = false, last = window.pageYOffset, stable = 0, tries = 0, elapsed 
105= 0;
106    var opts = { passive: true };
107
108    function stop() {
109      if (cancelled) return;
110      cancelled = true;
111      window.clearInterval(id);
112      window.removeEventListener('wheel', onUser, opts);
113      window.removeEventListener('touchstart', onUser, opts);
114      window.removeEventListener('keydown', onUserKey, true);
115    }
116    function onUser() { stop(); }                 // ホイール / タッチ=ユーザー操作 → 補正中止
117    function onUserKey(e) { if (SCROLL_KEYS.indexOf(e.key) > -1) stop(); }
118
119    window.addEventListener('wheel', onUser, opts);
120    window.addEventListener('touchstart', onUser, opts);
121    window.addEventListener('keydown', onUserKey, true);
122
123    var id = window.setInterval(function () {
124      if (cancelled) return;
125      elapsed += 60;
126      if (elapsed > 4000) { stop(); return; }     // 安全弁(無限監視の防止)
127
128      var y = window.pageYOffset;
129      var moving = Math.abs(y - last) >= 1;
130      last = y;
131      if (moving) { stable = 0; return; }         // まだ移動中(スムーズスクロール中)
132      if (++stable < 2) return;                    // 完全停止を待つ
133
134      var delta = target.getBoundingClientRect().top - (fixedHeaderBottom() + GAP);
135      if (Math.abs(delta) > 2 && tries < 6) {      // 着地ずれを微補正(即時/目立たない)
136        tries++;
137        window.scrollBy({ top: delta, behavior: 'auto' });
138        stable = 0; last = window.pageYOffset;
139      } else {
140        stop();                                    // 着地完了 → 監視終了
141      }
142    }, 60);
143  }
144
145  /**
146   * クリックされた <a> が「同一ページ内アンカー」なら対象要素を返す。
147   * 別ページ・別クエリ・空ハッシュは対象外。
148   */
149  function resolveTarget(a) {
150    var href = a.getAttribute('href');
151    if (!href || href.indexOf('#') < 0) return null;
152    var u;
153    try {
154      u = new URL(a.href, window.location.href);
155    } catch (e) {
156      return null;
157    }
158    if (u.pathname !== window.location.pathname || u.search !== window.location.search) return null;
159    var id = u.hash.slice(1);
160    if (!id) return null;
161    return document.getElementById(id) || document.getElementById(decodeURIComponent(id));
162  }
163
164  // クリックをキャプチャ段階で先取りし、テーマ既定のスムーズスクロール等より先に処理。
165  document.addEventListener('click', function (e) {
166    if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
167    var a = e.target.closest ? e.target.closest('a[href]') : null;
168    if (!a) return;
169    var target = resolveTarget(a);
170    if (!target) return;
171
172    e.preventDefault();
173    e.stopImmediatePropagation();
174
175    if (window.history && window.history.pushState && target.id) {
176      window.history.pushState(null, '', '#' + target.id);
177    }
178    scrollToTarget(target);
179  }, true);
180
181  // 直リンク(URL に #id 付きで読み込まれた場合)も着地を補正。
182  window.addEventListener('load', function () {
183    var hash = window.location.hash;
184    if (hash && hash.length > 1) {
185      var t = document.getElementById(hash.slice(1)) ||
186        document.getElementById(decodeURIComponent(hash.slice(1)));
187      if (t) {
188        window.setTimeout(function () { scrollToTarget(t); }, 100);
189      }
190    }
191  });
192
193  /**
194   * 汎用カードスライダー(ドラッグ/スワイプ + セグメントタップ + 任意で矢印・ループ)
195   * ---------------------------------------------------------------------
196   * TOP / ヒゲ など複数ページの FLOW・COLUMN・PLANS スライダーで共用。
197   * step は DOM の実カード幅 + gap から都度計算するので、PC/SP の vw 切替にも追従する。
198   * window.initCardSlider として公開し、各ページの script.js から初期化する。
199   */
200  function initCardSlider(opts) {
201    var slider = document.querySelector(opts.slider);
202    var cards = document.querySelector(opts.cards);
203    var segments = document.querySelectorAll(opts.segments);
204    if (!slider || !cards || segments.length === 0) return;
205
206    var ACTIVE = opts.activeClass;
207    var TRACK = opts.track;
208    var MAX_INDEX = segments.length - 1;
209    var currentIndex = 0;
210    var pointerDown = false;
211    var isDragging = false;
212    var didDrag = false;
213    var startX = 0;
214    var startY = 0;
215    var baseTranslate = 0;
216    var lockDir = null; // null | 'h' | 'v'
217
218    function getStep() {
219      var firstCard = cards.children[0];
220      if (!firstCard) return opts.step || 490;
221      var cs = getComputedStyle(cards);
222      var gap = parseFloat(cs.gap || cs.columnGap || '0');
223      return firstCard.getBoundingClientRect().width + gap;
224    }
225
226    // 同時表示枚数。CSS の --cols(PC=3 / ≤1050=1)から取得。未指定は1
227    function getCols() {
228      var v = parseInt(getComputedStyle(slider).getPropertyValue('--cols'), 10);
229      return (v && v > 0) ? v : 1;
230    }
231
232    // 右端の停止位置 = カード総数 − 表示枚数(最後のカードまで表示しきった位置で止める)
233    function getMaxIndex() {
234      var total = cards.children.length;
235      if (!total) return MAX_INDEX;
236      return Math.max(0, total - getCols());
237    }
238
239    // 先頭カードをスライダー左端(見出し左端)に揃えるため、中央寄せはせず常に左寄せ(gutter 0)
240    function getGutter() {
241      return 0;
242    }
243
244    // allowWrap=true(矢印・ドラッグ): 端でループ。false(ドット・リサイズ): 端でクランプ
245    function setIndex(i, allowWrap) {
246      var maxIdx = opts.loop ? getMaxIndex() : MAX_INDEX;
247      if (opts.loop && allowWrap) {
248        var count = maxIdx + 1;
249        currentIndex = ((i % count) + count) % count;
250      } else {
251        currentIndex = Math.max(0, Math.min(maxIdx, i));
252      }
253      // loop(=中央寄せ対象)は gutter を足して常に中央。それ以外は従来どおり左基準
254      var offset = opts.loop ? getGutter() : 0;
255      var tx = offset - currentIndex * getStep();
256
257      // alignEdges 指定時(FLOW):
258      //   ・先頭カードをスライダー左端(見出しライン)にそろえる
259      //   ・末尾カードがスライダー右端を越えない位置でクランプ(右側の空白=停止位置ズレを防ぐ)
260      //   実測値から都度計算するため、PC/SP の幅変化にも追従する。
261      if (opts.alignEdges && cards.children.length) {
262        var sr = slider.getBoundingClientRect();
263        // 現在適用済みの translateX を取得(アニメ途中でも tx=0 基準の実測に使える)
264        var curTx = 0;
265        var cm = window.getComputedStyle(cards).transform;
266        if (cm && cm !== 'none') {
267          var cmm = cm.match(/matrix.*\((.+)\)/);
268          if (cmm) { var cv = cmm[1].split(',').map(parseFloat); curTx = cv.length === 6 ? cv[4] : cv[12]; }
269        }
270        var firstEl = cards.children[0];
271        var lastEl = cards.children[cards.children.length - 1];
272        var firstLeft0 = firstEl.getBoundingClientRect().left - curTx;  // translateX=0 のときの先頭左
273        var lastRight0 = lastEl.getBoundingClientRect().right - curTx;  // translateX=0 のときの末尾右
274        var startOffset = sr.left - firstLeft0;   // 先頭をスライダー左端へそろえる補正
275        // フルブリード幅(スライダーが画面端まで広がり左に余白が無い=タブレット/SP等)では、
276        // 左端そろえだとカードが画面端に張り付き左のカードが切れて見える。
277        // その場合は補正せず、CSS が与える自然な左インセットを尊重する。
278        if (sr.left < 10) startOffset = 0;
279        if (startOffset > 0) startOffset = 0;       // 先頭を右へずらすことはしない(詰める方向のみ)
280        var minTx = sr.right - lastRight0;          // 末尾を右端にそろえる下限(最も左に動かせる量)
281        if (minTx > 0) minTx = 0;                   // 全カードが画面内に収まる場合は動かさない
282        tx = startOffset - currentIndex * getStep();
283        if (tx < minTx) tx = minTx;                 // 右端を越えてのスクロールを禁止
284      }
285
286      cards.style.transform = 'translateX(' + tx + 'px)';
287      segments.forEach(function (s, idx) {
288        // ループ時は「停止位置の数(maxIdx+1)」だけドットを表示し、余りは隠す(PC=3個 / SP=5個)
289        if (opts.loop) s.style.display = (idx <= maxIdx) ? '' : 'none';
290        s.classList.toggle(ACTIVE, idx === currentIndex);
291      });
292    }
293
294    segments.forEach(function (s, idx) {
295      s.addEventListener('click', function () { setIndex(idx, false); });
296    });
297
298    // 左右矢印ボタン(オプション)
299    // pointerdown を止めて slider のドラッグハンドラに横取りされないようにする
300    if (opts.prev) {
301      document.querySelectorAll(opts.prev).forEach(function (btn) {
302        btn.addEventListener('pointerdown', function (e) { e.stopPropagation(); });
303        btn.addEventListener('click', function (e) { e.stopPropagation(); setIndex(currentIndex - 1, true); });
304      });
305    }
306    if (opts.next) {
307      document.querySelectorAll(opts.next).forEach(function (btn) {
308        btn.addEventListener('pointerdown', function (e) { e.stopPropagation(); });
309        btn.addEventListener('click', function (e) { e.stopPropagation(); setIndex(currentIndex + 1, true); });
310      });
311    }
312
313    slider.addEventListener('pointerdown', function (e) {
314      if (TRACK && e.target.closest(TRACK)) return;
315      pointerDown = true;
316      isDragging = false;
317      didDrag = false;
318      lockDir = null;
319      startX = e.clientX;
320      startY = e.clientY;
321      // 現在のtransformから実際の位置を取得(アニメ途中で再ドラッグした時のジャンプを防ぐ)
322      var cs = window.getComputedStyle(cards);
323      var m = cs.transform;
324      if (m && m !== 'none') {
325        var match = m.match(/matrix.*\((.+)\)/);
326        if (match) {
327          var values = match[1].split(',').map(parseFloat);
328          baseTranslate = values.length === 6 ? values[4] : values[12];
329        } else {
330          baseTranslate = -currentIndex * getStep();
331        }
332      } else {
333        baseTranslate = -currentIndex * getStep();
334      }
335      // 方向ロック確定までは is-dragging もポインタキャプチャもまだ付けない
336    });
337
338    slider.addEventListener('pointermove', function (e) {
339      if (!pointerDown) return;
340      var dx = e.clientX - startX;
341      var dy = e.clientY - startY;
342
343      // 方向ロック判定(最初の数pxで横優位か縦優位か決める)
344      if (lockDir === null) {
345        var absDx = Math.abs(dx);
346        var absDy = Math.abs(dy);
347        if (absDx < 6 && absDy < 6) return; // 微小な揺れは待つ
348        if (absDx > absDy) {
349          lockDir = 'h';
350          isDragging = true;
351          cards.classList.add('is-dragging');
352          try { slider.setPointerCapture(e.pointerId); } catch (_) { }
353        } else {
354          lockDir = 'v';
355          pointerDown = false; // 縦はブラウザに任せる、JSは降りる
356          return;
357        }
358      }
359
360      if (lockDir === 'h') {
361        if (Math.abs(dx) > 4) didDrag = true;
362        cards.style.transform = 'translateX(' + (baseTranslate + dx) + 'px)';
363      }
364    });
365
366    // 横ロック中はブラウザのデフォルト縦スクロールを抑制
367    slider.addEventListener('touchmove', function (e) {
368      if (lockDir === 'h' && e.cancelable) e.preventDefault();
369    }, { passive: false });
370
371    function endDrag(e) {
372      if (!pointerDown && !isDragging) return;
373      pointerDown = false;
374      if (!isDragging) { lockDir = null; return; }
375      isDragging = false;
376      lockDir = null;
377      cards.classList.remove('is-dragging');
378      var dx = e.clientX - startX;
379      var moved = Math.round(-dx / (getStep() * 0.4));
380      setIndex(currentIndex + moved, true);
381    }
382
383    slider.addEventListener('pointerup', endDrag);
384    slider.addEventListener('pointercancel', endDrag);
385
386    // ドラッグ後の意図しないクリック抑制
387    slider.addEventListener('click', function (e) {
388      if (didDrag) { e.preventDefault(); e.stopPropagation(); didDrag = false; }
389    }, true);
390
391    // ビューポート変化(PC ↔ SP)でも現在位置を再計算
392    window.addEventListener('resize', function () { setIndex(currentIndex, false); });
393
394    // 初期位置を確定(中央寄せ offset・ドット表示数を初回から反映)
395    setIndex(0, false);
396  }
397
398  // 各ページの script.js から呼べるようにグローバル公開
399  window.initCardSlider = initCardSlider;
400})();

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.