スライドショー

最初にスライドショーを自作したのがおよそ11年前(2021年12月現在)。
当時の記事「スマートフォンでも使えるフォトスライド」
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');

これだけ。
ちなみに、第二引数にオブジェクトをセットすることで、カスタマイズできるようにした。

キー名説明
intervalnumber|boolean(false)初期値: 3000
スライドの間隔(ミリ秒)
false を指定すると自動切り替えしない
nonumber初期値: 1
初期時に表示するスライド番号
tobject初期値: $(‘.js-slide-link’)
jQueryによる要素指定
beforefunctionスライド移動直前に実行する関数
afterfunctionスライド移動直後に実行する関数
clickfunctionスライドをクリックした際に実行する関数
is_keyboardboolean左右のキーボードキーでスライドを移動するか

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

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

// 例: 3枚目のスライドを最初に表示し、5秒間隔で切り替える場合
slide.show('.slideshow', {
  interval: 5000,
  no: 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', {
  t: $('.js-sample')
});

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

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

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

<a class="slideshow__unit js-link-slideshow" data-href="{URL}"></a>

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

<div class="slideshow__unit">
  <a class="js-link-slideshow" data-href="{URL}"></a>
</div>

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

あらかじめ読み込んでおく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%;
    will-change: left;
  }
  &__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%;
  will-change: left;
}
.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';
slide.is_session = (('sessionStorage' in window) && window['sessionStorage'] !== null);
/**
 * セッションストレージの呼び出しと保存
 *
 * @param {string} n* key
 * @param {anything} v value vがない場合は、nの呼び出し
 * @return {anything} vがあるときはストレージの文字列、ない場合はnの値
 */
slide.storage = (n, v) => {
  let r = null;
  
  if (slide.is_session && typeof n !== 'undefined') {
    if (typeof v !== 'undefined') {
      if (typeof v === 'object') {
        sessionStorage[n] = JSON.stringify(v);
      } else {
        sessionStorage[n] = v;        
      }
    } else {
      try {
        r = (new Function("return " + sessionStorage[n]))();
//        r = JSON.parse(sessionStorage[n]);
      } catch (e) {
        r = sessionStorage[n];
      }
    }
  }
  return r;
}
/**
 * セッションストレージ削除
 *
 * @param {string} n* セッションストレージのキー
 */
slide.storageDel = (n) => {
  sessionStorage.removeItem(n);
}
/**
 * スライドショー
 *
 * @param (string) n: .slideshowに付与するclass/id e.g. .test1 or #test1
 * @param (object) obj
 * 
 * obj.interval (number|null|boolean(false)) スライドの間隔(ミリ秒)。nullもしくはfalseの場合はスライドショーは行わない
 * obj.no (number) 初期時に表示するスライド番号。初期は最初の「1」。
 * obj.t (object|null|boolean(false)) 外部リンクを有効にするときのトリガー object: jQuery e.g. $('.test')
 * obj.before (function|null|boolean(false)) スライド移動前に実行する関数
 * obj.after (function|null|boolean(false)) スライド移動後に実行する関数
 * obj.click (function|null|boolean(false)) スライド上をクリックした際に実行する関数
 * obj.is_keyboard (boolean|null) 左右のキーボードアクションを有効にするか
 */
slide.show = (n = '.slideshow', obj = {}) => {
  const tgt = $(n),
        tgt_flex = $('.slideshow__flex', tgt),
        tgt_unit = $('.slideshow__unit', tgt),
        _link = $('.js-slide-nav a', tgt),
        _link_out = (obj.t)? $('.js-slide-link', obj.t): $('.js-slide-link')
  const interval = (obj.interval || obj.interval === false)? obj.interval: 3000,        
        before_func = obj.before,
        after_func = obj.after,
        click_func = obj.click,
        is_keyboard = obj.is_keyboard
  let no = obj.no || 1
  
  let array_href = []
  
  let is_dragging,
      is_auto,
      is_reauto,
      anime_auto
  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
  let w = tgt_unit.width()
  if (w === 0) {
    setTimeout(() => {
      w = tgt_unit.width()
    }, 1)
  }
  
  // 初期時に表示するスライド番号、セッションがある場合はそれを採用する
  if (slide.storage(n)) {
    no = slide.storage(n)
    slide.storageDel(n)
  }
  /**
   * エリア切り替え
   */
  const slideChange = () => {
    const new_w = tgt_unit.width()
    if (before_func) before_func()
    if (new_w != 0) {
      w = new_w
    }    
    
    tgt_flex.stop().animate({
      left: w * (slide_no - 1) * (-1)
    }, () => {
      if (after_func) after_func()
    })
    
    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;
      
      // 一度手動にして、その後またオートにしたい場合
      if (is_reauto) clearTimeout(is_reauto);
      is_reauto = setTimeout(() => {
        if (!is_dragging && interval) {
          is_auto = true;          
        }
      }, interval);
    }
    /*
     * ドラッグ&ドロップ処理
     *
     * ATTENSION:
     * tgt_flex をトリガーにしたいが、
     * iPhoneでエリア外でスライドできないバグがあるため、
     * 不本意だが tgt をトリガーとしている
     */
    tgt
    .on(slide.start, (e) => {
      is_dragging = false;    
      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 (!is_dragging) {
        if (click_func) click_func()
        if (array_href[slide_no - 1]) {
          slide.storage(n, slide_no)
          location.href = array_href[slide_no - 1];
        }
      } else {
        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');
          }
          slideChange();
          e.preventDefault();
          
        }
           
        slideOut();
      }
    })
    .on(slide.leave, slideOut)
    .on(slide.move, (e) => {
      
      // ドラッグ&ドロップ中であるか常に監視する
      is_dragging = true;
      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);
  }
  /**
   * キーボードによる切り替え
   */
  if (is_keyboard || $('.slideshow').length === 1) {
    document.addEventListener('keydown', (e) => {
      
      switch (e.key) {
        case 'ArrowLeft':
          if (interval) {
            slideAuto()
          }
          slideNo('prev');
          slideChange();
          break;
        case 'ArrowRight':
          if (interval) {
            slideAuto()
          }
          slideNo('next');
          slideChange();
          break;
        // default なし
      }
      return false
    })
  }
  
  /**
   * リサイズ処理(幅情報を更新する)
   */
  $(window).on('resize', () => {
    w = tgt_unit.width();
    tgt_flex.css('left', w*(slide_no - 1)*(-1));
  });
  
  /*
   * リンク処理
   *
   * MEMO: アロー関数で記述すると、$(this)を認識しないので、無名関数で記述している
   */
  _link.on('click', function () {
    let val = $(this).attr('data-nav') || 'next'
    
    is_auto = false
    
    if (val === 'prev' || val === 'next') {
      slideNo(val)
    } else {
      slide_no = (+val)
    }
    
    slideChange()
    return false
  });
  
  // 外部からのリンク
  _link_out.on('click', function () {
    let val = $(this).attr('data-nav') || 'next'
    
    is_auto = false
    
    if (val === 'prev' || val === 'next') {
      slideNo(val)
    } else {
      slide_no = (+val)
    }
    
    slideChange()
    return false
  });
  
  /**
   * 初期処理
   */
  const init = () => {
    slide_num = tgt_unit.length;    // スライドの数を取得する
    // リンクがある場合に備え、配列に格納する
    tgt_unit.each(function () {
      const _this = $(this),
            href = _this.attr('data-href')
      if (href && href != '') {
        array_href.push(href);
      } else {
        array_href.push(null);
      }
    })
    // 初期表示
    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トップ