Skip to content

Commit 468fe96

Browse files
committed
Add browsable versions extension for scaladoc
1 parent d2dd083 commit 468fe96

File tree

8 files changed

+171
-5
lines changed

8 files changed

+171
-5
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
@@ -3,4 +3,5 @@ package dotty.tools.scaladoc
33
object Main extends App {
44
Searchbar()
55
SocialLinks()
6+
DropdownHandler()
67
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
31+
private def disableButton() =
32+
val btn = document.getElementById("dropdown-button").asInstanceOf[html.Button]
33+
btn.disabled = true
34+
btn.classList.remove("dropdownbtnactive")
35+
36+
private def getURLContent(url: String): Future[String] = Ajax.get(url).map(_.responseText)
37+
38+
window.localStorage.getItem(KEY) match
39+
case null => // If no key, returns null
40+
Globals.versionsDictionaryUrl match
41+
case null => // global property not defined
42+
// do nothing
43+
case url: String =>
44+
getURLContent(url).onComplete {
45+
case Success(json: String) =>
46+
window.localStorage.setItem(KEY, json)
47+
addVersionsList(json)
48+
case Failure(_) =>
49+
window.localStorage.setItem(KEY, UNDEFINED_VERSIONS)
50+
disableButton()
51+
}
52+
case value => value match
53+
case UNDEFINED_VERSIONS =>
54+
disableButton()
55+
case json =>
56+
addVersionsList(json)
57+
58+
59+
document.onclick = (e: Event) => {
60+
if e.target.asInstanceOf[html.Element].id != "dropdown-button" then
61+
document.getElementById("dropdown-content").classList.remove("show")
62+
}
63+
64+
document.getElementById("version").asInstanceOf[html.Span].onclick = (e: Event) => {
65+
e.stopPropagation
66+
}
67+
68+
69+
@JSExportTopLevel("dropdownHandler")
70+
def dropdownHandler() =
71+
if document.getElementById("dropdown-content").getElementsByTagName("a").size > 0 then
72+
document.getElementById("dropdown-content").classList.toggle("show")
73+
74+
@JSExportTopLevel("filterFunction")
75+
def filterFunction() =
76+
val input = document.getElementById("dropdown-input").asInstanceOf[html.Input]
77+
val filter = input.value.toUpperCase
78+
val div = document.getElementById("dropdown-content")
79+
val a = div.getElementsByTagName("a")
80+
for i <- 0 until a.length do
81+
val txtValue = a(i).innerText
82+
val disp = if txtValue.toUpperCase.indexOf(filter) > -1 then
83+
""
84+
else
85+
"none"
86+
a(i).asInstanceOf[html.Anchor].style.display = disp

scaladoc/resources/dotty_res/styles/scalastyle.css

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,3 +784,63 @@ footer .socials {
784784
footer {
785785
background-color: white;
786786
}
787+
788+
/* The container <div> - needed to position the dropdown content */
789+
.versions-dropdown {
790+
position: relative;
791+
display: inline-block;
792+
}
793+
794+
/* Dropdown Button */
795+
.dropdownbtn {
796+
background-color: var(--leftbar-bg);
797+
color: white;
798+
padding: 4px 12px;
799+
border: none;
800+
}
801+
802+
/* Dropdown button on hover & focus */
803+
.dropdownbtnactive:hover, .dropdownbtnactive:focus {
804+
background-color: var(--leftbar-hover-bg);
805+
cursor: pointer;
806+
}
807+
808+
/* The search field */
809+
#dropdown-input {
810+
box-sizing: border-box;
811+
background-image: url('searchicon.png');
812+
background-position: 14px 12px;
813+
background-repeat: no-repeat;
814+
font-size: 16px;
815+
padding: 14px 20px 12px 45px;
816+
border: none;
817+
border-bottom: 1px solid #ddd;
818+
}
819+
820+
/* The search field when it gets focus/clicked on */
821+
#dropdown-input:focus {outline: 3px solid #ddd;}
822+
823+
824+
/* Dropdown Content (Hidden by Default) */
825+
.dropdown-content {
826+
display: none;
827+
position: absolute;
828+
background-color: #f6f6f6;
829+
min-width: 230px;
830+
border: 1px solid #ddd;
831+
z-index: 1;
832+
}
833+
834+
/* Links inside the dropdown */
835+
.dropdown-content a {
836+
color: black;
837+
padding: 12px 16px;
838+
text-decoration: none;
839+
display: block;
840+
}
841+
842+
/* Change color of dropdown links on hover */
843+
.dropdown-content a:hover {background-color: #f1f1f1}
844+
845+
/* Show the dropdown menu (use JS to add this class to the .dropdown-content container when the user clicks on the dropdown button) */
846+
.show {display:block;}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ object Scaladoc:
4949
includePrivateAPI: Boolean = false,
5050
docCanonicalBaseUrl: String = "",
5151
documentSyntheticTypes: Boolean = false,
52+
versionsDictionaryUrl: Option[String] = None
5253
)
5354

5455
def run(args: Array[String], rootContext: CompilerContext): Reporter =
@@ -189,7 +190,8 @@ object Scaladoc:
189190
groups.get,
190191
visibilityPrivate.get,
191192
docCanonicalBaseUrl.get,
192-
YdocumentSyntheticTypes.get
193+
YdocumentSyntheticTypes.get,
194+
versionsDictionaryUrl.nonDefault
193195
)
194196
(Some(docArgs), newContext)
195197
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,12 @@ class ScaladocSettings extends SettingGroup with AllScalaSettings:
9292
"./docs"
9393
)
9494

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

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

Lines changed: 11 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,13 @@ 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+
span(id := "version")(
227+
div(cls := "versions-dropdown")(
228+
div(onclick := "dropdownHandler()", id := "dropdown-button", cls := "dropdownbtn dropdownbtnactive")(args.projectVersion.map(v => div(cls:="projectVersion")(v)).toList),
229+
div(id := "dropdown-content", cls := "dropdown-content")(
230+
input(`type` := "text", placeholder := "Search...", id := "dropdown-input", onkeyup := "filterFunction()"),
231+
),
232+
)
225233
),
226234
div(cls := "socials")(
227235
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)