// 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 _sections = new(); private IScrollSpy _scrollSpy; [Inject] IScrollSpyFactory ScrollSpyFactory { get; set; } /// /// The displayed section within the MudPageContentNavigation /// public IEnumerable Sections => _sections.AsEnumerable(); /// /// The currently active session. null if there is no section selected /// public PageContentSection ActiveSection => _sections.FirstOrDefault(x => x.IsActive == true); /// /// The text displayed about the section links. Defaults to "Conents" /// [Parameter] public string Headline { get; set; } = "Contents"; /// /// The css selector used to identifify the HTML elements that should be observed for viewport changes /// [Parameter] public string SectionClassSelector { get; set; } = string.Empty; /// /// 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 /// [Parameter] public IDictionary HierarchyMapper { get; set; } = new Dictionary(); /// /// If there are multiple levels, this property controls they visibility of them. /// [Parameter] public ContentNavigationExpandBehaviour ExpandBehaviour { get; set; } = ContentNavigationExpandBehaviour.Always; /// /// 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 /// [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(); /// /// Scrolls to a section based on the fragment of the uri. If there is no fragment, no scroll will occured /// /// The uri containing the fragment to scroll /// A task that completes when the viewport has scrolled public Task ScrollToSection(Uri uri) => _scrollSpy.ScrollToSection(uri); /// /// Add a section to the content navigation /// /// name of the section will be displayed in the navigation /// id of the section. It will be appending to the current url, if the section becomes active /// If true, StateHasChanged is called, forcing a rerender of the component public void AddSection(string sectionName, string sectionId, bool forceUpdate) => AddSection(new(sectionName, sectionId), forceUpdate); private Dictionary _parentMapper = new(); /// /// Add a section to the content navigation /// /// The section that needs to be added /// If true, StateHasChanged is called, forcing a rerender of the component 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(); } } /// /// Rerender the component /// 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(); } }