app/template/default/Block/slider.twig line 1

Open in your IDE?
  1. {#
  2. This file is part of EC-CUBE
  3. Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
  4. http://www.ec-cube.co.jp/
  5. For the full copyright and license information, please view the LICENSE
  6. file that was distributed with this source code.
  7. #}
  8. {% set sliders = get_sliders() %}
  9. {% if sliders|length > 0 %}
  10. <style>
  11.     /* スライダーコンテナ */
  12.     .custom-slider {
  13.         position: relative;
  14.         width: 100%;
  15.         overflow: hidden;
  16.         background-color: #ffffff;
  17.     }
  18.     @media (max-width: 767px) {
  19.         .custom-slider {
  20.             padding: 0 20px;
  21.         }
  22.     }
  23.     /* スライダーラッパー */
  24.     .custom-slider__wrapper {
  25.         position: relative;
  26.         width: 100%;
  27.         overflow: hidden;
  28.     }
  29.     /* スライダートラック */
  30.     .custom-slider__track {
  31.         display: flex;
  32.         transition: transform 0.5s ease-in-out;
  33.         height: 100%;
  34.     }
  35.     /* トランジション無効化用 */
  36.     .custom-slider__track.no-transition {
  37.         transition: none;
  38.     }
  39.     /* スライドアイテム */
  40.     .custom-slider__item {
  41.         flex-shrink: 0;
  42.         width: 80%;
  43.         margin: 0 2%;
  44.         aspect-ratio: 16 / 9;
  45.         height: auto;
  46.         position: relative;
  47.     }
  48.     /* デスクトップ: 3カラム表示 */
  49.     @media (min-width: 768px) {
  50.         .custom-slider__item {
  51.             width: 50%;
  52.             min-width: 700px;
  53.             margin: 0 1%;
  54.             opacity: 0.5;
  55.             transition: opacity 0.5s ease, transform 0.5s ease;
  56.         }
  57.         /* 中央のアクティブスライド */
  58.         .custom-slider__item.is-active {
  59.             opacity: 1;
  60.             z-index: 2;
  61.         }
  62.     }
  63.     /* リンク */
  64.     .custom-slider__link {
  65.         display: block;
  66.         width: 100%;
  67.         height: 100%;
  68.         text-decoration: none;
  69.         color: inherit;
  70.     }
  71.     /* 画像 */
  72.     .custom-slider__image {
  73.         width: 100%;
  74.         height: 100%;
  75.         aspect-ratio: 16 / 9;
  76.         object-fit: cover;
  77.     }
  78.     /* ナビゲーションボタン */
  79.     .custom-slider__nav {
  80.         position: absolute;
  81.         top: 50%;
  82.         transform: translateY(-50%);
  83.         background-color: rgba(255, 255, 255, 0.9);
  84.         border: none;
  85.         width: 40px;
  86.         height: 40px;
  87.         border-radius: 50%;
  88.         cursor: pointer;
  89.         z-index: 10;
  90.         display: flex;
  91.         align-items: center;
  92.         justify-content: center;
  93.         transition: background-color 0.3s ease, transform 0.2s ease;
  94.         box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  95.     }
  96.     .custom-slider__nav:hover {
  97.         background-color: rgba(255, 255, 255, 1);
  98.         transform: translateY(-50%) scale(1.1);
  99.     }
  100.     .custom-slider__nav:active {
  101.         transform: translateY(-50%) scale(0.95);
  102.     }
  103.     .custom-slider__nav--prev {
  104.         left: 15px;
  105.     }
  106.     .custom-slider__nav--next {
  107.         right: 15px;
  108.     }
  109.     .custom-slider__nav i {
  110.         font-size: 18px;
  111.         color: #333;
  112.     }
  113.     /* レスポンシブ調整 */
  114.     @media (max-width: 767px) {
  115.         .custom-slider__nav {
  116.             width: 30px;
  117.             height: 30px;
  118.         }
  119.     }
  120.     /* アニメーション用 */
  121.     .custom-slider__track.is-transitioning {
  122.         transition: transform 0.5s ease-in-out;
  123.     }
  124.     /* ミニスライダー */
  125.     .mini-slider {
  126.         position: relative;
  127.         width: 100%;
  128.         max-width: 1200px;
  129.         margin: 20px auto 0;
  130.         padding: 0 15px 40px;
  131.     }
  132.     .mini-slider__container {
  133.         position: relative;
  134.         overflow: hidden;
  135.     }
  136.     .mini-slider__track {
  137.         display: flex;
  138.         gap: 12px;
  139.         transition: transform 0.3s ease;
  140.         padding: 8px 0;
  141.     }
  142.     /* トランジション無効化用 */
  143.     .mini-slider__track.no-transition {
  144.         transition: none;
  145.     }
  146.     .mini-slider__item {
  147.         flex-shrink: 0;
  148.         width: calc(16.66% - 10px);
  149.         min-width: 120px;
  150.         aspect-ratio: 16 / 9;
  151.         cursor: pointer;
  152.         position: relative;
  153.         border-radius: 6px;
  154.         overflow: hidden;
  155.         border: 2px solid transparent;
  156.         transition: all 0.3s ease;
  157.         box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
  158.     }
  159.     /* アクティブでないミニスライダーに網掛け */
  160.     .mini-slider__item::before {
  161.         content: '';
  162.         position: absolute;
  163.         top: 0;
  164.         left: 0;
  165.         right: 0;
  166.         bottom: 0;
  167.         background-color: rgba(0, 0, 0, 0.5);
  168.         opacity: 1;
  169.         transition: opacity 0.3s ease;
  170.         z-index: 1;
  171.         pointer-events: none;
  172.     }
  173.     .mini-slider__item.is-active::before {
  174.         opacity: 0;
  175.     }
  176.     .mini-slider__item:hover {
  177.         transform: scale(1.05);
  178.         box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  179.     }
  180.     .mini-slider__item:hover::before {
  181.         opacity: 0.3;
  182.     }
  183.     .mini-slider__item.is-active {
  184.         border-color: #23ac38;
  185.         box-shadow: 0 4px 16px rgba(0, 123, 255, 0.3);
  186.     }
  187.     .mini-slider__item.is-active:hover::before {
  188.         opacity: 0;
  189.     }
  190.     .mini-slider__item img {
  191.         width: 100%;
  192.         height: 100%;
  193.         object-fit: cover;
  194.         display: block;
  195.     }
  196.     /* レスポンシブ: タブレット */
  197.     @media (max-width: 991px) {
  198.         .mini-slider__item {
  199.             width: calc(20% - 9.6px);
  200.             min-width: 100px;
  201.         }
  202.     }
  203.     /* レスポンシブ: モバイル(非表示) */
  204.     @media (max-width: 767px) {
  205.         .mini-slider {
  206.             display: none;
  207.         }
  208.     }
  209.     /* モバイルドットインジケーター */
  210.     .mobile-dots {
  211.         display: none; /* デフォルトは非表示 */
  212.         justify-content: center;
  213.         align-items: center;
  214.         gap: 8px;
  215.         padding: 15px 0;
  216.         margin-top: 10px;
  217.     }
  218.     @media (max-width: 767px) {
  219.         .mobile-dots {
  220.             display: flex; /* スマホのみ表示 */
  221.         }
  222.     }
  223.     .mobile-dot {
  224.         width: 10px;
  225.         height: 10px;
  226.         border-radius: 50%;
  227.         background-color: rgba(0, 0, 0, 0.3);
  228.         border: none;
  229.         cursor: pointer;
  230.         transition: all 0.3s ease;
  231.         padding: 0;
  232.     }
  233.     .mobile-dot:hover {
  234.         background-color: rgba(0, 0, 0, 0.5);
  235.     }
  236.     .mobile-dot.is-active {
  237.         background-color: #23ac38;
  238.         transform: scale(1.3);
  239.     }
  240. </style>
  241. <div class="custom-slider" id="customSlider">
  242.     <div class="custom-slider__wrapper">
  243.         <div class="custom-slider__track" id="sliderTrack">
  244.             {% for slider in sliders %}
  245.                 <div class="custom-slider__item" data-index="{{ loop.index0 }}">
  246.                     {% set link = slider.url ? slider.url : (slider.Category ? '/products/list?category_id=' ~ slider.Category.id : null) %}
  247.                     {% if link %}
  248.                         <a href="{{ link }}"
  249.                            class="custom-slider__link"
  250.                            {% if slider.targetBlank %}target="_blank" rel="noopener noreferrer"{% endif %}>
  251.                             <img src="{{ asset(slider.fileName, 'save_image') }}"
  252.                                  alt="{{ slider.altText }}"
  253.                                  class="custom-slider__image">
  254.                         </a>
  255.                     {% else %}
  256.                         <img src="{{ asset(slider.fileName, 'save_image') }}"
  257.                              alt="{{ slider.altText }}"
  258.                              class="custom-slider__image">
  259.                     {% endif %}
  260.                 </div>
  261.             {% endfor %}
  262.         </div>
  263.     </div>
  264.     <!-- ナビゲーションボタン -->
  265.     <button class="custom-slider__nav custom-slider__nav--prev" id="sliderPrev">
  266.         <i class="fas fa-chevron-left"></i>
  267.     </button>
  268.     <button class="custom-slider__nav custom-slider__nav--next" id="sliderNext">
  269.         <i class="fas fa-chevron-right"></i>
  270.     </button>
  271. </div>
  272. <!-- モバイルドットインジケーター -->
  273. <div class="mobile-dots" id="mobileDots">
  274.     {% for slider in sliders %}
  275.         <button class="mobile-dot" data-index="{{ loop.index0 }}"></button>
  276.     {% endfor %}
  277. </div>
  278. <!-- ミニスライダー -->
  279. <div class="mini-slider" id="miniSlider">
  280.     <div class="mini-slider__container">
  281.         <div class="mini-slider__track" id="miniSliderTrack">
  282.             {% for slider in sliders %}
  283.                 <div class="mini-slider__item" data-index="{{ loop.index0 }}">
  284.                     <img src="{{ asset(slider.fileName, 'save_image') }}"
  285.                          alt="{{ slider.altText }}">
  286.                 </div>
  287.             {% endfor %}
  288.         </div>
  289.     </div>
  290. </div>
  291. <script>
  292.     (function() {
  293.         var slideCount = {{ sliders|length }};
  294.         if (slideCount === 0) return;
  295.         const slider = {
  296.             currentIndex: 1, // クローンスライドを考慮して1からスタート
  297.             totalSlides: slideCount,
  298.             realSlides: slideCount,
  299.             miniCloneCount: Math.min(3, slideCount), // ミニスライダーのクローン枚数(スライド数に応じて調整)
  300.             miniCurrentIndex: Math.min(3, slideCount), // ミニスライダーはクローン枚数を考慮してスタート
  301.             miniTotalSlides: slideCount,
  302.             miniRealSlides: slideCount,
  303.             autoplayInterval: null,
  304.             autoplayDuration: 5000, // 5秒
  305.             isTransitioning: false,
  306.             isMiniTransitioning: false,
  307.             elements: {
  308.                 track: null,
  309.                 items: null,
  310.                 prevBtn: null,
  311.                 nextBtn: null,
  312.                 miniSlider: null,
  313.                 miniTrack: null,
  314.                 miniItems: null,
  315.                 mobileDots: null
  316.             },
  317.             init: function() {
  318.                 // DOM要素の取得
  319.                 this.elements.track = document.getElementById('sliderTrack');
  320.                 this.elements.prevBtn = document.getElementById('sliderPrev');
  321.                 this.elements.nextBtn = document.getElementById('sliderNext');
  322.                 this.elements.miniSlider = document.getElementById('miniSlider');
  323.                 this.elements.miniTrack = document.getElementById('miniSliderTrack');
  324.                 this.elements.mobileDots = document.querySelectorAll('.mobile-dot');
  325.                 // 1枚以下の場合はスライダー機能を無効化
  326.                 if (this.realSlides <= 1) {
  327.                     this.elements.prevBtn.style.display = 'none';
  328.                     this.elements.nextBtn.style.display = 'none';
  329.                     if (this.elements.miniSlider) this.elements.miniSlider.style.display = 'none';
  330.                     var dotsContainer = document.getElementById('mobileDots');
  331.                     if (dotsContainer) dotsContainer.style.display = 'none';
  332.                     var singleItems = this.elements.track.querySelectorAll('.custom-slider__item');
  333.                     if (singleItems.length > 0) singleItems[0].classList.add('is-active');
  334.                     return;
  335.                 }
  336.                 // クローンスライドの追加(メイン)
  337.                 this.createClones();
  338.                 // クローンスライドの追加(ミニ)
  339.                 this.createMiniClones();
  340.                 // 要素の再取得(クローン追加後)
  341.                 this.elements.items = document.querySelectorAll('.custom-slider__item');
  342.                 this.elements.miniItems = document.querySelectorAll('.mini-slider__item');
  343.                 this.totalSlides = this.elements.items.length;
  344.                 this.miniTotalSlides = this.elements.miniItems.length;
  345.                 // 初期位置設定
  346.                 this.updateSlider(false);
  347.                 // イベントバインド
  348.                 this.bindEvents();
  349.                 // 自動再生開始
  350.                 this.startAutoplay();
  351.             },
  352.             createClones: function() {
  353.                 const track = this.elements.track;
  354.                 const items = track.querySelectorAll('.custom-slider__item');
  355.                 // 最後のスライドのクローンを先頭に追加
  356.                 const lastClone = items[items.length - 1].cloneNode(true);
  357.                 lastClone.classList.add('clone');
  358.                 track.insertBefore(lastClone, items[0]);
  359.                 // 最初のスライドのクローンを最後に追加
  360.                 const firstClone = items[0].cloneNode(true);
  361.                 firstClone.classList.add('clone');
  362.                 track.appendChild(firstClone);
  363.             },
  364.             createMiniClones: function() {
  365.                 const track = this.elements.miniTrack;
  366.                 const items = track.querySelectorAll('.mini-slider__item');
  367.                 const itemCount = items.length;
  368.                 const cloneCount = this.miniCloneCount;
  369.                 // 先頭に最後のN枚のクローンを追加(逆順で追加)
  370.                 for (let i = cloneCount - 1; i >= 0; i--) {
  371.                     const clone = items[itemCount - 1 - i].cloneNode(true);
  372.                     clone.classList.add('clone');
  373.                     track.insertBefore(clone, items[0]);
  374.                 }
  375.                 // 末尾に最初のN枚のクローンを追加
  376.                 for (let i = 0; i < cloneCount; i++) {
  377.                     const clone = items[i].cloneNode(true);
  378.                     clone.classList.add('clone');
  379.                     track.appendChild(clone);
  380.                 }
  381.             },
  382.             bindEvents: function() {
  383.                 this.elements.prevBtn.addEventListener('click', () => this.prev());
  384.                 this.elements.nextBtn.addEventListener('click', () => this.next());
  385.                 // トランジション終了イベント(メイン)
  386.                 this.elements.track.addEventListener('transitionend', () => {
  387.                     this.handleTransitionEnd();
  388.                 });
  389.                 // トランジション終了イベント(ミニ)
  390.                 this.elements.miniTrack.addEventListener('transitionend', () => {
  391.                     this.handleMiniTransitionEnd();
  392.                 });
  393.                 // ミニスライダーのクリックイベント(クローンを含む全てのスライド)
  394.                 this.elements.miniItems.forEach((item, index) => {
  395.                     item.addEventListener('click', () => {
  396.                         // クローンの場合はクローン位置のまま同期、実スライドの場合は従来通り
  397.                         if (item.classList.contains('clone')) {
  398.                             // クローンがクリックされた場合、そのクローン位置に同期
  399.                             this.goToSlideByMiniIndex(index);
  400.                             } else {
  401.                             // 実スライドの場合は従来通り
  402.                             const realIndex = parseInt(item.getAttribute('data-index'));
  403.                             this.goToSlide(realIndex);
  404.                         }
  405.                     });
  406.                 });
  407.                 // モバイルドットのクリックイベント
  408.                 this.elements.mobileDots.forEach(dot => {
  409.                     dot.addEventListener('click', () => {
  410.                         const index = parseInt(dot.getAttribute('data-index'));
  411.                         this.goToSlide(index);
  412.                     });
  413.                 });
  414.             },
  415.             updateSlider: function(animate = true) {
  416.                 const isMobile = window.innerWidth < 768;
  417.                 const offset = isMobile ?
  418.                 -(this.currentIndex * 84) + 8 :
  419.                 -(this.currentIndex * 52) + 24;
  420.                 // アニメーションの有効/無効
  421.                 if (!animate) {
  422.                     this.elements.track.classList.add('no-transition');
  423.                     } else {
  424.                     this.elements.track.classList.remove('no-transition');
  425.                 }
  426.                 this.elements.track.style.transform = `translateX(${offset}%)`;
  427.                 // アクティブクラスの更新(デスクトップのみ)
  428.                 if (!isMobile) {
  429.                     this.elements.items.forEach((item, index) => {
  430.                         item.classList.toggle('is-active', index === this.currentIndex);
  431.                     });
  432.                 }
  433.                 // アニメーションなしの場合は即座にクラスを戻す
  434.                 if (!animate) {
  435.                     setTimeout(() => {
  436.                         this.elements.track.classList.remove('no-transition');
  437.                     }, 50);
  438.                 }
  439.                 // ミニスライダーの更新(同じanimate設定を適用)
  440.                 this.updateMiniSlider(animate);
  441.                 // モバイルドットの更新
  442.                 this.updateMobileDots();
  443.             },
  444.             updateMobileDots: function() {
  445.                 // モバイルドットが存在しない場合は何もしない
  446.                 if (!this.elements.mobileDots || this.elements.mobileDots.length === 0) return;
  447.                 // 現在のスライドインデックスを計算(クローンを除く)
  448.                 let realIndex = this.currentIndex - 1;
  449.                 if (realIndex < 0) realIndex = this.realSlides - 1;
  450.                 if (realIndex >= this.realSlides) realIndex = 0;
  451.                 // アクティブクラスの更新
  452.                 this.elements.mobileDots.forEach(dot => {
  453.                     const dotIndex = parseInt(dot.getAttribute('data-index'));
  454.                     dot.classList.toggle('is-active', dotIndex === realIndex);
  455.                 });
  456.             },
  457.             updateMiniSlider: function(animate = true) {
  458.                 if (!this.elements.miniItems || this.elements.miniItems.length === 0) return;
  459.                 // ミニスライダーのインデックスをメインに同期(オフセット調整)
  460.                 // メインはクローン1枚(index 1から)、ミニはクローン3枚(index 3から)
  461.                 this.miniCurrentIndex = this.currentIndex + (this.miniCloneCount - 1); // クローン枚数差でオフセットを調整
  462.                 // 実際のスライドインデックスを計算(クローンを除く)
  463.                 let realIndex = this.currentIndex - 1;
  464.                 if (realIndex < 0) realIndex = this.miniRealSlides - 1;
  465.                 if (realIndex >= this.miniRealSlides) realIndex = 0;
  466.                 // アクティブクラスの更新(クローンを除く実スライドのみ)
  467.                 this.elements.miniItems.forEach((item, index) => {
  468.                     if (!item.classList.contains('clone')) {
  469.                         const itemIndex = parseInt(item.getAttribute('data-index'));
  470.                         item.classList.toggle('is-active', itemIndex === realIndex);
  471.                     }
  472.                 });
  473.                 // ミニスライダーの位置計算(クローンを考慮)
  474.                 if (this.elements.miniTrack && this.elements.miniSlider) {
  475.                     // アニメーションの有効/無効
  476.                     if (!animate) {
  477.                         this.elements.miniTrack.classList.add('no-transition');
  478.                         } else {
  479.                         this.elements.miniTrack.classList.remove('no-transition');
  480.                     }
  481.                     // アクティブなアイテムを取得(クローン含む)
  482.                     const allItems = Array.from(this.elements.miniItems);
  483.                     const activeItemIndex = this.miniCurrentIndex;
  484.                     const activeItem = allItems[activeItemIndex];
  485.                     if (activeItem) {
  486.                         const containerWidth = this.elements.miniSlider.offsetWidth;
  487.                         const itemLeft = activeItem.offsetLeft;
  488.                         const itemWidth = activeItem.offsetWidth;
  489.                         const scrollPosition = itemLeft - (containerWidth / 2) + (itemWidth / 2);
  490.                         this.elements.miniTrack.style.transform = `translateX(-${Math.max(0, scrollPosition)}px)`;
  491.                     }
  492.                     // アニメーションなしの場合は即座にクラスを戻す
  493.                     if (!animate) {
  494.                         setTimeout(() => {
  495.                             this.elements.miniTrack.classList.remove('no-transition');
  496.                         }, 50);
  497.                     }
  498.                 }
  499.             },
  500.             goToSlide: function(index) {
  501.                 if (this.isTransitioning) return;
  502.                 this.isTransitioning = true;
  503.                 // クローンを考慮したインデックスに変換(+1)
  504.                 this.currentIndex = index + 1;
  505.                 this.updateSlider(true);
  506.                 this.resetAutoplay();
  507.             },
  508.             goToSlideByMiniIndex: function(miniIndex) {
  509.                 if (this.isTransitioning) return;
  510.                 this.isTransitioning = true;
  511.                 var cloneCount = this.miniCloneCount;
  512.                 var mainCloneCount = 1;
  513.                 var mainIndex;
  514.                 if (miniIndex < cloneCount) {
  515.                     mainIndex = 0;
  516.                 } else if (miniIndex >= this.miniTotalSlides - cloneCount) {
  517.                     mainIndex = this.realSlides + mainCloneCount;
  518.                 } else {
  519.                     mainIndex = miniIndex - cloneCount + mainCloneCount;
  520.                 }
  521.                 this.currentIndex = mainIndex;
  522.                 this.updateSlider(true);
  523.                 this.resetAutoplay();
  524.             },
  525.             next: function() {
  526.                 if (this.isTransitioning) return;
  527.                 this.isTransitioning = true;
  528.                 this.currentIndex++;
  529.                 this.updateSlider(true);
  530.                 this.resetAutoplay();
  531.             },
  532.             prev: function() {
  533.                 if (this.isTransitioning) return;
  534.                 this.isTransitioning = true;
  535.                 this.currentIndex--;
  536.                 this.updateSlider(true);
  537.                 this.resetAutoplay();
  538.             },
  539.             handleTransitionEnd: function() {
  540.                 this.isTransitioning = false;
  541.                 // 最後のクローンに到達したら、最初の実スライドに瞬間移動
  542.                 if (this.currentIndex === this.totalSlides - 1) {
  543.                     this.currentIndex = 1;
  544.                     this.updateSlider(false);
  545.                 }
  546.                 // 最初のクローンに到達したら、最後の実スライドに瞬間移動
  547.                 if (this.currentIndex === 0) {
  548.                     this.currentIndex = this.totalSlides - 2;
  549.                     this.updateSlider(false);
  550.                 }
  551.             },
  552.             handleMiniTransitionEnd: function() {
  553.                 this.isMiniTransitioning = false;
  554.                 // 最後のクローン領域に到達したら、最初の実スライドに瞬間移動
  555.                 if (this.miniCurrentIndex >= this.miniTotalSlides - this.miniCloneCount) {
  556.                     this.miniCurrentIndex = this.miniCloneCount; // 最初の実スライド位置
  557.                     this.updateMiniSliderPosition(false);
  558.                 }
  559.                 // 最初のクローン領域に到達したら、最後の実スライドに瞬間移動
  560.                 if (this.miniCurrentIndex < this.miniCloneCount) {
  561.                     this.miniCurrentIndex = this.miniTotalSlides - this.miniCloneCount - 1; // 最後の実スライド位置
  562.                     this.updateMiniSliderPosition(false);
  563.                 }
  564.             },
  565.             updateMiniSliderPosition: function(animate = true) {
  566.                 if (!this.elements.miniTrack || !this.elements.miniSlider) return;
  567.                 // アニメーションの有効/無効
  568.                 if (!animate) {
  569.                     this.elements.miniTrack.classList.add('no-transition');
  570.                     } else {
  571.                     this.elements.miniTrack.classList.remove('no-transition');
  572.                 }
  573.                 // アクティブなアイテムを取得(クローン含む)
  574.                 const allItems = Array.from(this.elements.miniItems);
  575.                 const activeItem = allItems[this.miniCurrentIndex];
  576.                 if (activeItem) {
  577.                     const containerWidth = this.elements.miniSlider.offsetWidth;
  578.                     const itemLeft = activeItem.offsetLeft;
  579.                     const itemWidth = activeItem.offsetWidth;
  580.                     const scrollPosition = itemLeft - (containerWidth / 2) + (itemWidth / 2);
  581.                     this.elements.miniTrack.style.transform = `translateX(-${Math.max(0, scrollPosition)}px)`;
  582.                 }
  583.                 // アニメーションなしの場合は即座にクラスを戻す
  584.                 if (!animate) {
  585.                     setTimeout(() => {
  586.                         this.elements.miniTrack.classList.remove('no-transition');
  587.                     }, 50);
  588.                 }
  589.             },
  590.             startAutoplay: function() {
  591.                 this.autoplayInterval = setInterval(() => {
  592.                     this.next();
  593.                 }, this.autoplayDuration);
  594.             },
  595.             stopAutoplay: function() {
  596.                 if (this.autoplayInterval) {
  597.                     clearInterval(this.autoplayInterval);
  598.                     this.autoplayInterval = null;
  599.                 }
  600.             },
  601.             resetAutoplay: function() {
  602.                 this.stopAutoplay();
  603.                 this.startAutoplay();
  604.             }
  605.         };
  606.         // スライダー初期化
  607.         if (document.readyState === 'loading') {
  608.             document.addEventListener('DOMContentLoaded', () => slider.init());
  609.             } else {
  610.             slider.init();
  611.         }
  612.         // リサイズ時の再計算
  613.         let resizeTimer;
  614.         window.addEventListener('resize', function() {
  615.             clearTimeout(resizeTimer);
  616.             resizeTimer = setTimeout(() => {
  617.                 slider.updateSlider(false);
  618.             }, 250);
  619.         });
  620.     })();
  621. </script>
  622. {% endif %}