Skip to content

Commit

Permalink
Scaladoc - add option for dynamic side menu
Browse files Browse the repository at this point in the history
  • Loading branch information
Florian3k committed Dec 22, 2023
1 parent beaf7b4 commit 075ee34
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 30 deletions.
4 changes: 4 additions & 0 deletions project/ScaladocGeneration.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ object ScaladocGeneration {
def key: String = "-quick-links"
}

case class DynamicSideMenu(value: Boolean) extends Arg[Boolean] {
def key: String = "-dynamic-side-menu"
}

import _root_.scala.reflect._

trait GenerationConfig {
Expand Down
195 changes: 170 additions & 25 deletions scaladoc/resources/dotty_res/scripts/ux.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const attrsToCopy = [
"data-githubContributorsUrl",
"data-githubContributorsFilename",
"data-pathToRoot",
"data-rawLocation",
"data-dynamicSideMenu",
]

/**
Expand All @@ -25,7 +27,7 @@ function savePageState(doc) {
}
return {
mainDiv: doc.querySelector("#main")?.innerHTML,
leftColumn: doc.querySelector("#leftColumn").innerHTML,
leftColumn: dynamicSideMenu ? null : doc.querySelector("#leftColumn").innerHTML,
title: doc.title,
attrs,
};
Expand All @@ -38,12 +40,15 @@ function savePageState(doc) {
function loadPageState(doc, saved) {
doc.title = saved.title;
doc.querySelector("#main").innerHTML = saved.mainDiv;
doc.querySelector("#leftColumn").innerHTML = saved.leftColumn;
if (!dynamicSideMenu)
doc.querySelector("#leftColumn").innerHTML = saved.leftColumn;
for (const attr of attrsToCopy) {
doc.documentElement.setAttribute(attr, saved.attrs[attr]);
}
}

const attachedElements = new WeakSet()

function attachAllListeners() {
if (observer) {
observer.disconnect();
Expand Down Expand Up @@ -97,19 +102,19 @@ function attachAllListeners() {
}
}

document
.querySelectorAll(".documentableElement .signature")
.forEach((signature) => {
const short = signature.querySelector(".signature-short");
const long = signature.querySelector(".signature-long");
const extender = document.createElement("span");
const extenderDots = document.createTextNode("...");
extender.appendChild(extenderDots);
extender.classList.add("extender");
if (short && long && signature.children[1].hasChildNodes()) {
signature.children[0].append(extender);
}
});
document
.querySelectorAll(".documentableElement .signature")
.forEach((signature) => {
const short = signature.querySelector(".signature-short");
const long = signature.querySelector(".signature-long");
const extender = document.createElement("span");
const extenderDots = document.createTextNode("...");
extender.appendChild(extenderDots);
extender.classList.add("extender");
if (short && long && signature.children[1].hasChildNodes()) {
signature.children[0].append(extender);
}
});

const documentableLists = document.getElementsByClassName("documentableList");
[...documentableLists].forEach((list) => {
Expand Down Expand Up @@ -151,6 +156,8 @@ document
return;
}
const url = new URL(href);
if (attachedElements.has(el)) return;
attachedElements.add(el);
el.addEventListener("click", (e) => {
if (
url.href.replace(/#.*/, "") === window.location.href.replace(/#.*/, "")
Expand All @@ -166,6 +173,7 @@ document
e.preventDefault();
e.stopPropagation();
$.get(href, function (data) {
const oldLoc = getRawLoc();
if (window.history.state === null) {
window.history.replaceState(savePageState(document), "");
}
Expand All @@ -174,6 +182,11 @@ document
const state = savePageState(parsedDocument);
window.history.pushState(state, "", href);
loadPageState(document, state);
const newLoc = getRawLoc();
if (dynamicSideMenu) {
updateMenu(oldLoc, newLoc);
}

window.dispatchEvent(new Event(DYNAMIC_PAGE_LOAD));
document
.querySelector("#main")
Expand All @@ -182,11 +195,15 @@ document
});
});

$(".ar").on("click", function (e) {
$(this).parent().parent().toggleClass("expanded");
$(this).toggleClass("expanded");
e.stopPropagation();
});
document.querySelectorAll('.ar').forEach((el) => {
if (attachedElements.has(el)) return;
attachedElements.add(el);
el.addEventListener('click', (e) => {
e.stopPropagation();
el.parentElement.parentElement.classList.toggle("expanded");
el.classList.toggle("expanded");
})
})

document.querySelectorAll(".documentableList .ar").forEach((arrow) => {
arrow.addEventListener("click", () => {
Expand All @@ -195,7 +212,9 @@ document
});
});

document.querySelectorAll(".nh").forEach((el) =>
document.querySelectorAll(".nh").forEach((el) => {
if (attachedElements.has(el)) return;
attachedElements.add(el);
el.addEventListener("click", () => {
if (
el.lastChild.href.replace("#", "") ===
Expand All @@ -206,8 +225,8 @@ document
} else {
el.lastChild.click();
}
}),
);
});
});

const toggleShowAllElem = (element) => {
if (element.textContent == "Show all") {
Expand Down Expand Up @@ -345,7 +364,7 @@ window.addEventListener(DYNAMIC_PAGE_LOAD, () => {
attachAllListeners();
});

window.addEventListener("dynamicPageLoad", () => {
window.addEventListener(DYNAMIC_PAGE_LOAD, () => {
const sideMenuOpen = sessionStorage.getItem("sideMenuOpen");
if (sideMenuOpen) {
if (document.querySelector("#leftColumn").classList.contains("show")) {
Expand All @@ -365,10 +384,136 @@ window.addEventListener("dynamicPageLoad", () => {
}
});

let dynamicSideMenu = false;
/** @param {Element} elem @param {boolean} hide */
function updatePath(elem, hide, first = true) {
if (elem.classList.contains("side-menu")) return;
const span = elem.firstElementChild
const btn = span.firstElementChild
if (hide) {
elem.classList.remove("expanded");
span.classList.remove("h100", "selected", "expanded", "cs");
if (btn) btn.classList.remove("expanded");
} else {
elem.classList.add("expanded");
span.classList.add("h100", "expanded", "cs");
if (btn) btn.classList.add("expanded");
if (first) span.classList.add("selected");
}
updatePath(elem.parentElement, hide, false);
}
let updateMenu = null;
function getRawLoc() {
return document.documentElement.getAttribute("data-rawLocation")?.split("/")?.filter(c => c !== "");
}

/**
* @template {keyof HTMLElementTagNameMap} T
* @param {T} el type of element to create
* @param {{ cls?: string | null, id?: string | null, href?: string | null }} attrs element attributes
* @param {Array<HTMLElement | string | null>} chldr element children
* @returns {HTMLElementTagNameMap[T]}
*/
function render(el, { cls = null, id = null, href = null, loc = null } = {}, chldr = []) {
const r = document.createElement(el);
if (cls) cls.split(" ").filter(x => x !== "").forEach(c => r.classList.add(c));
if (id) r.id = id;
if (href) r.href = href;
if (loc) r.setAttribute("data-loc", loc);
chldr.filter(c => c !== null).forEach(c =>
r.appendChild(typeof c === "string" ? document.createTextNode(c) : c)
);
return r;
}
function renderDynamicSideMenu() {
const pathToRoot = document.documentElement.getAttribute("data-pathToRoot")
const path = pathToRoot + "dynamicSideMenu.json";
const rawLocation = getRawLoc();
const baseUrl = window.location.pathname.split("/").slice(0,
-1 - pathToRoot.split("/").filter(c => c != "").length
);
function linkTo(loc) {
return `${baseUrl}/${loc.join("/")}.html`;
}
fetch(path).then(r => r.json()).then(menu => {
function renderNested(item, nestLevel, prefix, isApi) {
const name = item.name;
const newName =
isApi && item.kind === "package" && name.startsWith(prefix + ".")
? name.substring(prefix.length + 1)
: name;
const newPrefix =
prefix == ""
? newName
: prefix + "." + newName;
const chldr =
item.children.map(x => renderNested(x, nestLevel + 1, newPrefix, isApi));
const link = render("span", { cls: `nh ${isApi ? "" : "de"}` }, [
chldr.length ? render("button", { cls: "ar icon-button" }) : null,
render("a", { href: linkTo(item.location) }, [
item.kind && render("span", { cls: `micon ${item.kind.slice(0, 2)}` }),
render("span", {}, [newName]),
]),
]);
const loc = item.location.join("/");
const ret = render("div", { cls: `ni n${nestLevel}`, loc: item.location.join("/") }, [link, ...chldr]);
return ret;
}
const d = render("div", { cls: "switcher-container" }, [
menu.docs && render("a", {
id: "docs-nav-button",
cls: "switcher h100",
href: linkTo(menu.docs.location)
}, ["Docs"]),
menu.api && render("a", {
id: "api-nav-button",
cls: "switcher h100",
href: linkTo(menu.api.location)
}, ["API"]),
]);
const d1 = menu.docs && render("nav", { cls: "side-menu", id: "docs-nav" },
menu.docs.children.map(item => renderNested(item, 0, "", false))
);
const d2 = menu.api && render("nav", { cls: "side-menu", id: "api-nav" },
menu.api.children.map(item => renderNested(item, 0, "", true))
);

document.getElementById("leftColumn").appendChild(d);
d1 && document.getElementById("leftColumn").appendChild(d1);
d2 && document.getElementById("leftColumn").appendChild(d2);
updateMenu = (oldLoc, newLoc) => {
if (oldLoc) {
const elem = document.querySelector(`[data-loc="${oldLoc.join("/")}"]`);
if (elem) updatePath(elem, true);
}
if (d1 && d2) {
if (newLoc[0] && newLoc[0] == menu.api.location[0]) {
d1.hidden = true;
d2.hidden = false;
} else {
d1.hidden = false;
d2.hidden = true;
}
}
const elem = document.querySelector(`[data-loc="${newLoc.join("/")}"]`);
if (elem) updatePath(elem, false)
}
updateMenu(null, rawLocation);

window.dispatchEvent(new Event(DYNAMIC_PAGE_LOAD));
})
}

window.addEventListener("DOMContentLoaded", () => {
hljs.registerLanguage("scala", highlightDotty);
hljs.registerAliases(["dotty", "scala3"], "scala");
window.dispatchEvent(new Event(DYNAMIC_PAGE_LOAD));

dynamicSideMenu = document.documentElement.getAttribute("data-dynamicSideMenu") === "true";
if (dynamicSideMenu) {
renderDynamicSideMenu();
} else {
window.dispatchEvent(new Event(DYNAMIC_PAGE_LOAD));
}
});

const elements = document.querySelectorAll(".documentableElement");
Expand Down
6 changes: 4 additions & 2 deletions scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ object Scaladoc:
apiSubdirectory : Boolean = false,
scastieConfiguration: String = "",
defaultTemplate: Option[String] = None,
quickLinks: List[QuickLink] = List.empty
quickLinks: List[QuickLink] = List.empty,
dynamicSideMenu: Boolean = false,
)

def run(args: Array[String], rootContext: CompilerContext): Reporter =
Expand Down Expand Up @@ -228,7 +229,8 @@ object Scaladoc:
apiSubdirectory.get,
scastieConfiguration.get,
defaultTemplate.nonDefault,
quickLinksParsed
quickLinksParsed,
dynamicSideMenu.get,
)
(Some(docArgs), newContext)
}
Expand Down
5 changes: 4 additions & 1 deletion scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,8 @@ class ScaladocSettings extends SettingGroup with AllScalaSettings:
"List of quick links that is displayed in the header of documentation."
)

val dynamicSideMenu: Setting[Boolean] =
BooleanSetting("-dynamic-side-menu", "Generate side menu via JS instead of embedding it in every html file", false)

def scaladocSpecificSettings: Set[Setting[?]] =
Set(sourceLinks, legacySourceLink, syntax, revision, externalDocumentationMappings, socialLinks, skipById, skipByRegex, deprecatedSkipPackages, docRootContent, snippetCompiler, generateInkuire, defaultTemplate, scastieConfiguration, quickLinks)
Set(sourceLinks, legacySourceLink, syntax, revision, externalDocumentationMappings, socialLinks, skipById, skipByRegex, deprecatedSkipPackages, docRootContent, snippetCompiler, generateInkuire, defaultTemplate, scastieConfiguration, quickLinks, dynamicSideMenu)
34 changes: 32 additions & 2 deletions scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do
case _ => Nil
case _ => Nil)
:+ (Attr("data-pathToRoot") := pathToRoot(page.link.dri))
:+ (Attr("data-rawLocation") := rawLocation(page.link.dri).mkString("/"))
:+ (Attr("data-dynamicSideMenu") := ctx.args.dynamicSideMenu.toString)

val htmlTag = html(attrs*)(
head((mkHead(page) :+ docHead)*),
Expand All @@ -46,8 +48,35 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do

override def render(): Unit =
val renderedResources = renderResources()
if ctx.args.dynamicSideMenu then serializeSideMenu()
super.render()

private def serializeSideMenu() =
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.node.TextNode
val mapper = new ObjectMapper();

def serializePage(page: Page): ObjectNode =
import scala.jdk.CollectionConverters.SeqHasAsJava
val children = mapper.createArrayNode().addAll(page.children.filterNot(_.hidden).map(serializePage).asJava)
val location = mapper.createArrayNode().addAll(rawLocation(page.link.dri).map(TextNode(_)).asJava)
val obj = mapper.createObjectNode()
obj.set("name", new TextNode(page.link.name))
obj.set("location", location)
obj.set("kind", page.content match
case m: Member if m.needsOwnPage => new TextNode(m.kind.name)
case _ => null
)
obj.set("children", children)
obj

val rootNode = mapper.createObjectNode()
rootNode.set("docs", rootDocsPage.map(serializePage).orNull)
rootNode.set("api", rootApiPage.map(serializePage).orNull)
val jsonString = mapper.writer().writeValueAsString(rootNode);
renderResource(Resource.Text("dynamicSideMenu.json", jsonString))

private def renderResources(): Seq[String] =
import scala.util.Using
import scala.jdk.CollectionConverters._
Expand Down Expand Up @@ -218,7 +247,8 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do
)).dropRight(1)
div(cls := "breadcrumbs container")(innerTags*)

val (apiNavOpt, docsNavOpt): (Option[(Boolean, Seq[AppliedTag])], Option[(Boolean, Seq[AppliedTag])]) = buildNavigation(link)
val dynamicSideMenu = ctx.args.dynamicSideMenu
val (apiNavOpt, docsNavOpt) = if dynamicSideMenu then (None, None) else buildNavigation(link)

def textFooter: String =
args.projectFooter.getOrElse("")
Expand Down Expand Up @@ -266,7 +296,7 @@ class HtmlRenderer(rootPackage: Member, members: Map[DRI, Member])(using ctx: Do
),
span(id := "mobile-sidebar-toggle", cls := "floating-button"),
div(id := "leftColumn", cls := "body-small")(
Seq(
if dynamicSideMenu then Nil else Seq(
div(cls:= "switcher-container")(
docsNavOpt match {
case Some(isDocsActive, docsNav) =>
Expand Down

0 comments on commit 075ee34

Please sign in to comment.