見出し画像

Intersection Observerが扱いにくいのでラッパークラスを作った

JavaScript の Intersection Observer は指定領域への要素の出入りを scroll イベントに頼らずスマートに監視できて便利なのですが。

しかし個人的にはどうもクセがあって使いにくく思ってます。

const observer = new IntersectionObserver(callback, options);
observer.observe(target);

例えば上記で target は単体のDOMエレメントでなければならなく

  • NodeListのような配列

  • '.wrapper img' というようなセレクターテキスト

  • jQueryオブジェクト

は仕様に無いので当たり前ですが、ことごとくエラーで処理が止まってしまう。

callback メソッドの書き方も交差時にどうすればいいのだっけと忘れがちだし、複数個の要素の監視を行う際には??など、自分自身の不慣れの問題がだいぶ大きいのですが、なんだか嫌。

ということで個人的に使いやすいようにラッパークラスを作って使ってます。

• • •

MultipleIO class

ネーミングはなんだって良かったのですが、複数 (multiple) のターゲットを扱える Intersection Observer (IO) ってことで「MultipleIO」としてます。気に入らなかったら適当に変えてくれ。

class MultipleIO {
	#defaults = {
		onEnter: null,
		onLeave: null,
		triggerOnce: false,
		config: {}
	};

	#config = {
		root: null,
		rootMargin: '0px',
		threshold: 0
	};

	#enterTriggers = [];

	constructor (_target = null, _option = {}) {
		const that = this;

		if (typeof _target === 'string') {
			this.target = document.querySelectorAll(_target);
		}
		else {
			this.target = _target;
		}

		this.option = Object.assign(this.#defaults, _option);
		this.config = Object.assign(this.#config, _option.config);

		this.observer = new IntersectionObserver((_elements, _observerInstance) => {
			_elements.forEach((_element, _index) => {

				if (_element.isIntersecting) {
					if (that.#enterTriggers[_index] === undefined) {
						that.#enterTriggers[_index] = true;
					}
					if (that.option.triggerOnce) {
						_observerInstance.unobserve(_element.target);
					}
					if (that.option.onEnter && typeof that.option.onEnter === 'function') {
						that.option.onEnter(_element.target);
					}
				}
				else {
					if (that.#enterTriggers[_index] === true) {
						if (that.option.onLeave && typeof that.option.onLeave === 'function') {
							that.option.onLeave(_element.target);
						}
					}
				}

			});
		}, this.config);

		if (this.target instanceof jQuery) {
			this.target.each(function () {
				that.observer.observe($(this)[0]);
			});
		}
		else {
			if (NodeList.prototype.isPrototypeOf(this.target)) {
				this.target.forEach((_target) => {
					that.observer.observe(_target);
				})
			}
			else if (HTMLCollection.prototype.isPrototypeOf(this.target)) {
				[...this.target].forEach((_target) => {
					that.observer.observe(_target);
				});
			}
			else {
				that.observer.observe(this.target);
			}
		}
	}

	unobserve = (_target) => {
		this.observer.unobserve(_target);
	};

	destroy = () => {
		const that = this;

		if (this.target instanceof jQuery) {
			this.target.each(function () {
				that.observer.unobserve($(this)[0]);
			});
		}
		else {
			if (NodeList.prototype.isPrototypeOf(this.target)) {
				this.target.forEach((_target) => {
					that.observer.unobserve(_target);
				})
			}
			else if (HTMLCollection.prototype.isPrototypeOf(this.target)) {
				[...this.target].forEach((_target) => {
					that.observer.unobserve(_target);
				});
			}
			else {
				that.observer.unobserve(this.target);
			}
		}

		this.observer.disconnect();

		delete this.observer;
		delete this.target;
		delete this.option;
	};
}

• • •

要素の出入りの監視を始める

let mIO = new MultipleIO(target, {
	config: {
		root: null,
		rootMargin: '0px',
		threshold: 0
	},
	onEnter: (_element) => {
		
		〜要素 (_element) が交差したときに実行される処理〜

	},
	onLeave: (_element) => {

		〜要素 (_element) が交差から外れたときに実行される処理〜

	},
	triggerOnce: true
});

target

以下のものであれば対応している筈です。

  • '.wrapper img' などセレクターテキスト。クラス内で自動的に document.querySelectorAll で解析されます。

  • querySelector / querySelectorAll で取得される NodeList

  • getElementById / getElementsByClassName などで取得される HTMLCollection

  • jQueryオブジェクト。単体でも複数でも可。


config

Intersection Observer 実行時のオプションを指定します。仕様に定義されている root / rootMargin / threshhold の各プロパティがそのまま渡されます。

3つとも指定する必要はなく、各プロパティのデフォルトは以下で設定されます。

root: null
rootMargin: '0px'
threshold: 0

triggerOnce

true に設定すると、各要素が交差した際に一度だけ onEnter 内の処理が実行され、即座に監視対象から外されます。onLeaveイベントは実行されません。


イベント

onEnter

  • 要素が指定領域に交差したことを検知した際に実行されるメソッドを定義します。

  • 引数 _element には対象となっている要素が格納されています。

onLeave

  • 要素が指定領域から出たことを検知した際に実行されるメソッドを定義します。

  • 引数 _element には対象となっている要素が格納されています。

  • 監視下にある各要素が、一度は領域内に交差しないかぎり実行されません。

• • •

メソッド

destroy: 全ての要素の監視を停止

target として渡した全要素の監視を停止し、 Intersection Observer から disconnect します。

mIO.destroy();

unobserve: 単一要素の監視を停止

onEnter または onLeave イベント内の処理で利用し、任意のタイミングで対象の要素を Intersection Observer の監視から外すことが可能です。

例として。以下のようにすると、要素が領域内に一度交差し、再び出た場合に監視から外れます。

(このメソッドは用意したものの、あまり利用用途が思い浮かばない…)

onLeave: (_element) => {
    mIO.unobserve(_element);
}


読んでいただきありがとうございます。丁寧な記事作りをこころがけていますので、記事が気に入ったなどでカンパをよせていただけるのなら励みになります。