Skip to content

Commit 3485086

Browse files
authored
feat: api ref doc sidebar optimization (#1599)
* Api ref doc sidebar optimization * remove comment * pr feedbacks * lint * changelog * style * pr feedback: style * remove unused dependency * retrigger CI * retrigger CI
1 parent d7b2f2a commit 3485086

File tree

5 files changed

+240
-1
lines changed

5 files changed

+240
-1
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "3828400d-3899-4d31-9db7-0b973ce10e43",
3+
"type": "feature",
4+
"description": "Optimize Kotlin API Reference documentation navigation sidebar"
5+
}

build.gradle.kts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ val testJavaVersion = typedProp<String>("test.java.version")?.let {
3939
}
4040

4141
allprojects {
42+
apply(from = "${rootProject.file("buildSrc/src/main/kotlin/dokka-customization.gradle.kts")}")
43+
4244
tasks.withType<org.jetbrains.dokka.gradle.AbstractDokkaTask>().configureEach {
4345
val sdkVersion: String by project
4446
moduleVersion.set(sdkVersion)
@@ -54,7 +56,8 @@ allprojects {
5456
"customAssets": [
5557
"${rootProject.file("docs/dokka-presets/assets/logo-icon.svg")}",
5658
"${rootProject.file("docs/dokka-presets/assets/aws_logo_white_59x35.png")}",
57-
"${rootProject.file("docs/dokka-presets/scripts/accessibility.js")}"
59+
"${rootProject.file("docs/dokka-presets/scripts/accessibility.js")}",
60+
"${rootProject.file("docs/dokka-presets/scripts/custom-navigation-loader.js")}"
5861
],
5962
"footerMessage": "© $year, Amazon Web Services, Inc. or its affiliates. All rights reserved.",
6063
"separateInheritedMembers" : true,
@@ -137,6 +140,8 @@ project.afterEvaluate {
137140
// NOTE: these get concatenated
138141
rootProject.file("docs/dokka-presets/README.md"),
139142
)
143+
144+
finalizedBy("trimNavigations", "applyCustomNavigationLoader")
140145
}
141146
}
142147

buildSrc/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ plugins {
55
repositories {
66
mavenCentral()
77
}
8+
9+
dependencies {
10+
implementation(libs.jsoup)
11+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import org.jsoup.Jsoup
2+
3+
tasks.register("trimNavigations") {
4+
description = "Trims navigation files to remove unrelated child submenu"
5+
group = "documentation"
6+
7+
doLast {
8+
val dokkaOutputDir = rootProject.buildDir.resolve("dokka/htmlMultiModule")
9+
10+
if (!dokkaOutputDir.exists()) {
11+
logger.info("Dokka output directory not found at ${dokkaOutputDir.absolutePath}, skipping navigation trimming")
12+
return@doLast
13+
}
14+
15+
dokkaOutputDir.listFiles { file ->
16+
file.isDirectory && file.resolve("navigation.html").exists()
17+
}?.forEach { moduleDir ->
18+
val moduleName = moduleDir.name
19+
20+
val navFile = File(moduleDir, "navigation.html")
21+
22+
val doc = Jsoup.parse(navFile, "UTF-8")
23+
24+
// Fix navigation links
25+
doc.select("a[href^='../']").forEach { anchor ->
26+
val originalHref = anchor.attr("href")
27+
val trimmedHref = originalHref.replace("../", "")
28+
anchor.attr("href", trimmedHref)
29+
}
30+
31+
val sideMenuParts = doc.select("div.sideMenu > div.sideMenuPart")
32+
33+
sideMenuParts.forEach { submenu ->
34+
// If this is not the current module's submenu, remove all its nested content
35+
if (submenu.id() != "$moduleName-nav-submenu") {
36+
val overviewDiv = submenu.select("> div.overview").first()
37+
overviewDiv?.select("span.navButton")?.remove()
38+
submenu.children().remove()
39+
if (overviewDiv != null) {
40+
submenu.appendChild(overviewDiv)
41+
}
42+
}
43+
}
44+
45+
val wrappedContent = "<div class=\"sideMenu\">\n${sideMenuParts.outerHtml()}\n</div>"
46+
navFile.writeText(wrappedContent)
47+
}
48+
}
49+
}
50+
51+
tasks.register("applyCustomNavigationLoader") {
52+
group = "documentation"
53+
description = "Replace default Dokka navigation-loader.js with custom implementation"
54+
55+
doLast {
56+
val dokkaOutputDir = rootProject.buildDir.resolve("dokka/htmlMultiModule")
57+
58+
if (!dokkaOutputDir.exists()) {
59+
logger.info("Dokka output directory not found at ${dokkaOutputDir.absolutePath}, skipping apply custom navigation loader")
60+
return@doLast
61+
}
62+
63+
dokkaOutputDir.walkTopDown()
64+
.filter { it.isFile && it.name.endsWith(".html") }
65+
.forEach { file ->
66+
val updatedContent = file.readLines().filterNot { line ->
67+
line.contains("""scripts/navigation-loader.js""")
68+
}.joinToString("\n")
69+
70+
file.writeText(updatedContent)
71+
}
72+
}
73+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Custom navigation loader for AWS SDK for Kotlin documentation.
2+
3+
// Extracts the module name from a given URL href
4+
function extractModuleName(href) {
5+
try{
6+
const url = new URL(href, window.location.origin);
7+
const pathname = url.pathname;
8+
const pathSegments = pathname.split('/').filter(Boolean);
9+
10+
// For local hosting
11+
if (url.hostname === 'localhost') {
12+
return pathSegments.length >= 1 ? pathSegments[0] : null;
13+
}
14+
15+
return pathSegments.length >= 4 ? pathSegments[3] : null;
16+
}
17+
catch (error) {
18+
return null;
19+
}
20+
}
21+
22+
// Hides the sidebar and adjusts main content layout
23+
function hideSidebar() {
24+
const sidebar = document.getElementById('leftColumn');
25+
const main = document.getElementById('main');
26+
27+
if (sidebar) {
28+
sidebar.style.display = 'none';
29+
}
30+
31+
if (main) {
32+
main.style.marginLeft = '0';
33+
main.style.width = '100%';
34+
}
35+
}
36+
37+
function loadNavigation() {
38+
const moduleName = extractModuleName(window.location.href);
39+
40+
// Hide sidebar for root index page
41+
if (moduleName === "index.html") {
42+
hideSidebar()
43+
return Promise.resolve('');
44+
}
45+
46+
const navigationPath = moduleName
47+
? `${pathToRoot}${moduleName}/navigation.html`
48+
: `${pathToRoot}navigation.html`;
49+
50+
return fetch(navigationPath)
51+
.then(response => response.text())
52+
.catch(error => {
53+
// Use root navigation as a fallback
54+
return fetch(pathToRoot + "navigation.html")
55+
.then(response => response.text());
56+
});
57+
}
58+
59+
navigationPageText = loadNavigation()
60+
61+
// =================================================================
62+
// Everything below this is copied from Dokka's navigation-loader.js
63+
// =================================================================
64+
displayNavigationFromPage = () => {
65+
navigationPageText.then(data => {
66+
document.getElementById("sideMenu").innerHTML = data;
67+
}).then(() => {
68+
document.querySelectorAll(".overview > a").forEach(link => {
69+
link.setAttribute("href", pathToRoot + link.getAttribute("href"));
70+
})
71+
}).then(() => {
72+
document.querySelectorAll(".sideMenuPart").forEach(nav => {
73+
if (!nav.classList.contains("hidden"))
74+
nav.classList.add("hidden")
75+
})
76+
}).then(() => {
77+
revealNavigationForCurrentPage()
78+
}).then(() => {
79+
scrollNavigationToSelectedElement()
80+
})
81+
document.querySelectorAll('.footer a[href^="#"]').forEach(anchor => {
82+
anchor.addEventListener('click', function (e) {
83+
e.preventDefault();
84+
document.querySelector(this.getAttribute('href')).scrollIntoView({
85+
behavior: 'smooth'
86+
});
87+
});
88+
});
89+
}
90+
91+
revealNavigationForCurrentPage = () => {
92+
let pageId = document.getElementById("content").attributes["pageIds"].value.toString();
93+
let parts = document.querySelectorAll(".sideMenuPart");
94+
let found = 0;
95+
do {
96+
parts.forEach(part => {
97+
if (part.attributes['pageId'].value.indexOf(pageId) !== -1 && found === 0) {
98+
found = 1;
99+
if (part.classList.contains("hidden")) {
100+
part.classList.remove("hidden");
101+
part.setAttribute('data-active', "");
102+
}
103+
revealParents(part)
104+
}
105+
});
106+
pageId = pageId.substring(0, pageId.lastIndexOf("/"))
107+
} while (pageId.indexOf("/") !== -1 && found === 0)
108+
};
109+
revealParents = (part) => {
110+
if (part.classList.contains("sideMenuPart")) {
111+
if (part.classList.contains("hidden"))
112+
part.classList.remove("hidden");
113+
revealParents(part.parentNode)
114+
}
115+
};
116+
117+
scrollNavigationToSelectedElement = () => {
118+
let selectedElement = document.querySelector('div.sideMenuPart[data-active]')
119+
if (selectedElement == null) { // nothing selected, probably just the main page opened
120+
return
121+
}
122+
123+
let hasIcon = selectedElement.querySelectorAll(":scope > div.overview span.nav-icon").length > 0
124+
125+
// for instance enums also have children and are expandable, but are not package/module elements
126+
let isPackageElement = selectedElement.children.length > 1 && !hasIcon
127+
if (isPackageElement) {
128+
// if package is selected or linked, it makes sense to align it to top
129+
// so that you can see all the members it contains
130+
selectedElement.scrollIntoView(true)
131+
} else {
132+
// if a member within a package is linked, it makes sense to center it since it,
133+
// this should make it easier to look at surrounding members
134+
selectedElement.scrollIntoView({
135+
behavior: 'auto',
136+
block: 'center',
137+
inline: 'center'
138+
})
139+
}
140+
}
141+
142+
/*
143+
This is a work-around for safari being IE of our times.
144+
It doesn't fire a DOMContentLoaded, presumabely because eventListener is added after it wants to do it
145+
*/
146+
if (document.readyState == 'loading') {
147+
window.addEventListener('DOMContentLoaded', () => {
148+
displayNavigationFromPage()
149+
})
150+
} else {
151+
displayNavigationFromPage()
152+
}

0 commit comments

Comments
 (0)