TreeView — Navigation parity
This example recreates the navigation at the left from the same catalog through a typed model and bs-treeview. Expand branches, use arrow keys, follow links and reload to restore expansion. Both versions use the same navigation JavaScript; the example has its own saved expansion state.
The single BCL logo is omitted from the example's nodes. The website branding remains separate.
Razor
@model TreeViewExampleModel
<div class="view-example-nav-viewport docs-sticky">
<nav class="docs-nav-tree" aria-label="TagHelper navigation example">
<bs-treeview model="@Model.Navigation" class="docs-nav-treeview treeview-indent-1_25rem"
data-docs-treeview data-docs-selected-node-id="@TreeViewExampleModel.SelectedNodeId"
data-docs-storage-key="copelandsyst-docs-tree-example-expanded:v1" />
</nav>
</div>
<button id="navigation-example-reset" type="button" class="btn btn-outline-secondary mt-3">Reset this example</button>
<script type="module" src="/js/treeview-navigation-example.js"></script>
Model and hierarchy
using CopelandSyst.BootstrapComponents.ViewModels;
using CopelandSyst.Docs.DocsNavigation;
namespace CopelandSyst.Docs.Models;
public sealed class TreeViewExampleModel
{
public const string SelectedNodeId = "bcl:components:treeview:navigation";
public BootstrapTreeViewModel Navigation { get; } = new()
{
Id = "docsNavTagSample", Source = "dom", Size = "compact", Search = false,
SelectionMode = "single", Virtualize = false, StartAdornmentLayout = "adaptive",
};
public TreeViewExampleModel(DocsNavigationCatalog catalog)
{
foreach (var node in catalog.GetNodes(SelectedNodeId))
Navigation.NodesCapture.Add(ToModel(node));
}
private static BootstrapTreeViewNodeModel ToModel(DocsNavNode node)
{
var model = new BootstrapTreeViewNodeModel
{
NodeId = node.Id, Label = node.Label, BoundValue = node.Data.Href,
Kind = node.Kind, Expanded = node.Expanded,
Data = new { href = node.Data.Href, productId = node.Data.ProductId },
};
// The sample omits the single BCL logo; website branding is independent.
if (node.Id != "product:bcl" && node.Avatar is { } avatar)
{
model.AvatarSrc = avatar.Src;
model.AvatarAlt = avatar.Alt;
model.AvatarShape = avatar.Shape;
model.AvatarSize = avatar.Size;
model.AvatarLoading = avatar.Loading;
}
foreach (var child in node.Children)
model.ChildrenCapture.Add(ToModel(child));
return model;
}
}
Shared navigation behavior
import { TreeView } from '/_content/CopelandSyst.BootstrapComponents/lib/Bootstrap/dist/treeview/bs-treeview.js';
(function () {
function readExpanded(storageKey) {
try {
var stored = sessionStorage.getItem(storageKey);
if (!stored) {
return null;
}
var parsed = JSON.parse(stored);
return Array.isArray(parsed) ? parsed : [];
} catch {
return null;
}
}
function writeExpanded(tree, storageKey) {
var state = tree.getState();
sessionStorage.setItem(storageKey, JSON.stringify(state.expandedIds || []));
}
function selectedEventIsUserOriginated(event) {
var detail = event.detail || {};
return detail.inputType !== 'api';
}
function mergeSelectedPath(tree, expandedIds, selectedNodeId) {
var merged = new Set(expandedIds || []);
if (selectedNodeId && typeof tree.getAncestors === 'function') {
tree.getAncestors(selectedNodeId).forEach(function (id) {
merged.add(id);
});
}
return Array.from(merged);
}
function selectedRowIsVisible(host, selectedNodeId) {
if (!host || !selectedNodeId) {
return true;
}
var row = host.querySelector('.treeview-row[data-bs-id="' + CSS.escape(selectedNodeId) + '"]');
var scroller = host.closest('.docs-sticky');
if (!row || !scroller) {
return false;
}
var rowRect = row.getBoundingClientRect();
var scrollerRect = scroller.getBoundingClientRect();
return rowRect.top >= scrollerRect.top && rowRect.bottom <= scrollerRect.bottom;
}
function getNodeHref(node) {
return node && (node.boundValue || (node.data && node.data.href));
}
function openHref(href, originalEvent) {
if (!href) {
return;
}
if (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey || originalEvent.button === 1)) {
window.open(href, '_blank', 'noopener');
return;
}
window.location.href = href;
}
function initDocsNavigation() {
document.querySelectorAll('[data-docs-treeview]').forEach(function (host) {
var storageKey = host.getAttribute('data-docs-storage-key') || 'copelandsyst-docs-tree-expanded:v1';
var tree = TreeView.getOrCreateInstance(host, {
source: 'dom',
selectionMode: 'single',
size: 'compact',
virtualize: false
});
var selectedNodeId = host.getAttribute('data-docs-selected-node-id');
var savedExpandedIds = readExpanded(storageKey);
var expandedIds = savedExpandedIds ? mergeSelectedPath(tree, savedExpandedIds, selectedNodeId) : null;
tree.beginUpdate();
try {
if (expandedIds && expandedIds.length > 0) {
tree.setState({ expandedIds: expandedIds });
}
if (selectedNodeId) {
tree.setSelection([selectedNodeId], {
primaryId: selectedNodeId,
inputType: 'api',
focus: false,
reveal: false,
announce: false
});
}
} finally {
tree.endUpdate();
}
if (selectedNodeId && !selectedRowIsVisible(host, selectedNodeId)) {
tree.reveal(selectedNodeId);
}
host.addEventListener('expanded.bs.treeview', function () {
writeExpanded(tree, storageKey);
});
host.addEventListener('collapsed.bs.treeview', function () {
writeExpanded(tree, storageKey);
});
host.addEventListener('selected.bs.treeview', function (event) {
if (!selectedEventIsUserOriginated(event)) {
return;
}
var node = event.detail && event.detail.node;
var href = getNodeHref(node);
openHref(href, event.detail && event.detail.originalEvent);
});
host.addEventListener('auxclick', function (event) {
if (event.button !== 1) {
return;
}
var row = event.target.closest && event.target.closest('.treeview-row[data-bs-id]');
if (!row || !host.contains(row)) {
return;
}
var node = tree.getNode(row.getAttribute('data-bs-id'));
var href = getNodeHref(node);
if (!href) {
return;
}
event.preventDefault();
openHref(href, event);
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDocsNavigation);
} else {
initDocsNavigation();
}
})();
document.getElementById('navigation-example-reset').addEventListener('click', () => {
sessionStorage.removeItem('copelandsyst-docs-tree-example-expanded:v1');
location.reload();
});
Viewport CSS
.view-example-nav-viewport.docs-sticky {
position: relative;
top: auto;
height: 28rem;
max-height: 65vh;
overflow: auto;
border: 1px solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
}
#tasks-example {
max-height: 400px;
overflow-y: auto;
}
#tasks-example-output {
max-height: 24rem;
overflow: auto;
}
#ajax-demo { max-height: 400px; overflow: auto; }
#ajax-example pre { max-height: 24rem; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
#ajax-example[data-state="error"] #ajax-status { color: var(--bs-danger-text-emphasis); }
#ajax-example[data-state="saving"] #ajax-status { color: var(--bs-primary-text-emphasis); }
Original hand-written navigation markup
@model CopelandSyst.Docs.DocsNavigation.DocsNavViewModel
@inject INonceService nonceService
@using System.Text
@using System.Text.Encodings.Web
@using CopelandSyst.Docs.DocsNavigation
@functions {
private static string RenderNodes(IReadOnlyList<DocsNavNode> nodes, string? selectedNodeId)
{
var builder = new StringBuilder();
builder.AppendLine("<ul>");
foreach (var node in nodes)
{
RenderNode(builder, node, selectedNodeId, level: 1);
}
builder.AppendLine("</ul>");
return builder.ToString();
}
private static void RenderNode(StringBuilder builder, DocsNavNode node, string? selectedNodeId, int level)
{
var hasChildren = node.Children.Count > 0;
var isSelected = string.Equals(node.Id, selectedNodeId, StringComparison.Ordinal);
builder.Append("<li");
AppendAttribute(builder, "data-bs-id", node.Id);
AppendAttribute(builder, "data-bs-label", node.Label);
AppendAttribute(builder, "data-bs-kind", node.Kind);
AppendAttribute(builder, "data-docs-product-id", node.Data.ProductId);
if (!string.IsNullOrWhiteSpace(node.Data.Href))
{
AppendAttribute(builder, "data-bs-bound-value", node.Data.Href);
AppendAttribute(builder, "data-docs-href", node.Data.Href);
}
if (hasChildren)
{
AppendAttribute(builder, "data-bs-expanded", node.Expanded ? "true" : "false");
}
if (isSelected)
{
AppendAttribute(builder, "data-docs-selected", "true");
}
if (node.Avatar is not null)
{
AppendAttribute(builder, "data-bs-avatar-src", node.Avatar.Src);
AppendAttribute(builder, "data-bs-avatar-alt", node.Avatar.Alt);
AppendAttribute(builder, "data-bs-avatar-shape", node.Avatar.Shape);
AppendAttribute(builder, "data-bs-avatar-size", node.Avatar.Size);
AppendAttribute(builder, "data-bs-avatar-loading", node.Avatar.Loading);
}
builder.Append('>');
RenderNodePreview(builder, node, hasChildren, isSelected, level);
if (hasChildren)
{
builder.AppendLine();
builder.AppendLine("<ul>");
foreach (var child in node.Children)
{
RenderNode(builder, child, selectedNodeId, level + 1);
}
builder.AppendLine("</ul>");
}
builder.AppendLine("</li>");
}
private static void RenderNodePreview(StringBuilder builder, DocsNavNode node, bool hasChildren, bool isSelected, int level)
{
builder.Append("<div class=\"treeview-row");
if (isSelected)
{
builder.Append(" treeview-row-selected");
}
builder.Append(" treeview-level-");
builder.Append(level);
builder.Append('"');
if (hasChildren)
{
AppendAttribute(builder, "aria-expanded", node.Expanded ? "true" : "false");
}
builder.Append('>');
if (hasChildren)
{
builder.Append("<button type=\"button\" class=\"treeview-toggle\" data-bs-treeview-toggle=\"\" tabindex=\"-1\" aria-label=\"");
builder.Append(HtmlEncoder.Default.Encode(node.Expanded ? $"Collapse {node.Label}" : $"Expand {node.Label}"));
builder.Append("\"><span class=\"treeview-toggle-icon\" aria-hidden=\"true\"></span></button>");
}
else
{
builder.Append("<span class=\"treeview-toggle\" aria-hidden=\"true\"><span class=\"treeview-toggle-icon treeview-toggle-icon-empty\" aria-hidden=\"true\"></span></span>");
}
builder.Append("<span class=\"treeview-label-wrap\">");
if (node.Avatar is not null)
{
builder.Append("<img data-bs-treeview-avatar=\"\" src=\"");
builder.Append(HtmlEncoder.Default.Encode(node.Avatar.Src));
builder.Append("\" alt=\"");
builder.Append(HtmlEncoder.Default.Encode(node.Avatar.Alt));
builder.Append("\" loading=\"");
builder.Append(HtmlEncoder.Default.Encode(node.Avatar.Loading));
builder.Append("\" class=\"treeview-avatar treeview-avatar-");
builder.Append(HtmlEncoder.Default.Encode(node.Avatar.Shape));
builder.Append(" treeview-avatar-");
builder.Append(HtmlEncoder.Default.Encode(node.Avatar.Size));
builder.Append("\">");
}
builder.Append("<span class=\"treeview-label-stack\"><span class=\"treeview-label\">");
if (!string.IsNullOrWhiteSpace(node.Data.Href))
{
builder.Append("<a href=\"");
builder.Append(HtmlEncoder.Default.Encode(node.Data.Href));
builder.Append("\">");
builder.Append(HtmlEncoder.Default.Encode(node.Label));
builder.Append("</a>");
}
else
{
builder.Append(HtmlEncoder.Default.Encode(node.Label));
}
builder.Append("</span><span class=\"treeview-subtitle\" hidden></span></span><span class=\"treeview-defer-icon\" aria-hidden=\"true\" hidden></span></span></div>");
}
private static void AppendAttribute(StringBuilder builder, string name, string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
builder.Append(' ');
builder.Append(name);
builder.Append("=\"");
builder.Append(HtmlEncoder.Default.Encode(value));
builder.Append('"');
}
}
<nav class="docs-nav-tree" aria-label="Docs navigation">
<div id="docsNavTreeHost"
class="docs-nav-treeview treeview treeview-indent-1_25rem treeview-size-compact"
data-docs-treeview
data-bs-source="dom"
data-docs-selected-node-id="@Model.SelectedNodeId"
data-docs-selected-product-id="@Model.SelectedProductId">
@Html.Raw(RenderNodes(Model.Nodes, Model.SelectedNodeId))
</div>
<script nonce="@nonceService.Nonce">
(function () {
var storageKey = 'copelandsyst-docs-tree-expanded:v1';
var host = document.getElementById('docsNavTreeHost');
if (!host) {
return;
}
function getDirectChild(element, selector) {
for (var i = 0; i < element.children.length; i += 1) {
if (element.children[i].matches(selector)) {
return element.children[i];
}
}
return null;
}
function readExpandedIds() {
try {
var stored = sessionStorage.getItem(storageKey);
if (!stored) {
return null;
}
var parsed = JSON.parse(stored);
return Array.isArray(parsed) ? parsed : null;
} catch {
return null;
}
}
function addSelectedAncestors(expandedIds) {
var selectedNode = host.querySelector('[data-docs-selected="true"]');
var parentNode = selectedNode && selectedNode.parentElement
? selectedNode.parentElement.closest('li[data-bs-id]')
: null;
while (parentNode) {
expandedIds.add(parentNode.getAttribute('data-bs-id'));
parentNode = parentNode.parentElement
? parentNode.parentElement.closest('li[data-bs-id]')
: null;
}
}
function applyExpandedIds(expandedIds) {
host.querySelectorAll('li[data-bs-id]').forEach(function (node) {
var group = getDirectChild(node, 'ul');
if (!group) {
return;
}
var id = node.getAttribute('data-bs-id');
var expanded = expandedIds.has(id);
var row = getDirectChild(node, '.treeview-row');
var toggle = row ? getDirectChild(row, '.treeview-toggle') : null;
var label = node.getAttribute('data-bs-label') || '';
node.setAttribute('data-bs-expanded', expanded ? 'true' : 'false');
group.hidden = !expanded;
if (row) {
row.setAttribute('aria-expanded', expanded ? 'true' : 'false');
}
if (toggle) {
toggle.setAttribute('aria-label', (expanded ? 'Collapse ' : 'Expand ') + label);
}
});
}
var savedExpandedIds = readExpandedIds();
if (savedExpandedIds) {
var expandedIds = new Set(savedExpandedIds);
addSelectedAncestors(expandedIds);
applyExpandedIds(expandedIds);
}
var selected = host.querySelector('[data-docs-selected="true"] > .treeview-row');
var scroller = host && host.closest('.docs-sticky');
if (!selected || !scroller) {
return;
}
var selectedRect = selected.getBoundingClientRect();
var scrollerRect = scroller.getBoundingClientRect();
var targetOffset = selectedRect.top - scrollerRect.top - Math.max(0, (scroller.clientHeight - selectedRect.height) / 2);
scroller.scrollTop = Math.max(0, scroller.scrollTop + targetOffset);
})();
</script>
</nav>
Navigation data and types
namespace CopelandSyst.Docs.DocsNavigation;
public sealed record DocsProduct(
string Id,
string Label,
string Category,
string RootNodeId,
string? Href,
string? BrandMarkUrl,
string BrandMarkAlt,
string Description);
public sealed record DocsNavAvatar(
string Src,
string Alt,
string Shape,
string Size,
string Loading);
public sealed record DocsNavNodeData(
string? Href,
string ProductId);
public sealed record DocsNavNode(
string Id,
string Label,
DocsNavNodeData Data,
bool Expanded,
string Kind,
DocsNavAvatar? Avatar,
IReadOnlyList<DocsNavNode> Children);
public sealed record DocsNavViewModel(
IReadOnlyList<DocsNavNode> Nodes,
IReadOnlyList<DocsProduct> Products,
string? SelectedNodeId,
string? SelectedProductId,
DocsProduct? SelectedProduct);
namespace CopelandSyst.Docs.DocsNavigation;
public sealed class DocsNavigationCatalog
{
private static readonly DocsNavAvatar BclAvatar = new(
Src: "/images/logo.png",
Alt: "Bootstrap Component Library",
Shape: "rounded",
Size: "sm",
Loading: "lazy");
public IReadOnlyList<DocsProduct> GetProducts()
=>
[
new(
Id: "bcl",
Label: "Bootstrap Component Library",
Category: "Libraries",
RootNodeId: "product:bcl",
Href: "/Bcl/GettingStarted",
BrandMarkUrl: "~/images/logo.png",
BrandMarkAlt: "Bootstrap Component Library",
Description: "Bootstrap-based components, tag helpers, and docs examples."),
new(
Id: "aegisentry",
Label: "AegiSentry",
Category: "Libraries",
RootNodeId: "product:aegisentry",
Href: null,
BrandMarkUrl: null,
BrandMarkAlt: "AegiSentry",
Description: "Secure column, key ring, visitor, and protected upload services."),
new(
Id: "standard-tags",
Label: "Standard Tags Library",
Category: "Libraries",
RootNodeId: "product:standard-tags",
Href: null,
BrandMarkUrl: null,
BrandMarkAlt: "Standard Tags Library",
Description: "Standardized tag helper infrastructure and supporting UI primitives."),
new(
Id: "configuration",
Label: "Configuration",
Category: "Libraries",
RootNodeId: "product:configuration",
Href: "/Configuration/GettingStarted",
BrandMarkUrl: null,
BrandMarkAlt: "Configuration",
Description: "Credential-authority-aware configuration graphs and option models."),
new(
Id: "azure-services-management",
Label: "Azure Services Management",
Category: "Libraries",
RootNodeId: "product:azure-services-management",
Href: null,
BrandMarkUrl: null,
BrandMarkAlt: "Azure Services Management",
Description: "Azure service configuration, resource option, and credential helpers."),
new(
Id: "csaed",
Label: "csaed",
Category: "Tools",
RootNodeId: "tool:csaed",
Href: "/Csaed/GettingStarted",
BrandMarkUrl: null,
BrandMarkAlt: "csaed",
Description: "CopelandSyst Azure Environment Descriptor command-line tool."),
new(
Id: "dirty-bits",
Label: "DirtyBits",
Category: "Tools",
RootNodeId: "tool:dirty-bits",
Href: null,
BrandMarkUrl: null,
BrandMarkAlt: "DirtyBits",
Description: "Dirty bits generator command-line tool."),
];
public IReadOnlyList<DocsNavNode> GetNodes(string? selectedNodeId)
=>
[
CategoryNode("category:libraries", "Libraries", expanded: true,
[
ProductNode(
id: "product:bcl",
label: "Bootstrap Component Library",
href: "/Bcl/GettingStarted",
expanded: selectedNodeId?.StartsWith("bcl:", StringComparison.Ordinal) == true,
avatar: BclAvatar,
children:
[
Node("bcl:getting-started", "Getting Started", "/Bcl/GettingStarted", "bcl"),
Node("bcl:debug-diagnostics", "Debug Diagnostics", "/Bcl/DebugDiagnostics", "bcl"),
Branch("bcl:components", "Components", "bcl",
[
Node("bcl:components:accordion", "Accordion", "/Bcl/Accordion", "bcl"),
Node("bcl:components:alerts", "Alerts", "/Bcl/Alerts", "bcl"),
Node("bcl:components:badges", "Badges", "/Bcl/Badges", "bcl"),
Node("bcl:components:breadcrumbs", "Breadcrumbs", "/Bcl/Breadcrumbs", "bcl"),
Node("bcl:components:buttons", "Buttons", "/Bcl/Buttons", "bcl"),
Node("bcl:components:button-group", "Button Group", "/Bcl/ButtonGroup", "bcl"),
Node("bcl:components:cards", "Cards", "/Bcl/Cards", "bcl"),
Node("bcl:components:carousel", "Carousel", "/Bcl/Carousel", "bcl"),
Node("bcl:components:close-button", "Close Button", "/Bcl/CloseButton", "bcl"),
Node("bcl:components:collapse", "Collapse", "/Bcl/Collapse", "bcl"),
Node("bcl:components:dropdowns", "Dropdowns", "/Bcl/Dropdowns", "bcl"),
Node("bcl:components:list-group", "List Group", "/Bcl/ListGroup", "bcl"),
Branch("bcl:components:listview", "ListView", "bcl",
[
Node("bcl:components:listview:general", "General", "/Bcl/ListView", "bcl"),
Node("bcl:components:listview:checking", "Ajax (Checking)", "/Bcl/ListView/Ajax/Checking", "bcl"),
Node("bcl:components:listview:reordering", "Ajax (Drag and drop)", "/Bcl/ListView/Ajax/Reordering", "bcl"),
Node("bcl:components:listview:renaming", "Ajax (Renaming)", "/Bcl/ListView/Ajax/Renaming", "bcl"),
Node("bcl:components:listview:selection", "Ajax (Selection)", "/Bcl/ListView/Ajax/Selection", "bcl"),
Node("bcl:components:listview:scrolling", "Scrolling (100 items)", "/Bcl/ListView/Scrolling", "bcl"),
]),
Branch("bcl:components:treeview", "TreeView", "bcl",
[
Node("bcl:components:treeview:general", "General", "/Bcl/TreeView", "bcl"),
Node("bcl:components:treeview:checking", "Ajax (Checking)", "/Bcl/TreeView/Ajax/Checking", "bcl"),
Node("bcl:components:treeview:reordering", "Ajax (Drag and drop)", "/Bcl/TreeView/Ajax/Reordering", "bcl"),
Node("bcl:components:treeview:renaming", "Ajax (Renaming)", "/Bcl/TreeView/Ajax/Renaming", "bcl"),
Node("bcl:components:treeview:selection", "Ajax (Selection)", "/Bcl/TreeView/Ajax/Selection", "bcl"),
Node("bcl:components:treeview:lazy-loading", "Ajax (Lazy loading)", "/Bcl/TreeView/Ajax/LazyLoading", "bcl"),
Node("bcl:components:treeview:navigation", "Navigation parity", "/Bcl/TreeView/Navigation", "bcl"),
]),
Branch("bcl:components:treeview-dropdown", "TreeViewDropdown", "bcl",
[
Node("bcl:components:treeview-dropdown:general", "General", "/Bcl/TreeViewDropdown", "bcl"),
Node("bcl:components:treeview-dropdown:selection", "Ajax (Selection)", "/Bcl/TreeViewDropdown/Ajax/Selection", "bcl"),
Node("bcl:components:treeview-dropdown:checking", "Ajax (Checking)", "/Bcl/TreeViewDropdown/Ajax/Checking", "bcl"),
Node("bcl:components:treeview-dropdown:reordering", "Ajax (Drag and drop)", "/Bcl/TreeViewDropdown/Ajax/Reordering", "bcl"),
Node("bcl:components:treeview-dropdown:renaming", "Ajax (Renaming)", "/Bcl/TreeViewDropdown/Ajax/Renaming", "bcl"),
Node("bcl:components:treeview-dropdown:lazy-loading", "Ajax (Lazy loading)", "/Bcl/TreeViewDropdown/Ajax/LazyLoading", "bcl"),
Node("bcl:components:treeview-dropdown:modal", "Inside a modal / Mobile", "/Bcl/TreeViewDropdown/Modal", "bcl"),
]),
Branch("bcl:components:listview-dropdown", "ListViewDropdown", "bcl",
[
Node("bcl:components:listview-dropdown:general", "General", "/Bcl/ListViewDropdown", "bcl"),
Node("bcl:components:listview-dropdown:selection", "Ajax (Selection)", "/Bcl/ListViewDropdown/Ajax/Selection", "bcl"),
Node("bcl:components:listview-dropdown:checking", "Ajax (Checking)", "/Bcl/ListViewDropdown/Ajax/Checking", "bcl"),
Node("bcl:components:listview-dropdown:reordering", "Ajax (Drag and drop)", "/Bcl/ListViewDropdown/Ajax/Reordering", "bcl"),
Node("bcl:components:listview-dropdown:renaming", "Ajax (Renaming)", "/Bcl/ListViewDropdown/Ajax/Renaming", "bcl"),
Node("bcl:components:listview-dropdown:modal", "Inside a modal / Mobile", "/Bcl/ListViewDropdown/Modal", "bcl"),
]),
Node("bcl:components:view-model-merging", "TreeView / ListView Model Merging", "/Bcl/ViewModelMerging", "bcl"),
Node("bcl:components:modal", "Modal", "/Bcl/Modal", "bcl"),
Node("bcl:components:navbar", "Navbar", "/Bcl/Navbar", "bcl"),
Node("bcl:components:navs-tabs", "Navs and Tabs", "/Bcl/NavsTabs", "bcl"),
Node("bcl:components:offcanvas", "Offcanvas", "/Bcl/Offcanvas", "bcl"),
Node("bcl:components:pagination", "Pagination", "/Bcl/Pagination", "bcl"),
Node("bcl:components:placeholders", "Placeholders", "/Bcl/Placeholders", "bcl"),
Node("bcl:components:popovers", "Popovers", "/Bcl/Popovers", "bcl"),
Node("bcl:components:progress", "Progress", "/Bcl/Progress", "bcl"),
Node("bcl:components:scrollspy", "Scrollspy", "/Bcl/Scrollspy", "bcl"),
Node("bcl:components:spinners", "Spinners", "/Bcl/Spinners", "bcl"),
Node("bcl:components:toasts", "Toasts", "/Bcl/Toasts", "bcl"),
Node("bcl:components:tooltips", "Tooltips", "/Bcl/Tooltips", "bcl"),
]),
Branch("bcl:forms", "Forms", "bcl",
[
Node("bcl:forms:overview", "Overview", "/Bcl/Forms", "bcl"),
Node("bcl:forms:checks-radios", "Checks and Radios", "/Bcl/ChecksRadios", "bcl"),
Node("bcl:forms:floating-labels", "Floating Labels", "/Bcl/FloatingLabels", "bcl"),
Node("bcl:forms:input-groups", "Input Groups", "/Bcl/InputGroups", "bcl"),
Node("bcl:forms:validation", "Validation", "/Bcl/Validation", "bcl"),
]),
Branch("bcl:layout", "Layout and Content", "bcl",
[
Node("bcl:layout:additive-elements", "Additive Elements", "/Bcl/AdditiveElements", "bcl"),
Node("bcl:layout:grid", "Grid", "/Bcl/Grid", "bcl"),
Node("bcl:layout:images-figures", "Images and Figures", "/Bcl/ImagesFigures", "bcl"),
Node("bcl:layout:tables", "Tables", "/Bcl/Tables", "bcl"),
Node("bcl:layout:icons", "Icons", "/Bcl/Icons", "bcl"),
]),
Branch("bcl:helpers", "Helpers", "bcl",
[
Node("bcl:helpers:clearfix", "Clearfix", "/Bcl/Helpers/Clearfix", "bcl"),
Node("bcl:helpers:color-background", "Color and Background", "/Bcl/Helpers/ColorBackground", "bcl"),
Node("bcl:helpers:colored-links", "Colored Links", "/Bcl/Helpers/ColoredLinks", "bcl"),
Node("bcl:helpers:focus-ring", "Focus Ring", "/Bcl/Helpers/FocusRing", "bcl"),
Node("bcl:helpers:icon-link", "Icon Link", "/Bcl/Helpers/IconLink", "bcl"),
Node("bcl:helpers:position", "Position", "/Bcl/Helpers/Position", "bcl"),
Node("bcl:helpers:ratio", "Ratio", "/Bcl/Helpers/Ratio", "bcl"),
Node("bcl:helpers:stacks", "Stacks", "/Bcl/Helpers/Stacks", "bcl"),
Node("bcl:helpers:stretched-link", "Stretched Link", "/Bcl/Helpers/StretchedLink", "bcl"),
Node("bcl:helpers:text-truncation", "Text Truncation", "/Bcl/Helpers/TextTruncation", "bcl"),
Node("bcl:helpers:vertical-rule", "Vertical Rule", "/Bcl/Helpers/VerticalRule", "bcl"),
Node("bcl:helpers:visually-hidden", "Visually Hidden", "/Bcl/Helpers/VisuallyHidden", "bcl"),
]),
Branch("bcl:utilities", "Utilities", "bcl",
[
Node("bcl:utilities:api", "API", "/Bcl/Utilities/Api", "bcl"),
Node("bcl:utilities:background", "Background", "/Bcl/Utilities/Background", "bcl"),
Node("bcl:utilities:borders", "Borders", "/Bcl/Utilities/Borders", "bcl"),
Node("bcl:utilities:colors", "Colors", "/Bcl/Utilities/Colors", "bcl"),
Node("bcl:utilities:display", "Display", "/Bcl/Utilities/Display", "bcl"),
Node("bcl:utilities:flex", "Flex", "/Bcl/Utilities/Flex", "bcl"),
Node("bcl:utilities:float", "Float", "/Bcl/Utilities/Float", "bcl"),
Node("bcl:utilities:interactions", "Interactions", "/Bcl/Utilities/Interactions", "bcl"),
Node("bcl:utilities:link", "Link", "/Bcl/Utilities/Link", "bcl"),
Node("bcl:utilities:object-fit", "Object Fit", "/Bcl/Utilities/ObjectFit", "bcl"),
Node("bcl:utilities:opacity", "Opacity", "/Bcl/Utilities/Opacity", "bcl"),
Node("bcl:utilities:overflow", "Overflow", "/Bcl/Utilities/Overflow", "bcl"),
Node("bcl:utilities:position", "Position", "/Bcl/Utilities/Position", "bcl"),
Node("bcl:utilities:shadows", "Shadows", "/Bcl/Utilities/Shadows", "bcl"),
Node("bcl:utilities:sizing", "Sizing", "/Bcl/Utilities/Sizing", "bcl"),
Node("bcl:utilities:spacing", "Spacing", "/Bcl/Utilities/Spacing", "bcl"),
Node("bcl:utilities:text", "Text", "/Bcl/Utilities/Text", "bcl"),
Node("bcl:utilities:vertical-align", "Vertical Align", "/Bcl/Utilities/VerticalAlign", "bcl"),
Node("bcl:utilities:visibility", "Visibility", "/Bcl/Utilities/Visibility", "bcl"),
Node("bcl:utilities:z-index", "Z-Index", "/Bcl/Utilities/ZIndex", "bcl"),
]),
]),
ProductNode("product:aegisentry", "AegiSentry", null, expanded: false, avatar: null, children: []),
ProductNode("product:standard-tags", "Standard Tags Library", null, expanded: false, avatar: null, children: []),
ProductNode(
id: "product:configuration",
label: "Configuration",
href: "/Configuration/GettingStarted",
expanded: selectedNodeId?.StartsWith("configuration:", StringComparison.Ordinal) == true,
avatar: null,
children:
[
Node("configuration:getting-started", "Getting Started", "/Configuration/GettingStarted", "configuration"),
Node("configuration:credential-authority", "Credential Authority", "/Configuration/CredentialAuthority", "configuration"),
Node("configuration:configuration-graphs", "Configuration Graphs", "/Configuration/ConfigurationGraphs", "configuration"),
Node("configuration:break-glass", "Break-Glass", "/Configuration/BreakGlass", "configuration"),
Node("configuration:compatibility", "Compatibility", "/Configuration/Compatibility", "configuration"),
]),
ProductNode("product:azure-services-management", "Azure Services Management", null, expanded: false, avatar: null, children: []),
]),
CategoryNode("category:tools", "Tools", expanded: true,
[
ProductNode(
id: "tool:csaed",
label: "csaed",
href: "/Csaed/GettingStarted",
expanded: selectedNodeId?.StartsWith("csaed:", StringComparison.Ordinal) == true,
avatar: null,
children:
[
Node("csaed:getting-started", "Getting Started", "/Csaed/GettingStarted", "csaed"),
]),
ToolNode("tool:dirty-bits", "DirtyBits", "dirty-bits"),
]),
];
public DocsProduct? FindProductForNode(string? nodeId)
{
if (string.IsNullOrWhiteSpace(nodeId))
return null;
var products = GetProducts();
var product = products.FirstOrDefault(candidate => candidate.RootNodeId == nodeId);
if (product is not null)
return product;
var prefix = nodeId.Split(':', 2)[0];
return products.FirstOrDefault(candidate => candidate.Id == prefix);
}
private static DocsNavNode ProductNode(
string id,
string label,
string? href,
bool expanded,
DocsNavAvatar? avatar,
IReadOnlyList<DocsNavNode> children)
=> new(id, label, new DocsNavNodeData(href, ProductIdFromNodeId(id)), expanded, "container", avatar, children);
private static DocsNavNode CategoryNode(
string id,
string label,
bool expanded,
IReadOnlyList<DocsNavNode> children)
=> new(id, label, new DocsNavNodeData(null, ProductIdFromNodeId(id)), expanded, "container", null, children);
private static DocsNavNode ToolNode(string id, string label, string productId)
=> new(id, label, new DocsNavNodeData(null, productId), false, "leaf", null, []);
private static DocsNavNode Branch(string id, string label, string productId, IReadOnlyList<DocsNavNode> children)
=> new(id, label, new DocsNavNodeData(null, productId), true, "container", null, children);
private static DocsNavNode Node(string id, string label, string href, string productId)
=> new(id, label, new DocsNavNodeData(href, productId), false, "leaf", null, []);
private static string ProductIdFromNodeId(string nodeId)
=> nodeId.StartsWith("product:", StringComparison.Ordinal)
? nodeId["product:".Length..]
: nodeId.Split(':', 2)[0];
}
Model merging checks also demonstrate nested bs-treeview-node captures and a supplied node model with additional tag traits. ListView has its own example.