forked from ant-design-blazor/ant-design-blazor
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add anchor component (ant-design-blazor#281)
* feat(module: anchor): add component anchor * feat(module: anchor): add component anchor * feat(module: anchor): implement OnScroll * feat(module: anchor): activate anchorlink * feat(module: anchor): implement basic demo * feat(module: anchor): implement static demo * feat(module: anchor): add OnClick & OnChange * feat(module: anchor): implement demos * feat(module: anchor): move IAnchor to folder anchor * fix: click hash link * feat(module: anchor): improve cascading value & fix active anchor highlight * feat(module: anchor): cascading root/parent * fix(module: anchor): highlight active anchor link * fix: remove duplicated cascading value Co-authored-by: ElderJames <[email protected]>
- Loading branch information
1 parent
09e5f4e
commit 800d384
Showing
23 changed files
with
657 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
@namespace AntDesign | ||
@inherits AntDomComponentBase | ||
|
||
<div class="ant-anchor-wrapper" style="max-height: 100vh;" id="@Id" @ref="@_self"> | ||
<div class="ant-anchor"> | ||
<div class="ant-anchor-ink" @ref="@_ink"> | ||
<span class="@_ballClass" style="@_ballStyle"> | ||
</span> | ||
</div> | ||
<CascadingValue Value="this" Name="Root"> | ||
<CascadingValue Value="this" Name="Parent"> | ||
@ChildContent | ||
</CascadingValue> | ||
</CascadingValue> | ||
</div> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using System.Text.Json; | ||
using AntDesign.JsInterop; | ||
using Microsoft.AspNetCore.Components; | ||
using System.Diagnostics; | ||
using Microsoft.AspNetCore.Components.Web; | ||
|
||
namespace AntDesign | ||
{ | ||
public partial class Anchor : AntDomComponentBase, IAnchor | ||
{ | ||
private string _ballClass = "ant-anchor-ink-ball"; | ||
private string _ballStyle = string.Empty; | ||
private ElementReference _self; | ||
private ElementReference _ink; | ||
private DomRect _selfDom; | ||
private AnchorLink _activeLink; | ||
private AnchorLink _lastActiveLink; | ||
private Dictionary<string, decimal> _linkTops; | ||
private List<AnchorLink> _flatLinks; | ||
private List<AnchorLink> _links = new List<AnchorLink>(); | ||
|
||
[Inject] | ||
private DomEventService DomEventService { get; set; } | ||
|
||
#region Parameters | ||
|
||
[Parameter] | ||
public RenderFragment ChildContent { get; set; } | ||
|
||
/// <summary> | ||
/// Fixed mode of Anchor | ||
/// </summary> | ||
[Parameter] | ||
public bool Affix { get; set; } = true; | ||
|
||
/// <summary> | ||
/// Bounding distance of anchor area | ||
/// </summary> | ||
[Parameter] | ||
public int Bounds { get; set; } = 5; | ||
|
||
/// <summary> | ||
/// Scrolling container | ||
/// </summary> | ||
[Parameter] | ||
public Func<string> GetContainer { get; set; } = () => "window"; | ||
|
||
/// <summary> | ||
/// Pixels to offset from bottom when calculating position of scroll | ||
/// </summary> | ||
[Parameter] | ||
public int? OffsetBottom { get; set; } | ||
|
||
/// <summary> | ||
/// Pixels to offset from top when calculating position of scroll | ||
/// </summary> | ||
[Parameter] | ||
public int? OffsetTop { get; set; } = 0; | ||
|
||
/// <summary> | ||
/// Whether show ink-balls in Fixed mode | ||
/// </summary> | ||
[Parameter] | ||
public bool ShowInkInFixed { get; set; } = false; | ||
|
||
/// <summary> | ||
/// set the handler to handle click event | ||
/// </summary> | ||
[Parameter] | ||
public EventCallback<Tuple<MouseEventArgs, AnchorLink>> OnClick { get; set; } | ||
|
||
/// <summary> | ||
/// Customize the anchor highlight | ||
/// </summary> | ||
[Parameter] | ||
public Func<string> GetCurrentAnchor { get; set; } | ||
|
||
/// <summary> | ||
/// Anchor scroll offset, default as <see cref="OffsetTop"/> | ||
/// </summary> | ||
[Parameter] | ||
public int? TargetOffset { get; set; } | ||
|
||
[Parameter] | ||
public EventCallback<string> OnChange { get; set; } | ||
|
||
#endregion Parameters | ||
|
||
protected override void OnInitialized() | ||
{ | ||
base.OnInitialized(); | ||
|
||
if (GetCurrentAnchor is null) | ||
{ | ||
DomEventService.AddEventListener("window", "scroll", OnScroll); | ||
} | ||
} | ||
|
||
protected override async Task OnFirstAfterRenderAsync() | ||
{ | ||
_selfDom = await JsInvokeAsync<DomRect>(JSInteropConstants.getBoundingClientRect, _ink); | ||
_linkTops = new Dictionary<string, decimal>(); | ||
_flatLinks = FlatChildren(); | ||
foreach (var link in _flatLinks) | ||
{ | ||
_linkTops[link.Href] = 1; | ||
} | ||
|
||
if (GetCurrentAnchor != null) | ||
{ | ||
AnchorLink link = _flatLinks.SingleOrDefault(l => l.Href == GetCurrentAnchor()); | ||
if (link != null) | ||
{ | ||
try | ||
{ | ||
DomRect hrefDom = await link.GetHrefDom(true); | ||
if (hrefDom != null) | ||
{ | ||
await ActivateAsync(link, true); | ||
// the offset does not matter, since the dictionary's value will not change any more in case user set up GetCurrentAnchor | ||
_linkTops[link.Href] = hrefDom.top; | ||
StateHasChanged(); | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
Console.WriteLine(ex.Message); | ||
} | ||
} | ||
} | ||
|
||
await base.OnFirstAfterRenderAsync(); | ||
} | ||
|
||
public void Add(AnchorLink anchorLink) | ||
{ | ||
_links.Add(anchorLink); | ||
} | ||
|
||
public List<AnchorLink> FlatChildren() | ||
{ | ||
List<AnchorLink> results = new List<AnchorLink>(); | ||
|
||
foreach (IAnchor child in _links) | ||
{ | ||
results.AddRange(child.FlatChildren()); | ||
} | ||
|
||
return results; | ||
} | ||
|
||
private async void OnScroll(JsonElement obj) | ||
{ | ||
_activeLink = null; | ||
_flatLinks.ForEach(l => l.Activate(false)); | ||
|
||
int offset = OffsetBottom.HasValue ? OffsetBottom.Value : -OffsetTop.Value; | ||
foreach (var link in _flatLinks) | ||
{ | ||
try | ||
{ | ||
DomRect hrefDom = await link.GetHrefDom(); | ||
if (hrefDom != null) | ||
{ | ||
_linkTops[link.Href] = hrefDom.top + offset; | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
_linkTops[link.Href] = 1; | ||
} | ||
} | ||
|
||
string activeKey = _linkTops.Where(p => (int)p.Value <= 0).OrderBy(p => p.Value).LastOrDefault().Key; | ||
if (!string.IsNullOrEmpty(activeKey)) | ||
{ | ||
_activeLink = _flatLinks.Single(l => l.Href == activeKey); | ||
await ActivateAsync(_activeLink, true); | ||
} | ||
|
||
if (Affix && _activeLink != null) | ||
{ | ||
_ballClass = "ant-anchor-ink-ball visible"; | ||
decimal top = (_activeLink.LinkDom.top - _selfDom.top) + _activeLink.LinkDom.height / 2 - 2; | ||
_ballStyle = $"top: {top}px;"; | ||
} | ||
else | ||
{ | ||
_ballClass = "ant-anchor-ink-ball"; | ||
_ballStyle = string.Empty; | ||
} | ||
|
||
StateHasChanged(); | ||
} | ||
|
||
private async Task ActivateAsync(AnchorLink anchorLink, bool active) | ||
{ | ||
anchorLink.Activate(active); | ||
|
||
if (active && _activeLink != _lastActiveLink) | ||
{ | ||
_lastActiveLink = _activeLink; | ||
if (OnChange.HasDelegate) | ||
{ | ||
await OnChange.InvokeAsync(anchorLink.Href); | ||
} | ||
} | ||
} | ||
|
||
public async Task OnLinkClickAsync(MouseEventArgs args, AnchorLink anchorLink) | ||
{ | ||
await JsInvokeAsync("window.eval", $"window.location.hash='{anchorLink._hash}'"); | ||
|
||
if (OnClick.HasDelegate) | ||
{ | ||
await OnClick.InvokeAsync(new Tuple<MouseEventArgs, AnchorLink>(args, anchorLink)); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
@namespace AntDesign | ||
@inherits AntDomComponentBase | ||
|
||
<div class="@ClassMapper.Class" style="@Style" @ref="@_self" id="@Id"> | ||
<a title="@Title" class="@_titleClass.Class" href="javascript:void(0)" @onclick="(e)=>OnClick(e)">@Title</a> | ||
<CascadingValue Value="this" Name="Parent"> | ||
@ChildContent | ||
</CascadingValue> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using AntDesign.JsInterop; | ||
using Microsoft.AspNetCore.Components; | ||
using Microsoft.AspNetCore.Components.Web; | ||
using OneOf; | ||
|
||
namespace AntDesign | ||
{ | ||
public partial class AnchorLink : AntDomComponentBase, IAnchor | ||
{ | ||
private const string PrefixCls = "ant-anchor-link"; | ||
internal bool Active { get; private set; } | ||
|
||
private bool _hrefDomExist; | ||
private ClassMapper _titleClass = new ClassMapper(); | ||
private ElementReference _self; | ||
private List<AnchorLink> _links = new List<AnchorLink>(); | ||
public DomRect LinkDom { get; private set; } | ||
|
||
#region Parameters | ||
|
||
[CascadingParameter(Name = "Root")] | ||
public Anchor Root { get; set; } | ||
|
||
private IAnchor _parent; | ||
|
||
[CascadingParameter(Name = "Parent")] | ||
public IAnchor Parent | ||
{ | ||
get => _parent; | ||
set | ||
{ | ||
//Debug.WriteLine($"link:{Title} {GetHashCode()}\tparent:{value.GetHashCode()}"); | ||
_parent = value; | ||
_parent?.Add(this); | ||
} | ||
} | ||
|
||
[Parameter] | ||
public RenderFragment ChildContent { get; set; } | ||
|
||
/// <summary> | ||
/// target of hyperlink | ||
/// </summary> | ||
[Parameter] | ||
public string Href { get; set; } | ||
|
||
/// <summary> | ||
/// content of hyperlink | ||
/// </summary> | ||
[Parameter] | ||
public string Title { get; set; } | ||
|
||
/// <summary> | ||
/// Specifies where to display the linked URL | ||
/// </summary> | ||
[Parameter] | ||
public string Target { get; set; } | ||
|
||
#endregion Parameters | ||
|
||
internal string _hash = string.Empty; | ||
|
||
protected override void OnInitialized() | ||
{ | ||
base.OnInitialized(); | ||
|
||
ClassMapper.Clear() | ||
.Add($"{PrefixCls}") | ||
.If($"{PrefixCls}-active", () => Active); | ||
|
||
_titleClass.Clear() | ||
.Add($"{PrefixCls}-title") | ||
.If($"{PrefixCls}-title-active", () => Active); | ||
|
||
_hash = Href.Split('#')[1]; | ||
} | ||
|
||
protected async override Task OnFirstAfterRenderAsync() | ||
{ | ||
await base.OnFirstAfterRenderAsync(); | ||
|
||
LinkDom = await JsInvokeAsync<DomRect>(JSInteropConstants.getBoundingClientRect, _self); | ||
try | ||
{ | ||
await JsInvokeAsync<DomRect>(JSInteropConstants.getBoundingClientRect, "#" + Href.Split('#')[1]); | ||
_hrefDomExist = true; | ||
} | ||
catch { } | ||
} | ||
|
||
public void Add(AnchorLink anchorLink) | ||
{ | ||
_links.Add(anchorLink); | ||
} | ||
|
||
public List<AnchorLink> FlatChildren() | ||
{ | ||
List<AnchorLink> results = new List<AnchorLink>(); | ||
|
||
if (!string.IsNullOrEmpty(Href)) | ||
{ | ||
results.Add(this); | ||
} | ||
|
||
foreach (IAnchor child in _links) | ||
{ | ||
results.AddRange(child.FlatChildren()); | ||
} | ||
|
||
return results; | ||
} | ||
|
||
internal void Activate(bool active) | ||
{ | ||
Active = active; | ||
} | ||
|
||
internal async Task<DomRect> GetHrefDom(bool forced = false) | ||
{ | ||
DomRect domRect = null; | ||
if (forced || _hrefDomExist) | ||
{ | ||
domRect = await JsInvokeAsync<DomRect>(JSInteropConstants.getBoundingClientRect, "#" + Href.Split('#')[1]); | ||
} | ||
return domRect; | ||
} | ||
|
||
private async void OnClick(MouseEventArgs args) | ||
{ | ||
await Root.OnLinkClickAsync(args, this); | ||
} | ||
} | ||
} |
Oops, something went wrong.