#065
posted on 2022.02.06 (Sun) 2022.10.01 (Sat)

(ネイティブJavaScript版) マウスオンでカーソルに追従するツールチップを表示。

マウスオンでツールチップを表示するようにしたかったので、表示エリア内にマウスが入ったらマウスポインタに追従するツールチップを表示させる方法のメモ。

※ jQueryを使った方法は別記事を参照。

 

 

親要素の領域にマウスポインタが入ったら、子要素にしてあるツールチップをCSSで非表示から表示に変更する。

※ 親要素にカスタムdata属性(ここでは「data-ag2tip」)を付与しておき、マウスポインタが入った要素がカスタムdata属性を持つかをJavaScriptで判別して動的にクラス名の付与・削除の処理を実行する。

 

 

HTMLのマークアップとCSSの設定

マウスオンの判定領域となる親要素内にツールチップを入れ子にして記述。ツールチップの表示と非表示はCSSで設定する。

 

HTMLのマークアップ

親要素「.container」と、ツールチップ本体になる子要素「.tooltip」を記述。

※ JavaScriptでのターゲット要素の判別用に任意のカスタムdata属性を付与しておく。(ここでは「data-ag2tip」。)

<div class="container" data-ag2tip>
	<p>常に表示のコンテンツ内容...</p>
	<div class="tooltip"><p>ツールチップの内容...</p></div>
</div>

 

CSS

子要素「.tooltip」が親要素「.container」の左上の位置になるように「position」を指定し、非表示(「visibility: hidden;」)に設定。親要素「.container」内の他の要素よりも上位レイヤーに表示させるために「z-index: +1;」を設定。

マウスが親要素に入ったらJavaScriptで任意のクラス名(ここでは「ag2tipOn」)を動的に付与し、子要素「.tooltip」を表示(「visibility: visible;」)にする。

.container {
	position: relative;
}
.tooltip {
	display: inline-block;
	position: absolute;
	top: 0;
	left: 0;
	max-width: 100px;
	visibility: hidden;
	z-index: +1;
}
.ag2tipOn .tooltip {
	visibility: visible;
}

 

 

JavaScriptで実装

マウスイベントでマウスポインタの動きを捕捉し、親要素の領域への流入・流出を判別して任意のクラス名の付与・削除(ツールチップの表示・非表示)を実行する。

  • 「mouseover」、「mouseenter」 イベント : 指定した要素の領域へのマウスの流入で発火。
  • 「mouseout」、「mouseleave」 イベント : 指定した要素の領域からのマウスの流出で発火。
  • 「mousemove」イベント : 指定した要素の領域上でのマウスの移動で発火。

※ 「mouseover」、「mouseout」、「mousemove」はバブリングするので、documentだけにリスナー登録をしておけばよいが、イベントが発火しているターゲット要素を常に確認して処理する必要がある。

※ 「mouseenter」、「mouseleave」はバブリングしないので、リスナー登録した要素でのみ発火するが、処理を実行したい要素すべてにリスナー登録しておく必要がある。(イベントターゲットは常に「this」と同じ。)

 

 

1. 「mouseover」、「mouseout」イベントを使って実装する場合

イベントバブリングに対処するため、イベント発火時の「target」(イベントを発生させているターゲット)と「relatedTarget」(副ターゲット)を確認して分岐処理する。

  • 「mouseover」の場合、ポインティングデバイスの流入した要素が「event.target」、流入直前に居た要素が「event.relatedTarget」になる。(「relatedTarget」は、どこから来たかになる。)
  • 「mouseout」の場合、ポインティングデバイスの流出した要素が「event.target」、流出直後に居る要素が「event.relatedTarget」になる。(「relatedTarget」は、どこに行ったかになる。)

 

バブリングするので、処理を関数でまとめてdocumentにリスナー登録しておいて、発火したイベントターゲットを確認して必要なときだけ実行する。

  1. 「mouseover」イベントで、マウスポインタの流入を捕捉。
  2. マウスポインタがカスタムdata属性「data-ag2tip」を持つ要素に流入した場合、その要素に動的にクラス名(ここでは「ag2tipOn」)を付与。
  3. ポインタの位置座標を取得。
  4. 子要素「.tooltip」の位置をポインタの座標に変更。(「transform: translate(x,y);」のstyleを変更する。)
  5. マウスポインタが親要素「.container」上で移動している場合、ポインタ位置座標の取得と子要素「.tooltip」の座標変更を常に実行。(「mousemove」イベントで捕捉。)
  6. マウスポインタが親要素から流出した場合、動的に付与したクラスを削除。(「mouseout」イベントで捕捉。)

※ 処理に必要な「祖先要素(自身を含める)から指定のカスタムdata属性を持つ要素を取得するメソッド」と「指定のノードを子要素に持つかを調べるメソッド」を自作して、それぞれElementインターフェイスとNodeインターフェイスにプロトタイプで実装しておく。(継承とプロトタイプチェーンのMozillaの公式ドキュメント。)

//祖先から指定data属性を持つ要素を取得するメソッドを作成
if(!Element.prototype.ag2closest){
  Element.prototype.ag2closest = function(d){
    let _el = this;
    do{
      if(_el.dataset[d] !== undefined) return _el;
      _el = _el.parentElement || _el.parentNode;
    }while(_el !== null && _el.nodeType === 1);
    return null;
  };
}
//指定ノードを子要素に持つか確認するメソッドを作成
if(!Node.prototype.ag2contains){
  Node.prototype.ag2contains = function(n){
    while(n !== null){
      if(n === this) return true;
      n = n.parentElement || n.parentNode;
    }
    return false;
  };
}

//初期設定値
const ag2tipSettings = {
  dataName: 'ag2tip', //親要素に付与してあるdata属性名
  classActive: 'ag2tipOn', //マウスオンで親要素に付与するクラス名
  classTip: 'tooltip', //子要素(ツールチップ)に付与してあるクラス名
  offset: 20 //ツールチップの表示位置をポインタよりも右にずらすピクセル量
};
let currentTip = null; //現在表示中のツールチップを保持する変数
const ag2tip = {
  on: function(t){
    //クラスを付与
    t.classList.add(ag2tipSettings.classActive);
    //現表示の親要素を保持
    currentTip = t;
    //表示位置を補正
    ag2tip.pos();
  },
  off: function(){
    //クラスを削除
    currentTip.classList.remove(ag2tipSettings.classActive);
    currentTip = null;
  },
  pos: function(){
    let thisWrapRect = currentTip.getBoundingClientRect(),
        thisTip = currentTip.querySelector('.'+ag2tipSettings.classTip),
        x,y;
    //ツールチップの表示位置をポインタ座標に変更
    x = event.clientX - thisWrapRect.left + ag2tipSettings.offset;
    y = event.clientY - thisWrapRect.top;
    thisTip.style.transform = 'translate('+x+'px,'+y+'px)';

    let thisTipRect = thisTip.getBoundingClientRect(),
        winW = document.documentElement.clientWidth,
        winH = document.documentElement.clientHeight;
    //ツールチップがブラウザウィンドウからはみ出る場合にポインタの反対側へ移動させる
    if(thisTipRect.width + event.clientX + ag2tipSettings.offset > winW) {
      thisTip.style.left = -(thisTipRect.width + ag2tipSettings.offset + 10)+'px';
    }else{
      thisTip.style.left = '0px';
    }
    if(thisTipRect.height + event.clientY > winH) {
      thisTip.style.top = -thisTipRect.height+'px';
    }else{
      thisTip.style.top = '0px';
    }
  }
};

document.addEventListener('mouseover', function(){
  //ターゲット(またはその親要素)が「data-ag2tip」属性を持っていて、かつ現表示の要素ではない場合
  let tipTarget = event.target.ag2closest(ag2tipSettings.dataName);
  if(tipTarget && tipTarget !== currentTip){
    //親要素にクラスを付与
    ag2tip.on(tipTarget);
    //マウス追従させる関数をリスナー登録
    document.addEventListener('mousemove', ag2tip.pos);
  }
});
document.addEventListener('mouseout', function(){
  //ツールチップ表示中の場合
  if(currentTip){
    //移動先がブラウザウィンドウ外ではなく、かつ以下のどちらかの場合はreturn
    //移動元が現表示の親要素、かつ移動先がその子要素の場合
    //移動元が現表示の親要素の子要素、かつ移動先が現表示の親要素(またはその子要素)の場合
    if( event.relatedTarget && ((event.target === currentTip && event.target.ag2contains(event.relatedTarget)) || (currentTip.ag2contains(event.target) && currentTip === event.relatedTarget.ag2closest(ag2tipSettings.dataName))) ) return;

    //親要素からクラスを削除
    ag2tip.off();
    //マウス追従させる関数をリスナーから削除
    document.removeEventListener('mousemove', ag2tip.pos);
  }
});

要素.dataset.属性名 : 指定した要素の指定したカスタムdata属性の値を文字列で返す。存在しないdata属性は「undefined」、存在するが値が設定されていないdata属性は空文字を返す。値を指定して代入すれば属性値の設定ができる。(ブラケット記法「要素.dataset.[‘属性名の文字列’]」なら文字列や変数を使って属性名の指定ができる。)

ノード.parentElement : 指定したノードのDOMノード上の親要素を返す。親ノードが存在しない場合、または親ノードがDOMElementで無い場合は「null」を返す。

ノード.parentNode : 指定したノードのDOMツリー上の親ノードを返す。Attr 、Document、DocumentFragment、Entity、Notationの場合は「null」を返す。

ノード.nodeType : 指定したノードの種類を表す整数のコードを返す。「1」はエレメントノード、「3」はテキストノード。

要素.classList : 指定した要素のclass属性を返す読み取り専用のプロパティー。DOMTokenListのコレクション。

要素.classList.add(‘クラス名’) : 指定した要素のclassListに指定されたクラス名を追加する。

要素.classList.remove(‘クラス名’) : 指定した要素のclassListから指定されたクラス名を削除する。

要素.getBoundingClientRect() : 指定した要素の寸法と、ビューポートに対する位置情報を保持するDOMRectオブジェクトを返す。「left」、「top」、「right」、「bottom」、「x」、「y」、「width」、「height」の8つのプロパティーを持つ。

document.querySelectorAll(‘セレクター’) : 指定したセレクターのNodelistオブジェクトをDOMから取得する。

マウスイベント.clientX : MouseEventの読み取り専用のプロパティー。イベントが発生した時点のアプリケーションのビューポートにおけるx座標を(doubleの)浮動小数点値で返す。

マウスイベント.clientY : MouseEventの読み取り専用のプロパティー。イベントが発生した時点のアプリケーションのビューポートにおけるy座標を(doubleの)浮動小数点値で返す。

要素.addEventListener(‘イベントのタイプ’, ‘関数’, ‘イベント伝播順’) : 指定した要素に指定のイベントでコールする関数を指定して登録。第3引数(初期値 : false)でイベントの伝播する方向を指定できる。falseでDOM階層の下位から上位に伝播。

マウスイベント.relatedTarget : MouseEventの読み取り専用のプロパティー。副ターゲットとなるEventTargetオブジェクトを返す。副ターゲットが無いイベントでは「null」を返す。(マウスポインタがブラウザウィンドウ外に出た場合も「null」を返す。)

event.target : イベントを発生させたオブジェクトへの参照。オブジェクトは自身の情報をプロパティーに持つ。

 

 

2. 「mouseenter」、「mouseleave」イベントを使って実装する場合

「mouseenter」、「mouseleave」はイベントバブリングしないので、発火したイベントターゲットの確認処理を省けるが、すべての対象要素にリスナー登録しなければいけないので、対象となる要素をJavaScriptでまとめて取得できるように任意のクラス名を付与しておく必要がある。(ここでは「container」。)

//初期設定値
const ag2tipSettings = {
  classWrap: 'container', //親要素に付与してあるクラス名
  classActive: 'ag2tipOn', //マウスオンで親要素に付与するクラス名
  classTip: 'tooltip', //ツールチップに付与してあるクラス名
  offset: 20 //ツールチップの表示位置をポインタよりも右にずらすピクセル量
};
let currentTip = null;
const ag2tip = {
  on: function(){
    event.target.classList.add(ag2tipSettings.classActive);
    currentTip = event.target;
    ag2tip.pos();
  },
  off: function(){
    event.target.classList.remove(ag2tipSettings.classActive);
    currentTip = null;
  },
  pos: function(){
    let thisWrapRect = currentTip.getBoundingClientRect(),
        thisTip = currentTip.querySelector('.'+ag2tipSettings.classTip),
        x,y;
    x = event.clientX - thisWrapRect.left + ag2tipSettings.offset;
    y = event.clientY - thisWrapRect.top;
    thisTip.style.transform = 'translate('+x+'px,'+y+'px)';

    let thisTipRect = thisTip.getBoundingClientRect(),
        winW = document.documentElement.clientWidth,
        winH = document.documentElement.clientHeight;
    if(thisTipRect.width + event.clientX + ag2tipSettings.offset > winW) {
      thisTip.style.left = -(thisTipRect.width + ag2tipSettings.offset + 10)+'px';
    }else{
      thisTip.style.left = '0px';
    }
    if(thisTipRect.height + event.clientY > winH) {
      thisTip.style.top = -thisTipRect.height+'px';
    }else{
      thisTip.style.top = '0px';
    }
  }
};

//対象セレクターのNodelistを取得してすべてにリスナー登録
const ag2tips = document.querySelectorAll('.'+ag2tipSettings.classWrap),
      ag2tipsNum = ag2tips.length;
for(let i = 0; i < ag2tipsNum; i++){
  ag2tips[i].addEventListener('mouseenter', ag2tip.on);
  ag2tips[i].addEventListener('mousemove', ag2tip.pos);
  ag2tips[i].addEventListener('mouseleave', ag2tip.off);
};

 

 

この記事のURL

https://memo.ag2works.tokyo/post-4005/

Copyコピー
この記事のタイトル

(ネイティブJavaScript版) マウスオンでカーソルに追従するツールチップを表示。 | memo メモ [AG2WORKS]

Copyコピー
この記事のリンクタグ

<a href="https://memo.ag2works.tokyo/post-4005/" target="_blank" rel="noopener">(ネイティブJavaScript版) マウスオンでカーソルに追従するツールチップを表示。 | memo メモ [AG2WORKS]</a>

Copyコピー
※ フィールドをクリックでコピーするテキストの編集ができます。

この記事へのコメント

コメントの書き込みはまだありません。

  • コメント内のタグはエスケープ処理され、文字列として出力されます。
  • セキュリティーのため、投稿者のIPアドレスは取得されます。
  • 管理者が内容を不適切と判断したコメントは削除されます。
  • このフォームにはスパム対策として、Googleの提供するreCAPTCHAシステムが導入されています。
    (Google Privacy Policy and Terms of Service.)