了解です。
前回の階層ドロップダウンメニュー実装例を、さらに 実務向けの強化版 に改良します。
今回は Bootstrap 5 をベースに、多階層対応 + アクティブ強調 + アイコン対応 + ホバー展開 + アニメーション を組み込みます。
1. モデル(自己参照型 + アイコン + 表示順)
// Models/MenuItem.cs
using System.Collections.Generic;
namespace YourApp.Models
{
public class MenuItem
{
public int Id { get; set; }
public string Title { get; set; }
public string Url { get; set; }
public int? ParentId { get; set; }
public int DisplayOrder { get; set; } // 表示順
public List<MenuItem> Children { get; set; } = new();
public bool IsActive { get; set; }
public string IconClass { get; set; }
}
}
2. コントローラー(階層構築 + アクティブ判定 + 並び替え)
// Controllers/MenuController.cs
using Microsoft.AspNetCore.Mvc;
using YourApp.Models;
using System.Collections.Generic;
using System.Linq;
namespace YourApp.Controllers
{
public class MenuController : Controller
{
public IActionResult Index()
{
var flatMenu = new List<MenuItem>
{
new MenuItem { Id = 1, Title = "ホーム", Url = "/", IconClass = "fa fa-home", DisplayOrder = 1 },
new MenuItem { Id = 2, Title = "製品", Url = "/products", IconClass = "fa fa-box", DisplayOrder = 2 },
new MenuItem { Id = 3, Title = "サービス", Url = "/services", IconClass = "fa fa-cogs", DisplayOrder = 3 },
new MenuItem { Id = 4, Title = "製品A", Url = "/products/a", ParentId = 2, DisplayOrder = 1 },
new MenuItem { Id = 5, Title = "製品B", Url = "/products/b", ParentId = 2, DisplayOrder = 2 },
new MenuItem { Id = 6, Title = "サービスX", Url = "/services/x", ParentId = 3, DisplayOrder = 1 },
new MenuItem { Id = 7, Title = "サービスY", Url = "/services/y", ParentId = 3, DisplayOrder = 2 },
new MenuItem { Id = 8, Title = "製品B詳細", Url = "/products/b/detail", ParentId = 5, DisplayOrder = 1 }
};
var menuTree = BuildTree(flatMenu);
MarkActiveMenu(menuTree, HttpContext.Request.Path);
return View(menuTree.OrderBy(m => m.DisplayOrder).ToList());
}
private List<MenuItem> BuildTree(List<MenuItem> items)
{
var lookup = items.ToDictionary(x => x.Id);
var roots = new List<MenuItem>();
foreach (var item in items.OrderBy(i => i.DisplayOrder))
{
if (item.ParentId.HasValue && lookup.ContainsKey(item.ParentId.Value))
{
lookup[item.ParentId.Value].Children.Add(item);
lookup[item.ParentId.Value].Children = lookup[item.ParentId.Value].Children.OrderBy(c => c.DisplayOrder).ToList();
}
else
{
roots.Add(item);
}
}
return roots;
}
private bool MarkActiveMenu(List<MenuItem> menuItems, string currentPath)
{
bool found = false;
foreach (var item in menuItems)
{
if (item.Url?.Equals(currentPath, System.StringComparison.OrdinalIgnoreCase) == true)
{
item.IsActive = true;
found = true;
}
else if (item.Children.Any() && MarkActiveMenu(item.Children, currentPath))
{
item.IsActive = true;
found = true;
}
}
return found;
}
}
}
3. メインビュー(Bootstrap + 多階層CSS + ホバー展開 + アニメーション)
@model List<YourApp.Models.MenuItem>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.dropdown-submenu {
position: relative;
}
.dropdown-submenu > .dropdown-menu {
top: 0;
left: 100%;
margin-top: -0.125rem;
}
/* アクティブ強調 */
.nav-link.active, .dropdown-item.active {
background-color: #0d6efd;
color: #fff !important;
}
/* ホバー展開 */
.navbar-nav li:hover > .dropdown-menu {
display: block;
}
/* アニメーション */
.dropdown-menu {
display: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.navbar-nav li:hover > .dropdown-menu {
display: block;
opacity: 1;
}
</style>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<ul class="navbar-nav">
@foreach (var item in Model)
{
@Html.Partial("_MenuItemPartial", item)
}
</ul>
</div>
</nav>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
4. 再帰的部分ビュー(アイコン + アクティブ + 並び順)
@model YourApp.Models.MenuItem
@if (Model.Children.Any())
{
<li class="nav-item dropdown @(Model.ParentId != null ? "dropdown-submenu" : "")">
<a class="nav-link dropdown-toggle @(Model.IsActive ? "active" : "")"
href="@Model.Url"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false">
@if (!string.IsNullOrEmpty(Model.IconClass))
{
<i class="@Model.IconClass"></i>
}
@Model.Title
</a>
<ul class="dropdown-menu">
@foreach (var child in Model.Children.OrderBy(c => c.DisplayOrder))
{
@Html.Partial("_MenuItemPartial", child)
}
</ul>
</li>
}
else
{
<li>
<a class="dropdown-item @(Model.IsActive ? "active" : "")" href="@Model.Url">
@if (!string.IsNullOrEmpty(Model.IconClass))
{
<i class="@Model.IconClass"></i>
}
@Model.Title
</a>
</li>
}
改良点
- 表示順制御
DisplayOrder プロパティでメニューの並び順を管理。 - アイコン対応
Font Awesome を利用して視覚的にわかりやすく。 - ホバー展開 + アニメーション
マウスオーバー時にフェードインするようにCSS
Powered by Froala Editor