このページには広告が含まれています
WordPress

折りたたみ&アイコン表示ができるカテゴリー・アーカイブリスト:実装コードと使い方

この記事は約38分で読めます。

見なよ、オレのカテゴリーリストを……

標準のカテゴリーリスト・アーカイブリストが使いにくいしカッコ悪い!

これは伸びる。縦に。

WordPressにCocoonをインストールして使っているが、標準で提供されているカテゴリーリストとアーカイブリストが使いにくくて困っていた。
それらのリストは、カテゴリーや年月をシンプルなリストとして表示するようになっているが、要素数が増えてくると縦にどんどん長くなってしまう。

このブログははてなブログから移転したため、昔からの記事がそれなりに残っている。
その影響で、カテゴリーリスト、アーカイブリストとも、かなり縦長になっていた。

縦長のカテゴリーリスト 縦長のアーカイブリスト

こんなに長いとサイドバーで場所を取るし使い勝手が悪い。
なにより見た目がカッコ悪い。行間も間延びしているし、カテゴリーリストに至っては記事数の位置が揃っていないせいで乱雑な印象を受けてしまう。

リスト改造計画 → 作り直した方が早い。AIくん、よろしく!

それぞれのリストを折りたためるようにするプラグインを使って、何とかならないか試行錯誤したこともあったが、思うような結果は得られなかった。

その際、「めちゃくちゃ苦労した割には満足感がない……」と感じ、「こうなったらイチから作り直した方がいいんじゃ……コーディングするのはAIだけどな!」と考えた。
そして、ある程度仕様を固め、「これこれこういう仕様を満たすリストが欲しいのでコード生成ヨロシク!」とAI(Gemini・無料版)に指示した。

参考:実際に使用したプロンプト

今回は、カテゴリーリストとアーカイブリストを個別に作らせて動作を確認した後、二つを統合する形で最終的なコードを得た。
その際に使用したプロンプトは以下の通り。

カテゴリーリスト生成用

全体像
WordPressでサイドバーに表示するための折りたたみ可能なカテゴリーリストを作りたい。
そのために必要なスクリプト・スタイルシートを生成して欲しい。

仕様
*サイドバーへの表示は、カスタムHTMLでのショートコード埋め込みを考えているが、ウィジェットとして実装できるならその方が良い。ウィジェットとしても使えてショートコードの埋め込みでページ内でも使えるようだとさらに良い。
*サブカテゴリがあり、折りたたんでいる状態のカテゴリの前にはアイコンとして <i class="fa fa-folder" aria-hidden="true"></i> を表示する
*サブカテゴリがあり、展開している状態のカテゴリの前にはアイコンとして <i class="fa fa-folder-open" aria-hidden="true"></i> を表示する
*サブカテゴリがないカテゴリの前にはアイコンとして <i class="fa fa-list" aria-hidden="true"></i> を表示する
*サブカテゴリの開閉に伴うアニメーションはなくて良い(スライド・フェードなどのアニメーションを追加できるような拡張性があればなお良い)
*カテゴリ内の記事数を、カテゴリ名の後ろに表示する。サブカテゴリがあるものは、サブカテゴリ分の記事数も合計した数値を表示する。数値は()で囲って表示する。
*リンクが有効となるのは、カテゴリ名と数値の部分のみ。アイコン部分はカテゴリの開閉スイッチの役目だけとする(サブカテゴリがない場合は、アイコンもリンク先に含めても良いかも知れないが、操作が変わると利用者が混乱する虞があるため統一する)。
*サブカテゴリを展開している・していない状態が分かりやすいように、それぞれの状態で色を変える。展開していない状態をデフォルトとし、展開している状態の色を変える方向でスタイルをセットする。

その他
*大まかな仕様は以上となるが、他に必要な情報があれば適宜問い合わせること

アーカイブリスト生成用

全体像
WordPressでサイドバーに表示するための折りたたみ可能なアーカイブリストを作りたい。
そのために必要なスクリプト・スタイルシートを生成して欲しい。

仕様
*サイドバーへの表示は、カスタムHTMLでのショートコード埋め込みを考えているが、ウィジェットとして実装できるならその方が良い。ウィジェットとしても使えてショートコードの埋め込みでページ内でも使えるようだとさらに良い。
*年月の表示は、「YYYY年」、月の表示は「MM月」とするが、簡単にカスタマイズできる方法が提供されていると良い。
*「年」を折りたたんでいる状態では、 <i class="fa fa-folder" aria-hidden="true"></i> をアイコンとして「年」の前に表示する
*「年」展開している状態では <i class="fa fa-folder-open" aria-hidden="true"></i> をアイコンとして「年」の前に表示する
*「月」の前には <i class="fa fa-calendar" aria-hidden="true"></i> をアイコンとして表示する。
*開閉に伴うアニメーションはなくて良い(スライド・フェードなどのアニメーションを追加できるような拡張性があればなお良い)
*記事数を「年」「月」の後ろに()で囲って表示する。「年」の記事数は、その年の記事の合計数。
*リンクが有効となるのは、「年」「月」と数値の部分のみ。アイコン部分は開閉スイッチの役目だけとする。
*展開している・していない状態が分かりやすいように、それぞれの状態で色を変える。展開していない状態をデフォルトとし、展開している状態の色を変える方向でスタイルをセットする。

その他
*大まかな仕様は以上となるが、他に必要な情報があれば適宜問い合わせること

カテゴリーリスト・アーカイブリスト統合用

WordPressのアーカイブリストとカテゴリーリストで、以下のようなコードを使って「アイコン付き折りたたみリスト」を実現しています。
似たようなことをしているので、まとめられるところはまとめてしまってコード管理を簡略化したいのですが、可能でしょうか。

*アーカイブリスト用CSS*
(コード省略)

*アーカイブリスト用JavaScript*
(コード省略)

*アーカイブリスト用PHPコード*
(コード省略)

*カテゴリーリスト用CSS*
(コード省略)

*カテゴリーリスト用JavaScript*
(コード省略)

*カテゴリーリスト用PHPコード*
(コード省略)

大改造!!劇的ビフォーアフター

BGM:ビフォーアフターのテーマ曲

ナレーション
ナレーション

ただ漫然と並び、縦に長く伸び切ってしまったカテゴリーリストやアーカイブリスト。必要な情報にたどり着くことすら困難なその姿は、訪れる人々を惑わせる『情報の迷宮』のよう。

果たして、限られたサイドバーという空間を、どこまで機能的に再生できるのか。今回、この難題に立ち向かうのは、論理の海から最適解を導き出すコード生成の匠、”電脳の魔術師”ことGeminiです。

ビフォー・アフターを見比べる

※タブを切り替えることでビフォー・アフターを見比べられます。

カテゴリーリスト

  • ビフォー
  • アフター

縦長のカテゴリーリスト

改良後のカテゴリーリスト

アーカイブリスト

  • ビフォー
  • アフター

縦長のアーカイブリスト

改良後のアーカイブリスト

ナレーション
ナレーション

なんということでしょう。匠の鮮やかな手並みによって、あの間延びした印象を拭えなかったリストが、機能美あふれるスマートなリストへと生まれ変わったではありませんか。

フォルダの開閉という新たな息吹を吹き込まれたその姿に、かつての乱雑な面影はもうどこにもありません。


なお、改良点は「リストが折りたたまれてコンパクトになった」「項目の前にアイコンがついて分かりやすくなった(しかも開閉に連動して変化する)」「表示が整えられて見映えが良くなった」……だけではない。

カレント強調表示機能

カテゴリーリスト・アーカイブリストとも、カレント(現在地)強調表示機能を備えている。

アーカイブリストでは、特定の年、または月を開くと、開いている年、または月が強調表示される(記事を開いている場合は通常の表示)。

特定の年を開いているとき
特定の年を開いているときのアーカイブリスト

特定の月を開いているとき
特定の月を開いているときのアーカイブリスト

記事を開いているとき
記事を開いているときのアーカイブリスト


カテゴリーリストでは、特定のカテゴリーを開くと、開いているカテゴリーが強調表示される。
また、記事を開くと、その記事に設定されているカテゴリーが強調表示される(この際強調されるカテゴリーについては、「補足:記事に複数のカテゴリーが設定されている場合の動作」を参照)。

特定のカテゴリーを開いているとき
特定のカテゴリーを開いているときのカテゴリーリスト

記事を開いているとき
記事を開いているときのカテゴリーリスト


記事に設定されているカテゴリーが強調表示されるため、自分が今どのカテゴリーの記事を読んでいるのか分かりやすくなっている。
こうすることで、読者に「自分が読んでいる以外にもこんなカテゴリーがあるのか。他の記事も読んでみようかな」という気持ちが起きる……かもしれない。

補足:記事に複数のカテゴリーが設定されている場合の動作

記事に複数のカテゴリーが設定されている場合、記事上部のカテゴリー表示で、一番左側(最初)に表示されるカテゴリーをカレントカテゴリーと判断し、それを強調表示するようになっている。

実例は以下の通り。

設定するカテゴリ記事上部の表示カテゴリーリストの強調表示
「WordPress」と「お知らせ」を設定「WordPress」と「お知らせ」が設定されている「WordPress」が強調表示
「伺か」と「ゴーストの雑談(GToW)」を設定「伺か」と「ゴーストの雑談(GToW)」が設定されている「ゴーストの雑談(GToW)」が強調表示
「お知らせ」「伺か」「ゴースト更新情報」を設定「お知らせ」「伺か」「ゴースト更新情報」が設定されている「お知らせ」が強調表示

この順番はカテゴリーの順番を入れ替えるプラグインを使用すれば変更できる。
また、カテゴリーリストの表示順を入れ替えたいだけであれば、公開中のコードをベースに「表示するカテゴリーの優先順位をリストとして組み込み、その順番で表示させる」ようにカスタマイズすれば、プラグインに頼らずとも実現できるだろう。

カレントカテゴリーを強調表示する意味

「記事に設定されたカテゴリーなら記事の上部にも表示されているんだし、いちいちリストで強調表示しなくてもいいのでは?」と思う人もいるかも知れない。

だが、記事上部のカテゴリー表示は、その記事に設定されているカテゴリーしか表示されないのに対し、カテゴリーリストの方は、記事に設定されているカテゴリーを強調しつつ、他のカテゴリーの存在も表示されている。

記事上部のカテゴリー表示経由の移動では同一カテゴリー内の移動、つまり「線」の移動が中心になるのに対し、カテゴリーリスト経由であれば他のカテゴリーへの移動、つまり「面」の移動も期待できる。
なので、カテゴリーリスト内の強調表示は、ブログ内で読者の回遊性を高める一助になると思う。

大満足の結果に

自分が「こうしてほしい」とAIに頼んだ以外にも、AI側から「こうしたらどうですか?」と提案されて追加してもらった機能(カレント強調表示など)もある。
結果的に、予想以上に良いものができたので大満足である。

「コード」として実装する理由

このプログラムは「プラグイン」ではなく「コード」として実装するようになっている。
なぜプラグインにしないのか。その理由は以下の通り。

  • 「プラグインを可能な限りコードに置き換えてプラグインを減らすこと」がプロジェクトの出発点なので、プラグイン化すると目的地がブレてしまうため。
  • オープンなコードのまま実装・公開することで、以下のようなメリットが考えられるため。
    • 全てがオープンなので、後々カスタマイズしたくなったときに対応しやすい。
    • コードが公開されているため、第三者でも事前に安全性を十分評価できる。

導入手順

WPCode – Insert Headers and Footers + カスタムコードスニペット – WordPress コードマネージャー
WordPress にコードスニペットを簡単に追加できます。ヘッダーやフッターへのスクリプトの挿入、条件付きロジックによる PHP コードスニペットの追加、広告ピクセルコードの挿入などが可能です。

コードの導入手順は以下の通り。

  1. WPCodeでPHP・JavaScript・CSSのコードスニペットを新規作成し、それぞれに以下で公開しているコードをコピー&ペーストする。
    この時、PHPコードの「場所」は「あらゆる場所で実行」を選択すること。
    • 「フロントエンドのみ」ではウィジェット画面にウィジェットが表示されなくなる。
    • 「管理エリアのみ」ではフロントエンドにリストが表示されなくなる。
  2. コードスニペットを有効化。
  3. ここでは組み込み方によって工程が変わる
    • ウィジェットとしてサイドバーなどに組み込む場合
      1. 管理エリアのウィジェット画面を開く。
      2. 必要な場所にカテゴリーリスト・アーカイブリストのウィジェットを追加する。
    • ショートコードとして埋め込む場合
      以下のショートコードを必要な場所に埋め込む。
      • カテゴリーリストのショートコード: [custom_categories]
      • アーカイブリストのショートコード: [custom_archive]
  4. 必要に応じてカスタマイズする(アイコンや色など)。

※FontAwesome 4.7でアイコンを指定する場合、このサイトがとても便利です。

FontAwesome4.7のアイコンをクリックでコピーできる一覧表【日本語検索対応】
WebアイコンフォントFontAwesome4.7.0のコピペが面倒だったのでお目当てのアイコンをクリックでコピー出来るようにした一覧表です。自分用に作った物ですが頑張ったので公開。簡易的な検索とか絞り込みとかも出来ます。2019-07-0...

コードの公開

公開しているコードについて

  • この記事で公開しているコード(以下、本コード)は、2026年3月22日時点で実際にこのブログで使用しているものです。
  • 本コードがどのように動作するかは、当ブログのサイドバーにあるカテゴリーリスト・アーカイブリストで確かめられます。
  • 本コードの動作には、以下で公開しているPHP、JavaScript、CSSの全てが必要となります。
  • コード欄の右上に表示される「コピーする」ボタンをクリックまたはタップすると、内容をクリップボードにコピーできます。

WordPressコーディング規約への対応について

本コードは、以下のポリシーに基づいて作成・レビューを行っています。

  • AIによる規約準拠
    生成・修正の各プロセスにおいて、AIに対し「WordPressコーディング規約に準拠すること」を明示的に指示しています。
  • 実用性を重視したレビュー
    生成されたコードは、AI自身によるセルフレビューを複数回実施済みです。
  • 品質の考え方
    本コードの公開目的を「個人ブログでのカスタマイズの利便性」に置いているため、「動作の確実性と致命的な脆弱性の排除」を最優先としています。そのため、公式ディレクトリ登録レベルの厳密な規約チェック(命名規則の細部やCSSの!important非推奨ルールなど)においては、一部指摘事項が残る可能性があります。

あくまで「個人利用・カスタマイズ用」としての配布ですので、導入の際はご自身の環境に合わせてご活用ください。

本コードのライセンスについて

  • 本コードにはGPLライセンス(GPLv2以降)を適用します。
    • 利用者は、本コードを自由に使うことができます。
    • 利用者は、本コードを自由に改変し、また、再配布することができます。
    • このコードを利用したことによって生じたいかなる不具合や損害についても、管理者は一切の責任を負いかねます。自己責任でのご利用をお願い致します。

PHP

/**
 * Title:       Collapsible Category and Archive Lists for WordPress
 * Description: AI-optimized collapsible sidebar lists with FontAwesome icons and active path highlighting.
 * Version:     1.0.0
 * Author:      Hiroshi Sekiya
 * Author URI:  https://hironet.jp/blog/
 * License:     GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * 1. Archive (Shortcode / Widget)
 */
function render_custom_collapsible_archive() {
	global $wpdb;

	// NOTE: Performance-conscious archive aggregation.
	// Using direct SQL instead of wp_get_archives() for custom counts and structure.
	$results = $wpdb->get_results(
		$wpdb->prepare(
		 "
		 SELECT YEAR(post_date) AS year, MONTH(post_date) AS month, COUNT(ID) AS count
		 FROM {$wpdb->posts}
		 WHERE post_status = %s AND post_type = %s
		 GROUP BY year, month
		 ORDER BY year DESC, month DESC
		 ",
		 'publish',
		 'post'
		)
	);

	if ( ! $results ) {
		return '<p>' . esc_html__( '記事がありません。', 'custom-collapsible' ) . '</p>';
	}

	$cur_year  = get_query_var( 'year' );
	$cur_month = get_query_var( 'monthnum' );

	$cur_year  = (int) $cur_year;
	$cur_month = (int) $cur_month;

	$archive_data = array();
	foreach ( $results as $row ) {
		$archive_data[ $row->year ]['months'][ $row->month ] = $row->count;
		if ( ! isset( $archive_data[ $row->year ]['total'] ) ) {
			$archive_data[ $row->year ]['total'] = 0;
		}
		$archive_data[ $row->year ]['total'] += $row->count;
	}

	$output = '<div class="collapsible-list-container">';
	foreach ( $archive_data as $year => $data ) {
		$is_current_year = ( is_year() || is_month() ) && ( (int) $year === $cur_year );
		$should_open     = $is_current_year ? ' is-active-path is-open' : '';
		$icon_class      = $is_current_year ? 'fa-folder-open' : 'fa-folder';
		$display_style   = $is_current_year ? 'style="display:block;"' : 'style="display:none;"';
		$year_link_class = ( is_year() && ( (int) $year === $cur_year ) ) ? ' class="is-current-link"' : '';

		$output .= '<div class="collapsible-item' . esc_attr( $should_open ) . '">';
		$output .= '<div class="collapsible-header">';
		$output .= '<span class="collapsible-toggle"><i class="fa ' . esc_attr( $icon_class ) . '" aria-hidden="true"></i></span>';
		$output .= sprintf(
			'<a href="%s"%s>%d年 (%d)</a>',
			esc_url( get_year_link( $year ) ),
			$year_link_class,
			(int) $year,
			(int) $data['total']
		);
		$output .= '</div>';
		
		$output .= '<ul class="collapsible-child-list" ' . $display_style . '>';
		foreach ( $data['months'] as $month => $count ) {
			$is_this_month = is_month()
				&& ( (int) $year === $cur_year )
				&& ( (int) $month === $cur_month );
			$month_link_class = $is_this_month ? ' class="is-current-link"' : '';
			
			$output .= '<li>';
			$output .= '<span class="collapsible-icon-wrapper"><i class="fa fa-calendar" aria-hidden="true"></i></span>';
			$output .= sprintf(
				'<a href="%s"%s>%d月 (%d)</a>',
				esc_url( get_month_link( $year, $month ) ),
				$month_link_class,
				(int) $month,
				(int) $count
			);
			$output .= '</li>';
		}
		$output .= '</ul>';
		$output .= '</div>';
	}
	$output .= '</div>';

	return $output;
}

/**
 * 2. 共通のショートコード & ウィジェット用:カテゴリー生成
 */
if ( ! function_exists( 'build_custom_category_tree' ) ) {
	function build_custom_category_tree( $parent_id, $categories ) {
		$html = '';

		$children = array_filter(
			$categories,
			function ( $cat ) use ( $parent_id ) {
				return (int) $cat->category_parent === (int) $parent_id;
			}
		);

		if ( empty( $children ) ) {
			return '';
		}

		$current_cat_id = 0;
		if ( is_category() ) {
			$current_cat_id = get_queried_object_id();
		} elseif ( is_single() ) {
			$post_cats = get_the_category();
			if ( ! empty( $post_cats ) ) {
				$current_cat_id = (int) $post_cats[0]->term_id;
			}
		}

		$list_class = ( 0 === (int) $parent_id ) ? 'root-list' : 'collapsible-child-list';

		$has_active_child = false;
		foreach ( $children as $child_cat ) {
			$is_child_current = ( (int) $child_cat->term_id === (int) $current_cat_id );
			$is_child_ancestor = false;
			if ( $current_cat_id ) {
				$ancestors = get_ancestors( $current_cat_id, 'category' );
				$is_child_ancestor = in_array( (int) $child_cat->term_id, array_map( 'intval', $ancestors ), true );
			}
			if ( $is_child_current || $is_child_ancestor ) {
				$has_active_child = true;
				break;
			}
		}

		$is_parent_page = ( $current_cat_id && ( (int) $parent_id === (int) $current_cat_id ) );
		$display_style = ( 0 === (int) $parent_id || $has_active_child || $is_parent_page ) ? ' style="display:block;"' : ' style="display:none;"';
		$html .= '<ul class="' . esc_attr( $list_class ) . '"' . $display_style . '>';

		foreach ( $children as $cat ) {
			$cat_children = array_filter(
				$categories,
				function ( $c ) use ( $cat ) {
					return (int) $c->category_parent === (int) $cat->term_id;
				}
			);

			$has_children = ! empty( $cat_children );
			$is_current = ( (int) $cat->term_id === (int) $current_cat_id );
			$is_ancestor  = false;
			if ( $current_cat_id ) {
				$ancestors   = get_ancestors( $current_cat_id, 'category' );
				$is_ancestor = in_array( (int) $cat->term_id, array_map( 'intval', $ancestors ), true );
			}

			$classes = array( 'collapsible-item' );
			if ( $has_children ) {
				$classes[] = 'has-children';
			}
			if ( $is_current ) {
				$classes[] = 'is-current';
			}
			if ( $is_ancestor || $is_current ) {
				$classes[] = 'is-active-path';
			}

			$html .= '<li class="' . esc_attr( implode( ' ', $classes ) ) . '">';
			$html .= '<div class="collapsible-header">';
			
			if ( $has_children ) {
				$icon_class = ( $is_ancestor || $is_current ) ? 'fa-folder-open' : 'fa-folder';
				$html .= '<span class="collapsible-toggle"><i class="fa ' . esc_attr( $icon_class ) . '" aria-hidden="true"></i></span>';
			} else {
				$html .= '<span class="collapsible-icon-wrapper"><i class="fa fa-list" aria-hidden="true"></i></span>';
			}

			$html .= sprintf(
				'<a href="%s">%s (%d)</a>',
				esc_url( get_category_link( $cat->term_id ) ),
				esc_html( $cat->name ),
				(int) $cat->count
			);
			$html .= '</div>';

			if ( $has_children ) {
				$html .= build_custom_category_tree( $cat->term_id, $categories );
			}
			$html .= '</li>';
		}
		$html .= '</ul>';
		return $html;
	}
}

function render_custom_collapsible_categories() {
	$args       = array( 'hide_empty' => 1, 'hierarchical' => 1, 'pad_counts' => 1 );
	$categories = get_categories( $args );
	return '<div class="collapsible-list-container">' . build_custom_category_tree( 0, $categories ) . '</div>';
}

/**
 * 3. ショートコードの登録
 */
add_shortcode( 'custom_archive', 'render_custom_collapsible_archive' );
add_shortcode( 'custom_categories', 'render_custom_collapsible_categories' );

/**
 * 4. ウィジェットクラスの定義
 */
if ( ! class_exists( 'Custom_Collapsible_Archive_Widget' ) ) {
	class Custom_Collapsible_Archive_Widget extends WP_Widget {
		function __construct() {
			parent::__construct( 'custom_collapsible_archive', '折りたたみアーカイブ' );
		}
		public function widget( $args, $instance ) {
			echo $args['before_widget'];
			if ( ! empty( $instance['title'] ) ) {
				echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'];
			}
			echo render_custom_collapsible_archive();
			echo $args['after_widget'];
		}
		public function form( $instance ) {
			$title = ! empty( $instance['title'] ) ? $instance['title'] : 'アーカイブ';
			?>
			<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">タイトル:</label>
			<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
			</p>
			<?php
		}
		public function update( $new_instance, $old_instance ) {
			$instance = array();
			$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? sanitize_text_field( $new_instance['title'] ) : '';
			return $instance;
		}
	}
}

if ( ! class_exists( 'Custom_Collapsible_Category_Widget' ) ) {
	class Custom_Collapsible_Category_Widget extends WP_Widget {
		function __construct() {
			parent::__construct( 'custom_collapsible_category_widget', '折りたたみカテゴリー' );
		}
		public function widget( $args, $instance ) {
			echo $args['before_widget'];
			if ( ! empty( $instance['title'] ) ) {
				echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'];
			}
			echo render_custom_collapsible_categories();
			echo $args['after_widget'];
		}
		public function form( $instance ) {
			$title = ! empty( $instance['title'] ) ? $instance['title'] : 'カテゴリー';
			?>
			<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">タイトル:</label>
			<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
			</p>
			<?php
		}
		public function update( $new_instance, $old_instance ) {
			$instance = array();
			$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? sanitize_text_field( $new_instance['title'] ) : '';
			return $instance;
		}
	}
}

/**
 * 5. ウィジェットの登録
 */
function register_custom_collapsible_widgets() {
	register_widget( 'Custom_Collapsible_Archive_Widget' );
	register_widget( 'Custom_Collapsible_Category_Widget' );
}
add_action( 'widgets_init', 'register_custom_collapsible_widgets' );

JavaScript

/**
 * Title:       Collapsible List Toggle Script
 * Description: Controls the opening and closing of sidebar lists and swaps FontAwesome folder icons.
 * Version:     1.0.1
 * Author:      Hiroshi Sekiya
 * Author URI:  https://hironet.jp/blog/
 * License:     GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 */

( function() {
	'use strict';

	/**
	 * 折りたたみリスト制御スクリプト
	 * イベントデリゲーションを採用し、メモリ負荷を抑えた設計
	 */
	document.addEventListener( 'DOMContentLoaded', function() {

		// 2. クリックイベント(効率的なイベントデリゲーション)
		document.addEventListener( 'click', function( e ) {
			// クリックされた要素から最も近いトグルボタンを特定
			const toggle = e.target.closest( '.collapsible-toggle' );
			if ( ! toggle ) return;

			// トグルボタン自体にリンクが含まれる場合の誤動作を防止
			// (アクセシビリティ上の配慮:展開のみを目的とする)
			e.preventDefault();

			const item      = toggle.closest( '.collapsible-item' );
			const childList = item.querySelector( ':scope > .collapsible-child-list' );
			const icon      = toggle.querySelector( 'i' );

			if ( ! childList ) return;

			// クラスの切り替えと表示状態の同期
			const isOpen = item.classList.toggle( 'is-open' );
			childList.style.display = isOpen ? 'block' : 'none';

			// FontAwesome アイコンの動的な差し替え
			if ( isOpen ) {
				icon.classList.replace( 'fa-folder', 'fa-folder-open' );
			} else {
				icon.classList.replace( 'fa-folder-open', 'fa-folder' );
			}
		} );
	} );
} )();

CSS

※カスタマイズしやすいようにコメント特盛り仕様にしています。

/**
 * Title:       Collapsible List Styling (Blue Skin)
 * Description: Layout and styling for collapsible lists, featuring blue-themed highlighting (#8080ff).
 * Version:     1.0.1
 * Author:      Hiroshi Sekiya
 * Author URI:  https://hironet.jp/blog/
 * License:     GPLv2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 */

/**
 * =================================================================
 * WordPress 折りたたみリスト(アーカイブ・カテゴリー共通)
 * -----------------------------------------------------------------
 * このCSSは、サイドバーの「アーカイブ」と「カテゴリー」を青系スタイルに整えます。
 * =================================================================
 */

/* -----------------------------------------------------------------
 * 1. ベース・リセット設定
 * -----------------------------------------------------------------
 * テーマ(Swell/Cocoon等)が持っている「リストの点(・)」や
 * 勝手についてしまう余白を、!importantの力で徹底的にリセットします。
 */
.collapsible-list-container ul,
.collapsible-list-container ul li {
	list-style: none !important;      /* リストの「点」を消す(最重要) */
	margin: 0 !important;              /* 外側の余白をゼロに */
	padding: 0 !important;             /* 内側の余白をゼロに */
	text-indent: 0 !important;         /* 1行目の字下げを解除 */
}

/* -----------------------------------------------------------------
 * 2. 各アイテム(行)のレイアウト
 * -----------------------------------------------------------------
 * 1行ごとの高さや、アイコンと文字の並び方を整えます。
 */
.collapsible-list-container .collapsible-item {
	line-height: 1.8;                  /* 行間:1.8倍(クリックしやすさ重視) */
	margin-bottom: 2px !important;     /* 行と行の間のわずかな隙間 */
}

/* アイコンと文字を横一列に並べる魔法の箱(Flexbox) */
.collapsible-list-container .collapsible-header {
	display: flex;                     /* 横並びにする */
	align-items: center;               /* 上下の中央で揃える */
	gap: 5px;                          /* アイコンと文字の間の距離(ここを増やすと広がる) */
}

/* リンク文字の見た目 */
.collapsible-list-container .collapsible-header a {
	text-decoration: none;             /* リンクの下線を消す */
	color: #333;                       /* 文字の色:濃いグレー(本文に馴染む色) */
	transition: all 0.2s ease;         /* 変化をふわっとさせる(0.2秒) */
	display: inline-block;             /* 背景色を綺麗に塗るために必要 */
}

/* -----------------------------------------------------------------
 * 3. アイコン・トグル(開閉スイッチ)の設定
 * -----------------------------------------------------------------
 * フォルダやカレンダーのアイコンのサイズや色を決めます。
 */
.collapsible-list-container .collapsible-toggle,
.collapsible-list-container .collapsible-icon-wrapper {
	display: inline-flex;
	justify-content: center;           /* アイコンを枠の中央に */
	align-items: center;
	width: 22px;                       /* アイコンの専有幅(ここを揃えると縦の線が整う) */
	flex-shrink: 0;                    /* 文字が長くてもアイコンが潰れないように固定 */
	transition: color 0.2s ease;
}

/* クリックできる「フォルダ」の色(基本の青:#8080ff) */
.collapsible-list-container .collapsible-toggle {
	cursor: pointer;                   /* マウスを乗せると「指マーク」になる */
	color: #8080ff;                    /* ★全体のメインカラー */
}

/* 階層の末端にある「カレンダーやリスト」のアイコン色 */
.collapsible-list-container .collapsible-icon-wrapper {
	color: #999;                       /* 控えめなグレー */
}

/* -----------------------------------------------------------------
 * 4. 子階層(サブカテゴリー・月)のインデント
 * -----------------------------------------------------------------
 * 「親」の下にある「子」をどれくらい右にずらすか。
 */
.collapsible-list-container ul.collapsible-child-list {
	padding-left: 20px !important;     /* 左側の余白:20px分右にずらす(ツリー構造の肝) */
	margin-top: 2px !important;
	margin-bottom: 5px !important;
}

/* -----------------------------------------------------------------
 * 5. カレント(現在地)の強調表示
 * -----------------------------------------------------------------
 * いま見ているページがどこかを、読者に一目で伝える「案内板」の真髄。
 */
.collapsible-list-container .is-current-link,
.collapsible-list-container .is-current > .collapsible-header a {
	color: #ffffff !important;         /* 文字色:白(背景が濃いので白抜きに) */
	font-weight: bold !important;      /* 太字にして目立たせる */
	background-color: #8080ff;         /* ★背景色:メインの青(ここを変えると現在地の色が変わる) */
	padding: 1px 8px;                  /* 背景色の塗りつぶし範囲を広げる */
	border-radius: 4px;                /* 角丸:4px(少し丸めて柔らかい印象に) */
	box-shadow: 0 1px 3px rgba(128, 128, 255, 0.3); /* ほんの少し影をつけて浮かせる */
}

/* 現在地に関連するアイコンも、連動して青色に染める */
.collapsible-list-container .is-current > .collapsible-header .collapsible-toggle,
.collapsible-list-container .is-current > .collapsible-header .collapsible-icon-wrapper,
.collapsible-list-container .is-active-path > .collapsible-header .collapsible-toggle {
	color: #8080ff !important;
}

/* -----------------------------------------------------------------
 * 6. ホバー(マウスを乗せたとき)の演出
 * -----------------------------------------------------------------
 * 「ここは押せるよ」と読者に伝えるためのリアクション。
 */
.collapsible-list-container .collapsible-header:hover a {
	color: #8080ff;                    /* ホバー時に文字を青くする */
}

.collapsible-list-container .collapsible-header:hover .collapsible-toggle,
.collapsible-list-container .collapsible-header:hover .collapsible-icon-wrapper {
	color: #5a5aff;                    /* ホバー時にアイコンを少し濃い青にする */
}

コメント

タイトルとURLをコピーしました