// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using AntDesign.JsInterop; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; namespace AntDesign { public partial class Tabs : AntDomComponentBase { private const string PrefixCls = "ant-tabs"; private bool IsHorizontal { get => TabPosition == AntDesign.TabPosition.Top || TabPosition == AntDesign.TabPosition.Bottom; } //private ClassMapper _barClassMapper = new ClassMapper(); //private ClassMapper _prevClassMapper = new ClassMapper(); //private ClassMapper _nextClassMapper = new ClassMapper(); //private ClassMapper _navClassMapper = new ClassMapper(); private TabPane _activePane; private TabPane _renderedActivePane; private ElementReference _scrollTabBar; private ElementReference _tabBars; private string _inkStyle; private string _navStyle; private bool _wheelDisabled; //private string _contentStyle; //private bool? _prevIconEnabled; //private bool? _nextIconEnabled; private string _operationClass; private string _tabsNavWarpPingClass; private string _operationStyle; private int _scrollOffset; private int _listWidth; private int _listHeight; private int _navWidth; private int _navHeight; private bool _needRefresh; private bool _afterFirstRender; internal List _panes = new List(); [Inject] public DomEventService DomEventService { get; set; } #region Parameters [Parameter] public RenderFragment ChildContent { get; set; } private string _activeKey; /// /// Current 's /// [Parameter] public string ActiveKey { get { return _activeKey; } set { if (_activeKey != value) { _activeKey = value; if (_panes.Count == 0) return; var tabPane = _panes.Find(p => p.Key == value); if (tabPane == null) return; ActivatePane(tabPane); } } } [Parameter] public EventCallback ActiveKeyChanged { get; set; } /// /// Whether to change tabs with animation. Only works while = or /// [Parameter] public bool Animated { get; set; } /// /// Replace the TabBar /// [Parameter] public object RenderTabBar { get; set; } /// /// Initial active 's , if is not set /// [Parameter] public string DefaultActiveKey { get; set; } /// /// Hide plus icon or not. Only works while = /// [Parameter] public bool HideAdd { get; set; } = false; /// /// Preset tab bar size /// [Parameter] public string Size { get; set; } = TabSize.Default; /// /// Extra content in tab bar /// [Parameter] public RenderFragment TabBarExtraContent { get; set; } /// /// The gap between tabs /// [Parameter] public int TabBarGutter { get; set; } /// /// Tab bar style object /// [Parameter] public string TabBarStyle { get; set; } /// /// Position of tabs /// [Parameter] public string TabPosition { get; set; } = AntDesign.TabPosition.Top; /// /// Basic style of tabs /// [Parameter] public string Type { get; set; } = TabType.Line; /// /// Callback executed when active tab is changed /// [Parameter] public EventCallback OnChange { get; set; } /// /// Callback executed when tab is added or removed. Only works while = /// [Parameter] public Func> OnEdit { get; set; } = (key, action) => Task.FromResult(true); /// /// Callback when tab is closed /// [Parameter] public EventCallback OnClose { get; set; } [Parameter] public EventCallback OnAddClick { get; set; } [Parameter] public EventCallback AfterTabCreated { get; set; } /// /// Callback executed when next button is clicked /// [Parameter] public EventCallback OnNextClick { get; set; } /// /// Callback executed when prev button is clicked /// [Parameter] public EventCallback OnPrevClick { get; set; } /// /// Callback executed when tab is clicked /// [Parameter] public EventCallback OnTabClick { get; set; } /// /// Whether to turn on keyboard navigation /// [Parameter] public bool Keyboard { get; set; } = true; [Parameter] public bool Draggable { get; set; } [CascadingParameter] public Card Card { get; set; } #endregion Parameters protected override void OnInitialized() { base.OnInitialized(); ClassMapper.Clear() .Add(PrefixCls) .GetIf(() => $"{PrefixCls}-{TabPosition}", () => TabPosition.IsIn(AntDesign.TabPosition.Top, AntDesign.TabPosition.Bottom, AntDesign.TabPosition.Left, AntDesign.TabPosition.Right)) .GetIf(() => $"{PrefixCls}-{Type}", () => Type.IsIn(TabType.Card, TabType.EditableCard, TabType.Line)) .If($"{PrefixCls}-large", () => Size == TabSize.Large || Card != null) .If($"{PrefixCls}-head-tabs", () => Card != null) .If($"{PrefixCls}-small", () => Size == TabSize.Small) .GetIf(() => $"{PrefixCls}-{TabType.Card}", () => Type == TabType.EditableCard) .If($"{PrefixCls}-no-animation", () => !Animated) .If($"{PrefixCls}-rtl", () => RTL); } protected override void OnParametersSet() { base.OnParametersSet(); _needRefresh = true; _renderedActivePane = null; } public override Task SetParametersAsync(ParameterView parameters) { string type = parameters.GetValueOrDefault(nameof(Type)); // according to ant design documents, // Animated default to false when type="card" Animated = type != TabType.Card; return base.SetParametersAsync(parameters); } /// /// Add to /// /// The AntTabPane to be added /// Key is null /// An AntTabPane with the same key already exists internal void AddTabPane(TabPane tabPane) { if (string.IsNullOrEmpty(tabPane.Key)) { throw new ArgumentNullException(nameof(tabPane), "Key is null"); } if (_panes.Select(p => p.Key).Contains(tabPane.Key)) { throw new ArgumentException("An AntTabPane with the same key already exists"); } _panes.Add(tabPane); } internal void Complete(TabPane content) { var pane = _panes.FirstOrDefault(x => x.Key == content.Key); if (pane != null && pane.IsComplete()) { if (_panes.Any(x => !x.IsComplete())) { return; } if (!string.IsNullOrWhiteSpace(ActiveKey)) { var activedPane = _panes.Find(x => x.Key == ActiveKey); if (activedPane?.IsActive == false) { ActivatePane(activedPane); } } else if (!string.IsNullOrWhiteSpace(DefaultActiveKey)) { var defaultPane = _panes.FirstOrDefault(x => x.Key == DefaultActiveKey); if (defaultPane != null) { ActivatePane(defaultPane); } } if (_activePane == null || _panes.All(x => !x.IsActive)) { ActivatePane(_panes.FirstOrDefault()); } if (AfterTabCreated.HasDelegate) { AfterTabCreated.InvokeAsync(pane.Key); } } } public async Task RemoveTabPane(TabPane pane) { if (await OnEdit.Invoke(pane.Key, "remove")) { var index = _panes.IndexOf(pane); _panes.Remove(pane); if (pane != null && pane.IsActive && _panes.Count > 0) { ActivatePane(index > 1 ? _panes[index - 1] : _panes[0]); } _needRefresh = true; if (OnClose.HasDelegate) { await OnClose.InvokeAsync(pane.Key); } StateHasChanged(); } } internal void HandleTabClick(TabPane tabPane) { if (tabPane.IsActive) return; if (OnTabClick.HasDelegate) { OnTabClick.InvokeAsync(tabPane.Key); } ActivatePane(tabPane); } private void ActivatePane(TabPane tabPane) { if (!tabPane.Disabled && _panes.Contains(tabPane)) { if (_activePane != null) { _activePane.IsActive = false; } tabPane.IsActive = true; _activePane = tabPane; if (_activeKey != _activePane.Key) { if (!string.IsNullOrEmpty(_activeKey)) { if (ActiveKeyChanged.HasDelegate) { ActiveKeyChanged.InvokeAsync(_activePane.Key); } if (OnChange.HasDelegate) { OnChange.InvokeAsync(_activePane.Key); } } _activeKey = _activePane.Key; } _needRefresh = true; Card?.SetBody(_activePane.ChildContent); StateHasChanged(); } } protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); if (firstRender) { _afterFirstRender = true; } var element = await JsInvokeAsync(JSInteropConstants.GetDomInfo, _scrollTabBar); _listWidth = element.ClientWidth; _listHeight = element.ClientHeight; var navSection = await JsInvokeAsync(JSInteropConstants.GetDomInfo, _tabBars); _navWidth = navSection.ClientWidth; _navHeight = navSection.ClientHeight; if (IsHorizontal && !_wheelDisabled) { DomEventService.AddEventListener(_scrollTabBar, "wheel", OnWheel, true, true); _wheelDisabled = true; } if (!IsHorizontal && _wheelDisabled) { DomEventService.RemoveEventListerner(_scrollTabBar, "wheel", OnWheel); _wheelDisabled = false; } if (_afterFirstRender && _activePane != null) { await TryRenderInk(); await TryRenderNavOperation(); } _needRefresh = false; } private void OnWheel(string json) { int maxOffset; if (IsHorizontal) { maxOffset = _listWidth - _navWidth; } else { maxOffset = _listHeight - _navHeight; } int delta = JsonDocument.Parse(json).RootElement.GetProperty("wheelDelta").GetInt32(); if (delta >= 0) { _scrollOffset -= 100; } else { _scrollOffset += 100; } _scrollOffset = Math.Max(0, _scrollOffset); _scrollOffset = Math.Min(maxOffset, _scrollOffset); _renderedActivePane = null; if (IsHorizontal) { _navStyle = $"transform: translate(-{_scrollOffset}px, 0px);"; } else { _navStyle = $"transform: translate(0px, -{_scrollOffset}px);"; } StateHasChanged(); _renderedActivePane = _activePane; } private async Task TryRenderNavOperation() { int navWidth = (await JsInvokeAsync(JSInteropConstants.GetDomInfo, _tabBars)).ClientWidth; int navTotalWidth = (await JsInvokeAsync(JSInteropConstants.GetDomInfo, _scrollTabBar)).ClientWidth; if (navTotalWidth < navWidth) { _operationClass = "ant-tabs-nav-operations ant-tabs-nav-operations-hidden"; _operationStyle = "visibility: hidden; order: 1;"; _tabsNavWarpPingClass = string.Empty; } else { _operationClass = "ant-tabs-nav-operations"; _tabsNavWarpPingClass = "ant-tabs-nav-wrap-ping-right"; _operationStyle = string.Empty; } StateHasChanged(); } private async Task TryRenderInk() { if (_renderedActivePane == _activePane) { return; } // TODO: slide to activated tab // animate Active Ink // ink bar var element = await JsInvokeAsync(JSInteropConstants.GetDomInfo, _activePane.TabBar); var navSection = await JsInvokeAsync(JSInteropConstants.GetDomInfo, _tabBars); if (IsHorizontal) { //_inkStyle = "left: 0px; width: 0px;"; _inkStyle = $"left: {element.OffsetLeft}px; width: {element.ClientWidth}px"; if (element.OffsetLeft > _scrollOffset + navSection.ClientWidth || element.OffsetLeft < _scrollOffset) { // need to scroll tab bars _scrollOffset = element.OffsetLeft; _navStyle = $"transform: translate(-{_scrollOffset}px, 0px);"; } } else { _inkStyle = $"top: {element.OffsetTop}px; height: {element.ClientHeight}px;"; if (element.OffsetTop > _scrollOffset + navSection.ClientHeight || element.OffsetTop < _scrollOffset) { // need to scroll tab bars _scrollOffset = element.OffsetTop; _navStyle = $"transform: translate(0px, -{_scrollOffset}px);"; } } StateHasChanged(); _renderedActivePane = _activePane; } protected override bool ShouldRender() { return _needRefresh || _renderedActivePane != _activePane; } private IEnumerable GetInvisibleTabs() { double average; int invisibleHeadCount, visibleCount; if (IsHorizontal) { average = 1.0 * _listWidth / _panes.Count; visibleCount = (int)(_navWidth / average); } else { average = 1.0 * _listHeight / _panes.Count; visibleCount = (int)(_navHeight / average); } invisibleHeadCount = (int)Math.Ceiling(_scrollOffset / average); IEnumerable invisibleTabs = _panes.Take(invisibleHeadCount).Concat(_panes.Skip(invisibleHeadCount + visibleCount)); return invisibleTabs; } #region DRAG & DROP private TabPane _draggingPane; internal void HandleDragStart(DragEventArgs args, TabPane pane) { if (Draggable) { args.DataTransfer.DropEffect = "move"; args.DataTransfer.EffectAllowed = "move"; _draggingPane = pane; } } internal void HandleDrop(TabPane pane) { if (Draggable && _draggingPane != null) { int newIndex = _panes.IndexOf(pane); _panes.Remove(_draggingPane); _panes.Insert(newIndex, _draggingPane); _draggingPane = null; _needRefresh = true; _renderedActivePane = null; StateHasChanged(); } } #endregion DRAG & DROP } }