/** * 共通インタラクション * ------------------------------------------------------------ * ・ハンバーガーメニューの開閉、SP固定フッターバーは #main の中身に * 依存しないため、旧版同様ここで一度だけセットアップする。 * ・corner-frameのreveal監視/ギャラリー初期化/名前の自動フィットは、 * router.js が #main を差し替えるたびに再実行する必要があるため、 * window.SiteUI に再呼び出し可能な関数として公開する * (旧版はサーバーが毎回ページ全体を描画していたので「読み込み時に1回」 * で足りていたが、静的SPA化に伴い差し替えのたびに呼び直す方式に変更)。 * ------------------------------------------------------------ */ /* ハンバーガーメニューの開閉(PC/SP共通) */ (function () { var overlay = document.getElementById('menuOverlay'); var panel = document.getElementById('menuPanel'); var closeBtn = document.getElementById('menuCloseBtn'); var openBtns = [ document.getElementById('menuOpenBtn'), document.getElementById('menuOpenBtnSp') ]; function openMenu() { if (overlay) overlay.classList.add('is-open'); if (panel) panel.classList.add('is-open'); } function closeMenu() { if (overlay) overlay.classList.remove('is-open'); if (panel) panel.classList.remove('is-open'); } openBtns.forEach(function (btn) { if (btn) btn.addEventListener('click', openMenu); }); if (closeBtn) closeBtn.addEventListener('click', closeMenu); if (overlay) overlay.addEventListener('click', closeMenu); })(); /** * 会員ログインフォーム * ・バックエンド未接続のモックのため、送信してもページ遷移させない * (イベント委任にしているのは、SPA遷移で#main差し替え後もフォームが * 出るたびに動くようにするため。router.jsのfilter-tabs委任と同じ考え方) */ document.addEventListener('submit', function (e) { if (!e.target.closest('[data-login-form]')) return; e.preventDefault(); }); /** * スクロールフェードイン(corner-frame単位) * ・画面内に入るたびにふわっと表示し、画面外に出たら消える(毎回再生) * ・#main差し替えのたびに対象要素が変わるため、呼ばれるたびに * 前回のIntersectionObserverをdisconnectしてから対象を数え直す * ・[data-no-reveal] が付いた要素は対象外にする。ページ内アンカー * (#beginners等)の着地先を含むコンテナで、このtransform演出が * ジャンプ直後の着地位置計算を狂わせてしまうため、常に最終表示状態のまま * フェードインさせない */ var initCornerFrameReveal = (function () { var observer = null; return function initCornerFrameReveal() { if (observer) { observer.disconnect(); observer = null; } var targets = Array.prototype.slice.call(document.querySelectorAll('.corner-frame:not([data-no-reveal])')); if (!targets.length) return; if (!('IntersectionObserver' in window)) { targets.forEach(function (el) { el.classList.add('is-revealed'); }); return; } targets.forEach(function (el) { el.classList.add('reveal'); }); /* 表示になる判定(15%見えたら)と、非表示に戻る判定(完全に0%になったら)を わざと別のラインにする。表示/非表示の切り替え自体が transform で 見た目のサイズ・位置を変えるため、同じライン1本だと境界付近で 自分の判定を自分でまたぎ返してしまうが、2つのラインの間に transformの動く分より広い間隔を空けておけば、その揺れがどちらの ラインにも届かず点滅しなくなる */ observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { var el = entry.target; var revealed = el.classList.contains('is-revealed'); if (!revealed && entry.intersectionRatio >= 0.15) { el.classList.add('is-revealed'); } else if (revealed && entry.intersectionRatio <= 0) { el.classList.remove('is-revealed'); } }); }, { threshold: [0, 0.15], rootMargin: '0px 0px -60px 0px' }); targets.forEach(function (el) { observer.observe(el); }); }; })(); /** * プロフィール写真ギャラリー * ・大きい写真の左右矢印でサムネイルを順番に切り替え * ・サムネイルをクリックすると、その写真が大きい写真に表示される * ・プロフィールページが描画されたときだけ #main 内に対象が存在する */ function initGallery() { var mainWrap = document.querySelector('[data-gallery-main]'); var mainImg = mainWrap ? mainWrap.querySelector('[data-gallery-img]') : null; var prevBtn = mainWrap ? mainWrap.querySelector('[data-gallery-prev]') : null; var nextBtn = mainWrap ? mainWrap.querySelector('[data-gallery-next]') : null; var thumbs = Array.prototype.slice.call(document.querySelectorAll('[data-gallery-thumb]')); if (!mainImg || !thumbs.length) return; var current = 0; function show(index) { current = ((index % thumbs.length) + thumbs.length) % thumbs.length; var photo = thumbs[current].getAttribute('data-photo'); if (photo) mainImg.setAttribute('src', photo); thumbs.forEach(function (t, i) { t.classList.toggle('is-active', i === current); }); } if (prevBtn) prevBtn.addEventListener('click', function () { show(current - 1); }); if (nextBtn) nextBtn.addEventListener('click', function () { show(current + 1); }); thumbs.forEach(function (t, i) { t.setAttribute('role', 'button'); t.setAttribute('tabindex', '0'); t.addEventListener('click', function () { show(i); }); t.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); show(i); } }); }); } /** * 名前の自動フィット * ・.cast-mini-card / .cast-card の名前は、文字数が多いと折り返して * レイアウトが崩れるため、実際の表示幅を測って収まる範囲まで * フォントサイズを縮小し、1行に収める */ var fitCastNames = (function () { var SELECTOR = '.cast-mini-card .name, .cast-card .name'; var MIN_RATIO = 0.6; var STEP = 0.5; function fitOne(el) { el.style.fontSize = ''; var baseSize = parseFloat(window.getComputedStyle(el).fontSize); var minSize = baseSize * MIN_RATIO; var size = baseSize; while (el.scrollWidth > el.clientWidth && size > minSize) { size -= STEP; el.style.fontSize = size + 'px'; } } function fitAll() { Array.prototype.slice.call(document.querySelectorAll(SELECTOR)).forEach(fitOne); } var resizeTimer; window.addEventListener('resize', function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(fitAll, 150); }); return fitAll; })(); /** * イベントバナー(カルーセル) * ・PC版/SP版で同じ構造の .event-banner が同時にDOM上に2つ存在しうる * (CSSのpc-only/sp-onlyで片方を隠しているだけ)ため、instanceごとに * 独立して動かす。 * ・#main差し替えのたびに呼び直されるので、前回分のautoplayタイマーは * 必ず止めてから作り直す(止めないと、差し替えで浮いた古いDOMに対して * タイマーが動き続けてしまう) */ var initEventBanner = (function () { var timers = []; function clearTimers() { timers.forEach(function (t) { window.clearInterval(t); }); timers = []; } return function initEventBanner() { clearTimers(); var banners = Array.prototype.slice.call(document.querySelectorAll('[data-banner]')); banners.forEach(function (banner) { var track = banner.querySelector('[data-banner-track]'); var slides = track ? Array.prototype.slice.call(track.children) : []; if (!track || slides.length < 2) return; var prevBtn = banner.querySelector('[data-banner-prev]'); var nextBtn = banner.querySelector('[data-banner-next]'); var dots = Array.prototype.slice.call(banner.querySelectorAll('[data-banner-dot]')); var current = 0; var AUTOPLAY_MS = 5000; var timer = null; function show(index) { current = ((index % slides.length) + slides.length) % slides.length; track.style.transform = 'translateX(-' + (current * 100) + '%)'; dots.forEach(function (d, i) { d.classList.toggle('is-active', i === current); }); } function startAutoplay() { if (timer) window.clearInterval(timer); timer = window.setInterval(function () { show(current + 1); }, AUTOPLAY_MS); timers.push(timer); } if (prevBtn) prevBtn.addEventListener('click', function () { show(current - 1); startAutoplay(); }); if (nextBtn) nextBtn.addEventListener('click', function () { show(current + 1); startAutoplay(); }); dots.forEach(function (d, i) { d.addEventListener('click', function () { show(i); startAutoplay(); }); }); banner.addEventListener('mouseenter', function () { if (timer) window.clearInterval(timer); }); banner.addEventListener('mouseleave', startAutoplay); show(0); startAutoplay(); }); }; })(); window.SiteUI = { initCornerFrameReveal: initCornerFrameReveal, initGallery: initGallery, fitCastNames: fitCastNames, initEventBanner: initEventBanner }; /* 互換のため window.fitCastNames としても残す */ window.fitCastNames = fitCastNames; /** * SP固定フッターバーの追随表示 * ・ページ最上部では隠しておき、少しスクロールしたタイミングで * 画面下から追随して表示する * ・#main の中身に依存しないため、旧版同様ここで一度だけセットアップする */ (function () { var bar = document.querySelector('.sp-fixed-bar'); if (!bar) return; var SHOW_AFTER = 120; var ticking = false; function update() { bar.classList.toggle('is-visible', window.scrollY > SHOW_AFTER); ticking = false; } window.addEventListener('scroll', function () { if (ticking) return; ticking = true; window.requestAnimationFrame(update); }, { passive: true }); update(); // 上スクロール時にモバイルブラウザのアドレスバー(ツールバー)が // 再表示されると、position:fixed;bottom:0 のバーが実際に見えている // 範囲(visual viewport)の外に一瞬はみ出して隠れてしまうことがある。 // visualViewport の実寸に合わせて bottom位置を補正する if (window.visualViewport) { var vv = window.visualViewport; var vvTicking = false; function syncToVisualViewport() { var offset = window.innerHeight - (vv.height + vv.offsetTop); bar.style.bottom = Math.max(0, Math.round(offset)) + 'px'; vvTicking = false; } function onViewportChange() { if (vvTicking) return; vvTicking = true; window.requestAnimationFrame(syncToVisualViewport); } vv.addEventListener('resize', onViewportChange); vv.addEventListener('scroll', onViewportChange); syncToVisualViewport(); } })();