You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Components/Components/PageContentNavigation/PageContentNavigation.razor.cs

173 lines
5.8 KiB

// Copyright (c) MudBlazor 2021
// MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Connected.Utilities;
using Microsoft.AspNetCore.Components;
namespace Connected.Components;
public partial class PageContentNavigation : IAsyncDisposable
{
private List<PageContentSection> _sections = new();
private IScrollSpy _scrollSpy;
[Inject] IScrollSpyFactory ScrollSpyFactory { get; set; }
/// <summary>
/// The displayed section within the MudPageContentNavigation
/// </summary>
public IEnumerable<PageContentSection> Sections => _sections.AsEnumerable();
/// <summary>
/// The currently active session. null if there is no section selected
/// </summary>
public PageContentSection ActiveSection => _sections.FirstOrDefault(x => x.IsActive == true);
/// <summary>
/// The text displayed about the section links. Defaults to "Conents"
/// </summary>
[Parameter] public string Headline { get; set; } = "Contents";
/// <summary>
/// The css selector used to identifify the HTML elements that should be observed for viewport changes
/// </summary>
[Parameter] public string SectionClassSelector { get; set; } = string.Empty;
/// <summary>
/// If there are mutliple levels, this can specified to make a mapping between a level class likw "second-level" and the level in the hierarchy
/// </summary>
[Parameter] public IDictionary<string, int> HierarchyMapper { get; set; } = new Dictionary<string, int>();
/// <summary>
/// If there are multiple levels, this property controls they visibility of them.
/// </summary>
[Parameter] public ContentNavigationExpandBehaviour ExpandBehaviour { get; set; } = ContentNavigationExpandBehaviour.Always;
/// <summary>
/// If this option is true the first added section will become active when there is no other indication of an active session. Default value is false
/// </summary>
[Parameter] public bool ActivateFirstSectionAsDefault { get; set; } = false;
private Task OnNavLinkClick(string id)
{
SelectActiveSection(id);
return _scrollSpy.ScrollToSection(id);
}
private void ScrollSpy_ScrollSectionSectionCentered(object sender, ScrollSectionCenteredEventArgs e) =>
SelectActiveSection(e.Id);
private void SelectActiveSection(string id)
{
if (string.IsNullOrEmpty(id))
{
return;
}
var activelink = _sections.FirstOrDefault(x => x.Id == id);
if (activelink == null)
{
return;
}
_sections.ToList().ForEach(item => item.Deactive());
activelink.Activate();
StateHasChanged();
}
private string GetNavLinkClass(PageContentSection section) => new CssBuilder("page-content-navigation-navlink")
.AddClass("active", section.IsActive)
.AddClass($"navigation-level-{section.Level}")
.Build();
private string GetPanelClass() => new CssBuilder("page-content-navigation").AddClass(Class).Build();
/// <summary>
/// Scrolls to a section based on the fragment of the uri. If there is no fragment, no scroll will occured
/// </summary>
/// <param name="uri">The uri containing the fragment to scroll</param>
/// <returns>A task that completes when the viewport has scrolled</returns>
public Task ScrollToSection(Uri uri) => _scrollSpy.ScrollToSection(uri);
/// <summary>
/// Add a section to the content navigation
/// </summary>
/// <param name="sectionName">name of the section will be displayed in the navigation</param>
/// <param name="sectionId">id of the section. It will be appending to the current url, if the section becomes active</param>
/// <param name="forceUpdate">If true, StateHasChanged is called, forcing a rerender of the component</param>
public void AddSection(string sectionName, string sectionId, bool forceUpdate) => AddSection(new(sectionName, sectionId), forceUpdate);
private Dictionary<PageContentSection, PageContentSection> _parentMapper = new();
/// <summary>
/// Add a section to the content navigation
/// </summary>
/// <param name="section">The section that needs to be added</param>
/// <param name="forceUpdate">If true, StateHasChanged is called, forcing a rerender of the component</param>
public void AddSection(PageContentSection section, bool forceUpdate)
{
_sections.Add(section);
int diffRootLevel = 1_000_000;
int counter = 0;
foreach (var item in _sections.Where(x => x.Parent == null))
{
item.SetLevelStructure(counter, diffRootLevel);
counter += diffRootLevel;
}
if (section.Id == _scrollSpy.CenteredSection)
{
section.Activate();
}
else if (_sections.Count == 1 && ActivateFirstSectionAsDefault == true)
{
section.Activate();
_scrollSpy.SetSectionAsActive(section.Id).AndForget();
}
if (forceUpdate == true)
{
StateHasChanged();
}
}
/// <summary>
/// Rerender the component
/// </summary>
public void Update() => StateHasChanged();
protected override void OnInitialized()
{
_scrollSpy = ScrollSpyFactory.Create();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_scrollSpy.ScrollSectionSectionCentered += ScrollSpy_ScrollSectionSectionCentered;
if (string.IsNullOrEmpty(SectionClassSelector) == false)
{
await _scrollSpy.StartSpying(SectionClassSelector);
}
SelectActiveSection(_scrollSpy.CenteredSection);
}
}
public ValueTask DisposeAsync()
{
if (_scrollSpy == null) { return ValueTask.CompletedTask; }
_scrollSpy.ScrollSectionSectionCentered -= ScrollSpy_ScrollSectionSectionCentered;
return _scrollSpy.DisposeAsync();
}
}