通常の機能に加えて、ドラッグ&ドロップでスライドの移動が可能でレスポンシブ対応のスライドショーの作り方です。
Demo
スライドショーの骨組みを作る
始めに次のHTMLを書いてスライドショー全体の構造を定義します。
<div id="slideshow" class="slideshow">
<div id="slideshow-container" class="slideshow-container"></div>
<ul id="pager" class="pager">
<li>
<a href="#" class="pager-btn pager-btn-prev">
<</a>
</li>
<li><a href="#" class="pager-btn pager-btn-next">></a></li>
</ul>
<div id="nav" class="nav"></div>
</div>
今回のスライドショーは全体を示す#slideshow
を始め、画像が挿入されていく#slideshow-container
、ページ送り#pager
、現在何番目の画像が表示されているのかが分かるナビゲーション#nav
で構成されています。
画像は後にJavaScriptで挿入するため、現時点で#slideshow-container
は空の状態です。
スライドショーの見た目を作る
次に以下のCSS(SCSS)を書いてスライドショーの見た目を作ります。
.slideshow {
background-color: #ccc;
height: 40vw;
overflow: hidden;
position: absolute;
top: 0%;
left: 50%;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
width: 60vw;
@media (min-width: 800px) {
height: 300px;
width: 500px;
}
&-container {
position: absolute;
top: 0;
width: 100%;
> img {
display: block;
height: auto;
max-width: 100%;
position: absolute;
user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
}
}
}
.pager {
list-style: none;
margin: 0;
padding: 0;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
width: 100%;
&-btn {
background: #333;
border-radius: 100%;
color: #fff;
font-size: 2vw;
height: 8vw;
line-height: 8vw;
position: absolute;
top: 0;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
text-decoration: none;
text-align: center;
width: 8vw;
@media (min-width: 800px) {
height: 44px;
line-height: 44px;
width: 44px;
}
&-prev {
left: 1vw;
}
&-next {
right: 1vw;
}
}
}
.nav {
position: absolute;
bottom: 0;
left: 0;
text-align: center;
width: 100%;
&-btn {
background: #ccc;
border-radius: 100%;
display: inline-block;
height: 4vw;
margin: 0 5px;
width: 4vw;
&.active {
background: #333;
cursor: default;
}
@media (min-width: 800px) {
border-radius: 50%;
height: 20px;
width: 20px;
}
}
}
CSSのポイントは次の通りです。
アクティブ状態のナビボタンを表現する
ナビボタン.nav-btn
に.active
クラスが追加されるとアクティブ状態となります。
ナビボタン.nav-btn
は基本灰色としていますが、現在表示されているスライドの位置を示すアクティブなナビボタンは濃い色で表現します。また、アクティブなナビボタンはcursor: default;
でカーソルの形状が変わらないようにします。
レスポンシブ対応
要素のサイズやスペースの単位をvw
(vh
)にして常にブラウザのサイズに対する割合となるようにします。
スライドショーの機能を実装
最後に以下のJavaScriptで画像やナビボタンの生成を始め、ナビゲーションやページ送りのクリックやスライドをスワイプすることによるスライドの移動や、スライドの自動再生といったスライドショーの機能を実装します。
(() => {
//画像に関するクラス
class Asset {
constructor() {
//URL
this.urls = [
"http://tsukulog.net/wp-content/uploads/2015/08/a0006_001899.jpg",
"http://tsukulog.net/wp-content/uploads/2015/06/GAK88_nekokafeneko-thumb-1000xauto-16206.jpg",
"http://tsukulog.net/wp-content/uploads/2015/06/HIRA86_MBAkey-thumb-1000xauto-17206.jpg",
"http://tsukulog.net/wp-content/uploads/2015/06/N825_benchinouenoshironeko-thumb-autox1000-14809.jpg",
"http://tsukulog.net/wp-content/uploads/2014/05/https-www.pakutaso.com-assets_c-2015-05-26NJ_eigonobisuketto-thumb-1000xauto-14009.jpg"
];
//生成した画像を格納する配列
this._images = [];
//画像の枚数
this._imageLength = 0;
}
get images() {
return this._images;
}
get imageLength() {
return this._imageLength;
}
//画像を生成するメソッド
createImage() {
//URLの数だけ画像を生成
for(let i = 0; i < this.urls.length; i++) {
const image = new Image();
image.src = this.urls[i];
this._images[i] = image;
}
this._imageLength = this._images.length;
//console.log(this._images);
//console.log(this._imageLength);
}
}
//スライドの集合に関するクラス
class Slides {
constructor() {
this.elem = document.getElementById("slideshow-container");
//現在表示されているスライドの番号
this._currentIndex = 0;
//#slideshow-containerをmousedownしたときのポインターのx座標
this.downX = 0;
//#slideshow-containerをmousemoveしたときのポインターのx座標
this.moveX = 0;
//downX~moveXまでの距離
this.differenceX = 0;
//ドラッグしているかどうか
this.isDragging = false;
}
get currentIndex() {
return this._currentIndex;
}
//_currentIndexを更新するメソッド
set currentIndex(value) {
if(typeof value === 'number') {
this._currentIndex = value;
}
}
//ドラッグ&ドロップでスライドを移動できる状態にするメソッド
setupListener(timer, nav) {
this.elem.addEventListener("mousedown", e => this.mouseDown(e));
document.body.addEventListener("mousemove", e => this.mouseMove(e));
document.body.addEventListener("mouseup", e => this.mouseUp(e, nav));
document.body.addEventListener("mouseleave", e => this.mouseUp(e, nav));
this.elem.addEventListener("touchstart", e => this.mouseDown(e));
document.body.addEventListener("touchmove", e => this.mouseMove(e));
document.body.addEventListener("touchend", e => this.mouseUp(e, nav));
document.body.addEventListener("touchleave", e => this.mouseUp(e, nav));
this.elem.addEventListener('mouseover', (e) => timer.stop());
this.elem.addEventListener('mouseout', (e) => timer.start(this, nav));
}
//#slideshow-containerに画像を挿入するメソッド
insertImages(images) {
images.forEach((image) => this.elem.appendChild(image));
}
//挿入された画像を横並びに配置するメソッド
setImages(images) {
images.forEach((image, index) => image.style.left = `${100 * index}%`);
}
//スライドを移動させるメソッド
move(index) {
const image = this.elem.getElementsByTagName('img');
const max = image.length - 1;
//先頭のスライドを左に移動させると最後のスライドを表示する
if (index < 0) {
index = max;
}
//最後のスライドを右に移動させると先頭のスライドを表示する
if (index > max) {
index = 0;
}
this.elem.style.transition = "left 0.5s linear";
this.elem.style.left = `${-100 * index}%`;
//更新
this._currentIndex = index;
}
//mousedownされると実行されるメソッド
mouseDown(e) {
e.preventDefault();
let event;
if (e.type === "mousedown") {
event = e;
} else {
event = e.changedTouches[0];
}
this.downX = event.clientX;
//ドラッグ開始
this.isDragging = true;
}
//mousemoveされると実行されるメソッド
mouseMove(e) {
//ドラッグしていれば
if (this.isDragging) {
e.preventDefault();
let event;
if (e.type === "mousemove") {
event = e;
} else {
event = e.changedTouches[0];
}
this.moveX = event.clientX;
this.differenceX = this.moveX - this.downX;
//mousemoveされたときの#slideshow-containerの幅を取得
const slidesWidth = this.elem.clientWidth;
this.elem.style.transform = `translateX(${Slides.clamp(this.differenceX, this.differenceX - slidesWidth, this.differenceX + slidesWidth)}px)`;
}
}
//mouseupされると実行されるメソッド
mouseUp(e, nav) {
//ドラッグしていれば
if (this.isDragging) {
//ドラッグ終了
this.isDragging = false;
/*
if (this.differenceX == this.downX) {
return;
}
*/
//mouseupされたときの#slideshow-containerの幅
const slidesWidth = this.elem.clientWidth;
//右へslidesWidth / 4より多くドラッグしていれば移動する
if (this.moveX > this.downX && Math.abs(this.differenceX) > slidesWidth / 4) {
this._currentIndex = this._currentIndex - 1;
console.log('→');
}
//左へslidesWidth / 4より多くドラッグしていれば移動する
if (this.moveX < this.downX && Math.abs(this.differenceX) > slidesWidth / 4) {
this._currentIndex = this._currentIndex + 1;
console.log('←');
}
//スライドを移動
this.move(this._currentIndex);
//ナビゲーションを更新
nav.update(this._currentIndex);
this.elem.style.transition = "all 0.5s ease";
this.elem.style.transform = '';
//transition後に次に備えて削除
setTimeout(() => {
this.elem.style.transition = '';
}, 500);
}
}
//スライドの移動範囲を制限するメソッド
static clamp(number, min, max){ //numberをminからmaxまでの値で返す
return Math.max(min, Math.min(number, max));
}
}
//ナビゲーションに関するクラス
class Nav {
constructor() {
//#navの参照を取得
this.elem = document.getElementById("nav");
//ナビボタンを取得
this.btn = this.elem.getElementsByTagName("a");
}
//#navに画像の枚数だけボタンを挿入するメソッド
set(imageLength) {
for (let i = 0; i < imageLength; i++) {
//ナビボタンを生成
const navBtn = document.createElement("a");
navBtn.classList.add('nav-btn');
navBtn.setAttribute("href", "#");
//ナビボタンを#navに挿入
this.elem.appendChild(navBtn);
}
}
//ナビゲーションをクリックできる状態にするメソッド
setupListener(slides) {
const navBtn = this.btn;
//ナビボタンをクリックするとクリックしたナビボタンと同じ番号のスライドに切り替える
for (let i = 0; i < navBtn.length; i++) {
navBtn[i].addEventListener("click", e => {
e.preventDefault();
//クリックしたナビボタン
const target = e.target;
//クリックしたナビボタンの番号
const targetIndex = [].slice.call(navBtn).indexOf(target);
//https://lab.syncer.jp/Web/JavaScript/Snippet/54/
//https://lab.syncer.jp/Web/JavaScript/Snippet/53/
//クリックしたナビボタンと同じ番号のスライドに切り替える
slides.move(targetIndex);
//targetIndex番目のナビボタンをアクティブにする
this.update(targetIndex);
});
}
}
//現在表示されている画像と同じ番号のナビボタンをアクティブにするメソッド
update(currentIndex) {
const navBtn = this.btn;
//console.log(navBtn);
//一旦全てのナビボタンをinactiveにする
for (let i = 0; i < navBtn.length; i++) {
navBtn[i].classList.remove("active");
}
//currentIndex番目のナビボタンをactiveにする
navBtn[currentIndex].classList.add("active");
}
}
//ページャーに関するクラス
class Pager {
constructor() {
this.elem = document.getElementById("pager");
this.btn = this.elem.getElementsByTagName("a");
}
//ページャーをクリックできる状態にするメソッド
setupListener(slides, nav) {
const pagerBtn = this.btn;
for (let i = 0; i < pagerBtn.length; i++) {
pagerBtn[i].addEventListener("click", e => {
e.preventDefault();
const target = e.target;
//クリックしたページャーボタンが.pager-btn-prevであれば一つ前のスライドに切り替える
if (target.classList.contains("pager-btn-prev")) {
slides.currentIndex = slides.currentIndex - 1;
//.pager-btn-nextであれば一つ後のスライドに切り替える
} else {
slides.currentIndex = slides.currentIndex + 1;
}
slides.move(slides.currentIndex);
nav.update(slides.currentIndex);
});
}
}
}
//タイマーに関するクラス
class Timer {
constructor() {
this.timer = null;
}
start(slides, nav) {
//5秒毎にスライドを切り替える
this.timer = setInterval(() => {
slides.currentIndex = slides.currentIndex + 1;
slides.move(slides.currentIndex);
nav.update(slides.currentIndex);
}, 5000);
}
stop() {
clearInterval(this.timer);
}
}
/*
* MAIN SCRIPTS
*/
//インスタンス化
const asset = new Asset();
const slides = new Slides();
const nav = new Nav();
const pager = new Pager();
const timer = new Timer();
//全体を初期化する関数
function init() {
asset.createImage();
const images = asset.images;
const imageLength = asset.imageLength;
slides.insertImages(images);
slides.setImages(images);
slides.move(slides.currentIndex);
nav.set(imageLength);
nav.update(slides.currentIndex);
timer.start(slides, nav);
setupListeners();
}
//スライドショーを操作できる状態にする関数
function setupListeners() {
nav.setupListener(slides);
pager.setupListener(slides, nav);
slides.setupListener(timer, nav);
}
//起動
init();
})();
JavaScriptでは次のことを行っています。
画像の生成
画像を生成するためにAsset
クラスのcreateImage
メソッドでURLの数だけ画像を生成します。
画像の数だけナビボタンを生成
現時点ではナビゲーション内は空なのでNav
クラスのset
メソッドでナビボタンを画像の数だけ生成します。
画像を挿入・配置
生成した画像をslide
クラスのinsertImages
メソッドで#slideshow-container
内に挿入します。更にsetImages
メソッドで挿入した画像を横並びに配置します。
スライドが移動するようにする
slide
クラスのmove
メソッドでスライドが移動するようにします。move
メソッドはindex
を受け取り、受け取ったindex
はスライドの番号を示します。番号が0であれば先頭のスライドであり、1であれば次のスライドとなります。
受け取ったindex
と-100%を掛けた値を#slideshow-container
のleft
プロパティに指定することでスライドを枠外へ移動させます。
最後のスライドに到達して尚スライドを進めた場合は先頭のスライドに戻るようにします。逆も同様です。
スライドの移動と同時にナビボタンの状態を更新する
スライドが移動するとNav
クラスのupdate
メソッドでそれまでアクティブな状態であったナビボタンをインアクティブな状態に変更し、切り替わったスライドと同じ番号のナビボタンをアクティブな状態にします。
ページ送りをクリックするとスライドが移動するようにする
Pager
クラスのsetupListener
メソッドでページ送りボタン(.pager-btn-prev
又は.pager-btn-next
)をクリックするとスライドが移動するようにします。.pager-btn-prev
がクリックされるとスライドは一つ前に戻り、.pager-btn-next
がクリックされるとスライドが一つ後に進みます。
ナビボタンをクリックするとスライドが移動するようにする
Nav
クラスのsetupListener
メソッドでナビボタンをクリックするとその番号と同じスライドに切り替わるようにします。同時にナビボタンの更新も行い、切り替わったスライドと同じ番号のナビボタンをアクティブな状態にします。
スライドをドラッグ&ドロップで移動可能にする
スライド上をドラッグ&ドロップすることでスライドの移動を可能にします。ドラッグ&ドロップはSlides
クラスのmouseDown
・mouseMove
・mouseUp
メソッドで実装します。
mouseDown
メソッドでスライド上をマウスダウンしたときのx座標downX
を取得し、ドラッグ開始を示すisDragging
をtrueに切り替えます。mouseMove
メソッドでドラッグ中の座標moveX
を取得してdownX
からmoveX
までの距離differenceX
を取得後、clamp
メソッドで最小値と最大値を設定した値differenceX
を#slideshow-container
のtransform
プロパティのtranslateX
に指定することで、ドラッグでスライドを動かせるようにします。mouseUp
メソッドでドラッグを終了させてスライドの4分の1の幅slidesWidth / 4
より多くドラッグしていればスライドをその方向へ移動させます。ナビゲーションの更新も合わせて行います。
スライドが自動再生されるようにする
スライドを一定の間隔で自動で進むようにします。Timer
クラスのstart
メソッドで5秒毎にスライドを進めます。ナビゲーションの更新も同時に行います。
自動再生はスライド上をマウスオーバーするとstop
メソッドにより停止されます。マウスアウトすると再びstart
メソッドが実行されて自動再生が行われます。
さいごに
以上で【JavaScript】レスポンシブ+ドラッグ&ドロップでスライドの移動が可能なスライドショーの作り方を終わります。