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/Tabs/Tabs.razor.cs

747 lines
22 KiB

using System.Globalization;
using Connected.Annotations;
using Connected.Extensions;
using Connected.Interop;
using Connected.Services;
using Connected.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace Connected.Components;
public partial class Tabs : UIComponent, IAsyncDisposable
{
private bool _isDisposed;
private int _activePanelIndex = 0;
private int _scrollIndex = 0;
private bool _isRendered = false;
private bool _prevButtonDisabled;
private bool _nextButtonDisabled;
private bool _showScrollButtons;
private bool _disableSliderAnimation;
private ElementReference _tabsContentSize;
private double _size;
private double _position;
private double _toolbarContentSize;
private double _allTabsSize;
private double _scrollPosition;
private IResizeObserver _resizeObserver;
[CascadingParameter(Name = "RightToLeft")] public bool RightToLeft { get; set; }
[Inject] private IResizeObserverFactory _resizeObserverFactory { get; set; }
/// <summary>
/// If true, render all tabs and hide (display:none) every non-active.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Behavior)]
public bool KeepPanelsAlive { get; set; } = false;
/// <summary>
/// If true, sets the border-radius to theme default.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public bool Rounded { get; set; }
/// <summary>
/// If true, sets a border between the content and the toolbar depending on the position.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public bool Border { get; set; }
/// <summary>
/// If true, toolbar will be outlined.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public bool Outlined { get; set; }
/// <summary>
/// If true, centers the tabitems.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public bool Centered { get; set; }
/// <summary>
/// Hides the active tab slider.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public bool HideSlider { get; set; }
/// <summary>
/// Icon to use for left pagination.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public string PrevIcon { get; set; } = Icons.Filled.ChevronLeft;
/// <summary>
/// Icon to use for right pagination.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public string NextIcon { get; set; } = Icons.Filled.ChevronRight;
/// <summary>
/// If true, always display the scroll buttons even if the tabs are smaller than the required with, buttons will be disabled if there is nothing to scroll.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public bool AlwaysShowScrollButtons { get; set; }
/// <summary>
/// Sets the maxheight the component can have.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public int? MaxHeight { get; set; } = null;
/// <summary>
/// Sets the position of the tabs itself.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Behavior)]
public Position Position { get; set; } = Position.Top;
/// <summary>
/// The color of the component. It supports the theme colors.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public ThemeColor Color { get; set; } = ThemeColor.Default;
/// <summary>
/// The color of the tab slider. It supports the theme colors.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public ThemeColor SliderColor { get; set; } = ThemeColor.Inherit;
/// <summary>
/// The color of the icon. It supports the theme colors.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public ThemeColor IconColor { get; set; } = ThemeColor.Inherit;
/// <summary>
/// The color of the next/prev icons. It supports the theme colors.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public ThemeColor ScrollIconColor { get; set; } = ThemeColor.Inherit;
/// <summary>
/// The higher the number, the heavier the drop-shadow, applies around the whole component.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public int Elevation { set; get; } = 0;
/// <summary>
/// If true, will apply elevation, rounded, outlined effects to the whole tab component instead of just toolbar.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public bool ApplyEffectsToContainer { get; set; }
/// <summary>
/// If true, disables ripple effect.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public bool DisableRipple { get; set; }
/// <summary>
/// If true, disables slider animation
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public bool DisableSliderAnimation { get => _disableSliderAnimation; set => _disableSliderAnimation = value; }
/// <summary>
/// Child content of component.
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Behavior)]
public RenderFragment ChildContent { get; set; }
/// <summary>
/// This fragment is placed between toolbar and panels.
/// It can be used to display additional content like an address line in a browser.
/// The active tab will be the content of this RenderFragement
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Behavior)]
public RenderFragment<TabPanel> PrePanelContent { get; set; }
/// <summary>
/// Custom class/classes for TabPanel
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public string TabPanelClass { get; set; }
/// <summary>
/// Custom class/classes for Selected Content Panel
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Appearance)]
public string PanelClass { get; set; }
public TabPanel ActivePanel { get; private set; }
/// <summary>
/// The current active panel index. Also with Bidirectional Binding
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Behavior)]
public int ActivePanelIndex
{
get => _activePanelIndex;
set
{
if (_activePanelIndex != value)
{
_activePanelIndex = value;
if (_isRendered)
ActivePanel = _panels[_activePanelIndex];
ActivePanelIndexChanged.InvokeAsync(value);
}
}
}
/// <summary>
/// Fired when ActivePanelIndex changes.
/// </summary>
[Parameter]
public EventCallback<int> ActivePanelIndexChanged { get; set; }
/// <summary>
/// A readonly list of the current panels. Panels should be added or removed through the RenderTree use this collection to get informations about the current panels
/// </summary>
public IReadOnlyList<TabPanel> Panels { get; private set; }
private List<TabPanel> _panels;
/// <summary>
/// A render fragment that is added before or after (based on the value of HeaderPosition) the tabs inside the header panel of the tab control
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Behavior)]
public RenderFragment<Tabs> Header { get; set; }
/// <summary>
/// Additional content specified by Header is placed either before the tabs, after or not at all
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Behavior)]
public TabHeaderPosition HeaderPosition { get; set; } = TabHeaderPosition.After;
/// <summary>
/// A render fragment that is added before or after (based on the value of HeaderPosition) inside each tab panel
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Behavior)]
public RenderFragment<TabPanel> TabPanelHeader { get; set; }
/// <summary>
/// Additional content specified by Header is placed either before the tabs, after or not at all
/// </summary>
[Parameter]
[Category(CategoryTypes.Tabs.Behavior)]
public TabHeaderPosition TabPanelHeaderPosition { get; set; } = TabHeaderPosition.After;
/// <summary>
/// Can be used in derived class to add a class to the main container. If not overwritten return an empty string
/// </summary>
protected virtual string InternalClassName { get; } = string.Empty;
private string _prevIcon;
private string _nextIcon;
#region Life cycle management
public Tabs()
{
_panels = new List<TabPanel>();
Panels = _panels.AsReadOnly();
}
protected override void OnInitialized()
{
_resizeObserver = _resizeObserverFactory.Create();
base.OnInitialized();
}
protected override void OnParametersSet()
{
if (_resizeObserver == null)
{
_resizeObserver = _resizeObserverFactory.Create();
}
Rerender();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var items = _panels.Select(x => x.PanelRef).ToList();
items.Add(_tabsContentSize);
if (_panels.Count > 0)
ActivePanel = _panels[_activePanelIndex];
await _resizeObserver.Observe(items);
_resizeObserver.OnResized += OnResized;
Rerender();
StateHasChanged();
_isRendered = true;
}
}
public async ValueTask DisposeAsync()
{
if (_isDisposed == true)
return;
_isDisposed = true;
_resizeObserver.OnResized -= OnResized;
await _resizeObserver.DisposeAsync();
}
#endregion
#region Children
internal void AddPanel(TabPanel tabPanel)
{
_panels.Add(tabPanel);
if (_panels.Count == 1)
ActivePanel = tabPanel;
StateHasChanged();
}
internal async Task SetPanelRef(ElementReference reference)
{
if (_isRendered == true && _resizeObserver.IsElementObserved(reference) == false)
{
await _resizeObserver.Observe(reference);
Rerender();
StateHasChanged();
}
}
internal async Task RemovePanel(TabPanel tabPanel)
{
if (_isDisposed)
return;
var index = _panels.IndexOf(tabPanel);
var newIndex = index;
if (ActivePanelIndex == index && index == _panels.Count - 1)
{
newIndex = index > 0 ? index - 1 : 0;
if (_panels.Count == 1)
{
ActivePanel = null;
}
}
else if (_activePanelIndex > index)
{
_activePanelIndex--;
await ActivePanelIndexChanged.InvokeAsync(_activePanelIndex);
}
if (index != newIndex)
{
ActivePanelIndex = newIndex;
}
_panels.Remove(tabPanel);
await _resizeObserver.Unobserve(tabPanel.PanelRef);
Rerender();
StateHasChanged();
}
public void ActivatePanel(TabPanel panel, bool ignoreDisabledState = false)
{
ActivatePanel(panel, null, ignoreDisabledState);
}
public void ActivatePanel(int index, bool ignoreDisabledState = false)
{
var panel = _panels[index];
ActivatePanel(panel, null, ignoreDisabledState);
}
public void ActivatePanel(object id, bool ignoreDisabledState = false)
{
var panel = _panels.Where((p) => Equals(p.ID, id)).FirstOrDefault();
if (panel != null)
ActivatePanel(panel, null, ignoreDisabledState);
}
private void ActivatePanel(TabPanel panel, MouseEventArgs ev, bool ignoreDisabledState = false)
{
if (!panel.Disabled || ignoreDisabledState)
{
ActivePanelIndex = _panels.IndexOf(panel);
if (ev != null)
ActivePanel.OnClick.InvokeAsync(ev);
CenterScrollPositionAroundSelectedItem();
SetSliderState();
SetScrollabilityStates();
StateHasChanged();
}
}
#endregion
#region Style and classes
protected string TabsClassnames =>
new CssBuilder("mud-tabs")
.AddClass($"mud-tabs-rounded", ApplyEffectsToContainer && Rounded)
.AddClass($"mud-paper-outlined", ApplyEffectsToContainer && Outlined)
.AddClass($"mud-elevation-{Elevation}", ApplyEffectsToContainer && Elevation != 0)
.AddClass($"mud-tabs-reverse", Position == Position.Bottom)
.AddClass($"mud-tabs-vertical", IsVerticalTabs())
.AddClass($"mud-tabs-vertical-reverse", Position == Position.Right && !RightToLeft || (Position == Position.Left) && RightToLeft || Position == Position.End)
.AddClass(InternalClassName)
.AddClass(Class)
.Build();
protected string ToolbarClassnames =>
new CssBuilder("mud-tabs-toolbar")
.AddClass($"mud-tabs-rounded", !ApplyEffectsToContainer && Rounded)
.AddClass($"mud-tabs-vertical", IsVerticalTabs())
.AddClass($"mud-tabs-toolbar-{Color.ToDescriptionString()}", Color != ThemeColor.Default)
.AddClass($"mud-tabs-border-{ConvertPosition(Position).ToDescriptionString()}", Border)
.AddClass($"mud-paper-outlined", !ApplyEffectsToContainer && Outlined)
.AddClass($"mud-elevation-{Elevation}", !ApplyEffectsToContainer && Elevation != 0)
.Build();
protected string WrapperClassnames =>
new CssBuilder("mud-tabs-toolbar-wrapper")
.AddClass($"mud-tabs-centered", Centered)
.AddClass($"mud-tabs-vertical", IsVerticalTabs())
.Build();
protected string WrapperScrollStyle =>
new StyleBuilder()
.AddStyle("transform", $"translateX({(-1 * _scrollPosition).ToString(CultureInfo.InvariantCulture)}px)", Position is Position.Top or Position.Bottom)
.AddStyle("transform", $"translateY({(-1 * _scrollPosition).ToString(CultureInfo.InvariantCulture)}px)", IsVerticalTabs())
.Build();
protected string PanelsClassnames =>
new CssBuilder("mud-tabs-panels")
.AddClass($"mud-tabs-vertical", IsVerticalTabs())
.AddClass(PanelClass)
.Build();
protected string SliderClass =>
new CssBuilder("mud-tab-slider")
.AddClass($"mud-{SliderColor.ToDescriptionString()}", SliderColor != ThemeColor.Inherit)
.AddClass($"mud-tab-slider-horizontal", Position is Position.Top or Position.Bottom)
.AddClass($"mud-tab-slider-vertical", IsVerticalTabs())
.AddClass($"mud-tab-slider-horizontal-reverse", Position == Position.Bottom)
.AddClass($"mud-tab-slider-vertical-reverse", Position == Position.Right || Position == Position.Start && RightToLeft || Position == Position.End && !RightToLeft)
.Build();
protected string MaxHeightStyles =>
new StyleBuilder()
.AddStyle("max-height", MaxHeight.ToPx(), MaxHeight != null)
.Build();
protected string SliderStyle => RightToLeft ?
new StyleBuilder()
.AddStyle("width", _size.ToPx(), Position is Position.Top or Position.Bottom)
.AddStyle("right", _position.ToPx(), Position is Position.Top or Position.Bottom)
.AddStyle("transition", _disableSliderAnimation ? "none" : "right .3s cubic-bezier(.64,.09,.08,1);", Position is Position.Top or Position.Bottom)
.AddStyle("transition", _disableSliderAnimation ? "none" : "top .3s cubic-bezier(.64,.09,.08,1);", IsVerticalTabs())
.AddStyle("height", _size.ToPx(), IsVerticalTabs())
.AddStyle("top", _position.ToPx(), IsVerticalTabs())
.Build() : new StyleBuilder()
.AddStyle("width", _size.ToPx(), Position is Position.Top or Position.Bottom)
.AddStyle("left", _position.ToPx(), Position is Position.Top or Position.Bottom)
.AddStyle("transition", _disableSliderAnimation ? "none" : "left .3s cubic-bezier(.64,.09,.08,1);", Position is Position.Top or Position.Bottom)
.AddStyle("transition", _disableSliderAnimation ? "none" : "top .3s cubic-bezier(.64,.09,.08,1);", IsVerticalTabs())
.AddStyle("height", _size.ToPx(), IsVerticalTabs())
.AddStyle("top", _position.ToPx(), IsVerticalTabs())
.Build();
private bool IsVerticalTabs()
{
return Position is Position.Left or Position.Right or Position.Start or Position.End;
}
private Position ConvertPosition(Position position)
{
return position switch
{
Position.Start => RightToLeft ? Position.Right : Position.Left,
Position.End => RightToLeft ? Position.Left : Position.Right,
_ => position
};
}
string GetTabClass(TabPanel panel)
{
var tabClass = new CssBuilder("mud-tab")
.AddClass($"mud-tab-active", when: () => panel == ActivePanel)
.AddClass($"mud-disabled", panel.Disabled)
.AddClass($"mud-ripple", !DisableRipple)
.AddClass(TabPanelClass)
.AddClass(panel.Class)
.Build();
return tabClass;
}
private Placement GetTooltipPlacement()
{
if (Position == Position.Right)
return Placement.Left;
else if (Position == Position.Left)
return Placement.Right;
else if (Position == Position.Bottom)
return Placement.Top;
else
return Placement.Bottom;
}
string GetTabStyle(TabPanel panel)
{
var tabStyle = new StyleBuilder()
.AddStyle(panel.Style)
.Build();
return tabStyle;
}
#endregion
#region Rendering and placement
private void Rerender()
{
_nextIcon = RightToLeft ? PrevIcon : NextIcon;
_prevIcon = RightToLeft ? NextIcon : PrevIcon;
GetToolbarContentSize();
GetAllTabsSize();
SetScrollButtonVisibility();
SetSliderState();
SetScrollabilityStates();
}
private async void OnResized(IDictionary<ElementReference, BoundingClientRect> changes)
{
Rerender();
await InvokeAsync(StateHasChanged);
}
private void SetSliderState()
{
if (ActivePanel == null) { return; }
_position = GetLengthOfPanelItems(ActivePanel);
_size = GetRelevantSize(ActivePanel.PanelRef);
}
private void GetToolbarContentSize() => _toolbarContentSize = GetRelevantSize(_tabsContentSize);
private void GetAllTabsSize()
{
double totalTabsSize = 0;
foreach (var panel in _panels)
{
totalTabsSize += GetRelevantSize(panel.PanelRef);
}
_allTabsSize = totalTabsSize;
}
private double GetRelevantSize(ElementReference reference) => Position switch
{
Position.Top or Position.Bottom => _resizeObserver.GetWidth(reference),
_ => _resizeObserver.GetHeight(reference)
};
private double GetLengthOfPanelItems(TabPanel panel, bool inclusive = false)
{
var value = 0.0;
foreach (var item in _panels)
{
if (item == panel)
{
if (inclusive)
{
value += GetRelevantSize(item.PanelRef);
}
break;
}
value += GetRelevantSize(item.PanelRef);
}
return value;
}
private double GetPanelLength(TabPanel panel) => panel == null ? 0.0 : GetRelevantSize(panel.PanelRef);
#endregion
#region scrolling
private void SetScrollButtonVisibility()
{
_showScrollButtons = AlwaysShowScrollButtons || _allTabsSize > _toolbarContentSize;
}
private void ScrollPrev()
{
_scrollIndex = Math.Max(_scrollIndex - GetVisiblePanels(), 0);
ScrollToItem(_panels[_scrollIndex]);
SetScrollabilityStates();
}
private void ScrollNext()
{
_scrollIndex = Math.Min(_scrollIndex + GetVisiblePanels(), _panels.Count - 1);
ScrollToItem(_panels[_scrollIndex]);
SetScrollabilityStates();
}
/// <summary>
/// Calculates the amount of panels that are completely visible inside the toolbar content area. Panels that are just partially visible are not considered here!
/// </summary>
/// <returns>The amount of panels visible inside the toolbar area. CAUTION: Might return 0!</returns>
private int GetVisiblePanels()
{
var x = 0D;
var count = 0;
var toolbarContentSize = GetRelevantSize(_tabsContentSize);
foreach (var panel in _panels)
{
x += GetRelevantSize(panel.PanelRef);
if (x < toolbarContentSize)
{
count++;
}
else
{
break;
}
}
return count;
}
private void ScrollToItem(TabPanel panel)
{
var position = GetLengthOfPanelItems(panel);
_scrollPosition = RightToLeft ? -position : position;
}
private bool IsAfterLastPanelIndex(int index) => index >= _panels.Count;
private bool IsBeforeFirstPanelIndex(int index) => index < 0;
private void CenterScrollPositionAroundSelectedItem()
{
TabPanel panelToStart = ActivePanel;
var length = GetPanelLength(panelToStart);
if (length >= _toolbarContentSize)
{
ScrollToItem(panelToStart);
return;
}
var indexCorrection = 1;
while (true)
{
var panelAfterIndex = _activePanelIndex + indexCorrection;
if (!IsAfterLastPanelIndex(panelAfterIndex))
{
length += GetPanelLength(_panels[panelAfterIndex]);
}
if (length >= _toolbarContentSize)
{
_scrollIndex = _panels.IndexOf(panelToStart);
ScrollToItem(panelToStart);
break;
}
length = _toolbarContentSize - length;
var panelBeforeindex = _activePanelIndex - indexCorrection;
if (!IsBeforeFirstPanelIndex(panelBeforeindex))
{
length -= GetPanelLength(_panels[panelBeforeindex]);
}
else
{
break;
}
if (length < 0)
{
_scrollIndex = _panels.IndexOf(panelToStart);
ScrollToItem(panelToStart);
break;
}
length = _toolbarContentSize - length;
panelToStart = _panels[_activePanelIndex - indexCorrection];
indexCorrection++;
}
_scrollIndex = _panels.IndexOf(panelToStart);
ScrollToItem(panelToStart);
SetScrollabilityStates();
}
private void SetScrollabilityStates()
{
var isEnoughSpace = _allTabsSize <= _toolbarContentSize;
if (isEnoughSpace)
{
_nextButtonDisabled = true;
_prevButtonDisabled = true;
}
else
{
// Disable next button if the last panel is completely visible
_nextButtonDisabled = Math.Abs(_scrollPosition) >= GetLengthOfPanelItems(_panels.Last(), true) - _toolbarContentSize;
_prevButtonDisabled = _scrollIndex == 0;
}
}
#endregion
}