@typeparam TItem
@*
* dbMango
*
* Copyright 2025 Deutsche Bank AG
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*@
@if (IsExpanded)
{
@foreach (var child in Node.Children)
{
}
}
@code {
[Parameter] public TreeNode Node { get; set; } = null!;
[Parameter] public EventCallback> OnNodeChanged { get; set; }
[Parameter] public RenderFragment>? LabelTemplate { get; set; }
[Parameter] public RenderFragment>? BodyTemplate { get; set; }
[CascadingParameter(Name = "ReadOnly")] public bool ReadOnly { get; set; }
[CascadingParameter(Name = "CurrentlyEditingNodeRef")] public TreeNode? SharedCurrentlyEditingNode { get; set; }
[CascadingParameter(Name = "SetCurrentlyEditingNodeRef")] public Action?>? SetSharedCurrentlyEditingNode { get; set; }
[CascadingParameter(Name = "SelectedNodeRef")] public TreeNode? SharedCurrentlySelectedNode { get; set; }
[CascadingParameter(Name = "SetSelectedNodeRef")] public Action?>? SetSelectedNodeRef { get; set; }
private string Label
{
get => Node.Label;
set
{
if (!ShouldApply)
return;
Node.Label = value;
}
}
private string SelectedClass => SharedCurrentlySelectedNode == Node ? "node-selected" : "";
private bool IsHovered { get; set; }
private bool ShouldApply { get; set; }
private ElementReference _inputElementRef;
private bool _shouldFocusInput = false;
private string? _originalLabelBeforeEdit;
private bool IsThisNodeEditing => !ReadOnly && SharedCurrentlyEditingNode == Node;
private bool IsExpanded
{
get => Node.IsExpanded;
set
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (Node == null)
return;
Node.IsExpanded = value;
InvokeAsync(StateHasChanged);
}
}
private bool CanMoveUp => Node.Parent != null && Node.Parent.Children.IndexOf(Node) > 0;
private bool CanMoveDown
{
get
{
if (Node.Parent == null) return false;
var children = Node.Parent.Children;
int index = children.IndexOf(Node);
return index >= 0 && index < children.Count - 1;
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!ReadOnly && IsThisNodeEditing && _shouldFocusInput)
{
await _inputElementRef.FocusAsync();
_shouldFocusInput = false;
}
}
private void ToggleExpand()
{
Node.IsExpanded = !Node.IsExpanded;
}
private async Task EditNode()
{
if (ReadOnly || SetSharedCurrentlyEditingNode == null)
return; // Cascading value not provided
ShouldApply = true;
if (SharedCurrentlyEditingNode == null) // Nothing else is being edited
{
_originalLabelBeforeEdit = Node.Label; // Store original label
SetSharedCurrentlyEditingNode.Invoke(Node);
_shouldFocusInput = true;
// StateHasChanged will be triggered by the parent when SharedCurrentlyEditingNode updates
}
else if (SharedCurrentlyEditingNode == Node) // This node is already editing, ensure focus
{
_shouldFocusInput = true;
// _originalLabelBeforeEdit should already be set from when editing started
await InvokeAsync(StateHasChanged); // Ensure OnAfterRenderAsync is triggered for focus
}
// If SharedCurrentlyEditingNode is another node, do nothing to enforce single edit
}
private async Task SaveNode()
{
if ( ReadOnly)
return; // Do not save if in read-only mode
if (SetSharedCurrentlyEditingNode != null && SharedCurrentlyEditingNode == Node)
{
ShouldApply = true;
SetSharedCurrentlyEditingNode.Invoke(null); // Clear editing state
_originalLabelBeforeEdit = null; // Clear stored original label
}
await OnNodeChanged.InvokeAsync(Node);
}
private void AddChildNode()
{
if (ReadOnly)
return; // Do not save if in read-only mode
var newNode = new TreeNode { Data = default, Parent = Node }; // Or initialize with a default TItem
Node.Children.Add(newNode);
IsExpanded = true;
OnNodeChanged.InvokeAsync(Node); // Notify parent that this node (Node) has changed (added a child)
// Optionally, immediately start editing the new node:
// if (SetSharedCurrentlyEditingNode != null && SharedCurrentlyEditingNode == null)
// {
// SetSharedCurrentlyEditingNode.Invoke(newNode);
// _shouldFocusInput = true; // This would require newNode to be rendered and then focused.
// }
}
private async Task DeleteNode()
{
if (ReadOnly)
return; // Do not save if in read-only mode
if (SetSharedCurrentlyEditingNode != null && SharedCurrentlyEditingNode == Node)
{
SetSharedCurrentlyEditingNode.Invoke(null); // Clear editing state if deleting the node being edited
}
Node.Parent?.Children.Remove(Node);
await OnNodeChanged.InvokeAsync(Node.Parent ?? Node); // Notify about change, pass parent or node itself if root
}
private async Task MoveUp()
{
if (ReadOnly)
return; // Do not save if in read-only mode
if (CanMoveUp)
{
var index = Node.Parent!.Children.IndexOf(Node);
(Node.Parent.Children[index - 1], Node.Parent.Children[index]) = (Node.Parent.Children[index], Node.Parent.Children[index - 1]);
await OnNodeChanged.InvokeAsync(Node.Parent);
}
}
private async Task MoveDown()
{
if (ReadOnly)
return; // Do not save if in read-only mode
if (CanMoveDown)
{
var index = Node.Parent!.Children.IndexOf(Node);
(Node.Parent.Children[index + 1], Node.Parent.Children[index]) = (Node.Parent.Children[index], Node.Parent.Children[index + 1]);
await OnNodeChanged.InvokeAsync(Node.Parent);
}
}
private void OnMouseOver()
{
IsHovered = true;
}
private void OnMouseOut()
{
IsHovered = false;
}
private async Task HandleKeyDown(KeyboardEventArgs args)
{
if (ReadOnly)
return; // Do not save if in read-only mode
if (args.Key == "Escape")
{
await CancelEdit();
}
else if (args.Key == "Enter")
{
await SaveNode();
}
// Other keys will be handled by the browser's default behavior for input fields
}
private async Task CancelEdit()
{
if (ReadOnly)
return; // Do not save if in read-only mode
ShouldApply = false;
if (SetSharedCurrentlyEditingNode == null || SharedCurrentlyEditingNode != Node)
return;
if (_originalLabelBeforeEdit != null)
{
Node.Label = _originalLabelBeforeEdit; // Restore original label
}
SetSharedCurrentlyEditingNode.Invoke(null); // Clear editing state
_originalLabelBeforeEdit = null; // Clear stored original label
await InvokeAsync(StateHasChanged);
}
private async Task SelectNode()
{
SetSelectedNodeRef?.Invoke(Node);
await InvokeAsync(StateHasChanged);
}
}