ある2つの要素を監視し、要素同士が交差したかどうかを検知する処理をjsで制作しました。
この処理を活用すれば、ある一定量スクロールをするとDOM要素にclassを付与したり、ある要素が画面内に入ったか、または出ていったかを検知し、その結果に応じた処理を実行する事ができます。
使い方
IntersectionWatchをimportしてインスタンスを制作します。
インスタンスを制作する際に引数から監視したいタスクの情報を渡す事で、そのタスクを監視してくれるようになります。
initを実行する事で監視を開始します。
import IntersectionWatch from './IntersectionWatch';
const intersectionWatch = new IntersectionWatch({
// 監視したいタスクを記載する
});
intersectionWatch.init();
監視するタスクはobserveListプロパティに配列形式でセットします。
observeList: [
{
target: 'js-target', // 監視する要素
trigger: 'js-trigger', // 検知するライン
targetOffset: 20, // 監視要素のOffset(要素の位置をずらす事ができる)
triggerOffset: 20, // 検知するラインのOffset(ラインの位置をずらす事ができる)
callbackUp: (()=>{
// targetがtriggerラインの下から上に侵入した際に実行する処理を記載します。
}),
callbackBelow: (()=>{
// targetがtriggerラインの上から下に外れた際に実行する処理を記載します。
}),
},
]
上記のオブジェクトをセットとして配列に追加する事でこれらタスクを常に監視します。
引数からインスタンス生成時にセットするだけでなく、後からセットする事も可能です。
以下のようにaddObserveプロパティにタスクオブジェクトを渡してセットします。
intersectionWatch.addObserve({
target: 'js-target',
trigger: 'js-trigger',
targetOffset: 20,
triggerOffset: 20,
callbackUp: (()=>{
}),
callbackBelow: (()=>{
})
});
仕様
targetとtargetOffset
tartgetに監視したい要素のidもしくはclassをセットします。
要素の取得はjqueryを使用していますので、idなら「#」シャープ、classなら「.」ドットから始まり、名前を記載します。
class要素は複数あったとしても一番初めの要素のみを対象とします。
target: '#js-target' // idの場合 target: '.js-target' // classの場合
targetOffsetに数字をセットすると、targetで取得した要素のy座標にプラスされます。
targetはその要素のtopの座標を取得します。ですので、triggerがtarget要素のtopと交差するとcallbackが実行されます。
要素の半分の位置にtriggerが差し掛かった時にcallbackの実行を行いたい場合などにはtargetOffsetに要素の半分(要素の高さが100pxなら50)の値をセットする事で解決します。もちろんマイナスも可能です。

targetに何もセットされなかった場合、もしくは要素を取得できなかった場合
targetの値が存在しない場合、そのページのtopの値(x:0, y:0)がセットされます。
targetが無い状態でtargetOffsetをセットすると、0に対して画面の縦幅のn%の値がプラスされます。
例えば、targetOffsetが100だとすると、画面の縦幅(ここでは980pxだったとします)980pxがプラスされるので、targetの位置は980となります。
targetOffsetが50なら980pxの50%なので490です。
ですのでtargetOffsetは同じ数字をセットしてもtarget要素が存在するかしないかで単位がpxなのか%なのかが変わります。

triggerとtriggerOffset
triggerには検知するためのラインをセットします。
triggerのラインにtargetが交差するとcallbackが実行されます。
triggerもDOM要素をセットする事ができ、その要素のidもしくはclassを渡します。
要素の取得はjqueryで行っていますので、idの場合は「#」シャープ、classの場合は「.」ドットを指定します。
triggerOffsetに数字をセットすると、triggerで取得した要素のy座標にプラスされます。
triggerもその要素のtopの座標を取得します。
ラインの位置をずらしたい時はtriggerOffsetにその分の数字をセットします。(単位px)

triggerに何もセットされなかった場合、もしくは要素を取得できなかった場合
triggerの値が存在しない場合、そのページの画面のtopの座標がセットされます。
targetとは少し異なるので注意してください。画面のtopの座標ですので、スクロールで位置が変化します。
後はtargetに値がセットされなかった場合と同じです。
triggerが無い状態でtriggerOffsetをセットすると、0に対して画面の縦幅のn%の値がプラスされます。
例えば、triggerOffsetが100だとすると、画面の縦幅(ここでは980pxだったとします)980pxがプラスされるので、triggerの位置は980となります。
triggerOffsetが50なら980pxの50%なので490です。
ですのでtriggerOffsetは同じ数字をセットしてもtrigger要素が存在するかしないかで単位がpxなのか%なのかが変わります。

callbackUp
targetがtriggerの下にいる状態からtriggerの上にいる状態になったときに実行されます。
callbackに渡す事ができる引数はありません。
callbackBelow
targetがtriggerの上にいる状態からtriggerの下にいる状態になった時に実行されます。
callbackに渡せる引数はありません。
オプション
処理の間引き
要素の監視はスクロールが起こる度、ウィンドウのリサイズが起こる度にチェックします。
これらは1pxの変化があるごとにイベントを発火するので、処理の負荷が大きいです。
ウィンドウリサイズのリサイズ中に検知処理は必要ないですし、スクロールも1px毎に計算する必要はありません。
ですので、処理の間引きを行っていますが、その間引き具合を変更する事が可能です。
変更出来るのはスクロールの間引き具合になります。
リサイズの間引きは「リサイズ中は実行しない」という形で固定しています。
スクロールを間引くためにはscrollBatchプロパティにms単位で数字をセットします。
const intersectionWatch = new IntersectionWatch({
observeList: [
{
// 監視タスク
}
],
scrollBatch: 500, // 処理の間引き具合
});
scrollBatchはセットした数字(ms「ミリ秒」)間隔で計算を行います。
500とセットすると一度処理を実行してから500ms間は処理を実行しません。
scrollBatchに値がセットされていない場合は200ms間隔で間引きが行われます。
実装例
スクロールされたかどうかを検知してclassをつける
スクロールを検知してclassをつける処理の実装例を紹介します。
どのような状況で必要になるかと言うと、例えば、200pxほどスクロールするとheader要素にclassをつけて、styleからheader要素を小さく縮めたいという場合に活用できます。
head要素はfixedが設定されており、スクロールに対して画面のtopに常に追従しているものとします。
ですので、200pxのスクロールを検視してclassをつけたり、スクロールが200px以下ならclassを外す処理を実装しなければいけません。
IntersectionWatchを使用すると以下のようにして実装が可能です。classのつけ外しは簡単にしたいのでjqueryを使用します。
const intersectionWatch = new IntersectionWatch({
observeList: [
{
targetOffset: 20,
callbackUp: (()=>{
$('.js-header-target').addClass('-small');
}),
callbackBelow: (()=>{
$('.js-header-target').removeClass('-small');
}),
},
]
});
js-header-targetはheader要素となります。header要素に-smallというモディファイアをつけるとheader要素を小さくするスタイルをcssに記載しています。
targetは設定が無ければページのtopの座標を取得します。そこにoffsetで20を与える事で1画面の縦幅の20%の位置にtargetをセットする事ができます。
ここでは計算を簡単にするため、画面の縦幅を1000pxとすると20%の位置は200pxとなります。
triggerは設定が無ければ常に画面のtopの座標を取得しますのでページの200pxの位置が画面のtopと交差したら、つまり、200pxスクロールしたらcallbackUpが実行されます。
callbackUpにはheader要素に-smallを追加するという処理を記載しています。
スクロールが200px未満になるとcallbackBelowが実行されます。
callbackBelowにはheader要素から-smallを外す処理を記載しています。
要素が画面内に存在するかを検知してclassをつける
スクロールをして、ある要素が画面内に侵入したかどうかを検知し、その要素にclassをつける処理の実装例を紹介します。
例えば、要素が画面に表示されたタイミングで要素が下からふわっと出現するモーションなどを施したサイトを見かける事がありますが、その機能をIntersectionWatchで実装すると以下のようになります。
const intersectionWatch = new IntersectionWatch({
observeList: [
{
target: '.js-target',
triggerOffset: 100,
callbackUp: (()=>{
$('.js-target').addClass('-active');
}),
callbackBelow: (()=>{}),
},
]
});
画面に侵入する対象の要素をtargetにセットします。今回は対象の要素にjs-targetというclassが付いているとします。
triggerは設定が無ければ、画面のtopを指します。offsetで100をセットすると画面の高さの100%という事で画面のbottomがtriggerのラインとなりますので、これで画面内にtargetが侵入したかどうかを検知できます。
画面bottomではなく少し中央よりにしたい場合はoffsetを90,80とする事で画面の90%の位置、80%位置と指定をする事ができます。
targetが画面内に侵入するとcallbackUpが実行されるので、そこにclassをつける処理を記載しておきます。
targetが画面から外れたらcallbackBelowが実行されますが、今回は何もしなくてよいでしょう。
2つの要素が交差したかを検知してclassをつける
スクロールをして、ある要素が固定されているある要素と交差したかを検知し、その要素にclassをつける処理の例を紹介します。
例えば、ページの右側にスクロール追従で常に表示されている要素があるが、footerに差し掛かった時に消したい。もしくはスクロール追従を解除したいという場合に活用できます。
const intersectionWatch = new IntersectionWatch({
observeList: [
{
target: '.js-target',
trigger: '.js-footer',
callbackUp: (()=>{
$('.js-target').addClass('-fixed');
}),
callbackBelow: (()=>{
$('.js-target').removeClass('-fixed');
}),
},
]
});
固定表示されている要素がjs-taergetでfooter要素がjs-footerだとすると、targetにjs-targetを、triggerにjs-footerをセットする事でtargetがtriggerよりも上にある状態の時にcallbackUpが実行され、targetがtriggerの下にある状態の時にcallbackBelowが実行されます。
footerよりtargetが上にある状態の時に固定表示にしたいのでcallbackUpに-fixedをつけ、要素を固定します。
footerより下にある場合は固定を解除したいのでcallbackBelowに-fixedを外す処理を記載します。
コード
/**
* スクロールの交差点を監視して、targetとtriggerが交差すればcallbackを実行します。
* targetがtriggerのラインに侵入したときにcallbackUpが実行され、
* targetがtriggerのラインから外れたときにcallbackBelowが実行されます。
* @param {object} data - 監視するのに必要な情報
* @param {string || null} target - 監視対象(入力がない場合は画面top)
* @param {string || null} trigger - 交差点(入力がない場合は画面top)
* @param {number || null} targetOffset - 交差点のoffset(画面のtopをtargetしている場合は0%,buttonを100%とする)
* @param {number || null} triggerOffset - 交差点のoffset(画面のtopをtriggerとしている場合は0%,buttonを100%とする)
* @param {object || null} callbackUp - triggerに対して上から交点に侵入した場合の実行処理
* @param {object || null} callbackBelow - triggerにたいして下から交点に侵入した場合の実行処理
*/
export class IntersectionWatch {
constructor(obj) {
this.observeList = obj.observeList;
this.task = [];
this.resizeTimeout = 0;
this.scrollFlag = true;
this.scrollBatch = (obj.scrollBatch) ? obj.scrollBatch : 200;
};
/**
* callbackの実行
*/
CallbackInit() {
for(let i = 0; i < this.task.length; i++) {
let iterator = this.task[i];
let res = this.judge(iterator, iterator.callbackUp, iterator.callbackBelow);
}
}
judge(iterator, callbackUp, callbackBelow) {
let targetPosition = (iterator.target != null && $(iterator.target).length != 0) ? $(iterator.target).offset().top : 1; // ページのtopからアイテムのtopまでの高さ
targetPosition = (iterator.targetOffset != null && $(iterator.target).length != 0) ? targetPosition + Number(iterator.targetOffset) : targetPosition;
targetPosition = (iterator.targetOffset != null && $(iterator.target).length == 0) ? targetPosition + Number((iterator.targetOffset / 100) * $(window).height()) : targetPosition;
let triggerPosition = (iterator.trigger != null && $(iterator.trigger).length != 0) ? $(iterator.trigger).offset().top : $(window).scrollTop();
triggerPosition = (iterator.triggerOffset != null && $(iterator.trigger).length != 0) ? triggerPosition + Number(iterator.triggerOffset) : triggerPosition;
triggerPosition = (iterator.triggerOffset != null && $(iterator.trigger).length == 0) ? triggerPosition + Number((iterator.triggerOffset / 100) * $(window).height()) : triggerPosition;
let state = true;
iterator.isPosition = (targetPosition > triggerPosition);
if(iterator.isFirst){
// 初期の処理
iterator.isFirst = false;
iterator.isState = (!iterator.isPosition);
this.judge(iterator, iterator.callbackUp, iterator.callbackBelow);
} else if(iterator.isState === false && iterator.isPosition === true) {
// triggerの上からtriggerの下に移動したとき
if(callbackBelow) callbackBelow();
iterator.isState = true;
state = false;
} else if(iterator.isState === true && iterator.isPosition === false) {
// triggerの下からtriggerの上に移動したとき
if(callbackUp) callbackUp();
iterator.isState = false;
state = true;
}
return state;
}
addObserve(data = {}) {
let dataKeys = Object.keys(data);
if(dataKeys.length == 0) return false;
data['isFirst'] = true;// 初回かどうか
data['isState'] = false; // 状態。
data['isPosition'] = false; // ポジション。
this.judge(data); // 登録
this.task.push(data);
}
/**
* stackの初期値を制作
*/
buildTask() {
for(let i = 0; i < this.observeList.length; i++) {
this.addObserve(this.observeList[i]);
}
}
init() {
this.buildTask();
// ページのリロード処理
$(window).on('load', (e) => {
this.CallbackInit();
});
// スクロールの間引き処理
$(window).on('scroll', (e) => {
if(this.scrollFlag) {
this.scrollFlag = false;
setTimeout(()=>{
this.CallbackInit();
this.scrollFlag = true;
return;
}, this.scrollBatch);
};
});
// リサイズで何度も処理が走らないための対策
$(window).on('resize', (e) => {
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(()=>{
debug.log('resize');
this.CallbackInit();
}, 500);
});
};
}
export default IntersectionWatch;