【jsステップアップ】[3]スライダーを作成する

30/03/2019

ドラッグ値/スワイプ値を利用してスライダーを作成する

現場で役立つjs実装サンプル。として初心者の方に向けての連載3回目です。
今回は実際にスライダーのレイアウトをhtml/cssで作成して
前回内容で説明したドラッグ値/スワイプ値を利用して
javascriptでスライド機能を付加します。

【拡大機能付きスライダー】javascript実装サンプルの連載一覧

  1. [1]初心者の方向けに作成手順や説明等していきます
  2. [2]ドラッグ値/スワイプ値を取得する
  3. [3]スライダーを作成する
  4. [4]モーダルを作成する
  5. [5]拡大(ズーム)機能を作成する
  6. [6]拡大(ズーム)機能を作成する②

スライダーの実装サンプル

下記が今回の実装サンプルです。

スライダー機能に関しては今回でレイアウト・機能共に
一通り作成完了します。

See the Pen スライダーを作成する by admin@nocebo.jp (@nocebojp) on CodePen.

(1)スライダー機能
prev/nextボタンのクリック/タップでスライド
+PCの場合画像マウスドラッグでスライド
+SPの場合スワイプでスライド

スライダーのレイアウトを作成する

まずレイアウトに関してはhtml/cssで組みます。
多分割とオーソドックスな形だと思います。
ので抜粋と軽い補足だけ。

スライダー部分のHTML

<div class="slider">
    <nav class="slider__nav">
        <a href="javascript:;" class="slider__prev"><i class="fas fa-angle-left"></i></a>
        <a href="javascript:;" class="slider__next"><i class="fas fa-angle-right"></i></a>
    </nav>
    <div class="slider__inner">
        <div class="slider__main">
            <ul class="slider__list">
                <li class="slider__elm slider__elm--01">
                    <a href="javascript:;">
                        <img src="https://nocebo.jp/data/zoomslider/img/01.jpg" alt="images">
                    </a>
                </li>
                <li class="slider__elm slider__elm--02">
                    <a href="javascript:;">
                        <img src="https://nocebo.jp/data/zoomslider/img/02.jpg" alt="images">
                    </a>
                </li>
                <li class="slider__elm slider__elm--03">
                    <a href="javascript:;">
                        <img src="https://nocebo.jp/data/zoomslider/img/03.jpg" alt="images">
                    </a>
                </li>
            </ul>
        </div>
    </div>
</div>
<!-- ====================================__  .slider  ============================= -->

上記がスライダー部分のみ抜粋したhtmlです。

htmlに関しては普段からコーディングしている方ならば
難しい事も無いとは思うのですが一応数点下記に補足します。

FontAwesome

<i class="fas fa-angle-left"></i>

上記等のfa-**というようなクラスを指定しているのは
fontawesomeというアイコンフォントです。
今回でいうと左右の矢印ズームのアイコン等で利用してます。

無料ライセンスを利用していますが、
便利なアイコンが一通りは揃っているので、
個人的には今回のようなざっくりしたモック等を作成する場合に重宝しております。

https://fontawesome.com

href=”javascript:;”

<a href="javascript:;"></a>

色々書き方あると思いますが
上記はaタグのリンクを無効にするために使用しています。

javascript:void(0)とかjsの関数側に
preventDefault()とかでももちろん大丈夫だとは思いますが
href入れ忘れなのか、js側での処理なのかがhtml側を見て分かりやすい気がするので
何となく個人的にはつける派です。

そもそもリンクじゃないからaタグはマークアップ的に正しくないのでは?
なんて事も言われるかもですが、テイク・イット・イージーです。

実際作業するチームのルールとか指示等
状況に応じて適宜柔軟に対応しましょう。

スライダー部分のCSS

.slider
{
    position: relative;

    width: $baseWidth;

    .slider__inner
    {
        overflow: hidden;

        width: 100%;
    }

    .slider__elm
    {
        img
        {
            vertical-align: bottom;
        }
    }

    .slider__nav
    {
        position: absolute;
        z-index: 10;
        bottom: 0;

        display: flex;

        width: 100%;

        justify-content: space-between;

        a
        {
            font-size: 30px;
            font-weight: bold;

            display: block;

            width: 33.2%;
            padding: 25px;
            padding-bottom: 20px;

            text-align: center;
            text-decoration: none;

            color: #000;
            background-color: rgba(#fff, .7);

            &:hover
            {
                background-color: rgba(#fff, 1);
            }
        }
    }

    .slider__list
    {
        display: flex;

        width: max-content;

        li
        {
            width: $baseWidth;

            img
            {
                width: 100%;

                pointer-events: none;
            }
        }
    }
}

CSS側も特別な事はやってないかなと思います。
SCSSなので素のCSSが見たい方は
上部PenのサンプルのView Compiledで切り替えられます。

BEM(block element modifier)

見た目のレイアウトだけ整えばよいという方には
特に不要ではありますが、
命名にはBEMを利用しています。(利用しているつもりなだけかも)
不要という方は特に無理に同じ記法でなくても
特に問題はありません。受注する環境によるでしょうが
BEMでお願いしますって事が多いのと、
慣れるとまぁそれなりに見やすいので利用してます。

http://getbem.com/

あと

.block{
    &__elm{}
}

上記みたいなのが、本来の記述規則かなとは思うのですが、
個人的にエディタ等の検索で探しにくい気がするので、
Modifierだけ&–modifierという感じで記述してます。
オレオレルールかもしれないので参考までに、です。

javascriptでスライダーを実装する

スライド機能はjavascriptで実装していきます。

Sliderクラス

下記のSliderクラスが名前そのままですが、
実際にスライダーの機能を付加している部分です。

// /* ===========================================================
// # Slider
// =========================================================== */
class Slider {
    constructor() {
        // -----------------------------
        this.slider = document.querySelector(".slider");
        this.slider__list = document.querySelector(".slider__list");
        this.slider__main = document.querySelector(".slider__main");
        this.slider__prev = document.querySelector(".slider__prev");
        this.slider__next = document.querySelector(".slider__next");
        this.slider__zoom = document.querySelector(".slider__zoom");
        this.slider__elm = document.querySelectorAll(".slider .slider__elm");
        // -----------------------------
        this.state = "ready";
        this.currentID = 1;
        // -----------------------------
        this.factor = 0.15; // イージング 係数
        this.fraction = 0.001; // スライド切り替え終点位置の繰り上げ・切り下げ 端数
        this.sliderWidth = 0; //スライド自体の幅
        this.responseRatio = 0.1; // mouse&touchmove時にスライドorもと位置に戻すかの判定
        // -----------------------------
        this.currentPoint = new Point(); // 現在位置
        this.targetPoint = new Point(); // 目標位置
        // -----------------------------
        this.resize();
        this.events();
        this.elements();
        this.update();
    }

    events() {
        this.slider__prev.addEventListener("click", () => {
            this.slide("prev");
        }, false);
        this.slider__next.addEventListener("click", () => {
            this.slide("next");
        }, false);
        // this.slider__zoom.addEventListener("click", () => {
        //     Modal.instance.open(this.currentID);
        // }, false);

        this.slider__main.addEventListener("mousemove", this.onMove.bind(this), false);
        this.slider__main.addEventListener("touchmove", this.onMove.bind(this), false);
        this.slider__main.addEventListener("mouseup", this.onUp.bind(this), false);
        this.slider__main.addEventListener("touchend", this.onUp.bind(this), false);

        window.addEventListener("resize", this.resize.bind(this), false);
    }

    resize() {
        this.sliderWidth = this.slider.clientWidth;
        Object.assign(this.slider__main.style, {
            transform: `translate(${-this.sliderWidth}px, 0px)`
        });
    }

    onMove() {
        if (this.state != "ready") return;
        if (!EventTouch.instance.isDown) return;
        this.targetPoint.x = EventTouch.instance.diffPoint.x;
    }

    onUp() {
        if (this.state != "ready") return;
        const ratio = Math.abs(EventTouch.instance.diffPoint.x) / this.sliderWidth;
        if (ratio < this.responseRatio) {
            this.targetPoint.x = 0;
            return;
        }
        const state = (Math.sign(EventTouch.instance.diffPoint.x) != 1) ? "next" : "prev";
        this.slide(state);
    }

    clear() {
        this.currentPoint = new Point();
        this.targetPoint = new Point();
        this.state = "ready";
    }

    elements() {
        const slider__elm = document.querySelectorAll(".slider .slider__elm");
        const firstNode = slider__elm[0];
        const lastNode = slider__elm[slider__elm.length - 1];

        if (this.state == "next") {
            this.slider__list.append(firstNode);
        } else {
            this.slider__list.insertBefore(lastNode, firstNode);
        }
    }

    slide(_state) {
        if (this.state != "ready") return;
        this.state = _state;

        if (_state == "prev") {
            this.targetPoint.x = this.sliderWidth;
            this.currentID--;
        }
        if (_state == "next") {
            this.targetPoint.x = -this.sliderWidth;
            this.currentID++;
        }

        if (this.currentID < 1) this.currentID = this.slider__elm.length;
        if (this.currentID > this.slider__elm.length) this.currentID = 1;
    }

    update() {
        window.requestAnimationFrame(this.update.bind(this));

        this.currentPoint.x += Number(this.targetPoint.x - this.currentPoint.x) * this.factor;
        const ratio = Math.abs(this.currentPoint.x) / this.sliderWidth;

        if (ratio > (1.0 - this.fraction)) {
            this.currentPoint.x = this.targetPoint.x;
            this.elements();
            this.clear();
        }
        if (ratio < this.fraction) {
            this.currentPoint.x = this.targetPoint.x = 0;
        }

        Object.assign(this.slider__list.style, {
            transform: `translate(${this.currentPoint.x}px, 0px)`
        });
    }
}

以降は各メソッドのポイントと思われる箇所や処理の説明をします。

constructor()

querySelector

querySelector系でjsで利用する要素を取得します。

jqueryで言うと$(“.**”)的な感じです。

コンストラクタ内その他

this.state = "ready"; //スライダー遷移中などの判定用
this.currentID = 1; //現在のスライダーIDを入れる用、後でモーダルとかに使う。
this.factor = 0.15; // イージング 係数
this.fraction = 0.001; // スライド切り替え終点位置の繰り上げ・切り下げ 端数
this.sliderWidth = 0; //スライドコンテンツ自体の幅
this.responseWidth = 0.1; // mouse&touchmove時にスライドor元位置に戻すかの判定

this.currentPoint = new Point(); // 現在位置
this.targetPoint = new Point(); // 目標位置

その他はコメントベースですが、一応その他の
Sliderクラス内で利用する数値等を入れています。

こいった数値は出来るだけ場所や意味等をわかりやすく
まとめて用意するといいかもです。
マジックナンバーだ!って怖い人に怒られます。
でもまぁ自分が触るだけなら適当でもいいと思います。

events()

ここは特に説明不要かもですが。
constructor内querySelectorで取得した
各要素にaddEventListener等で各種イベントと関連付けます。

一点.slider__zoomはモーダルを開く処理と関連付けるのですが、
まだmodal自体が無い為今回はコメントアウトしてます。

resize(e)

リサイズ処理です。

this.sliderWidth = this.slider.clientWidth;
Object.assign(this.slider__main.style, {
            transform: `translate(${-this.sliderWidth}px, 0px)`
});

sliderWidth

上部サンプルでは固定幅なので、必要ないかもしれないですが。
window幅に合わせて等の場合はresize時にsliderWidthにclientWidth入れて、
スライダーする移動値とコンテンツの幅にズレが出ないようにします。
ウィンドウサイズによってコンテンツ幅が可変する場合等は必須になると思います。

Object.assign

.slider__main要素のスタイル付加に利用しています。
jqueryで言う所の$(“.hoge”).css({**})とかと同じ感じです。

transform

${-this.sliderWidth}pxというのは、
es6の変数展開${}を利用してます。
のでサンプルをそのまま展開すると-768pxです。

これはスライダーの画像を3つ並べていますが、
左右にドラッグした際前(左)と次(右)の
見切れで表示できるように要素を配置する必要があります。
ので.slider__elmを内包している
.slider__mainを左に-コンテンツ幅分ずらしています。

そのままだと初期表示時は
.slider__elmの二番目の要素が最初に表示されてしまう事になるので、
constructor内でも後述のelements()実行で要素の順番入れ替えを行っています。

GPU処理

因みにtopやleftでも表示的には同じ様にできますが、
translateで数値をあてるとGPU処理になる(ハズ)ので
特にスマホ等は動作がスムーズになります。

onMove()

if (this.state != "ready") return; //スライダーが遷移中等であればreturn;
if (!EventTouch.instance.isDown) return; // マウスダウン/画面にタッチしていなければreturn;
this.targetPoint.x = EventTouch.instance.diffPoint.x; 

onMoveはmousemove/touchmove中の処理です。

EventTouch.instance.isDown

EventTouch.instance.isDownは
そのままですが、EventTouchクラスのisDownです。

this.targetPoint.x

これもそのままですが、次項のonUp()するまでは
ドラッグした値=要素位置です。
なのでtargetPointにdiffPointの値をいれます。

onUp()

if (this.state != "ready") return; // スライダーが遷移中等であればreturn;
const ratio = Math.abs(EventTouch.instance.diffPoint.x) / this.sliderWidth; // ドラッグした幅の割合
if (ratio < this.responseRatio) {
    this.targetPoint.x = 0;
    return;
}
const state = (Math.sign(EventTouch.instance.diffPoint.x) != 1) ? "next" : "prev";
this.slide(state);

ratio

ratioはドラッグした幅の割合です。
例えば左に100px動かしたとすると-100pxです。

Math.abs(EventTouch.instance.diffPoint.x)でドラッグXの絶対値を取得します。
絶対値なので100pxになります。

この100pxを768pxで割る=多分0.13…..とか。

ratio < this.responseRatioは

()内が実際の数値とすると

ratio(0.13) < this.responseRatio(0.1)

でreturnせず後述のslide()を実行します。

逆にreturnする場合は
this.targetPoint.x = 0 はドラッグ開始する前の位置なので
ドラッグする前(スライドせずに)の元位置に戻る。
です。

state

Math.signは正確ではないかもですが、
ざっくりいうと数値が正か負を判定できます。
これをdiffPoint.xに利用してprevなのかnextなのかを判定してます。

const state = (Math.sign(EventTouch.instance.diffPoint.x) != 1) ? "next" : "prev";

因みに上記みたいなのは三項演算子です。

変数 = 条件 ? true場合 : false場合 ;

嫌いな人も多い印象なので、(多段とか)ほどほどにした方が無難かもしれない。

elements()

const slider__elm = document.querySelectorAll(".slider .slider__elm");
const firstNode = slider__elm[0];
const lastNode = slider__elm[slider__elm.length - 1];

if (this.state == "next") {
    this.slider__list.append(firstNode);
} else {
    this.slider__list.insertBefore(lastNode, firstNode);
}

elementsはスライド移動後に要素の順を入れ替えます。

“next”でスライドした場合は最初の要素を最後に
“prev”でスライドした場合は逆で最後の要素を最初に

querySelectorAll

constructorでは取得せずに、メソッド内で実行毎に要素を取得してるのは、
実際にメソッドが実行された時の要素順で取得する必要があるからです。
※newで実行された時では無く

slide(_state)

if (this.state != "ready") return;
this.state = _state;

if (_state == "prev") {
    this.targetPoint.x = this.sliderWidth;
    this.currentID--;
}
if (_state == "next") {
    this.targetPoint.x = -this.sliderWidth;
    this.currentID++;
}

if (this.currentID < 1) this.currentID = this.slider__elm.length;
if (this.currentID > this.slider__elm.length) this.currentID = 1;

this.targetPoint.x(prevとnext)

“next”の場合はスライドをコンテンツ幅分左にスライド
“prev”の場合はスライドをコンテンツ幅分右にスライド

するために目標値をいれます。

currentID

currentIDはprev/nextでIDを増減しますが

currentIDが要素数以上もしくは0以下になった場合は
currentIDがループするようになっています。

update(e)

updateはrequestAnimationFrameでフレーム毎に繰り返し処理を実行しています。

window.requestAnimationFrame(this.update.bind(this));

this.currentPoint.x += Number(this.targetPoint.x - this.currentPoint.x) * this.factor;
const ratio = Math.abs(this.currentPoint.x) / this.sliderWidth;

if (ratio > (1.0 - this.fraction)) {
    this.currentPoint.x = this.targetPoint.x;
    this.elements();
    this.clear();
}
if (ratio < this.fraction) {
    this.currentPoint.x = this.targetPoint.x = 0;
}

Object.assign(this.slider__list.style, {
    transform: `translate(${this.currentPoint.x}px, 0px)`
});

this.currentPoint.x

this.currentPoint.x(実際の要素の位置)は
updateで更新されます。

this.currentPoint.x += Number(this.targetPoint.x - this.currentPoint.x) * this.factor;

上記はイージングの計算で更新毎に
徐々にtarget.xに近づきつつ、
差が少なくなる程に変化値は減少します。

ratio

ratioに関しては
先述のonMove内の処理と同じような仕組みですが、
updateで更新される値が徐々に減少する為、
いつまで立っても目的の値(targetPoint)に達しないので
差が一定以下になった際等に、

fractionと組み合わせて
this.currentPoint.x = this.targetPoint.x;
としています。

fraction

if(ratio > (1.0 – this.fraction))
スライド遷移時の目的値に達した場合です。
elements()で要素の順番を入れ変え。
clear()でPointや状態をリセットしています。


if (ratio < this.fraction)
onMove時ドラッグが少なかった際
(スライドすんの?すんの?、せえへんのかいって時)
目的値をを元位置に更新(元の戻る場合に)

Object.assign

Object.assignで要素の位置を更新します。

イージングの計算によって
currentPointは徐々に変化するので、
スライド位置はスムーズに更新されます。

【拡大機能付きスライダー】javascript実装サンプル第3回 まとめと次回

今回は以上です。

スライダー作成という実践的な内容になっている。と思います。

次回はこのスライダーにモーダル機能を追加します。

【拡大機能付きスライダー】javascript実装サンプルの連載一覧

  1. [1]初心者の方向けに作成手順や説明等していきます
  2. [2]ドラッグ値/スワイプ値を取得する
  3. [3]スライダーを作成する
  4. [4]モーダルを作成する
  5. [5]拡大(ズーム)機能を作成する
  6. [6]拡大(ズーム)機能を作成する②

本連載に関して

本連載では、初心者向けに各所の解説をしていますが、
一歩踏み込んだ実践的な内容にはなっていると思います。
※フロントエンド(HTML/CSS/JavaScript)の基礎知識は必須になります。