JavaScriptでなるべく簡単にサムネイル付きスライダーを自作する
先週、JavaScriptを使ったスライドショーの記事が多く読まれているので、「CSSとJavaScriptでスワイプ操作にも対応した簡単なスライドを自作する」という記事を投稿しました。
自動再生と手動切り替えの違いはあるものの、似たような記事を上げても大して読まれないだろうと思っていたのですが、驚いたことに、1週間経たないうちに300人以上に読まれています。…………なぜスライダーを作りたがる人がこんなに多いのか謎です。
というわけで、味を占めたぼくは需要に従い、今回はサムネイル付きのスライダーをJavaScriptで自作してみたいと思います。
サムネイル付きスライダーの実行例
あれこれ書く前に、まずは実行例。
ネットショップの商品ページなどでよく見かけませんかね?
メインとなる商品画像と、サブとなる他画像を並べておくのに使ったり。
このスライダーのおおまかな仕様をまとめると下記のとおりです。
- スライドの左右に移動用の矢印を配置
- スマホで開いた場合はスワイプ操作も可能
- スワイプした際、画像がスナップ(吸着)する
- スライド下部のサムネイルをクリック(タップ)時はそのスライドを表示
- 逆にスライドを矢印やスワイプで切り替えた場合にもサムネイルの選択枠は同期する
HTML部分の実装と解説
<div class="slider">
<div>
<ul>
<li><a href="sample (1).jpg"><img src="sample (1).jpg"></a></li>
<li><a href="sample (2).jpg"><img src="sample (2).jpg"></a></li>
<li><a href="sample (3).jpg"><img src="sample (3).jpg"></a></li>
<li><a href="sample (4).jpg"><img src="sample (4).jpg"></a></li>
</ul>
<span class="arrow left"></span>
<span class="arrow right"></span>
</div>
<ul class="thumbs">
<li><img src="sample (1).jpg"></li>
<li><img src="sample (2).jpg"></li>
<li><img src="sample (3).jpg"></li>
<li><img src="sample (4).jpg"></li>
</ul>
</div>
前回とそう大きくは変わらないのですが、1ページ内に複数のスライダーを配置する可能性を考えると、スライド部分とサムネイル部分をひとつの塊として囲っておいたほうが便利なため、<div class="slider">でひとまとめにしています。
スライド左右に配置する矢印については、画像ファイルを用意すればulタグのbefore/after疑似要素を使って配置できるため、DIVタグで囲まなくても良いのですが………やはり不要な画像を用意するのはPageSpeedInsightsのスコアも低くなる原因になるし、今回もCSSで矢印を描画することにしました。
そのため、上部スライドについては <DIV>~<UL>~スライド~</UL><SPAN>~矢印1~</SPAN><SPAN>~矢印2~</SPAN></DIV>みたいな入れ子構造になっています。
CSS部分の実装と解説
<style>
.slider > div {
max-width:400px;
max-height:800px;
position:relative;
border:1px solid lightgray;
}
.slider > div > ul {
padding:0;
margin:0;
width:100%;
height:100%;
font-size:0;
overflow:hidden;
white-space:nowrap;
scroll-snap-type:x mandatory;
scroll-behavior:smooth;
}
.slider img {
width:100%;
height:100%;
object-fit:contain;
image-rendering:-webkit-optimize-contrast;
}
.slider > div > ul > li {
list-style:none;
display:inline-block;
scroll-snap-align:center;
width:100%;
height:100%;
}
.slider .arrow:before {
content:'';
position:absolute;
width:10px;
height:10px;
border-left:4px solid gray;
border-bottom:4px solid gray;
}
.slider .arrow {
top:0;
position:absolute;
width:8%;
height:100%;
background:rgba(255,255,255,0);
cursor:pointer;
}
.slider .arrow:hover {
background:rgba(255,255,255,0.3);
}
.slider .left {
left:0px;
}
.slider .left:before {
top:calc(50% - 10px);
left:calc(50% - 4px);
transform:rotate(45deg);
}
.slider .right {
right:0;
}
.slider .right:before {
top:calc(50% - 10px);
right:calc(50% - 6px);
transform:rotate(-135deg);
}
@media screen and (max-width:480px) {
.slider > div > ul {
overflow-x:auto;
}
}
.slider .thumbs {
max-width:400px;
border:1px solid lightgray;
list-style:none;
margin:0;
padding:0;
font-size:0;
}
.slider .thumbs li {
display:inline-block;
cursor:pointer;
width:calc(25% - 4px);
border:2px solid white;
opacity:0.5;
}
.slider .thumbs li.selected {
border:2px solid gray;
opacity:1;
}
.slider .thumbs img {
width:100%;
object-fit:contain;
padding:0;
}
</style>
CSS部分は長いので、前回説明した部分は省略します。
追加された部分は主にサムネイル用の .thumbs の部分ですが、これもまぁinline-blockにしたりcursor:pointer;でマウスカーソルを変えたりといった部分はいつもどおりなので解説は不要でしょう。
そのほかの部分についてちょっとだけ補足説明しておきます。
LIタグの余白はfont-size:0で消せる
スライド部分もサムネイル部分も、画像を横並びにするのにUL/LIタグを使っています。LIタグにdisplay:inline-block;を指定することで、横並び表示が可能になるわけですが、それだけだとどうしてもLIタグに謎の余白が出来ちゃうんですね。
この余白は <ul><li>ほにゃらら</li><li>ほにゃらら</li></ul> のように改行せずに並べて書けば消えるのですが、それではHTMLの可読性が著しく低下します。
よって、ULタグ、上の例では .slider .thumbs { ~ } と書かれた部分ですが、ここで font-size:0; を指定することで、LIタグの左右に余計な空白が入らないように調整しています。
前回はそこまで細かくしなくても、ほどほどの位置にスクロールしてやればscroll-snap-typeの効果で、いい感じに吸着したので良かったのですが、今回は「サムネイルがクリックされたらスライドを適切な位置にスクロールする」という動作をJavaScriptで計算して行っているため、謎の余白が入られると困るのです。
…厳密にはその謎の余白まで計算に入れれば良い話ですけど、知らない人がソースを読んだら何だろうこの謎の調整値は?と不思議に思うでしょう。ということで、きっちりfont-size:0;を指定させてもらいました。
li.selected
サムネイルが選択された際のデザインはクラス名 .selected を使用。自分用にいいかげんに作る場合はJavaScript内で elm.style.border = '2px solid gray'; みたいな感じで直接的にスタイルを書いてしまうのですが、それだと後でデザイン変更する際にJavaScriptのコードをいじらなければならないので、今回はCSSの .selected の部分でサムネイル選択時の枠線や透明度※の指定をしています。
※サムネイルは50%の透明度で表示され、選択中のサムネイルだけハッキリ表示されます
-webkit-optimize-contrast
ここで解説するようなことでもないのですが、Chromeの画像縮小処理ってデフォルトだとぼやけ気味なので、image-rendering:-webkit-optimize-contrast;を指定して、シャッキリとした縮小画像を表示するように指定しています。
Safariとかだと汚くなるという話も聞くのでこのへんは好みで調整してください。
JavaScript部分の実装と解説
題名どおり、なるべく少ないコードで済むようにしたつもりです。
その分、過剰なまでにコメントを入れておいたので、おおよそはコメント読むだけでだいたいご理解頂けるかと。
<script>
window.addEventListener('load', function () {
document.querySelectorAll('.slider').forEach(slider => {
//左矢印クリック
slider.querySelector('.left').onclick = function () {
let ul = this.parentNode.querySelector('ul');
ul.scrollLeft -= ul.clientWidth;
};
//右矢印クリック
slider.querySelector('.right').onclick = function () {
let ul = this.parentNode.querySelector('ul');
ul.scrollLeft += ul.clientWidth;
};
//サムネイル番号のカウント用
let thumbs_no = 0;
//サムネイル番号の割り振りとクリックイベントの追加
slider.querySelectorAll('.thumbs li').forEach(thumbs_elm => {
if (thumbs_no == 0) {
//最初は1つ目のサムネイルが選択された状態にしておく
thumbs_elm.classList.add('selected');
}
//自身が何番目の要素かdataset属性に保存しておく
thumbs_elm.dataset.no = thumbs_no++;
//サムネイルクリック時のイベント
thumbs_elm.onclick = function () {
//現在アクティブなサムネイルのクラスを変更(選択解除)
slider.querySelector('.thumbs .selected').classList.remove('selected');
//次にアクティブにするサムネイルのクラスを変更(選択状態)
this.classList.add('selected');
//サムネイル番号からスクロール量を算出する
let preview_ul = slider.querySelector('div > ul');
preview_ul.scrollLeft = (preview_ul.clientWidth * this.dataset.no);
}
});
//スクロール発生時のイベント
slider.querySelector('div > ul').onscroll = function () {
//onscrollはスクロールが終わるまでに何度も発生するため、
//100ミリ秒間onscrollが発生しなかったときに限り、処理を続行する
clearTimeout(slider.dataset.timeoutid);
slider.dataset.timeoutid = setTimeout(function () {
//現在アクティブなサムネイルのクラスを変更(選択解除)
slider.querySelector('.thumbs .selected').classList.remove('selected');
//プレビューとサムネイルのノードを取得
let preview_ul = slider.querySelector('div > ul');
let thumbs = slider.querySelector('.thumbs');
//現在のスクロール量からサムネイル番号を算出
let thumbs_no = Math.floor(preview_ul.scrollLeft / preview_ul.clientWidth);
//アクティブにするサムネイルのクラスを変更(選択状態)
thumbs.children[thumbs_no].classList.add('selected');
}, 100);
};
});
});
</script>
前回説明した矢印クリックの処理は解説を省略します。
サムネイルクリック時のスライド番号の算出方法
サムネイルのクリックはonclickイベントで検出するとして、問題はクリックされたサムネイルが、どのスライド番号なのかわからない点だと思います。
愚直に実現するならば、クリックされたのが何番目のサムネイルなのかチェックし、その番号と同じスライドまでスクロールする、という感じでしょうか。
しかしそれだとクリックするたびに毎回サムネイル全体を検索することになって無駄※な気がしたので、初期処理ですべてのサムネイルのdataset属性にサムネイル番号を割り振っておくことにしました。
※今回の例ではサムネイルは4つですが、数十枚~数百枚単位で載せる場合もあるかも知れませんし
//自身が何番目の要素かdataset属性に保存しておく
thumbs_elm.dataset.no = thumbs_no++;
この部分ですね。タグで書くなら <li data-no="0">~</li> <li data-no="1">~</li> <li data-no="2">~</li> みたいな感じでしょうか。HTMLで書くのは面倒だったし、サムネイルが増えるたびに1の次は2、2の次は3、と数えて書いていくと間違えそうだったので、プログラム側で勝手に採番することにしたわけです。
こうして採番しておけば、サムネイルのonclickイベント発生時、this.dataset.no を参照するだけで自身のサムネイル番号がわかります。
サムネイル番号さえわかれば、あとはULの横幅(=preview_ul.clientWidth)×サムネイル番号でスクロールに必要な量が算出できますよね。
実は最初に作ったときはもっと複雑で、全LIタグのX座標を取得しつつ、ULタグのscrollLeftを調整していたのですが、よく考えたらスライダーのLIの横幅は全部同じなので、掛け算で良いじゃん…となりました。
それぞれ横幅の違う画像を”余白なしで”数珠繋ぎにしたい、という場合はこれよりちょっとだけ複雑な計算をしなければなりませんが、長くなるので今回は省略します。
スライドスクロール時のサムネイル番号の算出方法
先ほどはサムネイルクリック時にどのスライドまでスクロールさせるのか、という話でしたが、今回はその逆。
スライドを矢印ボタン、ないしスマホのスワイプ操作でスクロールさせた場合、下に表示されているサムネイルの選択枠も変更したいな、と思ってこの機能を実装しました。
//スクロール発生時のイベント
slider.querySelector('div > ul').onscroll = function () { /* ~省略~ */ }
この部分ですね。スライドのスクロール時はonscrollイベントが発生するのですが、困ったことにこのイベントの発生頻度が高すぎるのです。1回スクロールする間に数十回イベントが発生するので、そのたびにサムネイル番号の判定処理を入れていたら、無駄すぎます。
ですので、ここではsetTimeoutを使い、100ミリ秒後にサムネイル番号の判定処理を実行しています。
なんだい100ミリ秒遅らせてるだけかい、と思うなかれ。100ミリ秒待っている間にまたonscrollイベントが発生した場合にはclearTimeoutを実行し、処理をキャンセルして、再度100ミリ秒後に処理を予約するんです。
これがスクロールイベント発生時に毎回判定されるため、「スクロールイベントが100ミリ秒発生しなかった場合に限り、次の処理を行う」という処理が実現できています。
スクロールが完了したよ!というイベントがあれば良いんですけどね。まぁ、考えてみればどこでスクロールを終了、と判断するのかは悩ましいところですもんね。今回は100ミリ秒間、スクロールしなかったら、終了と判断しているわけですが、場合によってはもっと長い間操作しなかったときに処理したほうが良いこともあるかも知れないもの。
ともかく、スクロールが完了した、と判定したなら、やることはさっきの逆。
現在のスクロール位置(=preview_ul.scrollLeft)をスライドの幅(=clientWidth)で割り算するだけ。
scroll-snap-typeが指定されているとはいえ、スマホでの操作時など、指を離さずに画像と画像の間の中途半端な位置で動かさなければスクロール終了と判断されてしまうので、この割り算は割り切れない場合があります。
その場合はMath.floorで切り捨てた値をサムネイル番号としています。つまり、動作的にはスクロール途中で指を止めたら前方のスライドのサムネイルが選択状態になる、というわけですね。
これだけだとスナップ処理が動いたときにサムネイル番号がズレるケースも出てきそうなものですが、そこはありがたいことにスナップ処理が動いたときにもonscrollイベントが発生するので問題ありません。
ついでに言うと、画面サイズが変わってスライドの横幅が変更されたときにすらonscrollイベントが発生します。つまり、レスポンシブデザインでも問題ないというわけ。やったね!
まとめ
今回は(も)jQuery等のライブラリを使わずに、サムネイル付きのスライダーを自作してみました。
思ったより簡単だったでしょう? コメントがたくさん書いてあるから行数増えてるけれど、実質2~30行くらいのコードだし。
最初、これを作ろうと思ったときはもっと複雑になる予定だったんですよ。先述の解説でも書きましたが、スライダーに表示する画像のサイズが異なるのは当たり前なので、それぞれの画像の横幅を考慮して計算しなければ…と。
でも考えてみたら、LIタグもIMGタグもwidth:100%指定なんですよね。object-fitでうまいことその枠内に異なるサイズの画像を収めていますが、そうなるとLIタグ自体の横幅は全スライドで共通なわけです。
なーんだ、それなら横幅×スライド番号でスクロール量が算出できるじゃないか、と想定していたコーディング量の半分くらいで済んでしまいました。
正直なところ、なんでスライダー関係の記事がよく読まれるのか、ぼくはよくわかっていませんw
ぼくは趣味だからこういうのを自作するだけでキャッキャと喜べますが、業務でやるならbxsliderとかjQuery系のライブラリが充実してるので、ポイっとほおりこんで済ませちゃうと思うんですよねー。
もちろん、自分が趣味で運営しているサイトではjQueryは滅多に使いませんけども。重いしPageSpeedInsightsのスコアが低くなるし、なにより何かあったときに自分で書いたコードのほうが直しやすいから。
同じような人がどのくらいいて、このスライダーの需要がどのくらいあるのか全くわかりませんが、もしたまたまここを訪れた方のお役に立てたのなら幸いです。