1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【JavaScript】スクロールして画面と要素が交差したら表示

Last updated at Posted at 2022-02-25

今や当たり前となったスクロールして要素をふわっと表示させるアレ。

  • プラグインを利用したくない
  • HTMLのdata属性でオプションを設定したい

というわけで、自作してみました!

仕様

単一要素への設定と複数要素への一括設定が可能です。

単一設定

  • 表示したい要素にclass(js-inview)を設定します
  • 表示したい要素のdata属性(data-inview-animation)を設定します
    • data属性の値にCSSアニメーション名を設定します
  • 画面と表示したい要素が交差した時にclass(is-inview)が付与されます

オプション

オプションは表示したい要素のdata属性に設定します。

data属性 デフォルト 設定内容
data-inview-animation なし(必須) CSSアニメーション名
data-inview-delay 0 交差してからの遅延秒数(ミリ秒)
data-inview-options   { rootMargin: '0px'} Intersection Observer のオプション
data-inview-toggle - 交差するごとに表示・非表示を繰り返す
data-inview-trigger なし 要素の表示をトリガーとして表示させたい要素

複数設定

  • 表示したい要素の親要素にclass(js-inviews)を設定します
  • 表示したい要素の親要素にdata属性(data-inview-targets-animation)を設定します
    • data属性の値にCSSアニメーション名を設定します
  • 表示したい要素にdata属性(data-inview-target)を設定します
    • data属性の値は設定しません
  • 画面と表示したい要素が交差した時にclass(is-inView)が付与されます

オプション

表示させたい要素の親要素のdata属性に設定します。

data属性 デフォルト 設定内容
data-inview-targets-animation なし(必須) 表示させたい子要素に設定するCSSアニメーション名
data-inview-delay   0 交差してからの遅延秒数(ミリ秒)
data-inview-delay-loop 0 表示させたい子要素を連続して表示する間隔(ミリ秒)
data-inview-options   { rootMargin: '0px'} Intersection Observer のオプション
data-inview-toggle - 交差するごとに表示・非表示を繰り返す

複数設定した場合でも、表示させたい要素に単一設定のオプションを設定することで上書きが可能です。

CSS

css
/* 要素を非表示にする設定 */
[data-inview-animation],
[data-inview-target] {
  opacity: 0;
}
/* アニメーション設定 */
[data-inview-animation='fade-in'].is-inView {
  animation: fade-in 1s 0s forwards;
}
@keyframes fade-in {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

JavaScript

JS
class Inview {
  #defaults
  #options
  #inviewList
  #inviewsList
  constructor(config) {
    this.#defaults = {
      inviewSelector: '.js-inview',
      inviewsSelector: '.js-inviews',
      activeClass: 'is-inview',
      rootMargin: '0px'
    }
    this.#options = { ...this.#defaults, ...config }
    this.#inviewList = document.querySelectorAll(this.#options.inviewSelector)
    this.#inviewsList = document.querySelectorAll(this.#options.inviewsSelector)
    this.#init()
  }

  #init() {
    this.#single()
    this.#multiple()
  }

  // 単一要素を表示
  #single() {
    for (const target of this.#inviewList) {
      const callback = (entries) => {
        for (const entry of entries) {
          const isInviewToggle = target.dataset.inviewToggle === ''
          // 画面と要素が交差
          if (entry.isIntersecting) {
            this.#show(target)
            this.#showTrigger(target)
            // target.dataset.inviewToggle属性がない場合
            // 表示後は監視を解除
            if (!isInviewToggle) {
              observer.unobserve(target)
            }
          }
          // target.dataset.inviewToggle属性がある場合
          // 画面内から消えたら要素を非表示
          if (!entry.isIntersecting && isInviewToggle) {
            this.#hide(target)
          }
        }
      }
      const observer = new IntersectionObserver(
        callback,
        this.#getOptions(target)
      )
      observer.observe(target)
    }
  }

  // 複数要素を表示
  #multiple() {
    for (const inviewsItem of this.#inviewsList) {
      const inviewTargets = inviewsItem.querySelectorAll('[data-inview-target]')
        .length
        ? inviewsItem.querySelectorAll('[data-inview-target]')
        : inviewsItem.querySelectorAll('[data-inview-animation]')

      for (const target of inviewTargets) {
        this.#setAnimation(target)
        const inviewsTarget = inviewsItem.dataset.inviewDelayLoop
          ? inviewsItem
          : target

        const callback = (entries) => {
          entries.forEach((entry, index) => {
            // 画面と要素が交差
            if (entry.isIntersecting) {
              // inview-delay-loopが設定されていたら
              if (inviewsItem.dataset.inviewDelayLoop) {
                inviewTargets.forEach((item, index) => {
                  this.#show(item, index)
                  this.#showTrigger(item)
                })
              }
              // inview-delay-loop設定なし
              else {
                this.#show(target, index)
                this.#showTrigger(target)
              }
              // target.dataset.inviewToggle属性がない場合
              // 表示後は監視を解除
              if (!this.#isInviewToggle(target)) {
                observer.unobserve(target)
              }
            }
            // target.dataset.inviewToggle属性がある場合
            // 画面内から消えたら要素を非表示
            if (!entry.isIntersecting && this.#isInviewToggle(target)) {
              this.#hide(target)
            }
          })
        }
        const observer = new IntersectionObserver(
          callback,
          this.#getOptions(inviewsTarget)
        )
        observer.observe(target)
      }
    }
  }

  // アニメーション対象となる子要素に
  // アニメーション設定用のdata属性を付与
  #setAnimation(el) {
    const closest = el.closest(this.#options.inviewsSelector)
    if (!el.dataset.inviewAnimation && closest.dataset.inviewTargetsAnimation) {
      el.dataset.inviewAnimation = closest.dataset.inviewTargetsAnimation
    }
  }

  // 交差するごとに表示・非表示を繰り返す
  #isInviewToggle(el) {
    const closest = el.closest(this.#options.inviewsSelector)
    if (
      el.dataset.inviewToggle === '' ||
      (closest && closest.dataset.inviewToggle === '')
    ) {
      return true
    }
  }

  // Intersection Observer のオプションを返す
  #getOptions(el) {
    const closest = el.closest(this.#options.inviewsSelector)
    let options = null
    if (el.dataset.inviewOptions) {
      options = el.dataset.inviewOptions
    } else if (closest && closest.dataset.inviewOptions) {
      options = closest.dataset.inviewOptions
    }
    return options ? JSON.parse(options) : this.#options
  }

  // 要素が見えてからの表示タイミング(ミリ秒)を返す
  #getDelay(el, index) {
    const closest = el.closest(this.#options.inviewsSelector)
    // 単一要素を表示
    if (el.dataset.inviewDelay) {
      return el.dataset.inviewDelay
    }
    // 複数要素を表示
    if (closest) {
      // 親要素にdata-inview-delay-loop属性あり
      if (closest.dataset.inviewDelayLoop) {
        // 親要素にdata-inview-delay属性あり
        if (closest.dataset.inviewDelay) {
          if (index === 0) {
            return closest.dataset.inviewDelay
          }
          return (
            closest.dataset.inviewDelayLoop * index +
            parseInt(closest.dataset.inviewDelay)
          )
        }
        // 親要素にdata-inview-delay属性なし
        return closest.dataset.inviewDelayLoop * index
      }
      // 親要素にdata-inview-delay属性あり
      if (closest.dataset.inviewDelay) {
        return closest.dataset.inviewDelay
      }
    }
  }

  // 要素にclassを付与して表示させる
  #show(el, index) {
    const duration = this.#getDelay(el, index)
    let startTime = null
    const timer = (currentTime) => {
      if (!startTime) startTime = currentTime
      const progress = Math.floor(currentTime - startTime)
      if (progress < duration) {
        window.requestAnimationFrame(timer)
      } else {
        el.classList.add(this.#options.activeClass)
      }
    }
    window.requestAnimationFrame(timer)
  }

  // 要素のclassを削除して非表示にする
  #hide(el) {
    el.classList.remove(this.#options.activeClass)
  }

  // 要素の表示と同時に指定した要素にclassを付与して表示させる
  #showTrigger(el) {
    if (el.dataset.inviewTrigger) {
      const triggers = document.querySelectorAll(el.dataset.inviewTrigger)
      for (const trigger of triggers) {
        this.#show(trigger)
      }
    }
  }
}
const inview = new Inview()

DEMO

See the Pen show element on scroll by Takuya Mori (@taqumo) on CodePen.

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?