Skip to content

Commit

Permalink
feat: add anchor component (ant-design-blazor#281)
Browse files Browse the repository at this point in the history
* 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
brian-ding and ElderJames authored Jun 30, 2020
1 parent 09e5f4e commit 800d384
Show file tree
Hide file tree
Showing 23 changed files with 657 additions and 2 deletions.
16 changes: 16 additions & 0 deletions components/anchor/Anchor.razor
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>
225 changes: 225 additions & 0 deletions components/anchor/Anchor.razor.cs
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));
}
}
}
}
9 changes: 9 additions & 0 deletions components/anchor/AnchorLink.razor
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>
139 changes: 139 additions & 0 deletions components/anchor/AnchorLink.razor.cs
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);
}
}
}
Loading

0 comments on commit 800d384

Please sign in to comment.