410 lines
10 KiB
Plaintext
410 lines
10 KiB
Plaintext
@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;
|
|
}
|
|
}
|