🍇

ページ内に複数置いた同一要素を検知する

2021/01/11に公開

TL;DR

少し前に詰まりかけたものとして、「同一ページに置いてある同一class名の複数要素について、反応(クリックとかinviewとか)があった場合、何番目の要素にその反応があったのか検知したい(そしてその要素に挙動を起こしたい)」というのがあったので、今後同じことで悩まないように備忘録として残しておきます

結論だけ

全部見返すの面倒な時用
onclickthis 渡す
addEventListenere.toElement 使う
これで自身を知らせられる

事象

<div class="image">
    <img src="images/DummyBlack_300x300.png" class="imageElement">
</div>

<br>
<div class="image">
    <img src="images/DummyRed_300x300.png" class="imageElement">
</div>

<br>
<div class="image">
    <img src="images/DummyGreen_300x300.png" class="imageElement">
</div>

同一ページ上にこんな感じの同じ要素が並んでるとして、「どこか1つ(例えば1番め、黒色のやつ)の class="image" 要素をクリックしたら、その1つ(1番め、黒色のやつ)のみ変化して欲しい」という挙動が欲しいとする

単純に考えるならこんな感じのJSになるはずです

function clickImage (t) {
  let tgt = document.getElementsByClassName("image")[0];
  tgt.addEventListener("click", function(){
       //console.log(tgt);  //デバッグ用
    let filter = document.createElement("div");
    filter.classList.add("imageFilter");
    filter.innerText = "Clicked!"
    tgt.appendChild(filter);
  }, false);
}

で、これを各image要素に onclick=clickImage(this) とかで実際に実装してみると、その挙動はこのようになります↓

確かに1番め、黒色のやつを押した時には変化しているが、変化がまとめて3つ分来てしまっており(各image要素に onclick=clickImage(this) として計3つページ上にあるせい?)、2,3番めの要素(赤、緑)を押したときには何も起こらない状態になってしまっています
それもそのはず、最初に変化の対象として let tgt = document.getElementsByClassName("image")[0]; と1番めしか指定できていないから、1番めの要素にしか変化が出ないから当たり前です(そもそもthisすら引数に設定しているのに使ってもいない)
なら指定する要素を変えられれば、と思いますが、 let tgt = document.getElementsByClassName("image")[0]; この書式だと結局何番めかを指定しないといけなくて、そのためには各要素で何番め要素かを指定した別関数を設定しないといけなくなり、かなり面倒です(3つならまだいいが…)

解決策その1

ではどうすればいいかというと、(既にonclick内の記述で察している人はいると思うが)引数 this 使って「自分自身が対象」というのを関数に伝えてやれば済むことになります


うまくいっている

実現時のJSコード

function clickImage (t) {
  //console.log(t);  //デバッグ用
  if(t.getElementsByClassName("imageFilter").length > 0) return;
  let filter = document.createElement("div");
  filter.classList.add("imageFilter");
  filter.innerText = "Clicked!"
  t.appendChild(filter);
}

実現時のHTML

<div class="image" onclick=clickImage(this)>
    <img src="images/DummyBlack_300x300.png" class="imageElement">
</div>

<br>
<div class="image" onclick=clickImage(this)>
    <img src="images/DummyRed_300x300.png" class="imageElement">
</div>

<br>
<div class="image" onclick=clickImage(this)>
    <img src="images/DummyGreen_300x300.png" class="imageElement">
</div>

やるなら onclick からthisを渡して「操作されたのは自分ですよ~」と知らせてやればいいだけの話でした

ただ、自分が対応することになった案件だと、HTML要素がいじれずJSコードの実装だけで対応しなければならない場合や、今回例示しているのはclickイベントですが実際は「その要素が画面内に入ったとき」(いわゆるinview)に発火しなければならない場合が出てきたりします
JSであれば後からJSでonclick生やしてやる形もできますが…

解決策その2

なので、 onclick によらない形を考えました
その場合に使ったJSコードが以下のようになります

window.addEventListener("click", function(e){
    //console.log(e);  //デバッグ用
    console.log(e.toElement);
    let targetElement = e.toElement.parentNode;
    if(targetElement.getElementsByClassName("imageFilter").length > 0) return;
    let filter = document.createElement("div");
    filter.classList.add("imageFilter");
    filter.innerText = "Clicked!"
    targetElement.appendChild(filter);
}, false);

クリックされた要素自体について、addEventListenerで取れるe(event)より、 .toElement で持ってくる事ができるので、(一応console.logとかで e.toElement の中身を確認した上で) .toElement を元に「操作されたのは自分ですよ~」をJSコードに渡すことができます
これができれば後はthisと同じようにするだけ


こっちでも成功している
デモ: https://daken91.github.io/Daken_PlainProducts/MultiElementDemo.html

なおinviewの場合については、ページ内での要素の高さを window.pageOffset  と getBoundingClientRect().top で取得し、 scrollTop で取得したページ内現在位置と比較して、画面内に入った要素を確定する、というだけの泥臭い形で対応しました

let targets = document.getElementsByClassName("image");
        let targetLength = targets.length;
        let targetLine = [];
        for (let i = 0; i < targetLength; i++) targetLine.push(window.pageOffset + targets[i].getBoundingClientRect().top);
        window.addEventListener("scroll", function (e) {
          let scrollTop = window.document.body.scrollTop || window.document.documentElement.scrollTop;
          let crossBorderNumber = -1;
          for (let j = 0; j < targetLength; j++) {
            if (scrollTop >= targetLine[j] - 200 && scrollTop < targetLine[j] + 300) {
              crossBorderNumber = j;
              break;
            }
          }
          if (crossBorderNumber !== -1) {
            let targetElement = targets[crossBorderNumber];
            clickImage(targetElement);
            crossBorderNumber = -1;
          }
        }, false);

こんな感じ

Discussion