Init
This commit is contained in:
409
Prefab.Web.Client/Layout/NavMenu.razor
Normal file
409
Prefab.Web.Client/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,409 @@
|
||||
@rendermode InteractiveAuto
|
||||
@using System.Linq
|
||||
@using Prefab.Web.Client.Models.Shared
|
||||
@using Prefab.Web.Client.Services
|
||||
@implements IDisposable
|
||||
|
||||
<nav class="header__nav main-nav">
|
||||
<ul class="main-nav__list">
|
||||
<li class="main-nav__item ">
|
||||
<NavLink class="main-nav__link" href="/" Match="NavLinkMatch.All">
|
||||
Home
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li class="@ProductsMenuItemClass"
|
||||
@onmouseenter="HandleMouseEnter"
|
||||
@onmouseleave="HandleMouseLeave">
|
||||
<span class="main-nav__trigger"
|
||||
@onclick="OnProductsMenuLinkClick"
|
||||
@onclick:preventDefault>
|
||||
<NavLink id="productsMenuLink"
|
||||
class="main-nav__link"
|
||||
href="/catalog"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="@(_productsMenuOpen.ToString().ToLowerInvariant())"
|
||||
aria-controls="productsMenuPanel">
|
||||
<span>Products</span>
|
||||
<svg class="main-nav__link-arrow" width="9px" height="6px">
|
||||
<use xlink:href="images/sprite.svg#arrow-down-9x6"></use>
|
||||
</svg>
|
||||
</NavLink>
|
||||
</span>
|
||||
<div id="productsMenuPanel"
|
||||
class="main-nav__sub-megamenu"
|
||||
role="region"
|
||||
aria-hidden="@((!_productsMenuOpen).ToString().ToLowerInvariant())"
|
||||
tabindex="-1"
|
||||
@ref="_panel">
|
||||
@if (!_menuLoaded)
|
||||
{
|
||||
<div class="megamenu" role="menu" aria-live="polite">
|
||||
<div class="row g-custom-30">
|
||||
@for (var i = 0; i < 4; i++)
|
||||
{
|
||||
<div class="col-3">
|
||||
<TelerikSkeleton Width="100%" Height="20px" Class="mb-2" />
|
||||
<ul class="megamenu__links megamenu__links--sub">
|
||||
@for (var j = 0; j < 4; j++)
|
||||
{
|
||||
<li class="mb-2">
|
||||
<TelerikSkeleton Height="16px" Width="80%" />
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_menu is null || _menu.Columns.Count == 0)
|
||||
{
|
||||
<div class="mega-menu__empty" role="status">@(_errorMessage ?? "Navigation unavailable.")</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="megamenu" role="menu" aria-live="polite">
|
||||
<div class="row g-custom-30">
|
||||
@foreach (var column in _menu.Columns.Take(4))
|
||||
{
|
||||
<div class="col-3">
|
||||
<ul class="megamenu__links megamenu__links--root">
|
||||
<li>
|
||||
<NavLink href="@column.Root.Url">@column.Root.Name</NavLink>
|
||||
@if (column.Items is { Count: > 0 })
|
||||
{
|
||||
<ul class="megamenu__links megamenu__links--sub">
|
||||
@foreach (var item in column.Items)
|
||||
{
|
||||
<li><NavLink href="@item.Url">@item.Name</NavLink></li>
|
||||
}
|
||||
<li class="megamenu__see-all">
|
||||
<NavLink href="@column.SeeAllUrl">See all @column.Root.Name</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="@ProductsFlyoutItemClass"
|
||||
@onmouseenter="HandleFlyoutMouseEnter"
|
||||
@onmouseleave="HandleFlyoutMouseLeave">
|
||||
<span class="main-nav__trigger"
|
||||
@onclick="OnProductsFlyoutLinkClick"
|
||||
@onclick:preventDefault>
|
||||
<NavLink class="main-nav__link"
|
||||
href="/catalog"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="@(_productsFlyoutOpen.ToString().ToLowerInvariant())"
|
||||
aria-controls="productsFlyoutMenu">
|
||||
<span>Products</span>
|
||||
<svg class="main-nav__link-arrow" width="9px" height="6px">
|
||||
<use xlink:href="images/sprite.svg#arrow-down-9x6"></use>
|
||||
</svg>
|
||||
</NavLink>
|
||||
</span>
|
||||
<div id="productsFlyoutMenu"
|
||||
class="main-nav__sub-menu"
|
||||
role="region"
|
||||
aria-live="polite">
|
||||
@if (!_menuLoaded)
|
||||
{
|
||||
<div class="p-3" role="status">
|
||||
@for (var i = 0; i < 4; i++)
|
||||
{
|
||||
<div class="mb-2">
|
||||
<TelerikSkeleton Width="180px" Height="16px" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (_menu is null || _menu.Columns.Count == 0)
|
||||
{
|
||||
<div class="menu__empty p-3" role="status">@(_errorMessage ?? "Navigation unavailable.")</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="menu">
|
||||
@foreach (var column in GetFlyoutColumns())
|
||||
{
|
||||
var hasChildren = column.Items is { Count: > 0 };
|
||||
var hasSeeAll = !string.IsNullOrWhiteSpace(column.SeeAllUrl);
|
||||
<li class="menu__item">
|
||||
<NavLink class="menu__link"
|
||||
href="@column.Root.Url">
|
||||
@column.Root.Name
|
||||
@if (hasChildren)
|
||||
{
|
||||
<svg class="menu__link-arrow" width="6px" height="9px">
|
||||
<use xlink:href="images/sprite.svg#arrow-right-6x9"></use>
|
||||
</svg>
|
||||
}
|
||||
</NavLink>
|
||||
|
||||
@if (hasChildren)
|
||||
{
|
||||
<div class="menu__sub">
|
||||
<ul class="menu">
|
||||
@if (column.Items is { Count: > 0 })
|
||||
{
|
||||
@foreach (var item in column.Items)
|
||||
{
|
||||
<li class="menu__item">
|
||||
<NavLink class="menu__link" href="@item.Url">@item.Name</NavLink>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@if (hasSeeAll)
|
||||
{
|
||||
<li class="menu__item">
|
||||
<NavLink class="menu__link menu__link--see-all" href="@column.SeeAllUrl">
|
||||
See all @column.Root.Name
|
||||
</NavLink>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="main-nav__item">
|
||||
<NavLink class="main-nav__link" href="Contact">
|
||||
Contact Us
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@code {
|
||||
private const string ProductsMenuCacheKey = "nav-menu/products";
|
||||
|
||||
[Inject]
|
||||
private NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
private INavMenuService NavMenuService { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
private IServiceProvider ServiceProvider { get; set; } = default!;
|
||||
|
||||
private bool _productsMenuOpen;
|
||||
private bool _productsFlyoutOpen;
|
||||
private ElementReference _panel;
|
||||
|
||||
private NavMenuModel? _menu;
|
||||
private bool _menuLoaded;
|
||||
private string? _errorMessage;
|
||||
private IDisposable? _persistRegistration;
|
||||
private CancellationTokenSource? _loadCancellationSource;
|
||||
|
||||
private string ProductsMenuItemClass =>
|
||||
$"main-nav__item main-nav__item--with--menu {(_productsMenuOpen ? "main-nav__item--open" : string.Empty)}";
|
||||
|
||||
private string ProductsFlyoutItemClass =>
|
||||
$"main-nav__item main-nav__item--with--menu {(_productsFlyoutOpen ? "main-nav__item--open" : string.Empty)}";
|
||||
|
||||
private PersistentComponentState? AppState => ServiceProvider.GetService<PersistentComponentState>();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
NavigationManager.LocationChanged += OnLocationChanged;
|
||||
|
||||
var appState = AppState;
|
||||
if (appState is not null)
|
||||
{
|
||||
_persistRegistration = appState.RegisterOnPersisting(PersistAsync);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender || _menuLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (AppState is not null && AppState.TryTakeFromJson(ProductsMenuCacheKey, out NavMenuModel? cached) && cached is not null)
|
||||
{
|
||||
_menu = cached;
|
||||
}
|
||||
else
|
||||
{
|
||||
await LoadAsync(forceReload: true);
|
||||
}
|
||||
|
||||
_menuLoaded = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task ToggleProductsMenuAsync()
|
||||
{
|
||||
if (_productsMenuOpen)
|
||||
{
|
||||
await CloseAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await OpenAsync(focusPanel: true);
|
||||
}
|
||||
|
||||
private Task OnProductsMenuLinkClick(MouseEventArgs _) => ToggleProductsMenuAsync();
|
||||
|
||||
private Task HandleMouseEnter() => OpenAsync(focusPanel: false);
|
||||
|
||||
private Task HandleMouseLeave() => CloseAsync();
|
||||
|
||||
private async Task HandleFlyoutMouseEnter()
|
||||
{
|
||||
await LoadAsync(forceReload: false);
|
||||
_productsFlyoutOpen = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private Task HandleFlyoutMouseLeave()
|
||||
{
|
||||
if (!_productsFlyoutOpen)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_productsFlyoutOpen = false;
|
||||
StateHasChanged();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnProductsFlyoutLinkClick(MouseEventArgs _)
|
||||
{
|
||||
if (_productsFlyoutOpen)
|
||||
{
|
||||
_productsFlyoutOpen = false;
|
||||
StateHasChanged();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return HandleFlyoutMouseEnter();
|
||||
}
|
||||
|
||||
private async Task OpenAsync(bool focusPanel)
|
||||
{
|
||||
if (_productsMenuOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadAsync(forceReload: false);
|
||||
|
||||
_productsMenuOpen = true;
|
||||
StateHasChanged();
|
||||
|
||||
if (focusPanel)
|
||||
{
|
||||
await Task.Yield();
|
||||
await _panel.FocusAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private Task CloseAsync()
|
||||
{
|
||||
if (!_productsMenuOpen)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_productsMenuOpen = false;
|
||||
StateHasChanged();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
_ = InvokeAsync(async () =>
|
||||
{
|
||||
await CloseAsync();
|
||||
_productsFlyoutOpen = false;
|
||||
await LoadAsync(forceReload: true);
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LoadAsync(bool forceReload)
|
||||
{
|
||||
if (!forceReload && _menu is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_loadCancellationSource?.Cancel();
|
||||
_loadCancellationSource?.Dispose();
|
||||
_loadCancellationSource = new CancellationTokenSource();
|
||||
var token = _loadCancellationSource.Token;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await NavMenuService.Get(2, token);
|
||||
if (result.IsSuccess && result.Value is { } model)
|
||||
{
|
||||
_menu = model;
|
||||
_errorMessage = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_menu = null;
|
||||
_errorMessage = result.Problem?.Detail ?? "Navigation unavailable.";
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Swallow expected cancellation when a new request supersedes the current one.
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_menu = null;
|
||||
_errorMessage = exception.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private Task PersistAsync()
|
||||
{
|
||||
var appState = AppState;
|
||||
if (_menu is not null && appState is not null)
|
||||
{
|
||||
appState.PersistAsJson(ProductsMenuCacheKey, _menu);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
NavigationManager.LocationChanged -= OnLocationChanged;
|
||||
_persistRegistration?.Dispose();
|
||||
_loadCancellationSource?.Cancel();
|
||||
_loadCancellationSource?.Dispose();
|
||||
}
|
||||
|
||||
private IEnumerable<NavMenuColumnModel> GetFlyoutColumns()
|
||||
{
|
||||
if (_menu is null || _menu.Columns.Count == 0)
|
||||
{
|
||||
return Array.Empty<NavMenuColumnModel>();
|
||||
}
|
||||
|
||||
return _menu.Columns;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user