using System.Globalization; using System.Windows.Input; using Connected.Annotations; using Connected.Utilities; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; namespace Connected.Components; public partial class TreeViewItem : UIComponent { private string _text; private bool _disabled; private bool _isChecked, _isSelected, _isServerLoaded; private Converter _converter = new DefaultConverter(); private readonly List> _childItems = new(); protected string Classname => new CssBuilder("mud-treeview-item") .AddClass(Class) .Build(); protected string ContentClassname => new CssBuilder("mud-treeview-item-content") .AddClass("cursor-pointer", TreeRoot?.IsSelectable == true || TreeRoot?.ExpandOnClick == true && HasChild) .AddClass($"mud-treeview-item-selected", _isSelected) .Build(); public string TextClassname => new CssBuilder("mud-treeview-item-label") .AddClass(TextClass) .Build(); [CascadingParameter] TreeView TreeRoot { get; set; } [CascadingParameter] TreeViewItem Parent { get; set; } /// /// Custom checked icon, leave null for default. /// [Parameter] [Category(CategoryTypes.TreeView.Selecting)] public string CheckedIcon { get; set; } = Icons.Material.Filled.CheckBox; /// /// Custom unchecked icon, leave null for default. /// [Parameter] [Category(CategoryTypes.TreeView.Selecting)] public string UncheckedIcon { get; set; } = Icons.Material.Filled.CheckBoxOutlineBlank; /// /// Value of the treeviewitem. Acts as the displayed text if no text is set. /// [Parameter] [Category(CategoryTypes.TreeView.Data)] public T Value { get; set; } [Parameter] [Category(CategoryTypes.TreeView.Behavior)] public CultureInfo Culture { get; set; } = CultureInfo.CurrentCulture; /// /// The text to display /// [Parameter] [Category(CategoryTypes.TreeView.Behavior)] public string Text { get => string.IsNullOrEmpty(_text) ? _converter.Set(Value) : _text; set => _text = value; } /// /// Tyopography for the text. /// [Parameter] [Category(CategoryTypes.TreeView.Appearance)] public Typo TextTypo { get; set; } = Typo.body1; /// /// User class names for the text, separated by space. /// [Parameter] [Category(CategoryTypes.TreeView.Appearance)] public string TextClass { get; set; } /// /// The text at the end of the item. /// [Parameter] [Category(CategoryTypes.TreeView.Behavior)] public string EndText { get; set; } /// /// Tyopography for the endtext. /// [Parameter] [Category(CategoryTypes.TreeView.Appearance)] public Typo EndTextTypo { get; set; } = Typo.body1; /// /// User class names for the endtext, separated by space. /// [Parameter] [Category(CategoryTypes.TreeView.Appearance)] public string EndTextClass { get; set; } /// /// If true, treeviewitem will be disabled. /// [Parameter] [Category(CategoryTypes.TreeView.Behavior)] public bool Disabled { get => _disabled || (TreeRoot?.Disabled ?? false); set => _disabled = value; } /// /// Child content of component used to create sub levels. /// [Parameter] [Category(CategoryTypes.TreeView.Data)] public RenderFragment ChildContent { get; set; } /// /// Content of the item, if used completly replaced the default rendering. /// [Parameter] [Category(CategoryTypes.TreeView.Behavior)] public RenderFragment Content { get; set; } [Parameter] [Category(CategoryTypes.TreeView.Data)] public HashSet Items { get; set; } /// /// Command executed when the user clicks on the CommitEdit Button. /// [Parameter] [Category(CategoryTypes.TreeView.ClickAction)] public ICommand Command { get; set; } /// /// Expand or collapse treeview item when it has children. Two-way bindable. Note: if you directly set this to /// true or false (instead of using two-way binding) it will force the item's expansion state. /// [Parameter] [Category(CategoryTypes.TreeView.Expanding)] public bool Expanded { get; set; } /// /// Called whenever expanded changed. /// [Parameter] public EventCallback ExpandedChanged { get; set; } [Parameter] [Category(CategoryTypes.TreeView.Selecting)] public bool Activated { get => _isSelected; set { _ = TreeRoot?.UpdateSelected(this, value); } } [Parameter] [Category(CategoryTypes.TreeView.Selecting)] public bool Selected { get => _isChecked; set { if (_isChecked == value) return; _isChecked = value; TreeRoot?.UpdateSelectedItems(); SelectedChanged.InvokeAsync(_isChecked); } } /// /// Icon placed before the text if set. /// [Parameter] [Category(CategoryTypes.TreeView.Behavior)] public string Icon { get; set; } /// /// The color of the icon. It supports the theme colors. /// [Parameter] [Category(CategoryTypes.TreeView.Appearance)] public ThemeColor IconColor { get; set; } = ThemeColor.Default; /// /// Icon placed after the text if set. /// [Parameter] [Category(CategoryTypes.TreeView.Behavior)] public string EndIcon { get; set; } /// /// The color of the icon. It supports the theme colors. /// [Parameter] [Category(CategoryTypes.TreeView.Appearance)] public ThemeColor EndIconColor { get; set; } = ThemeColor.Default; /// /// The expand/collapse icon. /// [Parameter] [Category(CategoryTypes.TreeView.Expanding)] public string ExpandedIcon { get; set; } = Icons.Material.Filled.ChevronRight; /// /// The color of the expand/collapse button. It supports the theme colors. /// [Parameter] [Category(CategoryTypes.TreeView.Expanding)] public ThemeColor ExpandedIconColor { get; set; } = ThemeColor.Default; /// /// The loading icon. /// [Parameter] [Category(CategoryTypes.TreeView.Appearance)] public string LoadingIcon { get; set; } = Icons.Material.Filled.Loop; /// /// The color of the loading. It supports the theme colors. /// [Parameter] [Category(CategoryTypes.TreeView.Appearance)] public ThemeColor LoadingIconColor { get; set; } = ThemeColor.Default; /// /// Called whenever the activated value changed. /// [Parameter] public EventCallback ActivatedChanged { get; set; } /// /// Called whenever the selected value changed. /// [Parameter] public EventCallback SelectedChanged { get; set; } /// /// Tree item click event. /// [Parameter] public EventCallback OnClick { get; set; } /// /// Tree item double click event. /// [Parameter] public EventCallback OnDoubleClick { get; set; } public bool Loading { get; set; } bool HasChild => ChildContent != null || (TreeRoot != null && Items != null && Items.Count != 0) || (TreeRoot?.ServerData != null && !_isServerLoaded && (Items == null || Items.Count == 0)); protected bool IsChecked { get => Selected; set { _ = SelectItem(value, this); } } protected internal bool ArrowExpanded { get => Expanded; set { if (value == Expanded) return; Expanded = value; ExpandedChanged.InvokeAsync(value); } } protected override void OnInitialized() { if (Parent != null) { Parent.AddChild(this); } else { TreeRoot?.AddChild(this); } } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender && _isSelected) { await TreeRoot.UpdateSelected(this, _isSelected); } await base.OnAfterRenderAsync(firstRender); } protected async Task OnItemClicked(MouseEventArgs ev) { if (TreeRoot?.IsSelectable ?? false) { await TreeRoot.UpdateSelected(this, !_isSelected); } if (HasChild && (TreeRoot?.ExpandOnClick ?? false)) { Expanded = !Expanded; TryInvokeServerLoadFunc(); await ExpandedChanged.InvokeAsync(Expanded); } await OnClick.InvokeAsync(ev); if (Command?.CanExecute(Value) ?? false) { Command.Execute(Value); } } protected async Task OnItemDoubleClicked(MouseEventArgs ev) { if (TreeRoot?.IsSelectable ?? false) { await TreeRoot.UpdateSelected(this, !_isSelected); } if (HasChild && (TreeRoot?.ExpandOnDoubleClick ?? false)) { Expanded = !Expanded; TryInvokeServerLoadFunc(); await ExpandedChanged.InvokeAsync(Expanded); } await OnDoubleClick.InvokeAsync(ev); } protected internal Task OnItemExpanded(bool expanded) { if (Expanded == expanded) return Task.CompletedTask; Expanded = expanded; TryInvokeServerLoadFunc(); return ExpandedChanged.InvokeAsync(expanded); } internal Task Select(bool value) { if (_isSelected == value) return Task.CompletedTask; _isSelected = value; StateHasChanged(); return ActivatedChanged.InvokeAsync(_isSelected); } internal async Task SelectItem(bool value, TreeViewItem source = null) { if (value == _isChecked) return; _isChecked = value; _childItems.ForEach(async c => await c.SelectItem(value, source)); StateHasChanged(); await SelectedChanged.InvokeAsync(_isChecked); if (source == this) { if (TreeRoot != null) { await TreeRoot.UpdateSelectedItems(); } } } private void AddChild(TreeViewItem item) => _childItems.Add(item); internal IEnumerable> GetSelectedItems() { if (_isChecked) yield return this; foreach (var treeItem in _childItems) { foreach (var selected in treeItem.GetSelectedItems()) { yield return selected; } } } internal async void TryInvokeServerLoadFunc() { if (Expanded && (Items == null || Items.Count == 0) && TreeRoot?.ServerData != null) { Loading = true; StateHasChanged(); Items = await TreeRoot.ServerData(Value); Loading = false; _isServerLoaded = true; StateHasChanged(); } } }