EC-CUBE4用スクロールページャー
商品一覧画面を無限スクロールに対応させるものです。
jScrollのような汎用的な無限スクロール支援ライブラリもいくつかありますが、EC-CUBE4に特化させたかったのと導入時の手間を極力少なくしたかったので今回はそれらは使用せず自前で対応しました。
スクリプト
verticalScrollPager.js
/**
* EC-CUBE4 スクロールページャー
*
* https://blog.akebi.jp/archives/2585
*/
$(function() {
let pMin,
pMax,
getPage = [],
pagerBlock = [],
pagerBlockOffset = [],
resizeId,
productsClassCategoriesBuffer = [],
vPage,
vPageLast,
storageName = 'ecCube4scrollPager',
currentPage,
maxPage,
topLinkLabel,
prevLinkLabel;
// 商品一覧ページ
if($('body')[0].id == 'page_product_list') {
// Object.assign 非対応時終了
if(Object.assign === undefined) return;
// fetch API 非対応時終了
if(!self.fetch) return;
currentPage = 1;
maxPage = 1;
// ページャーブロックから現在ページ番号、最終ページ番号を取得
let ret;
// liタグ取得
let li = $('li');
for(let i in li) {
for(let j in li[i].childNodes) {
// 子ノードの中からリンクのページ番号取得
if(li[i].childNodes[j].href && (ret = li[i].childNodes[j].href.match(/pageno=(\d+)/))) {
// 最大ページ番号
if(maxPage < parseInt(ret[1])) maxPage = parseInt(ret[1]);
// ページャーのアクティブページのクラス名
if(li[i].className == 'ec-pager__item--active') {
// 現在ページ番号
currentPage = parseInt(ret[1]);
}
}
}
}
// カート処理オーバーライド(1ページしかない場合用)
overRide();
if(maxPage == 1) return;
// スクロールアイコン
$('.ec-topicpath').append("<div class='ec-topicpath__item scrollDisabled'><span class='sdEnabled fas fa-arrows-alt-v'></span>"
+ "<span class='sdEnabled far fa-hand-point-left'></span></div>");
let scrollDisabled = $('.scrollDisabled');
scrollDisabled.css('position', 'absolute');
scrollDisabled.css('cursor', 'pointer');
scrollDisabled.css('left', $('.ec-searchnavRole__infos')[0].offsetLeft + $('.ec-searchnavRole__infos')[0].clientWidth - 50);
$(window).resize(function() {
scrollDisabled.css('left', $('.ec-searchnavRole__infos')[0].offsetLeft + $('.ec-searchnavRole__infos')[0].clientWidth - 50);
});
let storage = getStorage();
if(storage['scrollDisabled'] !== undefined && storage['scrollDisabled'] == 1) {
scrollDisabled.on('click', function() {
setStorage({'scrollDisabled':'0'});
location.reload();
});
return;
}
$('.scrollDisabled').addClass('sdActive');
scrollDisabled.on('click', function() {
setStorage({'scrollDisabled':'1'});
location.reload();
});
// 1ページ目のproductsClassCategories保持
productsClassCategoriesBuffer[currentPage] = eccube.productsClassCategories;
pMin = currentPage;
pMax = currentPage;
getPage[currentPage] = 1;
// URLから商品一覧ページでのクエリパラメータを取得
let category_id = '', disp_number = '', orderby = '', name = '';
let c;
if(c = location.href.match(/category_id=(\d+)?/)) { category_id = c[1];}
if(c = location.href.match(/disp_number=(\d+)?/)) { disp_number = c[1];}
if(c = location.href.match(/orderby=(\d+)?/)) { orderby = c[1];}
if(c = location.href.match(/name=([^&]+)?/)) { name = c[1];}
// セパレータ用文字列取得
$('body').before('<div id="spDummyElement" style="display:none; position:fixed;"></div>');
let tmp = $('#spDummyElement')[0];
tmp.className = 'pageSeparatorTopLink';
topLinkLabel = $(tmp).css('content').replace(/^[\"\']+/, '').replace(/[\"\']+$/, '') || '先頭ページへ';
tmp.className = 'pageSeparatorPrevLink';
prevLinkLabel = $(tmp).css('content').replace(/^[\"\']+/, '').replace(/[\"\']+$/, '') || '前のページへ';
$('#spDummyElement').remove();
// .ec-shelfRoleを#scrollAreaで囲む
$('.ec-shelfRole').wrap("<div id='scrollArea'>");
$('#scrollArea').css('position', 'relative');
$('#scrollArea').css('overflow-y', 'auto');
$('#scrollArea').css('overflow-x', 'hidden');
$('#scrollArea').css('overflowScrolling', 'touch');
let scrollAreaWidth = $('#scrollArea').css('width');
let scrollAreaHeight = $('#scrollArea').css('height');
$('#scrollArea').css('height', $(window).innerHeight());
$('#scrollArea').wrap("<div id='scrollAreaFrame'>");
$('.ec-blockTopBtn').css('bottom', '-200px');
pagerBlock[currentPage] = $('.ec-pagerRole')[0].outerHTML;
pagerBlockOffset[currentPage] = 0;
// ページャーブロックを非表示
$('.ec-pagerRole').css('display', 'none');
let lastElement = $('.ec-shelfRole')[0];
// 1ページ目のアンカー追加
let pageSeparator = "<a name='page0'></a><div class='pageSeparator page" + currentPage + "'>"
+ "<span class='pageSeparatorNumber'>" + currentPage + "/" + maxPage + "</span>" + "</div>";
$('#scrollArea').prepend(pageSeparator);
$('.pageSeparatorNumber').css('cursor', 'pointer');
// セパレータ内ページ番号クリック時イベント
$('.pageSeparatorNumber').on('click', function() {
floatingPagerDisplayToggle();
});
// スクロールエリア内フローティングページャーリンクブロック
$('#scrollAreaFrame').prepend('<div id="floatingPager">' + pagerBlock[currentPage] + '</div>');
$('#floatingPager .ec-pagerRole').css('display', '');
$('#floatingPager').css('position', 'absolute');
$('#floatingPager').css('z-index', '10');
$('#floatingPager').css('font-size', '10px');
$('#floatingPager').css('bottom', document.documentElement.clientHeight - $('#scrollArea')[0].offsetTop - parseInt($('#scrollArea').css('height')) );
$('#floatingPager').css('left', $('#scrollArea')[0].offsetLeft);
$('#floatingPager').css('background', !$('.ec-layoutRole').css('background-color').match(/rgba|transparent/) ? $('.ec-layoutRole').css('background-color') : $('body').css('background-color') );
$('#floatingPager').css('width', $('#scrollArea')[0].clientWidth);
$('#floatingPager').on('click', function() {
$('#floatingPager').css('display', 'none');
});
// 初期ec-shelfRoleのstyle更新
$('.ec-shelfRole')[0].id = 'pageBlock' + currentPage;
// 次ページ挿入
if(currentPage < maxPage) insertPage(category_id, currentPage +1, disp_number, orderby, name); //後方
getPage[currentPage +1] = 1;
// ローディングアイコン
$('#scrollAreaFrame').prepend('<span class="pagerLoadingIcon"><span></span></span>');
let ld = $('.pagerLoadingIcon');
ld.css('position', 'absolute');
ld.css('z-index', '-1');
ld.css('opacity', '0');
ld.css('top', $('#scrollAreaFrame')[0].offsetTop + (parseInt($('#scrollAreaFrame').css('height')) - parseInt(ld.css('height')) -40) + 'px');
ld.css('bottom', '40px');
ld.css('left', ($('#scrollAreaFrame')[0].offsetLeft + parseInt($('#scrollAreaFrame').css('width'))/2 - ld.outerWidth(true) /2) + 'px');
// ウィンドウリサイズ時イベント
let windowInnerWidth = $(window)[0].innerWidth;
$(window).resize(function() {
scrollAreaWidth = $('#scrollArea').css('width');
let windowInnerHeight = $(window)[0].innerHeight;
$('#floatingPager').css('left', $('#scrollAreaFrame')[0].offsetLeft);
$('#floatingPager').css('width', $('#scrollAreaFrame')[0].clientWidth);
$('#floatingPager').css('bottom', document.documentElement.clientHeight - $('#scrollAreaFrame')[0].offsetTop - windowInnerHeight);
$('#scrollArea').css('height', windowInnerHeight);
if($(window).scrollTop() == $('#scrollArea')[0].offsetTop)
$(window).scrollTop($('#scrollArea')[0].offsetTop);
ld.css('top', $('#scrollAreaFrame')[0].offsetTop + (parseInt($('#scrollAreaFrame').css('height')) - parseInt(ld.css('height')) -40) + 'px');
ld.css('left', ($('#scrollAreaFrame')[0].offsetLeft + parseInt($('#scrollAreaFrame').css('width'))/2 - ld.outerWidth(true) /2) + 'px');
if(resizeId) {
clearTimeout(resizeId);
resizeId = 0;
}
if(windowInnerWidth != $(window)[0].innerWidth) {
windowInnerWidth = $(window)[0].innerWidth;
resizeId = setTimeout(function() {
let saOt = $('#scrollArea')[0].offsetTop;
let ecSr = $('.ec-shelfRole');
let c;
for(let i = 0; i < ecSr.length; i++) {
let objSr = ecSr[i];
let pageNum = (c = objSr.id.match(/pageBlock(\d+)/)) ? c[1] : '';
pagerBlockOffset[pageNum] = objSr.offsetTop;
}
}, 100);
}
});
let lastScrollAreaScrollTop = 0;
// タッチ開始時処理
$('#scrollArea').on('touchstart', function() {
if($('#scrollArea').scrollTop() < $('#scrollArea')[0].offsetTop) {
lastScrollAreaScrollTop = 0;
} else lastScrollAreaScrollTop = $('#scrollArea').scrollTop() + ($('#scrollArea')[0].offsetTop - $(window).scrollTop());
});
let posAdjust;
let lastPos = 0;
setInterval(function() {
if(lastPos == $('#scrollArea').scrollTop()) posAdjust = 1;
lastPos = $('#scrollArea').scrollTop();
}, 200);
// スクロール時イベント
$('#scrollArea').scroll(function() {
let sa = $('#scrollArea');
let win = $(window);
if(posAdjust && win.scrollTop() != sa[0].offsetTop && sa.scrollTop() > sa[0].offsetTop - win.scrollTop()) {
let wt = sa[0].offsetTop - win.scrollTop();
win.scrollTop(sa[0].offsetTop);
sa.scrollTop(sa.scrollTop() -wt -(sa.scrollTop() - lastScrollAreaScrollTop));
posAdjust = 0;
}
let scrollAreaBottom = sa.scrollTop() + sa.innerHeight();
let lastElementBottom = sa[0].scrollHeight;
if(lastElementBottom - scrollAreaBottom - lastElement.clientHeight *.75 < 0 && getPage[pMax +1] === undefined) {
insertPage(category_id, pMax +1, disp_number, orderby, name);
}
// 表示ページ切替わり判別
let changeBorder = sa.scrollTop() + sa.innerHeight() / 2;
if(lastScrollAreaScrollTop < sa.scrollTop()) {
for(let p = maxPage; p>= 1; p--) {
if(pagerBlockOffset[p] !== undefined && pagerBlockOffset[p] <= changeBorder) {
vPage= p;
break;
}
}
} else {
for(let p = 1; p <= maxPage; p++) {
if(pagerBlockOffset[p] !== undefined && pagerBlockOffset[p] >= changeBorder) {
vPage= p - 1;
break;
}
}
}
// ページ切替わり時
if(vPageLast != vPage) {
vPageLast = vPage;
// 既存ページャー更新
$('#floatingPager')[0].innerHTML = pagerBlock[vPage];
// 商品オブジェクトを現表示ページ+前後1ページ分のものに更新
eccube.productsClassCategories = productsClassCategoriesBuffer[vPage];
if(productsClassCategoriesBuffer[vPage - 1])
Object.assign(eccube.productsClassCategories, productsClassCategoriesBuffer[vPage - 1]);
if(productsClassCategoriesBuffer[vPage + 1])
Object.assign(eccube.productsClassCategories, productsClassCategoriesBuffer[vPage + 1]);
}
lastScrollAreaScrollTop = sa.scrollTop();
});
}
function insertPage(category_id, page, disp_number, orderby, name) {
if(page < 1 || page > maxPage) return;
if(getPage[page] !== undefined) return;
getPage[page] = 1;
$('.pagerLoadingIcon').css('z-index', '9999');
$('.pagerLoadingIcon').css('opacity', '1');
let url = '?category_id=' + (category_id ? category_id : "")
+ '&disp_number=' + disp_number + '&orderby=' + orderby + '&pageno=' + page + '&name=' + (name ? name : "");
fetch(url, {
method: "GET",
})
.then(response => {
if(response.ok) {
return response.text();
} else {
throw new Error();
}
})
.then(text => {
insertPage_(text, page);
})
.catch(error => {
delete getPage[page];
$('.pagerLoadingIcon').css('z-index', '-1');
$('.pagerLoadingIcon').css('opacity', '0');
});
}
function insertPage_(ret, page) {
if(!ret.match(/ec-shelfRole/)) {
$('.pagerLoadingIcon').css('z-index', '-1');
$('.pagerLoadingIcon').css('opacity', '0');
return;
}
let pageHtml;
// テンポラリブロック作成
$('body').before('<div id="tmpArea" style="display:none; position:fixed;"></div>');
// 取得したページデータをテンポラリブロックに追加
$('#tmpArea')[0].innerHTML = ret;
// .ec-shelfRoleブロック取得
pageHtml = $('#tmpArea .ec-shelfRole')[0].outerHTML;
// ページャーブロック取得
pagerBlock[page] = $('#tmpArea .ec-pagerRole')[0].outerHTML;
// テンポラリブロック削除
$('#tmpArea').remove();
// productsClassCategoriesオブジェクト取得
let script = ret.match(/eccube\.productsClassCategories\s+?=\s+\{([\s\S]*)/i);
script[1] = script[1].replace(/\};[\s\S]*/, '');
// 追加ページ分のproductsClassCategoriesBuffer保持
productsClassCategoriesBuffer[page] = JSON.parse('{' + script[1] + '}');
// ページセパレータブロック
let pageSeparator = "<div class='pageSeparator page" + page + "'><span class='pageSeparatorTopLink pageMoveTop" + page + "'>"
+ topLinkLabel + "</span>  <span class='pageSeparatorPrevLink pageMove" + page + "'>"
+ prevLinkLabel + "</span>  <span class='pageSeparatorNumber pageSeparatorNumber" + page + "'>"
+ page + "/" + maxPage + "</span></div>";
pageHtml = pageSeparator + pageHtml.replace(/^(<div)/i, "$1 id='pageBlock" + page + "' style='display:inline-block;'");
pageHtml = pageHtml.replace(/\n\s+/g, "");
// 最終ページ
if(page == maxPage) {
pageHtml += "<div class='pageSeparator' style='height:8px;'></div><div style='height:50px;'></div>";
}
// スクロールエリア後方に挿入
$('#scrollArea').append(pageHtml);
lastElement = $('#pageBlock' + page)[0];
pagerBlockOffset[page] = lastElement.offsetTop;
// ページ移動文字クリック
$('.pageMoveTop' + page).on('click', function() {
$('#scrollArea').animate({'scrollTop': 0}, 500);
setTimeout(function(){
$('html,body').animate({'scrollTop': 0}, 500);
}, 300);
});
$('.pageMove' + page).on('click', function() {
$('#scrollArea').animate({'scrollTop': $('.page' + (page -1))[0].offsetTop}, 250);
});
// セパレータ内ページ番号クリック時イベント更新
$('.pageSeparatorNumber' + page).on('click', function() {
floatingPagerDisplayToggle();
});
$('.pageMoveTop' + page).css('cursor', 'pointer');
$('.pageMove' + page).css('cursor', 'pointer');
$('.pageSeparatorNumber' + page).css('cursor', 'pointer');
$('.pagerLoadingIcon').css('z-index', '-1');
$('.pagerLoadingIcon').css('opacity', '0');
if(pMin > page) pMin = page;
if(pMax < page) pMax = page;
// カート処理オーバーライド 挿入ページDOM構築待ち遅延
setTimeout(function(){
overRide();
}, 500);
return;
}
function floatingPagerDisplayToggle() {
$('#floatingPager').css('display', $('#floatingPager').css('display') == 'none' ? '' : 'none');
}
// localStorage取得
function getStorage() {
let storage = localStorage.getItem(storageName);
return storage ? JSON.parse(storage) : {};
}
// localStorage保存
function setStorage(o) {
if(typeof o !== 'object') return;
let c = getStorage();
if(Object.keys(c).length > 0) {
Object.assign(c, o);
} else {
c = o;
}
localStorage.setItem(storageName, JSON.stringify(c));
}
/* カート処理オーバーライド */
function overRide() {
// 規格1選択時
$('select[name=classcategory_id1]').off('change');
$('select[name=classcategory_id1]')
.change(function() {
var $form = $(this).parents('form');
var product_id = $form.find('input[name=product_id]').val();
var $sele1 = $(this);
var $sele2 = $form.find('select[name=classcategory_id2]');
// 規格1のみの場合
if (!$sele2.length) {
eccube.checkStock($form, product_id, $sele1.val(), null);
// 規格2ありの場合
} else {
eccube.setClassCategories($form, product_id, $sele1, $sele2);
}
});
// 規格2選択時
$('select[name=classcategory_id2]').off('change');
$('select[name=classcategory_id2]')
.change(function() {
var $form = $(this).parents('form');
var product_id = $form.find('input[name=product_id]').val();
var $sele1 = $form.find('select[name=classcategory_id1]');
var $sele2 = $(this);
eccube.checkStock($form, product_id, $sele1.val(), $sele2.val());
});
// イベント削除した後再登録
$('.add-cart').off('click');
$('.add-cart').on('click', function(e) {
var $form = $(this).parents('li').find('form');
// 個数フォームのチェック
var $quantity = $form.parent().find('.quantity');
if ($quantity.val() < 1) {
$quantity[0].setCustomValidity('1以上で入力してください。');
setTimeout(function() {
loadingOverlay('hide');
}, 100);
return true;
} else {
$quantity[0].setCustomValidity('');
}
e.preventDefault();
$.ajax({
url: $form.attr('action'),
type: $form.attr('method'),
data: $form.serialize(),
dataType: 'json',
beforeSend: function(xhr, settings) {
// Buttonを無効にする
$('.add-cart').prop('disabled', true);
}
}).done(function(data) {
// レスポンス内のメッセージをalertで表示
$.each(data.messages, function() {
$('#ec-modal-header').html(this);
});
$('#ec-modal-checkbox').prop('checked', true);
// カートブロックを更新する
$.ajax({
url: '../block/cart',
type: 'GET',
dataType: 'html'
}).done(function(html) {
if($('.ec-headerRole__cart').length) {
$('.ec-headerRole__cart').html(html);
} else {
// cartNavi周りをデザイン変更している場合の個別対応
$('body').before('<div id="cartTemp" style="display:none; position:fixed;"></div>');
// 戻り値を一旦テンポラリエリアに挿入後、各項目を保持
$('#cartTemp')[0].innerHTML = html;
let badge = $('#cartTemp .ec-cartNavi__badge')[0].innerText;
let price = $('#cartTemp .ec-cartNavi__price')[0].innerText;
let isset = $('#cartTemp .ec-cartNaviIsset')[0].innerText;
// テンポラリ削除
$('#cartTemp').remove();
// 保持した項目ごとに反映
if($('.ec-cartNavi__badge').length) $('.ec-cartNavi__badge')[0].innerText = badge;
if($('.ec-cartNavi__price').length) $('.ec-cartNavi__price')[0].innerText = price;
if($('.ec-cartNaviIsset').length) $('.ec-cartNaviIsset')[0].innerText = isset;
}
});
}).fail(function(data) {
alert('カートへの追加に失敗しました。');
}).always(function(data) {
// Buttonを有効にする
$('.add-cart').prop('disabled', false);
});
});
}
});
/**
* EC-CUBE4 スクロールページャー ここまで
*/
verticalScrollPager.css
/**
* スクロールページャー
*/
/* ページ区切り帯 */
.pageSeparator {
text-align: right;
background: #888888;
height: 32px;
margin-bottom: 30px;
padding-top: 6px;
padding-right:20px;
color: white;
font-weight: bold;
}
/* 「前のページへ」リンク文字 */
.pageSeparatorPrevLink {
color: white;
content: '前のページへ';
}
/* 「先頭ページへ」リンク文字 */
.pageSeparatorTopLink {
color: white;
content: '先頭ページへ';
}
/* スクロールアイコン */
.scrollDisabled {
/* 非アクティブ時 */
color: rgba(128, 128, 128, 0.3);
}
.scrollDisabled.sdActive {
/* アクティブ時 */
color: forestgreen;
}
/* ページローディングアイコン */
/* https://mamewaza.com/tools/loading.html */
span.pagerLoadingIcon, span.pagerLoadingIcon:after {
display: inline-block;
width: 50px;
height: 50px;
background-repeat: no-repeat;
background-image:
-webkit-gradient(radial,4 center,0,4 center,4,from(#aee650),color-stop(0.5,#aee650),color-stop(0.9,transparent),to(transparent)),
-webkit-gradient(radial,center 4,0,center 4,4,from(#aee650),color-stop(0.5,#aee650),color-stop(0.9,transparent),to(transparent)),
-webkit-gradient(radial,46 center,0,46 center,4,from(#aee650),color-stop(0.5,#aee650),color-stop(0.9,transparent),to(transparent)),
-webkit-gradient(radial,center 46,0,center 46,4,from(#aee650),color-stop(0.5,#aee650),color-stop(0.9,transparent),to(transparent));
background-image:
-webkit-radial-gradient(10% 50%, 4px 4px, #aee650, #aee650 95%, #aee650 95%, transparent),
-webkit-radial-gradient(50% 10%, 4px 4px, #aee650, #aee650 95%, #aee650 95%, transparent),
-webkit-radial-gradient(90% 50%, 4px 4px, #aee650, #aee650 95%, #aee650 95%, transparent),
-webkit-radial-gradient(50% 90%, 4px 4px, #aee650, #aee650 95%, #aee650 95%, transparent);
background-image:
radial-gradient(4px 4px at 10% 50%, #aee650, #aee650 95%, transparent),
radial-gradient(4px 4px at 50% 10%, #aee650, #aee650 95%, transparent),
radial-gradient(4px 4px at 90% 50%, #aee650, #aee650 95%, transparent),
radial-gradient(4px 4px at 50% 90%, #aee650, #aee650 95%, transparent);
}
span.pagerLoadingIcon {
position: relative;
margin: 0 10px;
vertical-align: middle;
-webkit-animation: pagerLoadingIcon_animation 1.5s linear infinite;
animation: pagerLoadingIcon_animation 1.5s linear infinite;
}
span.pagerLoadingIcon:after {
position: absolute;
content: " ";
left: 0;
top: 0;
margin: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
@-webkit-keyframes pagerLoadingIcon_animation {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes pagerLoadingIcon_animation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/**
* スクロールページャー ここまで
*/
導入方法
verticalScrollPager.js の中身を html/user_data/assets/js/customize.js へ、
verticalScrollPager.css の中身を html/user_data/assets/css/customize.css へ
それぞれコピーすれば導入完了です。
ブラウザキャッシュが邪魔になる場合がありますので、その場合は必要に応じてキャッシュ対策等も行なってください。
デフォルトでスクロールでのページングになっていますが、件数/並び順プルダウンの上にある指アイコンをクリックすることで従来のページ遷移にも切り替えられます。(押すごとにトグル切替)
スクロールでのページング時にも現表示ページ数確認用も兼ねて画面下部に従来の遷移型ページャーを表示させていますが、邪魔であればページャー部分の余白か、スクロールページ内にあるページ区切り帯のページ数表示部分をクリックすれば消すことができます。
再度押すと再び表示します。
通常のページ遷移で途中ページから表示させた場合、それより若いページへスクロールで遡る動作には対応していません。
使用を中止する場合
customize.js及びcustomize.cssから、それぞれコピーした該当コードを削除するだけで元に戻ります。
次ページローディング中に画面下部へ表示させているCSSアイコンは、以下のサイトで作成したCSSコードを使わせていただきました。
CSSだけで作るloadingアイコンメーカー