{#
This file is part of EC-CUBE
Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
http://www.ec-cube.co.jp/
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
#}
{% set sliders = get_sliders() %}
{% if sliders|length > 0 %}
<style>
/* スライダーコンテナ */
.custom-slider {
position: relative;
width: 100%;
overflow: hidden;
background-color: #ffffff;
}
@media (max-width: 767px) {
.custom-slider {
padding: 0 20px;
}
}
/* スライダーラッパー */
.custom-slider__wrapper {
position: relative;
width: 100%;
overflow: hidden;
}
/* スライダートラック */
.custom-slider__track {
display: flex;
transition: transform 0.5s ease-in-out;
height: 100%;
}
/* トランジション無効化用 */
.custom-slider__track.no-transition {
transition: none;
}
/* スライドアイテム */
.custom-slider__item {
flex-shrink: 0;
width: 80%;
margin: 0 2%;
aspect-ratio: 16 / 9;
height: auto;
position: relative;
}
/* デスクトップ: 3カラム表示 */
@media (min-width: 768px) {
.custom-slider__item {
width: 50%;
min-width: 700px;
margin: 0 1%;
opacity: 0.5;
transition: opacity 0.5s ease, transform 0.5s ease;
}
/* 中央のアクティブスライド */
.custom-slider__item.is-active {
opacity: 1;
z-index: 2;
}
}
/* リンク */
.custom-slider__link {
display: block;
width: 100%;
height: 100%;
text-decoration: none;
color: inherit;
}
/* 画像 */
.custom-slider__image {
width: 100%;
height: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
/* ナビゲーションボタン */
.custom-slider__nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: rgba(255, 255, 255, 0.9);
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease, transform 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.custom-slider__nav:hover {
background-color: rgba(255, 255, 255, 1);
transform: translateY(-50%) scale(1.1);
}
.custom-slider__nav:active {
transform: translateY(-50%) scale(0.95);
}
.custom-slider__nav--prev {
left: 15px;
}
.custom-slider__nav--next {
right: 15px;
}
.custom-slider__nav i {
font-size: 18px;
color: #333;
}
/* レスポンシブ調整 */
@media (max-width: 767px) {
.custom-slider__nav {
width: 30px;
height: 30px;
}
}
/* アニメーション用 */
.custom-slider__track.is-transitioning {
transition: transform 0.5s ease-in-out;
}
/* ミニスライダー */
.mini-slider {
position: relative;
width: 100%;
max-width: 1200px;
margin: 20px auto 0;
padding: 0 15px 40px;
}
.mini-slider__container {
position: relative;
overflow: hidden;
}
.mini-slider__track {
display: flex;
gap: 12px;
transition: transform 0.3s ease;
padding: 8px 0;
}
/* トランジション無効化用 */
.mini-slider__track.no-transition {
transition: none;
}
.mini-slider__item {
flex-shrink: 0;
width: calc(16.66% - 10px);
min-width: 120px;
aspect-ratio: 16 / 9;
cursor: pointer;
position: relative;
border-radius: 6px;
overflow: hidden;
border: 2px solid transparent;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
/* アクティブでないミニスライダーに網掛け */
.mini-slider__item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
opacity: 1;
transition: opacity 0.3s ease;
z-index: 1;
pointer-events: none;
}
.mini-slider__item.is-active::before {
opacity: 0;
}
.mini-slider__item:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.mini-slider__item:hover::before {
opacity: 0.3;
}
.mini-slider__item.is-active {
border-color: #23ac38;
box-shadow: 0 4px 16px rgba(0, 123, 255, 0.3);
}
.mini-slider__item.is-active:hover::before {
opacity: 0;
}
.mini-slider__item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* レスポンシブ: タブレット */
@media (max-width: 991px) {
.mini-slider__item {
width: calc(20% - 9.6px);
min-width: 100px;
}
}
/* レスポンシブ: モバイル(非表示) */
@media (max-width: 767px) {
.mini-slider {
display: none;
}
}
/* モバイルドットインジケーター */
.mobile-dots {
display: none; /* デフォルトは非表示 */
justify-content: center;
align-items: center;
gap: 8px;
padding: 15px 0;
margin-top: 10px;
}
@media (max-width: 767px) {
.mobile-dots {
display: flex; /* スマホのみ表示 */
}
}
.mobile-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.3);
border: none;
cursor: pointer;
transition: all 0.3s ease;
padding: 0;
}
.mobile-dot:hover {
background-color: rgba(0, 0, 0, 0.5);
}
.mobile-dot.is-active {
background-color: #23ac38;
transform: scale(1.3);
}
</style>
<div class="custom-slider" id="customSlider">
<div class="custom-slider__wrapper">
<div class="custom-slider__track" id="sliderTrack">
{% for slider in sliders %}
<div class="custom-slider__item" data-index="{{ loop.index0 }}">
{% set link = slider.url ? slider.url : (slider.Category ? '/products/list?category_id=' ~ slider.Category.id : null) %}
{% if link %}
<a href="{{ link }}"
class="custom-slider__link"
{% if slider.targetBlank %}target="_blank" rel="noopener noreferrer"{% endif %}>
<img src="{{ asset(slider.fileName, 'save_image') }}"
alt="{{ slider.altText }}"
class="custom-slider__image">
</a>
{% else %}
<img src="{{ asset(slider.fileName, 'save_image') }}"
alt="{{ slider.altText }}"
class="custom-slider__image">
{% endif %}
</div>
{% endfor %}
</div>
</div>
<!-- ナビゲーションボタン -->
<button class="custom-slider__nav custom-slider__nav--prev" id="sliderPrev">
<i class="fas fa-chevron-left"></i>
</button>
<button class="custom-slider__nav custom-slider__nav--next" id="sliderNext">
<i class="fas fa-chevron-right"></i>
</button>
</div>
<!-- モバイルドットインジケーター -->
<div class="mobile-dots" id="mobileDots">
{% for slider in sliders %}
<button class="mobile-dot" data-index="{{ loop.index0 }}"></button>
{% endfor %}
</div>
<!-- ミニスライダー -->
<div class="mini-slider" id="miniSlider">
<div class="mini-slider__container">
<div class="mini-slider__track" id="miniSliderTrack">
{% for slider in sliders %}
<div class="mini-slider__item" data-index="{{ loop.index0 }}">
<img src="{{ asset(slider.fileName, 'save_image') }}"
alt="{{ slider.altText }}">
</div>
{% endfor %}
</div>
</div>
</div>
<script>
(function() {
var slideCount = {{ sliders|length }};
if (slideCount === 0) return;
const slider = {
currentIndex: 1, // クローンスライドを考慮して1からスタート
totalSlides: slideCount,
realSlides: slideCount,
miniCloneCount: Math.min(3, slideCount), // ミニスライダーのクローン枚数(スライド数に応じて調整)
miniCurrentIndex: Math.min(3, slideCount), // ミニスライダーはクローン枚数を考慮してスタート
miniTotalSlides: slideCount,
miniRealSlides: slideCount,
autoplayInterval: null,
autoplayDuration: 5000, // 5秒
isTransitioning: false,
isMiniTransitioning: false,
elements: {
track: null,
items: null,
prevBtn: null,
nextBtn: null,
miniSlider: null,
miniTrack: null,
miniItems: null,
mobileDots: null
},
init: function() {
// DOM要素の取得
this.elements.track = document.getElementById('sliderTrack');
this.elements.prevBtn = document.getElementById('sliderPrev');
this.elements.nextBtn = document.getElementById('sliderNext');
this.elements.miniSlider = document.getElementById('miniSlider');
this.elements.miniTrack = document.getElementById('miniSliderTrack');
this.elements.mobileDots = document.querySelectorAll('.mobile-dot');
// 1枚以下の場合はスライダー機能を無効化
if (this.realSlides <= 1) {
this.elements.prevBtn.style.display = 'none';
this.elements.nextBtn.style.display = 'none';
if (this.elements.miniSlider) this.elements.miniSlider.style.display = 'none';
var dotsContainer = document.getElementById('mobileDots');
if (dotsContainer) dotsContainer.style.display = 'none';
var singleItems = this.elements.track.querySelectorAll('.custom-slider__item');
if (singleItems.length > 0) singleItems[0].classList.add('is-active');
return;
}
// クローンスライドの追加(メイン)
this.createClones();
// クローンスライドの追加(ミニ)
this.createMiniClones();
// 要素の再取得(クローン追加後)
this.elements.items = document.querySelectorAll('.custom-slider__item');
this.elements.miniItems = document.querySelectorAll('.mini-slider__item');
this.totalSlides = this.elements.items.length;
this.miniTotalSlides = this.elements.miniItems.length;
// 初期位置設定
this.updateSlider(false);
// イベントバインド
this.bindEvents();
// 自動再生開始
this.startAutoplay();
},
createClones: function() {
const track = this.elements.track;
const items = track.querySelectorAll('.custom-slider__item');
// 最後のスライドのクローンを先頭に追加
const lastClone = items[items.length - 1].cloneNode(true);
lastClone.classList.add('clone');
track.insertBefore(lastClone, items[0]);
// 最初のスライドのクローンを最後に追加
const firstClone = items[0].cloneNode(true);
firstClone.classList.add('clone');
track.appendChild(firstClone);
},
createMiniClones: function() {
const track = this.elements.miniTrack;
const items = track.querySelectorAll('.mini-slider__item');
const itemCount = items.length;
const cloneCount = this.miniCloneCount;
// 先頭に最後のN枚のクローンを追加(逆順で追加)
for (let i = cloneCount - 1; i >= 0; i--) {
const clone = items[itemCount - 1 - i].cloneNode(true);
clone.classList.add('clone');
track.insertBefore(clone, items[0]);
}
// 末尾に最初のN枚のクローンを追加
for (let i = 0; i < cloneCount; i++) {
const clone = items[i].cloneNode(true);
clone.classList.add('clone');
track.appendChild(clone);
}
},
bindEvents: function() {
this.elements.prevBtn.addEventListener('click', () => this.prev());
this.elements.nextBtn.addEventListener('click', () => this.next());
// トランジション終了イベント(メイン)
this.elements.track.addEventListener('transitionend', () => {
this.handleTransitionEnd();
});
// トランジション終了イベント(ミニ)
this.elements.miniTrack.addEventListener('transitionend', () => {
this.handleMiniTransitionEnd();
});
// ミニスライダーのクリックイベント(クローンを含む全てのスライド)
this.elements.miniItems.forEach((item, index) => {
item.addEventListener('click', () => {
// クローンの場合はクローン位置のまま同期、実スライドの場合は従来通り
if (item.classList.contains('clone')) {
// クローンがクリックされた場合、そのクローン位置に同期
this.goToSlideByMiniIndex(index);
} else {
// 実スライドの場合は従来通り
const realIndex = parseInt(item.getAttribute('data-index'));
this.goToSlide(realIndex);
}
});
});
// モバイルドットのクリックイベント
this.elements.mobileDots.forEach(dot => {
dot.addEventListener('click', () => {
const index = parseInt(dot.getAttribute('data-index'));
this.goToSlide(index);
});
});
},
updateSlider: function(animate = true) {
const isMobile = window.innerWidth < 768;
const offset = isMobile ?
-(this.currentIndex * 84) + 8 :
-(this.currentIndex * 52) + 24;
// アニメーションの有効/無効
if (!animate) {
this.elements.track.classList.add('no-transition');
} else {
this.elements.track.classList.remove('no-transition');
}
this.elements.track.style.transform = `translateX(${offset}%)`;
// アクティブクラスの更新(デスクトップのみ)
if (!isMobile) {
this.elements.items.forEach((item, index) => {
item.classList.toggle('is-active', index === this.currentIndex);
});
}
// アニメーションなしの場合は即座にクラスを戻す
if (!animate) {
setTimeout(() => {
this.elements.track.classList.remove('no-transition');
}, 50);
}
// ミニスライダーの更新(同じanimate設定を適用)
this.updateMiniSlider(animate);
// モバイルドットの更新
this.updateMobileDots();
},
updateMobileDots: function() {
// モバイルドットが存在しない場合は何もしない
if (!this.elements.mobileDots || this.elements.mobileDots.length === 0) return;
// 現在のスライドインデックスを計算(クローンを除く)
let realIndex = this.currentIndex - 1;
if (realIndex < 0) realIndex = this.realSlides - 1;
if (realIndex >= this.realSlides) realIndex = 0;
// アクティブクラスの更新
this.elements.mobileDots.forEach(dot => {
const dotIndex = parseInt(dot.getAttribute('data-index'));
dot.classList.toggle('is-active', dotIndex === realIndex);
});
},
updateMiniSlider: function(animate = true) {
if (!this.elements.miniItems || this.elements.miniItems.length === 0) return;
// ミニスライダーのインデックスをメインに同期(オフセット調整)
// メインはクローン1枚(index 1から)、ミニはクローン3枚(index 3から)
this.miniCurrentIndex = this.currentIndex + (this.miniCloneCount - 1); // クローン枚数差でオフセットを調整
// 実際のスライドインデックスを計算(クローンを除く)
let realIndex = this.currentIndex - 1;
if (realIndex < 0) realIndex = this.miniRealSlides - 1;
if (realIndex >= this.miniRealSlides) realIndex = 0;
// アクティブクラスの更新(クローンを除く実スライドのみ)
this.elements.miniItems.forEach((item, index) => {
if (!item.classList.contains('clone')) {
const itemIndex = parseInt(item.getAttribute('data-index'));
item.classList.toggle('is-active', itemIndex === realIndex);
}
});
// ミニスライダーの位置計算(クローンを考慮)
if (this.elements.miniTrack && this.elements.miniSlider) {
// アニメーションの有効/無効
if (!animate) {
this.elements.miniTrack.classList.add('no-transition');
} else {
this.elements.miniTrack.classList.remove('no-transition');
}
// アクティブなアイテムを取得(クローン含む)
const allItems = Array.from(this.elements.miniItems);
const activeItemIndex = this.miniCurrentIndex;
const activeItem = allItems[activeItemIndex];
if (activeItem) {
const containerWidth = this.elements.miniSlider.offsetWidth;
const itemLeft = activeItem.offsetLeft;
const itemWidth = activeItem.offsetWidth;
const scrollPosition = itemLeft - (containerWidth / 2) + (itemWidth / 2);
this.elements.miniTrack.style.transform = `translateX(-${Math.max(0, scrollPosition)}px)`;
}
// アニメーションなしの場合は即座にクラスを戻す
if (!animate) {
setTimeout(() => {
this.elements.miniTrack.classList.remove('no-transition');
}, 50);
}
}
},
goToSlide: function(index) {
if (this.isTransitioning) return;
this.isTransitioning = true;
// クローンを考慮したインデックスに変換(+1)
this.currentIndex = index + 1;
this.updateSlider(true);
this.resetAutoplay();
},
goToSlideByMiniIndex: function(miniIndex) {
if (this.isTransitioning) return;
this.isTransitioning = true;
var cloneCount = this.miniCloneCount;
var mainCloneCount = 1;
var mainIndex;
if (miniIndex < cloneCount) {
mainIndex = 0;
} else if (miniIndex >= this.miniTotalSlides - cloneCount) {
mainIndex = this.realSlides + mainCloneCount;
} else {
mainIndex = miniIndex - cloneCount + mainCloneCount;
}
this.currentIndex = mainIndex;
this.updateSlider(true);
this.resetAutoplay();
},
next: function() {
if (this.isTransitioning) return;
this.isTransitioning = true;
this.currentIndex++;
this.updateSlider(true);
this.resetAutoplay();
},
prev: function() {
if (this.isTransitioning) return;
this.isTransitioning = true;
this.currentIndex--;
this.updateSlider(true);
this.resetAutoplay();
},
handleTransitionEnd: function() {
this.isTransitioning = false;
// 最後のクローンに到達したら、最初の実スライドに瞬間移動
if (this.currentIndex === this.totalSlides - 1) {
this.currentIndex = 1;
this.updateSlider(false);
}
// 最初のクローンに到達したら、最後の実スライドに瞬間移動
if (this.currentIndex === 0) {
this.currentIndex = this.totalSlides - 2;
this.updateSlider(false);
}
},
handleMiniTransitionEnd: function() {
this.isMiniTransitioning = false;
// 最後のクローン領域に到達したら、最初の実スライドに瞬間移動
if (this.miniCurrentIndex >= this.miniTotalSlides - this.miniCloneCount) {
this.miniCurrentIndex = this.miniCloneCount; // 最初の実スライド位置
this.updateMiniSliderPosition(false);
}
// 最初のクローン領域に到達したら、最後の実スライドに瞬間移動
if (this.miniCurrentIndex < this.miniCloneCount) {
this.miniCurrentIndex = this.miniTotalSlides - this.miniCloneCount - 1; // 最後の実スライド位置
this.updateMiniSliderPosition(false);
}
},
updateMiniSliderPosition: function(animate = true) {
if (!this.elements.miniTrack || !this.elements.miniSlider) return;
// アニメーションの有効/無効
if (!animate) {
this.elements.miniTrack.classList.add('no-transition');
} else {
this.elements.miniTrack.classList.remove('no-transition');
}
// アクティブなアイテムを取得(クローン含む)
const allItems = Array.from(this.elements.miniItems);
const activeItem = allItems[this.miniCurrentIndex];
if (activeItem) {
const containerWidth = this.elements.miniSlider.offsetWidth;
const itemLeft = activeItem.offsetLeft;
const itemWidth = activeItem.offsetWidth;
const scrollPosition = itemLeft - (containerWidth / 2) + (itemWidth / 2);
this.elements.miniTrack.style.transform = `translateX(-${Math.max(0, scrollPosition)}px)`;
}
// アニメーションなしの場合は即座にクラスを戻す
if (!animate) {
setTimeout(() => {
this.elements.miniTrack.classList.remove('no-transition');
}, 50);
}
},
startAutoplay: function() {
this.autoplayInterval = setInterval(() => {
this.next();
}, this.autoplayDuration);
},
stopAutoplay: function() {
if (this.autoplayInterval) {
clearInterval(this.autoplayInterval);
this.autoplayInterval = null;
}
},
resetAutoplay: function() {
this.stopAutoplay();
this.startAutoplay();
}
};
// スライダー初期化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => slider.init());
} else {
slider.init();
}
// リサイズ時の再計算
let resizeTimer;
window.addEventListener('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
slider.updateSlider(false);
}, 250);
});
})();
</script>
{% endif %}