スライドショー

最初にスライドショーを自作したのがおよそ11年前。
当時の記事「スマートフォンでも使えるフォトスライド」
Web業界の11年は、とんでもなく昔のことのように感じる。
html4/xhtml1.0 から html5、CSS2 から CSS3 がスタンダードとなった。
デバイスやOS、ブラウザの対応状況もかなり進化した。

というわけで、技術的には11年前にもできたことだが、当時はデバイスや対応ブラウザが限定されてできなかったことも今ではできるようになっているということで、スライドショーを久しぶりに改変した。

サンプル: スライドショー

GitHub
ソースコードは、GitHubにも公開しています。

PHOTO記事「ロンドン、ミーハーに旅した8日間」
実例:PHOTO記事内の画像をクリックすると、この自作ライブラリを使ったスライドショーが表示されます。

個人BLOGトップ
実例: メインビジュアルにスライドショーを表示しています。クリックすると内部リンクするパターンと外部リンクするパターンがあります。

こだわったのは「シンプル」であること

html
<div class="slideshow">
  <div class="slideshow__frame">
    <div class="slideshow__flex">
      <div class="slideshow__unit"><!-- ここに1枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに2枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに3枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに4枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに5枚目のスライドをマークアップ --></div>
    </div>
  </div>
</div>
CSS
スライドの表示領域を指定する(.slideshow ではなく、その子要素の .slideshow__frame に指定する)。
.slideshow__frame {
  width: 100%;
  height: 250px;
}
JavaScript
htmlをセットした後にスライドショーを実行する。
slide.show('.slideshow');

これだけ。

OPTION 1: スライド時間や初期表示のスライド順番を変更する場合

デフォルトでは、初期は一番最初のスライドを表示し、3秒ごとにスライドを切り替える、としているが、スライド時間や初期表示のスライド順番を変更できるようにしている。
具体的には、第二引数にスライド切り替え時間(ミリ秒)、第三引数にデフォルトで表示するスライド番号をセットする。

// 例: 3枚目のスライドを最初に表示し、5秒間隔で切り替える場合
slide.show('.slideshow', 5000, 3);

OPTION 2: ナビゲーションを追加したい場合

.js-slide-nav エリアを追加する(aタグは、前・次のみや、マルポチのみなど、用途に合わせて、htmlを追加する)。表示位置はCSSで好みにデザインする。
ちなみにスライド表示中のaタグには、class「on」をセットするようにしている。

<div class="slideshow">
  <div class="slideshow__frame">
    <div class="slideshow__flex">
      <div class="slideshow__unit"><!-- ここに1枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに2枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに3枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに4枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに5枚目のスライドをマークアップ --></div>
    </div>
  </div>
  <div class="js-slide-nav">
    <a data-nav="prev">前</a>
    <a data-nav="next">次</a>
    <a data-nav="1">●</a>
    <a data-nav="2">●</a>
    <a data-nav="3">●</a>
    <a data-nav="4">●</a>
    <a data-nav="5">●</a>
  </div>
</div>

OPTION 3: 同じページ内に複数のスライドショーがある場合

同じページ内に複数のスライドショーがある場合は、slideshowのマークアップにidもしくはclassを追加して、実行する関数の引数にセットする。

例えば、sample1 と sample2 というクラス名をつける場合。

html
<div class="slideshow sample1">
  <div class="slideshow__frame">
    <div class="slideshow__flex">
      <div class="slideshow__unit"><!-- ここに1枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに2枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに3枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに4枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに5枚目のスライドをマークアップ --></div>
    </div>
  </div>
</div>

<div class="slideshow sample2">
  <div class="slideshow__frame">
    <div class="slideshow__flex">
      <div class="slideshow__unit"><!-- ここに1枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに2枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに3枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに4枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに5枚目のスライドをマークアップ --></div>
    </div>
  </div>
</div>
JavaScript
slide.show('.sample1');
slide.show('.sample2');

OPTION 4: slideshowのエリア外からもスライドショーの切り替えをする場合

html
<div class="slideshow">
  <div class="slideshow__frame">
    <div class="slideshow__flex">
      <div class="slideshow__unit"><!-- ここに1枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに2枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに3枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに4枚目のスライドをマークアップ --></div>
      <div class="slideshow__unit"><!-- ここに5枚目のスライドをマークアップ --></div>
    </div>
  </div>
</div>

<div class="js-sample">
  <a class="js-slide-link" data-nav="3">3番目のスライドを表示する</a>
</div>
JavaScript
slide.show('.slideshow', 3000, 1, '.js-sample');

OPTION 5: スライド内にリンクを設置する場合

普通に、aタグにhref属性をつければ問題ないように感じるかもしれないが、PCなどのタッチデバイス以外で想定しない挙動をしてしまため、若干トリッキーな記述が必要となる。
具体的には、href属性を、data-href属性で記述する。

.slideshow__unit 自体の要素をaタグにする場合

.slideshow__unit 内にaタグでリンクを設置する場合

あらかじめ読み込んでおく内容

あらかじめ読み込んでおくCSSとJavaScriptは下記。ちなみにJavaScriptはjQueryを別途読み込んでいる。

Sass
.slideshow {

  &__frame {
    position: relative;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1;
    overflow: hidden;
  }  

  &__flex {
    position: relative;
    top: 0;
    left: 0;
    z-index: 2;
    display: flex;
    justify-content: flex-start;
    align-items: center;
    height: 100%;
  }

  &__unit {
    display: block;
    width: 100%;
    min-width: 100%;
    height: 100%;
    overflow: hidden;
  }
}
SassファイルをコンパイルしたCSS
@charset "UTF-8";

.slideshow__frame {
  position: relative;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  overflow: hidden;
}

.slideshow__flex {
  position: relative;
  top: 0;
  left: 0;
  z-index: 2;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: start;
      -ms-flex-pack: start;
          justify-content: flex-start;
  -webkit-box-align: center;
      -ms-flex-align: center;
          align-items: center;
  height: 100%;
}

.slideshow__unit {
  display: block;
  width: 100%;
  min-width: 100%;
  height: 100%;
  overflow: hidden;
}
jQueryを読み込んでおく(jQuery)
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
JavaScript
const slide = {};

// メソッド設定(タッチイベントに配慮)
slide.is_touch = (('ontouchstart' in window && 'ontouchend' in window) || navigator.msPointerEnabled);
slide.start = (slide.is_touch)? 'touchstart': 'mousedown';
slide.move = (slide.is_touch)? 'touchmove': 'mousemove';
slide.end = (slide.is_touch)? 'touchend': 'mouseup';
slide.leave = (slide.is_touch)? 'touchleave': 'mouseleave';

/**
 * スライドショー
 *
 * @param (string) n: .slideshowに付与するclass/id e.g. .test1 or #test1
 * @param (number|null) interval: スライドの間隔(ミリ秒)。nullの場合はスライドショーは行わない
 * @param (number) no: 初期時に表示するスライド番号。初期は最初の「1」。
 * @param (string|null) t: 外部リンクを有効にするときのトリガー
 */
slide.show = (n = '.slideshow', interval = 3000, no = 1, t) => {
  const tgt = $(n),
        tgt_flex = $('.slideshow__flex', tgt),
        tgt_unit = $('.slideshow__unit', tgt),
        _slide_link = $('.js-link-slideshow', tgt),
        _link = $('.js-slide-nav a', tgt),
        _link_out = (t)? $(t + ' .js-slide-link'): $('.js-slide-link');
  
  let w = tgt_unit.outerWidth();
  
  let is_dragging,
      is_auto,
      is_reauto,
      anime_auto,
      is_click;

  let slide_num = 0,
      slide_no = 1;

  let slide_pos_x,
      slide_pos_y,
      slide_pos_start_x,
      slide_pos_start_y,
      pos_scroll_x,
      pos_scroll_y,
      pos_scroll_x_diff,
      pos_scroll_y_diff,
      pos_left;
  

  /**
   * エリア切り替え
   */
  const slideChange = () => {
    w = tgt_unit.outerWidth();
    tgt_flex.stop().animate({
      left: w*(slide_no - 1)*(-1)
    });
    
    slideSetNav();
  }
  
  /**
   * ナビ表示
   */
  const slideSetNav = () => {
    tgt.attr('data-no', slide_no);
    
    if (_link.length > 0) {
      _link.removeClass('on');
      $('[data-nav="' + slide_no + '"]', tgt).addClass('on');
    }
  }  
  
  /**
   * ナンバー取得
   */
  const slideNo = (status) => {
    switch (status) {
      case 'next':
        slide_no = slide_no + 1;
        if (slide_no > slide_num) {
          slide_no = 1;
        }
        break;
        
      case 'prev':
        slide_no = slide_no - 1;
        if (slide_no < 1) {
          slide_no = slide_num;
        }
        break;
    }
  }

  /**
   * 手動切り替え
   */
  const slideManual = () => {

    /**
     * ドラッグ&ドロップもしくはエリア外に移動後
     */
    const slideOut = () => {
      is_dragging = false;
      slideChange();
      
      // 一度手動にして、その後またオートにしたい場合
      if (is_reauto) clearTimeout(is_reauto);
      is_reauto = setTimeout(() => {
        if (!is_dragging) {
          is_auto = true;          
        }
      }, interval);
    }

    /*
     * ドラッグ&ドロップ処理
     *
     * ATTENSION:
     * tgt_flex をトリガーにしたいが、
     * iPhoneでエリア外でスライドできないバグがあるため、
     * 不本意だが tgt をトリガーとしている
     */
    tgt
    .on(slide.start, (e) => {
      is_dragging = true;    
      is_auto = false;   // オート切り替えを無効にする

      slide_pos_x = (slide.is_touch)? e.touches[0].pageX: e.pageX;
      slide_pos_y = (slide.is_touch)? e.touches[0].pageY: e.pageY;
      slide_pos_start_x = slide_pos_x;
      slide_pos_start_y = slide_pos_y;
    })
    .on(slide.end, (e) => {
      pos_scroll_x_diff = slide_pos_start_x - pos_scroll_x;
      pos_scroll_y_diff = slide_pos_start_y - pos_scroll_y;
      
      if (Math.abs(pos_scroll_y_diff) < 50) {            

        if (pos_scroll_x_diff > 50) {            
          slideNo('next');
        } else if (pos_scroll_x_diff < -50) {
          slideNo('prev');
        }
        
        e.preventDefault();
      }    
      slideOut();
    })
    .on(slide.leave, slideOut)
    .on(slide.move, (e) => {
      
      // ドラッグ&ドロップ中であるか常に監視する
      if (is_dragging) {
        pos_scroll_x = (slide.is_touch)? e.touches[0].pageX: e.pageX;
        pos_scroll_y = (slide.is_touch)? e.touches[0].pageY: e.pageY;
        
        pos_scroll_x_diff = slide_pos_start_x - pos_scroll_x;
        pos_scroll_y_diff = slide_pos_start_y - pos_scroll_y;
        
        if (Math.abs(pos_scroll_x_diff) > 50) {
            
          tgt_flex.css({
            left: pos_left - (slide_pos_x - pos_scroll_x)
          });

          slide_pos_x = pos_scroll_x;
          
          e.preventDefault();
        }

      }
    });
  }

  /**
   * 自動切り替え
   */
  const slideAuto = () => {  
    if (anime_auto) clearTimeout(anime_auto);
    anime_auto = setTimeout(() => {
      if (is_auto) {

        slideNo('next');
        slideChange();
      }    
      slideAuto();
    }, interval);
  }
  
  /**
   * リサイズ処理(幅情報を更新する)
   */
  $(window).on('resize', () => {
    w = tgt_unit.outerWidth();
    tgt_flex.css('left', w*(slide_no - 1)*(-1));
  });
  
  /*
   * リンク処理
   *
   * MEMO: アロー関数で記述すると、$(this)を認識しないので、無名関数で記述している
   */
  _link.on('click', function () {
    let val = $(this).attr('data-nav');
    
    is_auto = false;
    
    if (val === 'prev' || val === 'next') {
      slideNo($(this).attr('data-nav'));      
    } else {
      slide_no = (+val);
    }
    
    slideChange();
    return false;
  });
  
  // スライド内リンク
  // NOTICE: hrefをセットするとPCで手動スライドの挙動がおかしくなる
  // e.g. 
  if (slide.is_touch) {
    _slide_link.each(function() {
      let _this = $(this),
          href = _this.attr('data-href');

      _this.attr('href', href);
    });
  } else {
    _slide_link
    .on('mousedown', function () {
      is_click = true;
    })  
    .on('mousemove', function() {
      is_click = false;
    })
    .on('mouseup', function () {
      let _this = $(this),
          href = _this.attr('data-href');

      if (is_click) {

        if (_this.attr('target')) {
          window.open(href);
        } else {
          location.href = href;            
        }
        
        is_dragging = false;
        return false;
      }
    });    
  }
  
  // 外部からのリンク
  _link_out.on('click', function () {
    let val = $(this).attr('data-nav');
    
    w = tgt_unit.outerWidth();
    
    is_auto = false;
    slide_no = (+val);
    tgt_flex.css('left', w*(slide_no - 1)*(-1));
    slideSetNav();
  });
  
  /**
   * 初期処理
   */
  const init = () => {
    slide_num = tgt_unit.length;    // スライドの数を取得する

    // 初期表示
    slide_no = no;
    tgt_flex.css('left', w*(slide_no - 1)*(-1));
    slideSetNav();

    // 手動スライド
    slideManual();

    // 自動スライド
    if (interval) {
      is_auto = true;
      slideAuto();
    }
  }
  init();
}

サンプル: スライドショー
ソースコード: GitHub
実例1: PHOTO記事「ロンドン、ミーハーに旅した8日間」
実例2: 個人BLOGトップ