2019-12-10 15:03:50 +08:00
|
|
|
|
using System;
|
2020-04-23 17:13:56 +08:00
|
|
|
|
using System.Collections.Generic;
|
2020-04-24 18:32:50 +08:00
|
|
|
|
using System.Globalization;
|
2020-05-10 15:42:02 +08:00
|
|
|
|
using System.Text;
|
2019-12-10 15:03:50 +08:00
|
|
|
|
using Microsoft.AspNetCore.Components;
|
|
|
|
|
using Microsoft.AspNetCore.Components.Rendering;
|
|
|
|
|
using Microsoft.AspNetCore.Components.Routing;
|
|
|
|
|
|
2020-05-29 00:33:49 +08:00
|
|
|
|
namespace AntDesign
|
2019-12-10 15:03:50 +08:00
|
|
|
|
{
|
2020-05-18 14:46:42 +08:00
|
|
|
|
public class MenuLink : AntDomComponentBase
|
2019-12-10 15:03:50 +08:00
|
|
|
|
{
|
|
|
|
|
private const string DefaultActiveClass = "active";
|
|
|
|
|
private bool _isActive;
|
2020-04-24 18:32:50 +08:00
|
|
|
|
private string _hrefAbsolute;
|
2019-12-10 15:03:50 +08:00
|
|
|
|
private string _class;
|
2019-12-16 15:33:29 +08:00
|
|
|
|
internal string Href => _hrefAbsolute;
|
|
|
|
|
|
2019-12-10 15:03:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the CSS class name applied to the NavLink when the
|
|
|
|
|
/// current route matches the NavLink href.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[Parameter]
|
|
|
|
|
public string ActiveClass { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the computed CSS class based on whether or not the link is active.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected string CssClass { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets the child content of the component.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[Parameter]
|
|
|
|
|
public RenderFragment ChildContent { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets or sets a value representing the URL matching behavior.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[Parameter]
|
|
|
|
|
public NavLinkMatch Match { get; set; }
|
|
|
|
|
|
2020-04-23 17:13:56 +08:00
|
|
|
|
[Parameter(CaptureUnmatchedValues = true)]
|
|
|
|
|
public Dictionary<string, object> Attributes { get; set; }
|
|
|
|
|
|
2019-12-10 15:03:50 +08:00
|
|
|
|
[CascadingParameter]
|
2020-05-18 14:46:42 +08:00
|
|
|
|
public MenuItem MenuItem { get; set; }
|
2019-12-10 15:03:50 +08:00
|
|
|
|
|
2020-05-10 15:42:02 +08:00
|
|
|
|
[CascadingParameter]
|
2020-05-18 14:46:42 +08:00
|
|
|
|
public Menu Menu { get; set; }
|
2020-05-10 15:42:02 +08:00
|
|
|
|
|
2019-12-16 15:33:29 +08:00
|
|
|
|
[CascadingParameter]
|
2020-05-29 12:55:15 +08:00
|
|
|
|
public Button Button { get; set; }
|
2019-12-16 15:33:29 +08:00
|
|
|
|
|
2019-12-10 15:03:50 +08:00
|
|
|
|
[Inject] private NavigationManager NavigationManger { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
protected override void OnInitialized()
|
|
|
|
|
{
|
|
|
|
|
// We'll consider re-rendering on each location change
|
|
|
|
|
NavigationManger.LocationChanged += OnLocationChanged;
|
2020-05-10 15:42:02 +08:00
|
|
|
|
//if (Button != null)
|
|
|
|
|
//{
|
|
|
|
|
// Button.Link = this;
|
|
|
|
|
//}
|
2019-12-10 15:03:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
protected override void OnParametersSet()
|
|
|
|
|
{
|
|
|
|
|
// Update computed state
|
2020-04-24 18:32:50 +08:00
|
|
|
|
string href = (string)null;
|
|
|
|
|
if (Attributes != null && Attributes.TryGetValue("href", out object obj))
|
2019-12-10 15:03:50 +08:00
|
|
|
|
{
|
2020-04-24 18:32:50 +08:00
|
|
|
|
href = Convert.ToString(obj, CultureInfo.CurrentCulture);
|
2019-12-10 15:03:50 +08:00
|
|
|
|
}
|
|
|
|
|
_hrefAbsolute = href == null ? null : NavigationManger.ToAbsoluteUri(href).AbsoluteUri;
|
|
|
|
|
_isActive = ShouldMatch(NavigationManger.Uri);
|
|
|
|
|
_class = (string)null;
|
|
|
|
|
if (Attributes != null && Attributes.TryGetValue("class", out obj))
|
|
|
|
|
{
|
2020-04-24 18:32:50 +08:00
|
|
|
|
_class = Convert.ToString(obj, CultureInfo.CurrentCulture);
|
2019-12-10 15:03:50 +08:00
|
|
|
|
}
|
|
|
|
|
UpdateCssClass();
|
2020-05-18 14:46:42 +08:00
|
|
|
|
if (MenuItem != null && _isActive && !MenuItem.IsSelected)
|
2020-05-10 15:42:02 +08:00
|
|
|
|
{
|
|
|
|
|
Menu?.SelectItem(MenuItem);
|
|
|
|
|
}
|
2019-12-10 15:03:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2020-04-24 18:32:50 +08:00
|
|
|
|
protected override void Dispose(bool disposing)
|
2019-12-10 15:03:50 +08:00
|
|
|
|
{
|
|
|
|
|
// To avoid leaking memory, it's important to detach any event handlers in Dispose()
|
|
|
|
|
NavigationManger.LocationChanged -= OnLocationChanged;
|
2020-04-24 18:32:50 +08:00
|
|
|
|
base.Dispose(disposing);
|
2019-12-10 15:03:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateCssClass()
|
|
|
|
|
{
|
|
|
|
|
CssClass = _isActive ? CombineWithSpace(_class, ActiveClass ?? DefaultActiveClass) : _class;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
|
|
|
|
|
{
|
|
|
|
|
// We could just re-render always, but for this component we know the
|
|
|
|
|
// only relevant state change is to the _isActive property.
|
2020-04-24 18:32:50 +08:00
|
|
|
|
bool shouldBeActiveNow = ShouldMatch(args.Location);
|
2019-12-10 15:03:50 +08:00
|
|
|
|
if (shouldBeActiveNow != _isActive)
|
|
|
|
|
{
|
|
|
|
|
_isActive = shouldBeActiveNow;
|
|
|
|
|
UpdateCssClass();
|
2020-05-18 14:46:42 +08:00
|
|
|
|
if (MenuItem != null)
|
2020-05-10 15:42:02 +08:00
|
|
|
|
{
|
2020-05-18 14:46:42 +08:00
|
|
|
|
if (_isActive)
|
|
|
|
|
{
|
|
|
|
|
MenuItem.Select();
|
|
|
|
|
Menu.SelectItem(MenuItem);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
MenuItem.Deselect();
|
|
|
|
|
}
|
2020-05-10 15:42:02 +08:00
|
|
|
|
}
|
2020-05-18 14:46:42 +08:00
|
|
|
|
|
2019-12-10 15:03:50 +08:00
|
|
|
|
StateHasChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool ShouldMatch(string currentUriAbsolute)
|
|
|
|
|
{
|
|
|
|
|
if (EqualsHrefExactlyOrIfTrailingSlashAdded(currentUriAbsolute))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (Match == NavLinkMatch.Prefix
|
2020-05-10 15:42:02 +08:00
|
|
|
|
&& IsStrictlyPrefixWithSeparator(currentUriAbsolute, _hrefAbsolute))
|
2019-12-10 15:03:50 +08:00
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool EqualsHrefExactlyOrIfTrailingSlashAdded(string currentUriAbsolute)
|
|
|
|
|
{
|
2020-05-18 14:46:42 +08:00
|
|
|
|
if (string.Equals(currentUriAbsolute, _hrefAbsolute, StringComparison.OrdinalIgnoreCase))
|
2019-12-10 15:03:50 +08:00
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (currentUriAbsolute.Length == _hrefAbsolute.Length - 1)
|
|
|
|
|
{
|
|
|
|
|
// Special case: highlight links to http://host/path/ even if you're
|
|
|
|
|
// at http://host/path (with no trailing slash)
|
|
|
|
|
//
|
|
|
|
|
// This is because the router accepts an absolute URI value of "same
|
|
|
|
|
// as base URI but without trailing slash" as equivalent to "base URI",
|
|
|
|
|
// which in turn is because it's common for servers to return the same page
|
|
|
|
|
// for http://host/vdir as they do for host://host/vdir/ as it's no
|
|
|
|
|
// good to display a blank page in that case.
|
2020-03-30 14:33:39 +08:00
|
|
|
|
if (_hrefAbsolute[^1] == '/'
|
2020-05-18 14:46:42 +08:00
|
|
|
|
&& _hrefAbsolute.StartsWith(currentUriAbsolute, StringComparison.OrdinalIgnoreCase))
|
2019-12-10 15:03:50 +08:00
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
|
|
|
|
{
|
2020-04-24 18:32:50 +08:00
|
|
|
|
if (builder != null)
|
|
|
|
|
{
|
|
|
|
|
builder.OpenElement(0, "a");
|
|
|
|
|
builder.AddMultipleAttributes(1, Attributes);
|
|
|
|
|
builder.AddAttribute(2, "class", CssClass);
|
|
|
|
|
builder.AddContent(3, ChildContent);
|
|
|
|
|
builder.CloseElement();
|
|
|
|
|
}
|
2019-12-10 15:03:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-24 18:32:50 +08:00
|
|
|
|
private static string CombineWithSpace(string str1, string str2)
|
2020-05-10 15:42:02 +08:00
|
|
|
|
=> str1 == null ? str2
|
|
|
|
|
: (str2 == null ? str1 : $"{str1} {str2}");
|
2019-12-10 15:03:50 +08:00
|
|
|
|
|
|
|
|
|
private static bool IsStrictlyPrefixWithSeparator(string value, string prefix)
|
|
|
|
|
{
|
2020-04-24 18:32:50 +08:00
|
|
|
|
int prefixLength = prefix.Length;
|
2019-12-10 15:03:50 +08:00
|
|
|
|
if (value.Length > prefixLength)
|
|
|
|
|
{
|
2020-05-18 14:46:42 +08:00
|
|
|
|
return value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
2020-05-10 15:42:02 +08:00
|
|
|
|
&& (
|
|
|
|
|
// Only match when there's a separator character either at the end of the
|
|
|
|
|
// prefix or right after it.
|
|
|
|
|
// Example: "/abc" is treated as a prefix of "/abc/def" but not "/abcdef"
|
|
|
|
|
// Example: "/abc/" is treated as a prefix of "/abc/def" but not "/abcdef"
|
|
|
|
|
prefixLength == 0
|
|
|
|
|
|| !char.IsLetterOrDigit(prefix[prefixLength - 1])
|
|
|
|
|
|| !char.IsLetterOrDigit(value[prefixLength])
|
|
|
|
|
);
|
2019-12-10 15:03:50 +08:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-23 17:13:56 +08:00
|
|
|
|
}
|