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