Skip to content

Commit

Permalink
Add nested groups support in side nav
Browse files Browse the repository at this point in the history
  • Loading branch information
qtzar committed Sep 11, 2023
1 parent 10e3974 commit 053a675
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ fun copySiteWideAssets(exportDir: File) {
copySiteWideAsset(exportDir, "/css/admonition.css")
copySiteWideAsset(exportDir, "/js/admonition.js")
copySiteWideAsset(exportDir, "/js/reformat-mermaid.js")
copySiteWideAsset(exportDir, "/css/treeview.css")
copySiteWideAsset(exportDir, "/js/treeview.js")
}

private fun copySiteWideAsset(exportDir: File, asset: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,30 @@ class MenuViewModel(generatorContext: GeneratorContext, private val pageViewMode

private fun createMenuItem(title: String, href: String, exact: Boolean = true) =
LinkViewModel(pageViewModel, title, href, exact)

data class Node(val name: String, val children: MutableList<Node>)

val nestedSoftwareSystems = generatorContext.workspace.model.includedSoftwareSystems
.map {it.group + "/" + it.name}
.sortedBy {it.lowercase()}

fun buildTree(data: List<String>, delimiter: Char):MutableList<Node> {
val rootNode = Node("", mutableListOf())
for (path in data) {
val parts = path.split(delimiter)
var currentNode = rootNode

for (part in parts) {
val existingNode = currentNode.children.find { it.name == part }
currentNode = if (existingNode == null) {
val newNode = Node(part, mutableListOf())
currentNode.children.add(newNode)
newNode
} else {
existingNode
}
}
}
return rootNode.children
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ abstract class PageViewModel(protected val generatorContext: GeneratorContext) {
val headerBar by lazy { HeaderBarViewModel(this, generatorContext) }
val menu by lazy { MenuViewModel(generatorContext, this) }
val includeAutoReloading = generatorContext.serving
val flexmarkConfig by lazy {
buildFlexmarkConfig(generatorContext)

val flexmarkConfig by lazy {
buildFlexmarkConfig(generatorContext)
}
val includeAdmonition = flexmarkConfig.selectedExtensionMap.containsKey("Admonition")
val includeKatex = flexmarkConfig.selectedExtensionMap.containsKey("GitLab")

val configuration = generatorContext.workspace.views.configuration.properties
val includeTreeview = configuration.getOrDefault("generatr.nav.nestGroups", "false").toBoolean()

abstract val url: String
abstract val pageSubTitle: String
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package nl.avisi.structurizr.site.generatr.site.views

import kotlinx.html.*
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
import nl.avisi.structurizr.site.generatr.site.model.LinkViewModel
import nl.avisi.structurizr.site.generatr.site.model.MenuViewModel

fun DIV.menu(viewModel: MenuViewModel) {
fun DIV.menu(viewModel: MenuViewModel, nestGroups:Boolean) {
aside(classes = "menu p-3") {
generalSection(viewModel.generalItems)
softwareSystemsSection(viewModel.softwareSystemItems)
softwareSystemsSection(viewModel, nestGroups)
}
}

Expand All @@ -16,9 +17,18 @@ private fun ASIDE.generalSection(items: List<LinkViewModel>) {
menuItemLinks(items)
}

private fun ASIDE.softwareSystemsSection(items: List<LinkViewModel>) {
private fun ASIDE.softwareSystemsSection(viewModel: MenuViewModel, nestGroups:Boolean) {
p(classes = "menu-label") { +"Software systems" }
menuItemLinks(items)
if(nestGroups){
// Display SoftwareSystems as a nested lists
val rootNode = MenuViewModel.Node("", viewModel.buildTree(viewModel.nestedSoftwareSystems, "/".toCharArray()[0]))
ul(classes = "listree menu-list has-site-branding"){
buildHtmlTree(rootNode, viewModel).invoke(this)
}
} else {
// Display SoftwareSystems as a flat list
menuItemLinks(viewModel.softwareSystemItems)
}
}

private fun ASIDE.menuItemLinks(items: List<LinkViewModel>) {
Expand All @@ -30,3 +40,43 @@ private fun ASIDE.menuItemLinks(items: List<LinkViewModel>) {
}
}
}

private fun buildHtmlTree(node: MenuViewModel.Node, viewModel: MenuViewModel): UL.() -> Unit = {

if (node.name.isNotEmpty() && node.children.isEmpty()) {
val itemLink = viewModel.softwareSystemItems.find { it.title == node.name }
li {
if (itemLink != null) {
link(itemLink)
}
}
}

if (node.name.isNotEmpty() && node.children.isNotEmpty()) {
li {
if(node.name == "null"){
div(classes = "listree-submenu-heading") {
+"No Group"
}
} else {
div(classes = "listree-submenu-heading") {
+node.name
}
}

ul(classes = "listree-submenu-items") {
for (child in node.children) {
buildHtmlTree(child,viewModel).invoke(this)
}
}
}
}
if (node.name.isEmpty() && node.children.isNotEmpty()) {
ul(classes = "listree-submenu-items") {
for (child in node.children) {
buildHtmlTree(child, viewModel).invoke(this)
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ private fun HTML.headFragment(viewModel: PageViewModel) {
href = "./" + "/style-branding.css".asUrlToFile(viewModel.url)
)

if (viewModel.includeAdmonition)
if(viewModel.includeTreeview){
link(
rel = "stylesheet",
href = "../" + "/treeview.css".asUrlToFile(viewModel.url)
)
}

if (viewModel.includeAdmonition)
markdownAdmonitionStylesheet(viewModel)

if (viewModel.includeKatex)
Expand All @@ -54,7 +61,7 @@ private fun HTML.bodyFragment(viewModel: PageViewModel, block: DIV.() -> Unit) {

div(classes = "site-layout") {
id = "site"
menu(viewModel.menu)
menu(viewModel.menu, viewModel.includeTreeview)
div(classes = "container is-fluid has-background-white") {
block()
}
Expand All @@ -64,6 +71,19 @@ private fun HTML.bodyFragment(viewModel: PageViewModel, block: DIV.() -> Unit) {
updateSiteErrorHero()
if (viewModel.includeAdmonition)
markdownAdmonitionScript(viewModel)
if (viewModel.includeTreeview)

mermaidScript(viewModel)

if (viewModel.includeTreeview){
script(
type = ScriptType.textJavaScript,
src = "../" + "/treeview.js".asUrlToFile(viewModel.url)
) { }

script(
type = ScriptType.textJavaScript
) { unsafe { +"listree();" } }
}
}
}
35 changes: 35 additions & 0 deletions src/main/resources/assets/css/treeview.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.listree-submenu-heading {
cursor: pointer
}

ul.listree {
list-style: none
}

ul.listree-submenu-items {
list-style: none;
white-space: nowrap;
padding-left: 10px
}

div.listree-submenu-heading.collapsed:before {
content: "\25B6";
margin-right: 4px
}

div.listree-submenu-heading.expanded:before {
content: "\25BC";
margin-right: 4px
}

.scrollable-menu {
height: auto;
max-width: 800px;
overflow-y: hidden
}

.menu-list li ul {
border-left: 0;
margin: .25em;
padding-left: .75em;
}
65 changes: 65 additions & 0 deletions src/main/resources/assets/js/treeview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
function listree() {

const subMenuHeadingClass = "listree-submenu-heading";
const expandedClass = "expanded";
const collapsedClass = "collapsed";
const activeClass = "is-active";
const subMenuHeadings = document.getElementsByClassName(subMenuHeadingClass);

Array
.from(subMenuHeadings)
.forEach(function(subMenuHeading) {
// Collapse all the subMenuHeadings while searching for is-active class
let foundActive = false;
subMenuHeading.classList.add(collapsedClass);
subMenuHeading.nextElementSibling.style.display = "none";

// Check if this sub-menu heading is active
const liElements = subMenuHeading.nextElementSibling.children;
for (let i = 0; i < liElements.length; i++) {
if (liElements[i].hasChildNodes()) {
if (liElements[i].children[0].tagName === "A") {
if (liElements[i].children[0].classList.contains(activeClass)) {
foundActive = true;
break;
}
}
}
}

// Expand all parent sub-menus until root menu is reached
if (foundActive) {
subMenuHeading.classList.remove(collapsedClass);
subMenuHeading.classList.add(expandedClass);
subMenuHeading.nextElementSibling.style.display = "block";

let currentSubMenu = subMenuHeading.parentElement;
while (currentSubMenu !== null && !currentSubMenu.classList.contains("listree")) {
if(currentSubMenu.tagName === "UL"){
if(currentSubMenu.previousElementSibling != null) {
currentSubMenu.previousElementSibling.classList.remove(collapsedClass);
currentSubMenu.previousElementSibling.classList.add(expandedClass);
currentSubMenu.style.display = "block";
}
}
currentSubMenu = currentSubMenu.parentElement
}
}

// Add the eventlistener to all subMenuHeadings
subMenuHeading.addEventListener("click", function(event) {
event.preventDefault();
const subMenuList = event.target.nextElementSibling;
if (subMenuList.style.display === "none") {
subMenuHeading.classList.remove(collapsedClass);
subMenuHeading.classList.add(expandedClass);
subMenuHeading.nextElementSibling.style.display = "block";
} else {
subMenuHeading.classList.remove(expandedClass);
subMenuHeading.classList.add(collapsedClass);
subMenuHeading.nextElementSibling.style.display = "none";
}
event.stopPropagation();
});
});
}

0 comments on commit 053a675

Please sign in to comment.