PHPで入れ子になったul要素の最上位のli要素の数を取得する

CSSでレイアウトを設定するとき、フレックスレイアウト (flex)、グリッドレイアウト (grid)、マルチカラムレイアウト (columns)、これらを使うことが多くなりました。いずれも親要素への設定によって子要素のレイアウトが変化する仕組みになっています。

子要素の数が決まっている場合は非常に便利ですが、子要素の数が少ない場合と多い場合でレイアウトを変えたいといったケースでは不便に感じます。

そこで今回は、子要素の数を取得して、class属性値に反映させるPHPコードを紹介します。

タイトルにもあるように、入れ子になったul要素の最上位のli要素の数を取得するコードになりますが、例えば、div要素の子要素のsection要素の数といったケースにも応用できると思います。

HTMLのサンプル

<div class="nested-list">
  <ul>
    <li>アイテム</li>
    <li>アイテム
      <ul>
        <li>アイテム</li>
        <li>アイテム</li>
      </ul>
    </li>
    <li>アイテム</li>
    <li>アイテム
      <ul>
        <li>アイテム</li>
        <li>アイテム
          <ul>
            <li>アイテム</li>
            <li>アイテム</li>
          </ul>
        </li>
        <li>アイテム</li>
        <li>アイテム
          <ul>
            <li>アイテム</li>
            <li>アイテム
              <ul>
                <li>アイテム</li>
                <li>アイテム</li>
              </ul>
            </li>
          </ul>
        </li>
      </ul>
    </li>
  </ul>
</div>

最大で4つul要素が入れ子になったリストです。このHTMLコードから、最上位のli要素の数である「4」を取得します。

参考までにJavaScriptの場合

JavaScriptの場合、非常にシンプルなコードで実装可能です。

次のコードを、body終了タグ (</body>) の直前にscript要素で追加するだけです。

!function( d ) {
  Array.prototype.forEach.call( d.querySelectorAll( '.nested-list > ul' ), function( list ) {
    list.classList.add( 'ul-item-count-' + list.children.length );
  } );
} ( document );

PHPの場合

PHPは文字列を処理するため、JavaScriptに比べ複雑になります。

次のコードは、リストを生成するコードの途中に追加することを前提としています。WordPressであれば、wp_nav_menuテンプレートタグwp_nav_menuフック等で追加します。

$output = '<div class="nested-list"><ul>…省略…</ul></div>';
$count = 0;
$depth = 0;
$items = explode( '</li>', $output );
foreach( $items as $item ) {
  if( 1 < $depth ) {
    if( false !== strpos( $item, '</ul>' ) ) {
      $depth--;
    } elseif( false !== strpos( $item, '<ul>' ) || false !== strpos( $item, '<ul ' ) ) {
      $depth++;
    }
  } elseif( false !== strpos( $item, '<li>' ) || false !== strpos( $item, '<li ' ) ) {
    $count++;
    if( false !== strpos( $item, '<ul>' ) || false !== strpos( $item, '<ul ' ) ) {
      $depth++;
    }
  }
}
$pos = strpos( $output, '<ul>' );
$output = substr( $output, 0, $pos ). '<ul class="ul-item-count-'. $count. '">'. substr( $output, $pos + 4 );

このコードでは次のような処理を行っています。

1. 配列の生成

li終了タグ (</li>) で分割して、配列itemsを作ります。配列の各値は次の5つのパターンのいずれかになります (見やすさのためインデントや改行を取り除いています)。

  • <div class="nested-list"><ul><li>アイテム
  • <li>アイテム
  • <li>アイテム<ul><li>アイテム
  • </ul>
  • </ul></div>

2. ループ処理

まず、最上位のli要素を数を代入する変数count (初期値0) と、ul要素の深さを代入する変数depth (初期値0) を用意します。

配列itemsをループさせ、ul開始タグ (<ul>もしくは<ul ) があったら変数depthをインクリメント (+1)、ul終了タグ (</ul>) があったら変数depthをデクリメント (-1) します。

変数depthが1以下のときにli開始タグが見つかったら、変数countをインクリメント (+1) します。

このような処理で、最上位のli要素の数が変数countに代入されます。

3. class属性値を追加

最後に、最上位のul要素 (最初に登場するul要素) に「ul-item-count-{変数count}」のようなclass属性値を追加してあげれば終わりです。

追加したclass属性値の使用例

ヘッダーナビゲーションをフレックスレイアウトで作った場合、最上位のli要素の数が少ない場合は中央にまとめて、数が多い場合は要素間の余白を均等にするというのはどうでしょうか?

.nested-list > ul {
  display: flex;
  justify-content: space-between;
  list-style: none;
  padding: 0;
}
.nested-list > .ul-item-count-1,
.nested-list > .ul-item-count-2,
.nested-list > .ul-item-count-3 {
  justify-content: center;
}

策定途中のCSS Selectors Level 4では、:has疑似クラスが登場しています。

この:has疑似クラスは、特定の要素を親に持つ要素 (.parent > .child) や、特定の要素が前にある要素 (.prev + .next) のように、一方向だったセレクタを一変させるもので、特定の要素を子に持つ要素 (.parent:has( > .child )) や、特定の要素が後にある要素 (.prev:has( + .next )) を選択することができます。

この:has疑似クラスが使えるようになれば、今回紹介したコードはいらなくなります。

.nested-list > ul:has( > li:first-child:nth-last-child( 1 ) ),
.nested-list > ul:has( > li:first-child:nth-last-child( 2 ) ),
.nested-list > ul:has( > li:first-child:nth-last-child( 3 ) ) {
  …
}

しかし、2021年6月現在、:has疑似クラスを先行実装しているブラウザは皆無なので、しばらくは今回のコードが役に立つでしょう。

関連記事