PageSourceSearch

https://www.joqr.co.jp/wp/wp-content/themes/joqr2026/assets/js/common.js

js joqr.co.jp collected 2026-10-01 10:13:52 UTC 17,959 bytes, 577 lines download raw bytes

1/**
2 * 今日の番組表 日付スライダー — common.js 読込直後に位置合わせ
3 */
4(function dailyDateSwiperInstantPrepare() {
5  'use strict';
6
7  function getEstimatedContainerWidth(containerEl) {
8    var vw = window.innerWidth;
9    var selector = containerEl.closest('.c-dailyprogram__date-selector');
10    var arrowSpace = vw >= 600 ? 96 : 0;
11    var swiperMargin = vw >= 960 ? 64 : (vw >= 600 ? 20 : 0);
12    var contentWidth = Math.min(vw, 1220);
13
14    if (selector) {
15      var rect = selector.getBoundingClientRect();
16      if (rect.width > 0) {
17        return Math.max(0, rect.width - arrowSpace);
18      }
19    }
20
21    return Math.max(0, contentWidth - arrowSpace - swiperMargin - 40);
22  }
23
24  function preAlign(containerEl) {
25    var activeItem = containerEl.querySelector('.c-dailyprogram__date-item--active');
26    if (!activeItem) {
27      return;
28    }
29
30    var slides = containerEl.querySelectorAll('.swiper-slide.c-dailyprogram__date-item');
31    var activeIndex = -1;
32    for (var i = 0; i < slides.length; i++) {
33      if (slides[i] === activeItem) {
34        activeIndex = i;
35        break;
36      }
37    }
38    if (activeIndex <= 0) {
39      return;
40    }
41
42    var targetIndex = activeIndex - 1;
43    var vw = window.innerWidth;
44    var gap = vw >= 960 ? 32 : (vw >= 600 ? 24 : 0);
45    var containerWidth = containerEl.clientWidth || getEstimatedContainerWidth(containerEl);
46    var slideWidth = vw >= 600 ? (containerWidth - gap * 6) / 7 : 92;
47    var wrapper = containerEl.querySelector('.swiper-wrapper');
48
49    if (wrapper) {
50      wrapper.style.transform = 'translate3d(-' + (targetIndex * (slideWidth + gap)) + 'px, 0, 0)';
51      wrapper.style.transitionDuration = '0ms';
52    }
53  }
54
55  var swipers = document.querySelectorAll('.c-dailyprogram__date-swiper');
56  for (var s = 0; s < swipers.length; s++) {
57    preAlign(swipers[s]);
58  }
59})();
60
61/**
62 * joqr build.js は sweet-scroll と a[href^="#"] 向けの click ハンドラで
63 * アンカー挙動を上書きする。ここでキャプチャより先に伝播を止め、
64 * ブラウザ標準のフラグメント遷移だけ残す(_layout.pug には手を入れない)。
65 * テーマ側の .js-scroll は除外する。
66 */
67(function () {
68  function shouldUseNativeFragmentNavigation(anchor) {
69    var raw = anchor.getAttribute('href');
70    if (raw === null || raw === '') return false;
71    if (/^\s*javascript:/i.test(raw)) return false;
72    if (anchor.classList && anchor.classList.contains('js-scroll')) return false;
73    var t = raw.replace(/^\s+|\s+$/g, '');
74    if (t === '#') return true;
75    try {
76      var u = new URL(anchor.href, window.location.href);
77      if (u.origin !== window.location.origin) return false;
78      if (u.pathname !== window.location.pathname || u.search !== window.location.search) return false;
79      return !!(u.hash && u.hash.length > 1);
80    } catch (err) {
81      return false;
82    }
83  }
84
85  document.addEventListener(
86    'click',
87    function (e) {
88      if (e.defaultPrevented) return;
89      if (e.button !== 0) return;
90      if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
91      var el = e.target;
92      if (!el || typeof el.closest !== 'function') return;
93      var a = el.closest('a[href]');
94      if (!a || !shouldUseNativeFragmentNavigation(a)) return;
95      if (a.closest && a.closest('.l-header')) {
96        document.querySelectorAll('.l-header__nav-menu-bar-inner').forEach(function (el) {
97          el.classList.remove('is-active');
98        });
99        document.querySelectorAll('.l-header__nav-menu-inner').forEach(function (el) {
100          el.classList.remove('is-open');
101        });
102        document.querySelectorAll('.l-header__nav-menu-overlay').forEach(function (el) {
103          el.classList.remove('is-open');
104        });
105        document.body.classList.remove('is-active');
106      }
107      // 検索モーダル用 #search-btn(href="#")は jQuery の click へ届ける必要がある
108      if (a.id === 'search-btn') return;
109      e.stopImmediatePropagation();
110    },
111    true
112  );
113})();
114
115/**
116 * joqr の build.js が document.oncontextmenu で右クリックを禁止するため解除する。
117 * build.js は common.js より先に読み込まれる(_layout.pug)。
118 */
119(function () {
120  document.oncontextmenu = null;
121})();
122
123(function ($) {
124  'use strict';
125
126  /**
127   * =======================================================
128   * header (Show / Hide)
129   * =======================================================
130   */
131
132  const $header = $('.l-header');
133
134  let lastScrollY = $(window).scrollTop();
135  let ticking = false;
136
137  const SCROLL_THRESHOLD = 30;
138  const TOP_OFFSET = 10;
139
140  function showHeaderAtPageTop() {
141    const currentScrollY = $(window).scrollTop();
142    if (currentScrollY > TOP_OFFSET) return false;
143
144    $header.removeClass('is-hidden');
145    lastScrollY = currentScrollY;
146    return true;
147  }
148
149  function onScroll() {
150    if (showHeaderAtPageTop()) {
151      ticking = false;
152      return;
153    }
154
155    const currentScrollY = $(window).scrollTop();
156    const diff = currentScrollY - lastScrollY;
157
158    if (Math.abs(diff) < SCROLL_THRESHOLD) {
159      ticking = false;
160      return;
161    }
162
163    if (diff > 0) {
164      $header
165        .addClass('is-hidden')
166    } else {
167      $header
168        .removeClass('is-hidden');
169    }
170
171    lastScrollY = currentScrollY;
172    ticking = false;
173  }
174
175  showHeaderAtPageTop();
176
177  $(window).on('scroll', function () {
178    if (!ticking) {
179      window.requestAnimationFrame(onScroll);
180      ticking = true;
181    }
182  });
183
184  if ('onscrollend' in window) {
185    window.addEventListener('scrollend', showHeaderAtPageTop, { passive: true });
186  }
187
188  /**
189   * =======================================================
190   * header (Navigation Menu)
191   * =======================================================
192   */
193  const openHeaderMenu = () => {
194    $('.l-header__nav-menu-bar-inner').addClass('is-active');
195    $('.l-header__nav-menu-inner').addClass('is-open');
196    $('.l-header__nav-menu-overlay').addClass('is-open');
197    $('body').addClass('is-active');
198  };
199  const closeHeaderMenu = () => {
200    $('.l-header__nav-menu-bar-inner').removeClass('is-active');
201    $('.l-header__nav-menu-inner').removeClass('is-open');
202    $('.l-header__nav-menu-overlay').removeClass('is-open');
203    $('body').removeClass('is-active');
204  };
205
206  $('.l-header__toggle').on('click', function () {
207    $('.l-header__nav-menu-bar-inner').toggleClass('is-active');
208    $('.l-header__nav-menu-inner').toggleClass('is-open');
209    $('.l-header__nav-menu-overlay').toggleClass('is-open');
210    $('body').toggleClass('is-active');
211  });
212
213  $('.l-header__nav-menu-btn').on('click', function () {
214    $('.l-header__nav-menu-bar-inner').toggleClass('is-active');
215    $('.l-header__nav-menu-inner').toggleClass('is-open');
216    $('.l-header__nav-menu-overlay').toggleClass('is-open');
217    $('body').toggleClass('is-active');
218  });
219
220  $('.l-header__nav-menu-overlay').on('click', closeHeaderMenu);
221
222  $('.l-header').on('click', 'a[href*="#"]', function () {
223    const href = $(this).attr('href');
224    if (!href || href === '#') return;
225    closeHeaderMenu();
226  });
227
228  /**
229   * joqr の build.js が同一サイト内リンクなどに target="_blank" を付与する。
230   * _layout で build より前に記録した「元HTMLにあった target=_blank」以外から target / rel を外す。
231   */
232  $(function () {
233    var nativeBlank = window.__joqrNativeTargetBlank;
234    if (!nativeBlank || typeof nativeBlank.has !== 'function') return;
235
236    function isBlankTarget(a) {
237      var t = a.getAttribute('target');
238      return t && String(t).replace(/\s/g, '').toLowerCase() === '_blank';
239    }
240
241    document.querySelectorAll('a[target]').forEach(function (a) {
242      if (!isBlankTarget(a) || nativeBlank.has(a)) return;
243      a.removeAttribute('target');
244      a.removeAttribute('rel');
245    });
246  });
247
248  /**
249   * 検索ボックス
250   */
251  $(function () {
252    const $openBtn = $('#search-btn');
253    const $overlay = $('#search-overlay');
254    const $modal = $('#search-modal');
255    const $closeBtn = $('.search-modal-close');
256    const $body = $('body');
257
258    if (!$overlay.length || !$modal.length) return;
259
260    const openModal = () => {
261      $overlay.addClass('is-active');
262      $modal.addClass('is-active');
263      $body.addClass('search-modal-open');
264    };
265
266    const closeModal = () => {
267      $overlay.removeClass('is-active');
268      $modal.removeClass('is-active');
269      $body.removeClass('search-modal-open');
270    };
271
272    if ($openBtn.length) {
273      $openBtn.on('click', (e) => {
274        e.preventDefault();
275        openModal();
276      });
277    }
278
279    $overlay.on('click', closeModal);
280
281    if ($closeBtn.length) {
282      $closeBtn.on('click', closeModal);
283    }
284
285    $modal.on('click', (e) => {
286      e.stopPropagation();
287    });
288  });
289
290
291  /**
292   * -------------------------------------------------------
293   * ranking text (Truncate)
294   * -------------------------------------------------------
295   */
296  const truncateRankingText = () => {
297    if ($(window).width() <= 960) {
298      $('.c-ranking__item-right .c-ranking__link-title').each(function () {
299        const $this = $(this);
300        const text = $this.text();
301        const limit = 26;
302        if (text.length > limit) {
303          $this.text(text.substring(0, limit) + '…');
304        }
305      });
306    }
307  };
308
309  truncateRankingText();
310  $(window).on('resize', truncateRankingText);
311
312
313  /**
314   * =======================================================
315   * アンカーリンク スムーズスクロール
316   * =======================================================
317   */
318  $(function () {
319    $(document).on('click', '.js-scroll', function (e) {
320      const href = $(this).attr('href');
321      if (!href || !href.startsWith('#')) return;
322
323      e.preventDefault();
324      e.stopPropagation();
325
326      const $target = $(href);
327      if (!$target.length) return;
328
329      const headerOffset = 120;
330      const offsetPosition = $target.offset().top - headerOffset;
331
332      $('html, body').animate({
333        scrollTop: offsetPosition
334      }, 500); // 500ms スムーズ
335    });
336  });
337
338
339  /**
340   * =======================================================
341   * クリップボードにコピー機能
342   * =======================================================
343   */
344  $(function () {
345    const showCopiedTooltip = ($button) => {
346      $button.find('.js-copy-tooltip').remove();
347
348      const $tooltip = $('<span class="js-copy-tooltip">Copied!</span>');
349      if ($button.css('position') === 'static') {
350        $button.css('position', 'relative');
351      }
352
353      $button.append($tooltip);
354
355      setTimeout(() => { $tooltip.addClass('is-visible'); }, 10);
356
357      setTimeout(() => {
358        $tooltip.removeClass('is-visible');
359        setTimeout(() => { $tooltip.remove(); }, 300);
360      }, 2000);
361    };
362
363    $(document).on('click', '.js-copy', async function (e) {
364      e.preventDefault();
365      e.stopPropagation();
366
367      const copyText = $(this).attr('data-copy-text');
368      if (!copyText) return;
369
370      let copySuccess = false;
371
372      try {
373        if (navigator.clipboard && navigator.clipboard.writeText) {
374          await navigator.clipboard.writeText(copyText);
375          copySuccess = true;
376        } else {
377          const $textArea = $('<textarea>').val(copyText).css({
378            position: 'fixed', left: '-9999px', top: '-9999px'
379          }).appendTo('body');
380          $textArea.select();
381          try {
382            if (document.execCommand('copy')) copySuccess = true;
383          } finally {
384            $textArea.remove();
385          }
386        }
387      } catch (err) {
388        console.error('Copy failed:', err);
389      }
390
391      if (copySuccess) {
392        showCopiedTooltip($(this));
393      }
394    });
395  });
396
397
398  /**
399   * =======================================================
400   * アコーディオン機能
401   * =======================================================
402   */
403  $(function () {
404    $(document).on('click', '.js-accordion', function (e) {
405      e.preventDefault();
406      e.stopPropagation();
407
408      const $button = $(this);
409      const $accordion = $button.closest('.c-accordion');
410      const $content = $accordion.find('.c-accordion__content');
411
412      if (!$content.length) return;
413
414      const isOpen = $content.is(':visible') || $content.hasClass('is-open');
415
416      if (isOpen) {
417        $content.slideUp(300, function () {
418          $content.removeClass('is-open');
419          $button.removeClass('is-active');
420        });
421      } else {
422        $button.addClass('is-active');
423        $content.slideDown(300, function () {
424          $content.addClass('is-open');
425        });
426      }
427    });
428  });
429
430
431  /**
432   * =======================================================
433   * Personality Modal
434   * =======================================================
435   */
436  $(function () {
437    let isModalOpen = false;
438    let currentModal = null;
439
440    const getScrollbarWidth = () => {
441      const outer = document.createElement('div');
442      outer.style.cssText = 'visibility:hidden;overflow:scroll;width:100px;position:absolute;';
443      document.body.appendChild(outer);
444      const inner = document.createElement('div');
445      inner.style.width = '100%';
446      outer.appendChild(inner);
447      const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
448      outer.parentNode.removeChild(outer);
449      return scrollbarWidth;
450    };
451
452    const normalizePersonalityModalUrl = (href) => {
453      if (!href) return href;
454      if (typeof window === 'undefined' || !window.location || !window.location.pathname) return href;
455      if (href.startsWith('/qr/')) return href;
456      if (window.location.pathname.startsWith('/qr/') && href.startsWith('/personality/')) {
457        return '/qr' + href;
458      }
459      return href;
460    };
461
462    const openPersonalityModal = (url) => {
463      if (isModalOpen) return;
464      if (!url || (!url.startsWith('/') && !url.startsWith('./') && !url.startsWith('../'))) return;
465
466      const loadUrl = normalizePersonalityModalUrl(url);
467
468      isModalOpen = true;
469      $('.l-header__nav-menu-bar-inner').removeClass('is-active');
470      $('.l-header__nav-menu-inner').removeClass('is-open');
471      $('.l-header__nav-menu-overlay').removeClass('is-open');
472      $('body').removeClass('is-active');
473      const scrollbarWidth = getScrollbarWidth();
474      const $body = $('body');
475      const bodyScrollTop = window.pageYOffset || document.documentElement.scrollTop;
476      $body.addClass('personality-modal-open').css({
477        overflow: 'hidden',
478        paddingRight: scrollbarWidth > 0 ? scrollbarWidth + 'px' : ''
479      });
480
481      const $overlay = $('<div class="personality-modal-overlay"></div>');
482      const $modalBox = $('<div class="personality-modal-box"><button class="personality-modal-close" type="button">×</button><div class="personality-modal-body"></div></div>');
483      const $modalBody = $modalBox.find('.personality-modal-body').html('<div style="text-align: center; padding: 40px;">読み込み中...</div>');
484
485      $overlay.append($modalBox).appendTo($body);
486
487      setTimeout(() => { $overlay.addClass('is-active'); $modalBox.addClass('is-active'); }, 10);
488
489      $modalBody.load(loadUrl, (response, status) => {
490        if (status === 'error') {
491          $modalBody.html('<div style="text-align: center; padding: 40px;">読み込みに失敗しました。</div>');
492        }
493        requestAnimationFrame(() => {
494          requestAnimationFrame(() => {
495            window.scrollTo(0, bodyScrollTop);
496          });
497        });
498      });
499
500      currentModal = { overlay: $overlay, box: $modalBox, bodyScrollTop };
501    };
502
503    const closePersonalityModal = () => {
504      if (!isModalOpen || !currentModal) return;
505      const { overlay: $overlay, box: $modalBox, bodyScrollTop } = currentModal;
506      $overlay.removeClass('is-active');
507      $modalBox.removeClass('is-active');
508      setTimeout(() => {
509        $overlay.remove();
510        $('body').removeClass('personality-modal-open').css({ overflow: '', paddingRight: '' });
511        if (typeof bodyScrollTop === 'number') {
512          window.scrollTo(0, bodyScrollTop);
513        }
514        isModalOpen = false;
515        currentModal = null;
516      }, 300);
517    };
518
519    $(document).on('click', '.js-personality-modal', function (e) {
520      e.preventDefault();
521      e.stopPropagation();
522      const url = $(this).attr('href');
523      if (url) openPersonalityModal(url);
524    });
525
526    $(document).on('click', '.personality-modal-close', function (e) {
527      e.preventDefault(); e.stopPropagation(); closePersonalityModal();
528    });
529
530    $(document).on('click', '.personality-modal-overlay', function (e) {
531      if ($(e.target).hasClass('personality-modal-overlay')) closePersonalityModal();
532    });
533
534    $(document).on('keydown', (e) => {
535      if (e.key === 'Escape' && isModalOpen) closePersonalityModal();
536    });
537
538    $(document).on('click', '.personality-modal-box', (e) => { e.stopPropagation(); });
539  });
540
541
542})(jQuery);
543
544const setupPersonalityScroll = () => {
545  document.querySelectorAll('.p-hero__program-personality').forEach(p => {
546    const span = p.querySelector('span');
547    const textLength = span.textContent.trim().length;
548    const windowWidth = window.innerWidth;
549
550    let scrollLimit = 16;
551
552    if (windowWidth <= 500) {
553      scrollLimit = 22;
554    } else if (windowWidth <= 960) {
555      scrollLimit = 30;
556    }
557    
558    p.classList.remove('is-scroll');
559    span.style.removeProperty('--scroll-distance');
560    span.style.animationDuration = '';
561
562    if (textLength >= scrollLimit) {
563      p.classList.add('is-scroll');
564
565      const hiddenWidth = span.scrollWidth - p.clientWidth;
566      if (hiddenWidth <= 0) return;
567
568      span.style.setProperty('--scroll-distance', `-${hiddenWidth}px`);
569
570      const speed = 30;
571      span.style.animationDuration = `${hiddenWidth / speed}s`;
572    }
573  });
574};
575
576window.addEventListener('load', setupPersonalityScroll);
577window.addEventListener('resize', setupPersonalityScroll);

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.