Skip to content

Commit 61a6833

Browse files
authored
Merge pull request #12596 from BarkingBad/scaladoc/drop-down-revision
Add browsable versions extension for scaladoc
2 parents d7d4a9f + 040c774 commit 61a6833

File tree

9 files changed

+196
-11
lines changed

9 files changed

+196
-11
lines changed

scaladoc-js/src/Globals.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import scala.scalajs.js.annotation.JSGlobalScope
77
@JSGlobalScope
88
object Globals extends js.Object {
99
val pathToRoot: String = js.native
10+
val versionsDictionaryUrl: String = js.native
1011
}
1112

1213
object StringUtils {
1314
def createCamelCaseTokens(s: String): List[String] =
1415
if s.isEmpty then List.empty
1516
else if s.tail.indexWhere(_.isUpper) == -1 then List(s)
1617
else List(s.take(s.tail.indexWhere(_.isUpper) + 1)) ++ createCamelCaseTokens(s.drop(s.tail.indexWhere(_.isUpper) + 1))
17-
}
18+
}

scaladoc-js/src/Main.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ object Main extends App {
44
Searchbar()
55
SocialLinks()
66
CodeSnippets()
7+
DropdownHandler()
78
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package dotty.tools.scaladoc
2+
3+
import scala.concurrent.ExecutionContext.Implicits.global
4+
import scala.concurrent.Future
5+
import scala.util.{Success,Failure}
6+
7+
import org.scalajs.dom._
8+
import org.scalajs.dom.ext._
9+
import scala.scalajs.js.annotation.JSExportTopLevel
10+
import org.scalajs.dom.ext.Ajax
11+
import scala.scalajs.js
12+
import scala.scalajs.js.JSON
13+
14+
trait Versions extends js.Object:
15+
def versions: js.Dictionary[String]
16+
17+
class DropdownHandler:
18+
19+
val KEY = "versions-json"
20+
val UNDEFINED_VERSIONS = "undefined_versions"
21+
22+
private def addVersionsList(json: String) =
23+
val ver = JSON.parse(json).asInstanceOf[Versions]
24+
val ddc = document.getElementById("dropdown-content")
25+
for (k, v) <- ver.versions do
26+
var child = document.createElement("a").asInstanceOf[html.Anchor]
27+
child.href = v
28+
child.text = k
29+
ddc.appendChild(child)
30+
val arrow = document.createElement("span").asInstanceOf[html.Span]
31+
arrow.classList.add("ar")
32+
document.getElementById("dropdown-button").appendChild(arrow)
33+
34+
private def disableButton() =
35+
val btn = document.getElementById("dropdown-button").asInstanceOf[html.Button]
36+
btn.disabled = true
37+
btn.classList.remove("dropdownbtnactive")
38+
39+
private def getURLContent(url: String): Future[String] = Ajax.get(url).map(_.responseText)
40+
41+
window.sessionStorage.getItem(KEY) match
42+
case null => // If no key, returns null
43+
js.typeOf(Globals.versionsDictionaryUrl) match
44+
case "undefined" =>
45+
window.sessionStorage.setItem(KEY, UNDEFINED_VERSIONS)
46+
disableButton()
47+
case _ =>
48+
getURLContent(Globals.versionsDictionaryUrl).onComplete {
49+
case Success(json: String) =>
50+
window.sessionStorage.setItem(KEY, json)
51+
addVersionsList(json)
52+
case Failure(_) =>
53+
window.sessionStorage.setItem(KEY, UNDEFINED_VERSIONS)
54+
disableButton()
55+
}
56+
case value => value match
57+
case UNDEFINED_VERSIONS =>
58+
disableButton()
59+
case json =>
60+
addVersionsList(json)
61+
62+
document.addEventListener("click", (e: Event) => {
63+
if e.target.asInstanceOf[html.Element].id != "dropdown-button" then
64+
document.getElementById("dropdown-content").classList.remove("show")
65+
document.getElementById("dropdown-button").classList.remove("expanded")
66+
})
67+
68+
document.getElementById("version").asInstanceOf[html.Span].onclick = (e: Event) => {
69+
e.stopPropagation
70+
}
71+
end DropdownHandler
72+
73+
@JSExportTopLevel("dropdownHandler")
74+
def dropdownHandler() =
75+
if document.getElementById("dropdown-content").getElementsByTagName("a").size > 0 &&
76+
window.getSelection.toString.length == 0 then
77+
document.getElementById("dropdown-content").classList.toggle("show")
78+
document.getElementById("dropdown-button").classList.toggle("expanded")
79+
80+
@JSExportTopLevel("filterFunction")
81+
def filterFunction() =
82+
val input = document.getElementById("dropdown-input").asInstanceOf[html.Input]
83+
val filter = input.value.toUpperCase
84+
val div = document.getElementById("dropdown-content")
85+
val as = div.getElementsByTagName("a")
86+
87+
as.foreach { a =>
88+
val txtValue = a.innerText
89+
val cl = a.asInstanceOf[html.Anchor].classList
90+
if txtValue.toUpperCase.indexOf(filter) > -1 then
91+
cl.remove("filtered")
92+
else
93+
cl.add("filtered")
94+
}

scaladoc/resources/dotty_res/styles/scalastyle.css

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ th {
199199
#logo .projectVersion {
200200
color: var(--leftbar-fg);
201201
font-size: 12px;
202+
display: flex;
203+
padding-left: calc(0.05 * var(--side-width));
204+
padding-right: calc(0.08 * var(--side-width));
202205
}
203206

204207
.scaladoc_logo {
@@ -277,7 +280,7 @@ th {
277280
}
278281

279282
/* spans represent a expand button */
280-
#sideMenu2 span.ar {
283+
span.ar {
281284
align-items: center;
282285
cursor: pointer;
283286
position: absolute;
@@ -286,7 +289,7 @@ th {
286289
padding: 4px;
287290
}
288291

289-
#sideMenu2 span.ar::before {
292+
span.ar::before {
290293
content: "\e903"; /* arrow down */
291294
font-family: "dotty-icons" !important;
292295
font-size: 20px;
@@ -297,11 +300,11 @@ th {
297300
align-items: center;
298301
justify-content: center;
299302
}
300-
#sideMenu2 .expanded>span.ar::before {
303+
.expanded>span.ar::before {
301304
content: "\e905"; /* arrow up */
302305
}
303306

304-
#sideMenu2 .div:hover>span.ar::before {
307+
.div:hover>span.ar::before {
305308
color: var(--leftbar-current-bg);
306309
}
307310

@@ -861,3 +864,69 @@ footer .socials {
861864
footer {
862865
background-color: white;
863866
}
867+
868+
/* The container <div> - needed to position the dropdown content */
869+
.versions-dropdown {
870+
position: relative;
871+
}
872+
873+
/* Dropdown Button */
874+
.dropdownbtn {
875+
background-color: var(--leftbar-bg);
876+
color: white;
877+
padding: 4px 12px;
878+
border: none;
879+
}
880+
881+
/* Dropdown button on hover & focus */
882+
.dropdownbtnactive:hover, .dropdownbtnactive:focus {
883+
background-color: var(--leftbar-hover-bg);
884+
cursor: pointer;
885+
}
886+
887+
/* The search field */
888+
#dropdown-input {
889+
box-sizing: border-box;
890+
background-image: url('searchicon.png');
891+
background-position: 14px 12px;
892+
background-repeat: no-repeat;
893+
font-size: 16px;
894+
padding: 14px 20px 12px 45px;
895+
border: none;
896+
border-bottom: 1px solid #ddd;
897+
}
898+
899+
/* The search field when it gets focus/clicked on */
900+
#dropdown-input:focus {outline: 3px solid #ddd;}
901+
902+
903+
/* Dropdown Content (Hidden by Default) */
904+
.dropdown-content {
905+
display: none;
906+
position: absolute;
907+
background-color: #f6f6f6;
908+
min-width: 230px;
909+
border: 1px solid #ddd;
910+
z-index: 1;
911+
}
912+
913+
/* Links inside the dropdown */
914+
.dropdown-content a {
915+
color: black;
916+
padding: 12px 16px;
917+
text-decoration: none;
918+
display: block;
919+
}
920+
921+
/* Change color of dropdown links on hover */
922+
.dropdown-content a:hover {background-color: #f1f1f1}
923+
924+
/* Show the dropdown menu (use JS to add this class to the .dropdown-content container when the user clicks on the dropdown button) */
925+
.show {
926+
display:block;
927+
}
928+
929+
/* Filtered entries in dropdown menu */
930+
.dropdown-content a.filtered {
931+
display: none;
932+
}

scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ object Scaladoc:
5151
docCanonicalBaseUrl: String = "",
5252
documentSyntheticTypes: Boolean = false,
5353
snippetCompiler: List[String] = Nil,
54-
snippetCompilerDebug: Boolean = false
54+
snippetCompilerDebug: Boolean = false,
55+
versionsDictionaryUrl: Option[String] = None
5556
)
5657

5758
def run(args: Array[String], rootContext: CompilerContext): Reporter =
@@ -195,7 +196,8 @@ object Scaladoc:
195196
docCanonicalBaseUrl.get,
196197
YdocumentSyntheticTypes.get,
197198
snippetCompiler.get,
198-
snippetCompilerDebug.get
199+
snippetCompilerDebug.get,
200+
versionsDictionaryUrl.nonDefault
199201
)
200202
(Some(docArgs), newContext)
201203
}

scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ class ScaladocSettings extends SettingGroup with AllScalaSettings:
9393
"./docs"
9494
)
9595

96+
val versionsDictionaryUrl: Setting[String] = StringSetting(
97+
"-versions-dictionary-url",
98+
"versions dictionary url",
99+
"A URL pointing to a JSON document containing a dictionary version -> documentation location. Useful for libraries that maintain different releases docs",
100+
""
101+
)
102+
96103
val YdocumentSyntheticTypes: Setting[Boolean] =
97104
BooleanSetting("-Ydocument-synthetic-types", "Documents intrinsic types e. g. Any, Nothing. Setting is useful only for stdlib", false)
98105

scaladoc/src/dotty/tools/scaladoc/SourceLinks.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ object SourceLinks:
125125
| €{FILE_PATH}, and €{FILE_LINE} patterns
126126
|
127127
|
128-
|Template can defined only by subset of sources defined by path prefix represented by `<sub-path>`.
128+
|Template can be defined only by subset of sources defined by path prefix represented by `<sub-path>`.
129129
|In such case paths used in templates will be relativized against `<sub-path>`""".stripMargin
130130

131131
def load(config: Seq[String], revision: Option[String], projectRoot: Path = Paths.get("").toAbsolutePath)(using CompilerContext): SourceLinks =

scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,10 @@ class HtmlRenderer(rootPackage: Member, val members: Map[DRI, Member])(using ctx
141141
href := resolveLink(page.link.dri, "favicon.ico")
142142
),
143143
linkResources(page.link.dri, resources).toList,
144-
script(raw(s"""var pathToRoot = "${pathToRoot(page.link.dri)}";"""))
144+
script(raw(s"""var pathToRoot = "${pathToRoot(page.link.dri)}";""")),
145+
ctx.args.versionsDictionaryUrl match
146+
case Some(url) => script(raw(s"""var versionsDictionaryUrl = "$url";"""))
147+
case None => ""
145148
)
146149

147150
private def buildNavigation(pageLink: Link): AppliedTag =
@@ -220,8 +223,15 @@ class HtmlRenderer(rootPackage: Member, val members: Map[DRI, Member])(using ctx
220223
span(
221224
div(cls:="projectName")(args.name)
222225
),
223-
span(
224-
args.projectVersion.map(v => div(cls:="projectVersion")(v)).toList
226+
div(id := "version")(
227+
div(cls := "versions-dropdown")(
228+
div(onclick := "dropdownHandler()", id := "dropdown-button", cls := "dropdownbtn dropdownbtnactive")(
229+
args.projectVersion.map(v => div(cls:="projectVersion")(v)).getOrElse("")
230+
),
231+
div(id := "dropdown-content", cls := "dropdown-content")(
232+
input(`type` := "text", placeholder := "Search...", id := "dropdown-input", onkeyup := "filterFunction()"),
233+
),
234+
)
225235
),
226236
div(cls := "socials")(
227237
socialLinks()

scaladoc/src/dotty/tools/scaladoc/util/html.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ object HTML:
9999
val value = Attr("value")
100100
val onclick=Attr("onclick")
101101
val titleAttr =Attr("title")
102+
val onkeyup = Attr("onkeyup")
102103

103104
def raw(content: String): AppliedTag = new AppliedTag(content)
104105
def raw(content: StringBuilder): AppliedTag = content

0 commit comments

Comments
 (0)