アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

Element から Selector を取得する

June 07, 2015開発JavaScript

CSS や JavaScript で document.querySelector に指定する Selector を DOM Element から取得したくなって方法を調べていたら以下の記事をみつけた。

Element を指定すると Selector を返すような API を想定していたのだが自前で Node/Element をチェックする必要があるようだ。動作の理解と実証のため簡単なサンプルを作成する。

以下のような HTML から <h1><p><th><td> がクリックされたとき、その Element から得た Selector を表示。Selector が正しいことを確認するために document.querySelector へ指定して得られた Element の背景色を変更する。

サンプルでは jQuery などのライブラリは使用しない。すべて標準の DOM API で処理する。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Get selector from element</title>
  <link rel="stylesheet" href="app.css">
  <script src="app.js"></script>
</head>
<body>
<nav class="addressbar"></nav>
<div class="container">
  <div class="main">
    <h1>Get selector from element</h1>

    <p>The quick brown fox jumps over the lazy dog</p>

    <table>
      <thead>
        <tr><th>#</th><th>Name</th><th>Comment</th></tr>
      </thead>
      <tbody>
        <tr><td>1</td><td>Name 1</td><td>Comment 1</td></tr>
        <tr><td>2</td><td>Name 2</td><td>Comment 2</td></tr>
        <tr><td>3</td><td>Name 3</td><td>Comment 3</td></tr>
        <tr><td>4</td><td>Name 4</td><td>Comment 4</td></tr>
        <tr><td>5</td><td>Name 5</td><td>Comment 5</td></tr>
      </tbody>
    </table>
  </div>
</div>
</body>
</html>

クリック イベントは EventTarget.addEventListener でハンドリング。コールバックに指定された EventListener には対象 Element の参照が格納されている (EventListener.target)。これを以下の関数に指定することで Selector を得られるはず。

/**
 * 指定された要素と同一階層、同名の要素コレクション内におけるインデックスを取得します。
 *
 * @param {Element} el   要素。
 * @param {String}  name 要素名。
 *
 * @return {Number} インデックス。範囲は 1 〜 N となります。
 */
function getSiblingElemetsIndex( el, name ) {
  var index = 1;
  var sib   = el;

  while( ( sib = sib.previousElementSibling ) ) {
    if( sib.nodeName.toLowerCase() === name ) {
      ++index;
    }
  }

  return index;
}

/**
 * 指定された要素を示すセレクターを取得します。
 *
 * @see http://stackoverflow.com/questions/3620116/get-css-path-from-dom-element
 *
 * @param {Element} el 要素。
 *
 * @return {Array} セレクター名コレクション。
 */
function getSelectorFromElement( el ) {
  var names = [];
  if( !( el instanceof Element ) ) { return names; }

  while( el.nodeType === Node.ELEMENT_NODE ) {
    var name = el.nodeName.toLowerCase();
    if( el.id ) {
      // id はページ内で一意となるため、これ以上の検索は不要
      name += '#' + el.id;
      names.unshift( name );
      break;
    }

    // 同じ階層に同名要素が複数ある場合は識別のためインデックスを付与する
    // 複数要素の先頭 ( index = 1 ) の場合、インデックスは省略可能
    //
    var index = getSiblingElemetsIndex( el, name );
    if( 1 < index ) {
      name += ':nth-of-type(' + index + ')';
    }

    names.unshift( name );
    el = el.parentNode;
  }

  return names;
}

処理を要約。

  • Elementid を持つならページで一意となるため、それを Selector にする
  • 同じ階層に同名 Element が複数あるなら CSS3 :nth-of-type() Selector によるインデックスを割り当てる (先頭の場合は省略可能)
  • DOM のルートに到達するまで Node/Element をさかのぼり、継ぎ足してゆく

例えばある <table> 内の <td> に対して得られる Selector は以下のようになるはず。

html > body > div > div > table > tbody > tr:nth-of-type(2) > td:nth-of-type(2)

Stack Overflow のサンプル関数だと Selector 文字列を返すようになっている。しかし Selector が無効なら成功するまで階層をさかのぼりたくなるかもしれない。よって getSelectorFromElementArray を返して、呼び出し側で join するように設計を変更してある。

この機能の使い道としては Ajax や Node + jsdom などで Web ページ検索を実装する際、検索結果となる Element 位置を Selector として記録することなどが考えられる。DOM の変更に影響されるのが難点。しかし事前に不変なことが判明、または更新頻度の低いページに限定するなら有用ではなかろうか。

サンプル プロジェクトを以下に公開。

index.html を Web ブラウザーで表示してなにか要素をクリックすると、ページ最上段に Selector が表示されて対象 Element が緑色になる。