diff --git a/scaladoc-js/src/Globals.scala b/scaladoc-js/src/Globals.scala
index c04235dd607e..15c3fd81c5b8 100644
--- a/scaladoc-js/src/Globals.scala
+++ b/scaladoc-js/src/Globals.scala
@@ -7,6 +7,7 @@ import scala.scalajs.js.annotation.JSGlobalScope
@JSGlobalScope
object Globals extends js.Object {
val pathToRoot: String = js.native
+ val versionsDictionaryUrl: String = js.native
}
object StringUtils {
@@ -14,4 +15,4 @@ object StringUtils {
if s.isEmpty then List.empty
else if s.tail.indexWhere(_.isUpper) == -1 then List(s)
else List(s.take(s.tail.indexWhere(_.isUpper) + 1)) ++ createCamelCaseTokens(s.drop(s.tail.indexWhere(_.isUpper) + 1))
-}
\ No newline at end of file
+}
diff --git a/scaladoc-js/src/Main.scala b/scaladoc-js/src/Main.scala
index c31ad58568fa..66e370af1da3 100644
--- a/scaladoc-js/src/Main.scala
+++ b/scaladoc-js/src/Main.scala
@@ -4,4 +4,5 @@ object Main extends App {
Searchbar()
SocialLinks()
CodeSnippets()
+ DropdownHandler()
}
diff --git a/scaladoc-js/src/versions-dropdown/DropdownHandler.scala b/scaladoc-js/src/versions-dropdown/DropdownHandler.scala
new file mode 100644
index 000000000000..01ea7df6548a
--- /dev/null
+++ b/scaladoc-js/src/versions-dropdown/DropdownHandler.scala
@@ -0,0 +1,94 @@
+package dotty.tools.scaladoc
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.Future
+import scala.util.{Success,Failure}
+
+import org.scalajs.dom._
+import org.scalajs.dom.ext._
+import scala.scalajs.js.annotation.JSExportTopLevel
+import org.scalajs.dom.ext.Ajax
+import scala.scalajs.js
+import scala.scalajs.js.JSON
+
+trait Versions extends js.Object:
+ def versions: js.Dictionary[String]
+
+class DropdownHandler:
+
+ val KEY = "versions-json"
+ val UNDEFINED_VERSIONS = "undefined_versions"
+
+ private def addVersionsList(json: String) =
+ val ver = JSON.parse(json).asInstanceOf[Versions]
+ val ddc = document.getElementById("dropdown-content")
+ for (k, v) <- ver.versions do
+ var child = document.createElement("a").asInstanceOf[html.Anchor]
+ child.href = v
+ child.text = k
+ ddc.appendChild(child)
+ val arrow = document.createElement("span").asInstanceOf[html.Span]
+ arrow.classList.add("ar")
+ document.getElementById("dropdown-button").appendChild(arrow)
+
+ private def disableButton() =
+ val btn = document.getElementById("dropdown-button").asInstanceOf[html.Button]
+ btn.disabled = true
+ btn.classList.remove("dropdownbtnactive")
+
+ private def getURLContent(url: String): Future[String] = Ajax.get(url).map(_.responseText)
+
+ window.sessionStorage.getItem(KEY) match
+ case null => // If no key, returns null
+ js.typeOf(Globals.versionsDictionaryUrl) match
+ case "undefined" =>
+ window.sessionStorage.setItem(KEY, UNDEFINED_VERSIONS)
+ disableButton()
+ case _ =>
+ getURLContent(Globals.versionsDictionaryUrl).onComplete {
+ case Success(json: String) =>
+ window.sessionStorage.setItem(KEY, json)
+ addVersionsList(json)
+ case Failure(_) =>
+ window.sessionStorage.setItem(KEY, UNDEFINED_VERSIONS)
+ disableButton()
+ }
+ case value => value match
+ case UNDEFINED_VERSIONS =>
+ disableButton()
+ case json =>
+ addVersionsList(json)
+
+ document.addEventListener("click", (e: Event) => {
+ if e.target.asInstanceOf[html.Element].id != "dropdown-button" then
+ document.getElementById("dropdown-content").classList.remove("show")
+ document.getElementById("dropdown-button").classList.remove("expanded")
+ })
+
+ document.getElementById("version").asInstanceOf[html.Span].onclick = (e: Event) => {
+ e.stopPropagation
+ }
+end DropdownHandler
+
+@JSExportTopLevel("dropdownHandler")
+def dropdownHandler() =
+ if document.getElementById("dropdown-content").getElementsByTagName("a").size > 0 &&
+ window.getSelection.toString.length == 0 then
+ document.getElementById("dropdown-content").classList.toggle("show")
+ document.getElementById("dropdown-button").classList.toggle("expanded")
+
+@JSExportTopLevel("filterFunction")
+def filterFunction() =
+ val input = document.getElementById("dropdown-input").asInstanceOf[html.Input]
+ val filter = input.value.toUpperCase
+ val div = document.getElementById("dropdown-content")
+ val as = div.getElementsByTagName("a")
+
+ as.foreach { a =>
+ val txtValue = a.innerText
+ val cl = a.asInstanceOf[html.Anchor].classList
+ if txtValue.toUpperCase.indexOf(filter) > -1 then
+ cl.remove("filtered")
+ else
+ cl.add("filtered")
+ }
diff --git a/scaladoc/resources/dotty_res/styles/scalastyle.css b/scaladoc/resources/dotty_res/styles/scalastyle.css
index 452093eddfc0..25b4c26f7034 100644
--- a/scaladoc/resources/dotty_res/styles/scalastyle.css
+++ b/scaladoc/resources/dotty_res/styles/scalastyle.css
@@ -199,6 +199,9 @@ th {
#logo .projectVersion {
color: var(--leftbar-fg);
font-size: 12px;
+ display: flex;
+ padding-left: calc(0.05 * var(--side-width));
+ padding-right: calc(0.08 * var(--side-width));
}
.scaladoc_logo {
@@ -277,7 +280,7 @@ th {
}
/* spans represent a expand button */
-#sideMenu2 span.ar {
+span.ar {
align-items: center;
cursor: pointer;
position: absolute;
@@ -286,7 +289,7 @@ th {
padding: 4px;
}
-#sideMenu2 span.ar::before {
+span.ar::before {
content: "\e903"; /* arrow down */
font-family: "dotty-icons" !important;
font-size: 20px;
@@ -297,11 +300,11 @@ th {
align-items: center;
justify-content: center;
}
-#sideMenu2 .expanded>span.ar::before {
+.expanded>span.ar::before {
content: "\e905"; /* arrow up */
}
-#sideMenu2 .div:hover>span.ar::before {
+.div:hover>span.ar::before {
color: var(--leftbar-current-bg);
}
@@ -861,3 +864,69 @@ footer .socials {
footer {
background-color: white;
}
+
+/* The container
- needed to position the dropdown content */
+.versions-dropdown {
+ position: relative;
+}
+
+ /* Dropdown Button */
+.dropdownbtn {
+ background-color: var(--leftbar-bg);
+ color: white;
+ padding: 4px 12px;
+ border: none;
+}
+
+/* Dropdown button on hover & focus */
+.dropdownbtnactive:hover, .dropdownbtnactive:focus {
+ background-color: var(--leftbar-hover-bg);
+ cursor: pointer;
+}
+
+/* The search field */
+#dropdown-input {
+ box-sizing: border-box;
+ background-image: url('searchicon.png');
+ background-position: 14px 12px;
+ background-repeat: no-repeat;
+ font-size: 16px;
+ padding: 14px 20px 12px 45px;
+ border: none;
+ border-bottom: 1px solid #ddd;
+}
+
+/* The search field when it gets focus/clicked on */
+#dropdown-input:focus {outline: 3px solid #ddd;}
+
+
+/* Dropdown Content (Hidden by Default) */
+.dropdown-content {
+ display: none;
+ position: absolute;
+ background-color: #f6f6f6;
+ min-width: 230px;
+ border: 1px solid #ddd;
+ z-index: 1;
+}
+
+/* Links inside the dropdown */
+.dropdown-content a {
+ color: black;
+ padding: 12px 16px;
+ text-decoration: none;
+ display: block;
+}
+
+/* Change color of dropdown links on hover */
+.dropdown-content a:hover {background-color: #f1f1f1}
+
+/* Show the dropdown menu (use JS to add this class to the .dropdown-content container when the user clicks on the dropdown button) */
+.show {
+ display:block;
+}
+
+/* Filtered entries in dropdown menu */
+.dropdown-content a.filtered {
+ display: none;
+}
diff --git a/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala b/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala
index 44dcb15b65b2..8b3dd57697c2 100644
--- a/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/Scaladoc.scala
@@ -51,7 +51,8 @@ object Scaladoc:
docCanonicalBaseUrl: String = "",
documentSyntheticTypes: Boolean = false,
snippetCompiler: List[String] = Nil,
- snippetCompilerDebug: Boolean = false
+ snippetCompilerDebug: Boolean = false,
+ versionsDictionaryUrl: Option[String] = None
)
def run(args: Array[String], rootContext: CompilerContext): Reporter =
@@ -195,7 +196,8 @@ object Scaladoc:
docCanonicalBaseUrl.get,
YdocumentSyntheticTypes.get,
snippetCompiler.get,
- snippetCompilerDebug.get
+ snippetCompilerDebug.get,
+ versionsDictionaryUrl.nonDefault
)
(Some(docArgs), newContext)
}
diff --git a/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala b/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala
index ec326bf57178..542cba0bc84d 100644
--- a/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/ScaladocSettings.scala
@@ -93,6 +93,13 @@ class ScaladocSettings extends SettingGroup with AllScalaSettings:
"./docs"
)
+ val versionsDictionaryUrl: Setting[String] = StringSetting(
+ "-versions-dictionary-url",
+ "versions dictionary url",
+ "A URL pointing to a JSON document containing a dictionary version -> documentation location. Useful for libraries that maintain different releases docs",
+ ""
+ )
+
val YdocumentSyntheticTypes: Setting[Boolean] =
BooleanSetting("-Ydocument-synthetic-types", "Documents intrinsic types e. g. Any, Nothing. Setting is useful only for stdlib", false)
diff --git a/scaladoc/src/dotty/tools/scaladoc/SourceLinks.scala b/scaladoc/src/dotty/tools/scaladoc/SourceLinks.scala
index 307d5b05c100..1496b6847316 100644
--- a/scaladoc/src/dotty/tools/scaladoc/SourceLinks.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/SourceLinks.scala
@@ -125,7 +125,7 @@ object SourceLinks:
| €{FILE_PATH}, and €{FILE_LINE} patterns
|
|
- |Template can defined only by subset of sources defined by path prefix represented by ``.
+ |Template can be defined only by subset of sources defined by path prefix represented by ``.
|In such case paths used in templates will be relativized against ``""".stripMargin
def load(config: Seq[String], revision: Option[String], projectRoot: Path = Paths.get("").toAbsolutePath)(using CompilerContext): SourceLinks =
diff --git a/scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala b/scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala
index 057e7cfbcada..2c7a9a1a4fa8 100644
--- a/scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/renderers/HtmlRenderer.scala
@@ -141,7 +141,10 @@ class HtmlRenderer(rootPackage: Member, val members: Map[DRI, Member])(using ctx
href := resolveLink(page.link.dri, "favicon.ico")
),
linkResources(page.link.dri, resources).toList,
- script(raw(s"""var pathToRoot = "${pathToRoot(page.link.dri)}";"""))
+ script(raw(s"""var pathToRoot = "${pathToRoot(page.link.dri)}";""")),
+ ctx.args.versionsDictionaryUrl match
+ case Some(url) => script(raw(s"""var versionsDictionaryUrl = "$url";"""))
+ case None => ""
)
private def buildNavigation(pageLink: Link): AppliedTag =
@@ -220,8 +223,15 @@ class HtmlRenderer(rootPackage: Member, val members: Map[DRI, Member])(using ctx
span(
div(cls:="projectName")(args.name)
),
- span(
- args.projectVersion.map(v => div(cls:="projectVersion")(v)).toList
+ div(id := "version")(
+ div(cls := "versions-dropdown")(
+ div(onclick := "dropdownHandler()", id := "dropdown-button", cls := "dropdownbtn dropdownbtnactive")(
+ args.projectVersion.map(v => div(cls:="projectVersion")(v)).getOrElse("")
+ ),
+ div(id := "dropdown-content", cls := "dropdown-content")(
+ input(`type` := "text", placeholder := "Search...", id := "dropdown-input", onkeyup := "filterFunction()"),
+ ),
+ )
),
div(cls := "socials")(
socialLinks()
diff --git a/scaladoc/src/dotty/tools/scaladoc/util/html.scala b/scaladoc/src/dotty/tools/scaladoc/util/html.scala
index 8033f39bb52b..e3065bbafbdd 100644
--- a/scaladoc/src/dotty/tools/scaladoc/util/html.scala
+++ b/scaladoc/src/dotty/tools/scaladoc/util/html.scala
@@ -99,6 +99,7 @@ object HTML:
val value = Attr("value")
val onclick=Attr("onclick")
val titleAttr =Attr("title")
+ val onkeyup = Attr("onkeyup")
def raw(content: String): AppliedTag = new AppliedTag(content)
def raw(content: StringBuilder): AppliedTag = content