かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript classList からクラス名を文字列で取得する方法

シンプルに Element.classNameElement.getAttribute('class') 使いなよ。って話ではあるが、JavaScriptElement.classList で取得できるクラス名を HTML にあるように半角スペース区切りの文字列で得たいときってどうするんだっけ?と思ったのでメモ

環境

  • TypeScript: v5.5.4
<body class="ichigo aoi ran akari">
document.body.className;
// => "ichigo aoi ran akari"
document.body.getAttribute('class');
// => 'ichigo aoi ran akari'

1. ループを使って結合する

Element.classListDOMTokenList 型 で配列ではないので map や reduce は使えないが、forEach メソッドは持っているので素直にループで結合する

let cx = '';
document.body.classList.forEach((item) => cx += ` ${item}`));
cx = cx.trim()
console.log(cx); // => "ichigo aoi ran akari"

シンプルに Element.classNameElement.getAttribute('class') でいい

2. Array.from() で配列化して結合する

Array.from(document.body.classList).join(' ');
// => "ichigo aoi ran akari"

わざわざ Array.fromせんでも...と仰々しすぎる感じ。

3. 👍️ Element.classList.value を使う

DOMTokenList が文字列化の valueメソッドを持っている

DOMTokenList.value
A stringifier property that returns the value of the list as a string.
cf. Element: classList property - Web APIs | MDN
DOMTokenList: value property
The value property of the DOMTokenList interface is a stringifier that returns the value of the list serialized as a string, or clears and sets the list to the given value.
cf. DOMTokenList: value property - Web APIs | MDN

document.body.classList.value;
// => "ichigo aoi ran akari"

4. .toString() を使う

DOMTokenList そのものには .toString() メソッドはないが、プロトタイプでメソッドを継承しており .value どうよう半角スペース区切りに文字列が取得できる

document.body.classList.toString();
// => "ichigo aoi ran akari"

シンプルに Element.classList.value プロパティが用意されているので、そちらを使う方えばいい

結論

シンプルに Element.className でいい。
敢えて classList を使うなら Element.classList.value かな。

おわり
もしかして prototype 継承 ってもしかして死語レベルの話題!?


[参考]

prototype継承はこの本で学びました


このクラスにギャルはいない 1 (ジャンプコミックス) 単行本すごく楽しみ!

GSAP batch でアニメーションが実行された要素だけをアレコレしたい

GSAP の ScrollTrigger.batch を使えば、リストなどに簡単にアニメーションを設定することができる。

https://gsap.com/docs/v3/Plugins/ScrollTrigger/static.batch()/

batch でアニメーションを設定したときに、アニメーションが実行された要素に class を付けたいとか、要素に何か処理をしたい要望があったのでそのメモ

  • TypeScript: ^5.5.4
  • GSAP: ^3.12.5

ScrollTrigger.batch を使ったアニメーションの基本

cf. static-batch | GSAP | Docs & Learning

import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);

// アニメーションさせる要素の表示を初期化
gsap.set(".listItem", {
  autoAlpha: 0,
  y: 50
});

ScrollTrigger.batch('.listItem', {
  interval: 0.1, // バッチ処理が行われるまでの時間 (秒)
  start: "top-=200 center", // ターゲットの top - 200px に画面の中央が来たら開始
  end: "bottom+=50 center" // ターゲットの bottom + 50px に画面の中央が来たら終了
  once: true, // 一度だけ実行する
  markers: true, // debug 用に start, end のガイドを表示させる
  onEnter: (elements) => {
    // onEnter 内でアニメーションの設定をする
    gsap.to(elements, {
      stagger: 0.5, // 同じバッチ内でアニメーションが実行されたときの時差
      autoAlpha: 1,
      y: 0,
    });
  },
});

対象要素に class 名を付けたいだけなら attr が便利

例えばアニメーションが始まった際に isLoaded クラスを付け、data-status="loaded" にするなら下記のような感じ

ScrollTrigger.batch('.listItem', {
  interval: 0.1,
  start: "top-=200 center",
  end: "bottom+=50 center"
  once: true,
  markers: true,
  onEnter: (elements) => {
    gsap.to(elements, {
      stagger: 0.5,
      autoAlpha: 1,
      y: 0,
      // attr は完全に置換するので元のクラス名を残す必要があるなら書いておく
      attr: { class: ".listItem .isLoaded", "data-status": "loaded" }
    });
  },
});

⚠️ attr の注意点

  • attribute をまるっと置換してしまうので、必要なものは記述する必要があり HTML と密結合になってしまう
  • attr は trigger になっている要素しか対象にしないので、子要素や親要素になにかしたい場合は対応できない (CSSで対応できる範囲でしか操作できない)
  • 文字列を返す関数を設置できたが、ドキュメントには書かれてないので動作は担保されてなさそう
ScrollTrigger.batch('.listItem', {
  // 略
  onEnter: (elements) => {
    gsap.to(elements, {
      stagger: 0.5,
      autoAlpha: 1,
      y: 0,
      attr: {
        class: (_index, item) => {
          const cx = item.classList.value;
          return cx + ' isLoaded';
        },
      },
    });
  },
});

↑ これも動作したが、ドキュメントにこの方法は載ってないっぽいので動作保証はなさそう

🙆 1度だけ対象要素以外も操作するなら onEnter 内の gsap.to 内にイベントを設定するのが良さそう

後述するが、onToggle は start, end を跨ぐ度に実行され、onLeave はスクロール速度では発火しないケースがあった。
onEnter さえすればアニメーションは発火するので、その中で操作する処理を書いてしまうのが確実かなという印象

ScrollTrigger.batch('.listItem', {
  // 略
  onEnter: (elements) => {
    gsap.to(elements, {
      stagger: 0.5,
      autoAlpha: 1,
      y: 0,
      // attr は置換で扱いづらいので使わない
      // attr: { class: ".listItem .isLoaded", "data-status": "loaded" } 
      },
      onStart: (index, item) => {
        // アニメーションが開始された時点で実行される
        item?.classList.add('isLoaded');
        item?.dataset.status = 'loaded';
        const parent = item?.parentElement;
        parent?.classList.add('isChildLoaded');
      },
      onLeave: (index, item) => {
        // gsap.to の設定的に end が無いので onStart 直後に発火するので使い分ける意味はほぼ無い
      },
      onComplete: (arg: undefined) => {
        // バッチで実行されたアニメーションが全て完了したら呼び出されるが、引数が `undefined` なので使いづらい
      },
    });
  },
});

onComplete

onComplete はバッチになっているアニメーションが全て完了したら呼び出されるので便利だが、引数が無いので扱いづらい
Arrow function ではなく通常の関数にすることで this から対象を引っ張ってくることもできるが _target というアンダースコア付きのプロパティなので、アンチパターンではないかと思う。(操作できるにはできる)

ScrollTrigger.batch('.listItem', {
  // 略
  onEnter: (elements) => {
    gsap.to(elements, {
      stagger: 0.5,
      autoAlpha: 1,
      y: 0,
      // attr は置換で扱いづらいので使わない
      // attr: { class: ".listItem .isLoaded", "data-status": "loaded" } 
      },
      onStart: (index, item) => {
        // アニメーションが開始された時点で実行される
        item?.classList.add('isLoaded');
        item?.dataset.status = 'loaded';
        const parent = item?.parentElement;
        parent?.classList.add('isChildLoaded');
      },
      onComplete: function() {
        const self = this;
        const elemens = self?._targets;
        if ( !Array.isArray(elemens) ) {
          return;
        }
        elemens.forEach((el) => {
          if (!el || el instanceof HTMLElement === false) {
            return;
          }
          el.classList.add('isComplete');
          el.dataset.status = 'complete';
        });
      }
    });
  },
});

🤔 要素を操作したい場合は onToggle が使える

アニメーションが始まった際に親要素に isLoaded クラスを付け、子要素のdata-status 属性をを loaded にするなら下記のような感じ
ただし、onToggle は start, end を跨いだタイミングで実行されるので 1回だけ実行させたいようなケースでは工夫が必要

ScrollTrigger.batch('.listItem', {
  // 略
  onEnter: (elements) => {
    // onEnter 内で各 .listItem のアニメーションを設定
    gsap.to(elements, {
      stagger: 0.5,
      autoAlpha: 1,
      y: 0,
    });
  },
  onToggle: (elements) = > {
     // 今回のバッチで実行される要素の配列が引数として渡される
     elements.forEach((el) = > {
       const parent = el.parentElement;
       const child = el.firstElementChild;
       parent?.classList.add("isLoaded");
       child?.dataset.status = "loaded";
     });
  },
});

⚠️ onToggle の注意点

  • onToggle は start, end をまたぐ度に実行されるので、1度だけ操作したい・終了時に操作したい場合は扱いに注意が必要
  • once のときスクロール速度が早いと onToggle は実行されないケースがあったので確実性は微妙

🤔 対象からスクロールが出た際に何か実行するなら onLeave

公式ドキュメントに例が載っている onLeave はバッチの対象から出る時に実行されるので、アニメーションが完了した後に要素に対して何か処理を行う際に使える

ScrollTrigger.batch('.listItem', {
  // 略
  onEnter: (elements) => {
    // onEnter 内で各 .listItem のアニメーションを設定
    gsap.to(elements, {
      stagger: 0.5,
      autoAlpha: 1,
      y: 0,
    });
  },
  onLeave: (elements) = > {
     // 対象外になったバッチに含まれる要素の配列が引数として渡される
     elements.forEach((el) = > {
       el.dataset.status = "leaved";
       const parent = el.parentElement;
       parent?.classList.add("isLeaved");
     });
  },
});

⚠️ onLeave の注意点

  • スクロール速度によっては結構な確率で発火しない、感覚的に end の onToggle の方がまだ発火される可能性が高い気がする
  • onEnter からアニメーションの時間分 setTimeout させるほうが確実性はありそう

🙅 onStart, onCompleteScrollTrigger.batch にはない

Timeline .varsonStart, onComplete の項目もあったが、ScrollTrigger.batch では onStart, onComplete は発火しないっぽい (※ ScrollTrigger .vars のドキュメントが薄く vars の中身は同じだと思っていたが)
対象要素が複数なので、何をもって start なのか complete なのか判断が難しいので使えないのは分からなくもない

ScrollTrigger.batch('.listItem', {
  // 略
  onEnter: (elements) => {
    // onEnter 内で各 .listItem のアニメーションを設定
    gsap.to(elements, {
      stagger: 0.5,
      autoAlpha: 1,
      y: 0,
    });
  },
  // onStart, onComplete は発火ししなかった
  onStart: (evt: any) => {
    console.log('onStart', evt);
  },
  onComplete: (evt: any) => {
    console.log('onComplete', evt);
  },
});

ScrollTrigger.batch でアニメーション実行時に要素に変更を加えたいときのまとめ

  • onEnter 内で実行するのが無難
    • once: true のとき、onToggle, onLeave はスクロール速度によっては発火されない場合がある
    • onEnter, onToggleonLeave, onToggle の順番は担保されない
      • attr, onToggle の発火順も担保されてない
  • onToggle は start 時と end 時に実行される
    • end になる前に start を逆方向にスクロールして跨いでも再度実行される
  • class 名を追加したいだけなら attr が便利だが、class 名をまるっと置換してしまうので注意が必要
    • attr は対象の要素しか操作できないので、親や子要素など対象要素を起点に他の要素を操作したい場合は別のアプローチが必要
    • attrvalue に文字列を返す関数を設定できたがドキュメントに載ってないので動作が担保されるかは怪しい
  • アニメーション完了時に何かをしたいのであれば onEnter 内でアニメーションの時間分 setTimeout させるのが無難そう (微妙だけど)
  • onLoad 時に、既にアニメーションが終了しているスクロール位置の場合は、スクロール量とコンテンツの位置から GSAP を動作させず完了とさせる処理を挟むほうが良さそう

Sample

↓ スクロールを上下させると once: true でも end に到達する前に start を跨ぐ度に onToggle が実行されるのが確認できる

See the Pen GSAP batch animation callback by KIKIKI (@kikiki_kiki) on CodePen.

batch でアニメーションが開始された要素と、その要素を起点に DOM 操作したかっただけだったんだけど、結構沼だった。
そして、このあと ScrollTrigger.batch 使わなくてもできるじゃん… となってしまった訳ですが、それはまた別のお話…

おわり。゚(゚ ◜ᴗ◝゚)゚。


[参考]

GSAP ScrollTrigger で CSS アニメーションを発火させ、アニメーション終了のコールバックを使いたい

いわゆるガチャガチャ動かしたいという要望のホームページ制作(非 React)で初めて GSAP というアニメーションライブラリを使ってる。 できることがありすぎて、まだ理解しきれてないが今回は ScrollTrigger を使って CSS のクラスを変更しアニメーションを発火させた後に、アニメーション終了を持ってコールバックを実行したかったのメモ

ライブラリ選定のメモ

パララックス・アニメーションライブラリで比較をした

aos vs gsap vs scrollmagic vs t-scroll vs velocityjs vs waypoints vs wowjs | npm trends

多くのライブラリが DT であまりメンテされておらず、ほぼ唯一 GSAP が TypeScript 化されており比較的メンテナンスがされていたので選定した

JavaScriptIntersection information があるので、ある程度のことはこれで実装可能だし、昨今の Web アプリケーションではガチャガチャ動くのはあまり意味をなさない装飾なのでこの分野の需要が減少しているのかも知れない。といっても広告的な "ほうむぺーじ" ではまだまだ必要とされる分野だとも思う (jQueryだってめちゃくちゃ使われてるし)

ScrollTrigger を使ってクラス名を変更し CSS アニメーションを設定する

シンプルな CSS アニメーションを作成する

.js-fadeUp {
  opacity: 0;
  transform: translateY(20px);
  transition: all 0.6s ease-in-out;
}
.js-fadeUp.fade-in {
  opacity: 1;
  transform: translateY(0);
}

.js-fadeup クラスがある要素をターゲットに、.fade-in クラスが付けられるとアニメーションが開始される想定

GSAP でクラス名を変更する

import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

// registerPlugin しないとエラーになる
gsap.registerPlugin(ScrollTrigger);

const targets = document.querySelectorAll('.js-fadeUp');
targets.forEach((target, index) => {
  ScrollTrigger.create({
    trigger: target,
    id: `fadeUpContent-${index + 1}`,
    start: 'top-=100 center',
    end: 'bottom top',
    toggleClass: { targets: target, className: 'fade-in' },
    once: true,
  });
});

これで .js-fadeup クラスのある要素がスクロールに応じてフェードインするようになった!

📝 note.

once: Boolean - If true, the ScrollTrigger will kill() itself as soon as the end position is reached once. This causes it to stop listening for scroll events and it becomes eligible for garbage collection. This will only call onEnter a maximum of one time as well. It does not kill the associated animation. It's perfect for times when you only want an animation to play once when scrolling forward and never get reset or replayed. It also sets the toggleActions to "play none none none".
cf. ScrollTrigger | GSAP | Docs & Learning

once を付けておくと一度だけ発火し、kill() されメモリを開放してくれるトノコト。
こってりとしたアニメーションを何度も見せられるのは、あまりユーザー体験が良いとは思わないので once を設定することにした。

CSS アニメーションが完了したらコールバックで対象要素を操作したい

アニメーション完了時に CSS のクラス名を変更するなど、アニメーション完了のコールバックで要素を操作したい
ScrollTrigger: onComplete callback - GSAP - GreenSock このスレッドには refresh イベントリスナーを使えば良いと回答されていたがうまく動作せず、scrollEnd を使ってみたが、アニメーションが発火した要素ごとにイベントが発火するものでは無さそうだった。 cf. static-addEventListener | GSAP | Docs & Learning

onEnter, onLeave コールバックを使えば、対象ごとにコールバックを設定できる

onEnter: Function - A callback for when the scroll position moves forward past the "start" (typically when the trigger is scrolled into view). It receives one parameter onLeave: Function - A callback for when the scroll position moves forward past the "end" (typically when the trigger is scrolled out of view). It receives one parameter
cf. static-addEventListener | GSAP | Docs & Learning

CSS アニメーションの時間を測って即コールバックをするなら onEnter を使いコールバック内で setTimeoutCSS アニメーションの時間分待てば良さそう 厳密さがあまり必要でないなら、onLeave を使う方が無駄にタイマーを使わずに済むので効率的だと思う

import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

const targets = document.querySelectorAll('.js-fadeUp');
targets.forEach((target, index) => {
  ScrollTrigger.create({
    trigger: target,
    id: `fadeUpContent-${index + 1}`,
    start: 'top-=100 center',
    end: 'bottom top',
    toggleClass: { targets: target, className: 'fade-in' },
    once: true,
    // css アニメーションを待って実行させる場合は onEnter を使う
    onEnter: (self) => {
      // self は globalThis.ScrollTrigger 型
      const target = self.vars.trigger; // gsap.DOMTarget | undefined
      // ⚠ HTMLElement 型として扱わないと target.classList が type error になる
      if (!target || target instanceof HTMLElement !== true) {
        return;
      }
      // css animation が終わるまで待つ
      window.setTimeout(() => {
        target.classList.add('fade-in-complete');
      }, 600);
    },
    // css アニメーションを特に考慮しないなら onLeave で十分
    onLeave: (self) => {
      const target = self.vars.trigger;
      if (!target || target instanceof HTMLElement !== true) {
        return;
      }
      target.classList.add('leaved');
    }
  });
});

ポイントとしては self.vars.trigger で取得できる、アニメーションのターゲット要素が gsap.DOMTarget | undefined 型なので classList などを使いたい場合は target instanceof HTMLElement で type guard してあげる必要がある部分

Sample

See the Pen GSAP CSS animation callback by KIKIKI (@kikiki_kiki) on CodePen.

onLeave はスクロールの高さが足りないと発火しない場合がある

なんとなくな理解だけど、GSAP の ScrollTrigger を使って CSS アニメーション発火した後にコールバックで処理ができるようになった!

おわり


[参考]