🍣

スクロールすると追従させたい要素を固定させるやつ

2022/12/25に公開

スクロールすると追従させたい要素を固定させるコードです
ターゲットがobserver領域内にいる時のみscrollイベントを発動させてます

可能な限り関数化し冗長な部分を減らしました

html

  <div class="group">
    <div class="group-contents --01b js-fixed-contents">
      <p><img src="https://picsum.photos/id/1084/800/600" alt=""></p>
      <p><img src="https://picsum.photos/id/200/600/500" alt=""></p>
    </div>
    <div class="group-aside js-fixed-area">
      <div class="group-nav js-fixed-elm 02">
        <h2>追従パーツ(JS)2</h2>
        <p>指定エリアの中でrrrr<br>追従します。(JS)2</p>
      </div>
    </div>
  </div>

js

'use strict'
const { log } = console;

/**
 * 指定エリア内に要素を追従させる関数(追従要素,追従エリア)
 * @param {*} fixedElm 追従させたい要素のクラス名
 * @param {*} fixedArea エリアのクラス名
 * @param {*} addOptions observer追加オプション
 * @returns 
 */
export function contentsFixed(fixedElm, fixedArea, addOptions) {
  //エリアチェック
  const areas = document.querySelectorAll(fixedArea);
  if (areas.length === 0) {
    return;
  }

  /**
   * 追従要素への処理
   * @param {*} target 追従要素
   * @param {*} area エリア
   */
  const targetProcess = (target, area) => {
    //要素の位置と高さを取得
    const startPosi = area.getBoundingClientRect().top;
    const targetHeight = target.clientHeight;
    const areaHeight = area.clientHeight;
    const endPosi = startPosi + areaHeight;

    //エリア内の処理
    if (0 > startPosi && targetHeight < endPosi) {
      target.classList.add('is-fixed');
      target.style.top = '';

      //エリアより上の処理
    } else if (0 <= startPosi) {
      target.classList.remove('is-fixed');
      target.style.top = '';

      //エリアより下の処理
    } else {
      target.classList.remove('is-fixed');
      //停止位置を設定
      target.style.top = (areaHeight - targetHeight) + 'px';
    }
  }

  /**
   * 追従チェック
   * @param {*} target 追従させたい要素
   * @param {*} area エリア要素
   */
  const checkFixed = (target, area) => {
    targetProcess(target, area)
  }

  /**
   * observer callback
   * @param {*} entry observer領域と交差した対象
   * @param {*} area エリア
   */
  const callback = (entry, area) => {
    //observer時の追従チェック
    const checkFixedIntersect = () => {
      targetProcess(entry.target, area);
    }

    //determine whether or not in the observer and excute addEvent or removeEvent
    if (entry.isIntersecting) {
      window.addEventListener('scroll', checkFixedIntersect);
    } else {
      window.removeEventListener('scroll', checkFixedIntersect);
    }
  }

  //observer base options(2nd argument)
  const options = {
    root: null,
    rootMargin: "0px",
    threshold: 0,
    once: false,
  };
  const mergeOptions = Object.assign(options, addOptions);


  /**
   * エリア毎に処理を実行
   */
  areas.forEach(area => {
    const target = area.querySelector(fixedElm);

    //エリア内に追従要素が存在する場合のみ処理する
    if (target) {
      checkFixed(target, area);

      //resize
      window.addEventListener('resize', () => {
        checkFixed(target, area);
      });

      //observer 1st argument
      const obs = (entries) => {
        entries.forEach(entry => {
          //excute callback func to each entry
          callback(entry, area);
        });
      }
      new IntersectionObserver(obs, mergeOptions).observe(target);
    }
  });
}

クラス

'use strict'
const { log } = console;

export class ContentsFixed {
  constructor(fixedElm, fixedArea, options) {
    this.areas = document.querySelectorAll(fixedArea); //エリア
    if (this.areas.length === 0) return;

    this.fixedElm = fixedElm; //追従要素名
    this.options = options; //observer基本オプション
    this.mergeOptions; //observerオプション

    this.startPosi; //エリアのブラウザ上端からの相対位置
    this.targetHeight; //追従対象の高さ
    this.areaHeight; //エリアの高さ
    this.endPosi; //エリア下端の位置

    this._areaProcess(); //エリア毎に処理
  }

  //追従チェック
  _checkFixed(target, area) {
    //処理
    this._targetProcess(target, area);
  }

  /**
   * observer callback
   * @param {*} entry observer対象
   * @param {*} area エリア
   */
  _callback(entry, area) {
    const checkFixedIntersect = () => {
      //処理
      this._targetProcess(entry.target, area);
    }

    if (entry.isIntersecting) {
      window.addEventListener('scroll', checkFixedIntersect);
    } else {
      window.removeEventListener('scroll', checkFixedIntersect);
    }
  }

  /**
   * 追従チェックの処理内容
   * @param {*} target 追従対象
   * @param {*} area エリア
   */
  _targetProcess(target, area) {
    //要素の位置と高さを取得
    this.startPosi = area.getBoundingClientRect().top;
    this.targetHeight = target.clientHeight;
    this.areaHeight = area.clientHeight;
    this.endPosi = this.startPosi + this.areaHeight;

    //エリア内の処理
    if (0 > this.startPosi && this.targetHeight < this.endPosi) {
      target.classList.add('is-fixed');
      target.style.top = '';

      //エリアより上の処理
    } else if (0 <= this.startPosi) {
      target.classList.remove('is-fixed');
      target.style.top = '';

      //エリアより下の処理
    } else {
      target.classList.remove('is-fixed');
      //停止位置を設定
      target.style.top = (this.areaHeight - this.targetHeight) + 'px';
    }
  }

  /**
   * merge observer options
   */
  _options() {
    const options = {
      root: null,
      rootMargin: "0px",
      threshold: 0,
      once: false,
    };
    this.mergeOptions = Object.assign(options, this.options);
  }

  /**
   * エリア毎の処理
   */
  _areaProcess() {
    this.areas.forEach(area => {
      const target = area.querySelector(this.fixedElm);

      //エリア内に追従要素が存在する場合のみ処理する
      if (target) {
        this._checkFixed(target, area);

        //resize
        window.addEventListener('resize', () => {
          this._checkFixed(target, area);
        });

        //observer 1st argument
        const obs = (entries) => {
          entries.forEach(entry => {
            this._callback(entry, area);
          });
        }
        new IntersectionObserver(obs, this.mergeOptions).observe(target);
      }
    });
  }
}

Discussion