@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 (Node.Children.Any()) { } else if (LabelTemplate == null) {
}
@if (!ReadOnly && IsThisNodeEditing) { } else { if (LabelTemplate != null) { @LabelTemplate(Node) } else { @Node.Label } }
@if (!ReadOnly) {
@if (CanMoveUp) { } @if (CanMoveDown) { }
}
@if (BodyTemplate != null) {
@BodyTemplate(Node)
}
@if (IsExpanded) { }
@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); } }