|
6 | 6 | } |
7 | 7 |
|
8 | 8 | <div class="fancy-table-topbar d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2"> |
9 | | - @if (ShouldRenderPageSizeSelector) |
10 | | - { |
11 | | - <div class="fancy-page-size"> |
12 | | - @if (PageSizeTemplate is not null) |
13 | | - { |
14 | | - @PageSizeTemplate(pagedNodes.Context) |
15 | | - } |
16 | | - else |
17 | | - { |
18 | | - <label class="d-inline-flex align-items-center gap-2 small text-muted"> |
19 | | - <span>Rows per page</span> |
20 | | - <select class="form-select form-select-sm w-auto" value="@PaginationPageSize" @onchange="OnPageSizeChanged"> |
21 | | - @foreach (int option in EffectivePageSizeOptions) |
22 | | - { |
23 | | - <option value="@option">@(option == 0 ? "All" : option.ToString())</option> |
24 | | - } |
25 | | - </select> |
26 | | - </label> |
27 | | - } |
28 | | - </div> |
29 | | - } |
| 9 | + <div class="d-flex flex-wrap align-items-center gap-2"> |
| 10 | + @if (TreeActionsTemplate is not null) |
| 11 | + { |
| 12 | + @TreeActionsTemplate(TreeContext) |
| 13 | + } |
| 14 | + |
| 15 | + @if (ShouldRenderPageSizeSelector) |
| 16 | + { |
| 17 | + <div class="fancy-page-size"> |
| 18 | + @if (PageSizeTemplate is not null) |
| 19 | + { |
| 20 | + @PageSizeTemplate(pagedNodes.Context) |
| 21 | + } |
| 22 | + else |
| 23 | + { |
| 24 | + <label class="d-inline-flex align-items-center gap-2 small text-muted"> |
| 25 | + <span>Rows per page</span> |
| 26 | + <select class="form-select form-select-sm w-auto" value="@PaginationPageSize" @onchange="OnPageSizeChanged"> |
| 27 | + @foreach (int option in EffectivePageSizeOptions) |
| 28 | + { |
| 29 | + <option value="@option">@(option == 0 ? "All" : option.ToString())</option> |
| 30 | + } |
| 31 | + </select> |
| 32 | + </label> |
| 33 | + } |
| 34 | + </div> |
| 35 | + } |
| 36 | + </div> |
30 | 37 |
|
31 | 38 | @if (ToolbarTemplate is not null) |
32 | 39 | { |
|
244 | 251 | [Parameter] |
245 | 252 | public RenderFragment<FancyPaginationContext<TItem>>? PageSizeTemplate { get; set; } |
246 | 253 |
|
| 254 | + [Parameter] |
| 255 | + public RenderFragment<FancyTreeTableContext<TItem>>? TreeActionsTemplate { get; set; } |
| 256 | + |
247 | 257 | private readonly HashSet<string> HiddenColumns = new(StringComparer.Ordinal); |
248 | 258 | private readonly Dictionary<TItem, bool> ExpandedStates = new(); |
249 | 259 | private readonly List<TreeNodeState<TItem>> RootNodes = []; |
|
263 | 273 | private bool IsPaginationActive => PaginationEnabled && PaginationPageSize > 0; |
264 | 274 | private bool ShouldRenderPageSizeSelector => PaginationEnabled && ShowPageSizeSelector; |
265 | 275 | private int PaginationPageSize => paginationPageSize > 0 ? paginationPageSize : 0; |
| 276 | + private FancyTreeTableContext<TItem> TreeContext => BuildTreeContext(); |
266 | 277 |
|
267 | 278 | private string CurrentFilter => |
268 | 279 | string.IsNullOrWhiteSpace(AppliedSearchText) ? SearchText : AppliedSearchText; |
|
457 | 468 | private Task ToggleNodeAsync(TreeNodeState<TItem> node) |
458 | 469 | { |
459 | 470 | node.IsExpanded = !node.IsExpanded; |
460 | | - ExpandedStates[node.Item] = node.IsExpanded; |
| 471 | + if (node.IsExpanded) |
| 472 | + { |
| 473 | + ExpandedStates[node.Item] = true; |
| 474 | + } |
| 475 | + else |
| 476 | + { |
| 477 | + ExpandedStates.Remove(node.Item); |
| 478 | + } |
| 479 | + |
461 | 480 | return Task.CompletedTask; |
462 | 481 | } |
463 | 482 |
|
| 483 | + public async Task ExpandAllAsync() |
| 484 | + { |
| 485 | + ExpandedStates.Clear(); |
| 486 | + SetExpandedStateForAllNodes(expanded: true); |
| 487 | + await InvokeAsync(StateHasChanged); |
| 488 | + } |
| 489 | + |
| 490 | + public async Task CollapseAllAsync() |
| 491 | + { |
| 492 | + ExpandedStates.Clear(); |
| 493 | + SetExpandedStateForAllNodes(expanded: false); |
| 494 | + await InvokeAsync(StateHasChanged); |
| 495 | + } |
| 496 | + |
464 | 497 | private void HandleSearchKeyPress(KeyboardEventArgs e) |
465 | 498 | { |
466 | 499 | if (e.Key == "Enter") |
|
534 | 567 | setPageSizeAsync: SetPageSizeAsync); |
535 | 568 | } |
536 | 569 |
|
| 570 | + private FancyTreeTableContext<TItem> BuildTreeContext() |
| 571 | + { |
| 572 | + TreeNodeStatistics stats = GetTreeNodeStatistics(); |
| 573 | + |
| 574 | + return new FancyTreeTableContext<TItem>( |
| 575 | + rootItemCount: RootNodes.Count, |
| 576 | + visibleNodeCount: BuildVisibleNodes().Count, |
| 577 | + expandedNodeCount: stats.ExpandedNodeCount, |
| 578 | + hasExpandableNodes: stats.ExpandableNodeCount > 0, |
| 579 | + canExpandAny: stats.CollapsedExpandableNodeCount > 0, |
| 580 | + canCollapseAny: stats.ExpandedNodeCount > 0, |
| 581 | + expandAllAsync: ExpandAllAsync, |
| 582 | + collapseAllAsync: CollapseAllAsync); |
| 583 | + } |
| 584 | + |
| 585 | + private void SetExpandedStateForAllNodes(bool expanded) |
| 586 | + { |
| 587 | + foreach (TreeNodeState<TItem> rootNode in RootNodes) |
| 588 | + { |
| 589 | + SetExpandedStateRecursive(rootNode, expanded); |
| 590 | + } |
| 591 | + } |
| 592 | + |
| 593 | + private void SetExpandedStateRecursive(TreeNodeState<TItem> node, bool expanded) |
| 594 | + { |
| 595 | + if (node.HasChildren) |
| 596 | + { |
| 597 | + node.IsExpanded = expanded; |
| 598 | + |
| 599 | + if (expanded) |
| 600 | + { |
| 601 | + ExpandedStates[node.Item] = true; |
| 602 | + } |
| 603 | + } |
| 604 | + else |
| 605 | + { |
| 606 | + node.IsExpanded = false; |
| 607 | + } |
| 608 | + |
| 609 | + foreach (TreeNodeState<TItem> child in node.Children) |
| 610 | + { |
| 611 | + SetExpandedStateRecursive(child, expanded); |
| 612 | + } |
| 613 | + } |
| 614 | + |
| 615 | + private TreeNodeStatistics GetTreeNodeStatistics() |
| 616 | + { |
| 617 | + TreeNodeStatistics stats = new(); |
| 618 | + |
| 619 | + foreach (TreeNodeState<TItem> rootNode in RootNodes) |
| 620 | + { |
| 621 | + CollectTreeNodeStatistics(rootNode, stats); |
| 622 | + } |
| 623 | + |
| 624 | + return stats; |
| 625 | + } |
| 626 | + |
| 627 | + private static void CollectTreeNodeStatistics(TreeNodeState<TItem> node, TreeNodeStatistics stats) |
| 628 | + { |
| 629 | + if (node.HasChildren) |
| 630 | + { |
| 631 | + stats.ExpandableNodeCount++; |
| 632 | + |
| 633 | + if (node.IsExpanded) |
| 634 | + { |
| 635 | + stats.ExpandedNodeCount++; |
| 636 | + } |
| 637 | + else |
| 638 | + { |
| 639 | + stats.CollapsedExpandableNodeCount++; |
| 640 | + } |
| 641 | + } |
| 642 | + |
| 643 | + foreach (TreeNodeState<TItem> child in node.Children) |
| 644 | + { |
| 645 | + CollectTreeNodeStatistics(child, stats); |
| 646 | + } |
| 647 | + } |
| 648 | + |
537 | 649 | private IReadOnlyList<TreeNodeState<TItem>> ApplyPagination( |
538 | 650 | IReadOnlyList<TreeNodeState<TItem>> items, |
539 | 651 | FancyPaginationContext<TItem> paginationContext) |
|
638 | 750 | public required FancyPaginationContext<TItem> Context { get; init; } |
639 | 751 | } |
640 | 752 |
|
| 753 | + private sealed class TreeNodeStatistics |
| 754 | + { |
| 755 | + public int ExpandableNodeCount { get; set; } |
| 756 | + public int ExpandedNodeCount { get; set; } |
| 757 | + public int CollapsedExpandableNodeCount { get; set; } |
| 758 | + } |
| 759 | + |
641 | 760 | public void Dispose() => debounceTimer?.Dispose(); |
642 | 761 | } |
643 | 762 |
|
|
0 commit comments