From cc264686ae7715a6e491ab94c671c8bae9f2688a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Zyba=C5=82a?= Date: Tue, 1 Mar 2022 12:59:38 +0100 Subject: [PATCH 1/2] Introduce framework to generate html --- scaladoc-js/common/src/utils/html.scala | 111 ++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 scaladoc-js/common/src/utils/html.scala diff --git a/scaladoc-js/common/src/utils/html.scala b/scaladoc-js/common/src/utils/html.scala new file mode 100644 index 000000000000..042d7f719459 --- /dev/null +++ b/scaladoc-js/common/src/utils/html.scala @@ -0,0 +1,111 @@ +package dotty.tools.scaladoc +package utils + +import scala.scalajs.js +import org.scalajs.dom.{html => domhtml, _} + +object HTML { + type TagArg = domhtml.Element | Seq[domhtml.Element | String] | String + + type AttrArg = AppliedAttr | Seq[AppliedAttr] + + case class Tag[T <: domhtml.Element](private val elemFactory: () => T): + private def textNode(s: String): Text = document.createTextNode(s) + + def apply(tags: TagArg*): T = apply()(tags:_*) + def apply(first: AttrArg, rest: AttrArg*): T = apply((first +: rest):_*)() + def apply(attrs: AttrArg*)(tags: TagArg*): T = + val elem: T = elemFactory() + def unpackTags(tags: TagArg*): Unit = tags.foreach { + case e: domhtml.Element => elem.appendChild(e) + case s: String => elem.appendChild(textNode(s.escapeReservedTokens)) + case elemSeq: (Seq[domhtml.Element | String] @unchecked) => unpackTags(elemSeq*) + } + + def unpackAttributes(attrs: AttrArg*): Unit = attrs.foreach { + case ("id", id) => elem.id = id + case ("class", value) => value.split("\\s+").foreach(cls => elem.classList.add(cls)) + case (attr, value) => elem.setAttribute(attr, value) + case s: Seq[AppliedAttr] => unpackAttributes(s*) + } + + unpackTags(tags:_*) + unpackAttributes(attrs:_*) + elem + + object Tag: + def apply[T <: domhtml.Element](s: String): Tag[T] = + Tag[T](() => document.createElement(s).asInstanceOf[T]) + + extension (s: String) def escapeReservedTokens: String = + s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") + + case class Attr(name: String): + def :=(value: String): AppliedAttr = new AppliedAttr(name, value) + + opaque type AppliedAttr = (String, String) + + val div = Tag[domhtml.Div]("div") + val span = Tag[domhtml.Span]("span") + val a = Tag[domhtml.Anchor]("a") + val p = Tag[domhtml.Paragraph]("p") + val h1 = Tag[domhtml.Heading]("h1") + val h2 = Tag[domhtml.Heading]("h2") + val h3 = Tag[domhtml.Heading]("h3") + val h4 = Tag[domhtml.Heading]("h4") + val h5 = Tag[domhtml.Heading]("h5") + val h6 = Tag[domhtml.Heading]("h6") + val dl = Tag[domhtml.DList]("dl") + val dd = Tag[domhtml.Element]("dd") + val dt = Tag[domhtml.Element]("dt") + val svg = Tag[domhtml.Element]("svg") + val button = Tag[domhtml.Button]("button") + val input = Tag[domhtml.Input]("input") + val label = Tag[domhtml.Label]("label") + val script = Tag[domhtml.Script]("script") + val link = Tag[domhtml.Link]("link") + val footer = Tag[domhtml.Element]("footer") + val htmlelem = Tag[domhtml.Html]("html") + val head = Tag[domhtml.Head]("head") + val meta = Tag[domhtml.Element]("meta") + val main = Tag[domhtml.Element]("main") + val title = Tag[domhtml.Title]("title") + val body = Tag[domhtml.Body]("body") + val nav = Tag[domhtml.Element]("nav") + val img = Tag[domhtml.Image]("img") + val ul = Tag[domhtml.UList]("ul") + val ol = Tag[domhtml.OList]("ol") + val li = Tag[domhtml.LI]("li") + val code = Tag[domhtml.Element]("code") + val pre = Tag[domhtml.Pre]("pre") + val table = Tag[domhtml.Table]("table") + val thead = Tag[domhtml.Element]("thead") + val tbody = Tag[domhtml.Element]("tbody") + val th = Tag[domhtml.TableCell]("th") + val tr = Tag[domhtml.TableRow]("tr") + val td = Tag[domhtml.TableCell]("td") + + val cls = Attr("class") + val href = Attr("href") + val style = Attr("style") + val id = Attr("id") + val `type` = Attr("type") + val placeholder = Attr("placeholder") + val defer = Attr("defer") + val src = Attr("src") + val rel = Attr("rel") + val charset = Attr("charset") + val name = Attr("name") + val content = Attr("content") + val testId = Attr("data-test-id") + val alt = Attr("alt") + val value = Attr("value") + val onclick=Attr("onclick") + val titleAttr =Attr("title") + val onkeyup = Attr("onkeyup") + +} \ No newline at end of file From 39e3fcf80e6fda6a19a61e8dc2454bc0b1f5a19b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Zyba=C5=82a?= Date: Tue, 1 Mar 2022 14:10:21 +0100 Subject: [PATCH 2/2] Refactor scaladoc-js to use new utils --- project/Build.scala | 2 +- .../src/code-snippets/CodeSnippets.scala | 119 +++---- scaladoc-js/common/src/utils/html.scala | 9 +- .../ContentContributors.scala | 22 +- .../src/searchbar/SearchbarComponent.scala | 314 ++++++------------ .../main/src/social-links/SocialLinks.scala | 8 +- .../versions-dropdown/DropdownHandler.scala | 9 +- 7 files changed, 181 insertions(+), 302 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 7c6f5e5953a5..1618c42c949b 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1253,7 +1253,7 @@ object Build { lazy val `scaladoc-js-contributors` = project.in(file("scaladoc-js/contributors")). enablePlugins(DottyJSPlugin). - dependsOn(`scala3-library-bootstrappedJS`). + dependsOn(`scaladoc-js-common`). settings( Test / fork := false, scalaJSUseMainModuleInitializer := true, diff --git a/scaladoc-js/common/src/code-snippets/CodeSnippets.scala b/scaladoc-js/common/src/code-snippets/CodeSnippets.scala index 3d4b4af2ab64..6595aa77c372 100644 --- a/scaladoc-js/common/src/code-snippets/CodeSnippets.scala +++ b/scaladoc-js/common/src/code-snippets/CodeSnippets.scala @@ -4,6 +4,9 @@ import scala.scalajs.js import org.scalajs.dom._ import org.scalajs.dom.ext._ +import utils.HTML._ +import scala.util.chaining._ + import CodeSnippetsGlobals._ class CodeSnippets: @@ -35,22 +38,13 @@ class CodeSnippets: case _ => } def createShowHideButton(toggleRoot: html.Element) = { - val div = document.createElement("div") - div.classList.add("snippet-showhide") - val p = document.createElement("p") - p.textContent = "Show collapsed lines" - val showHideButton = document.createElement("label") - showHideButton.classList.add("snippet-showhide-button") - val checkbox = document.createElement("input").asInstanceOf[html.Input] - checkbox.`type` = "checkbox" - val slider = document.createElement("span") - slider.classList.add("slider") - showHideButton.appendChild(checkbox) - showHideButton.appendChild(slider) - checkbox.addEventListener("change", _ => toggleHide(toggleRoot)) - div.appendChild(showHideButton) - div.appendChild(p) - div + div(cls := "snippet-showhide")( + label(cls := "snippet-showhide-button")( + input("type" := "checkbox").tap(_.addEventListener("change", _ => toggleHide(toggleRoot))), + span(cls := "slider") + ), + p("Show collapsed lines") + ) } toggleHide(snippet) @@ -65,8 +59,7 @@ class CodeSnippets: private def snippetAnchor(snippet: html.Element): Unit = snippet.querySelector(".snippet-meta .snippet-label") match { case e: html.Element => val name = e.textContent.trim - val anchor = document.createElement("a").asInstanceOf[html.Anchor] - anchor.id = s"snippet-$name" + val anchor = a(id := s"snippet-$name") snippet.insertBefore(anchor, snippet.firstChild) case _ => } @@ -75,23 +68,20 @@ class CodeSnippets: val included = snippet.querySelectorAll("code span.include") val pre = snippet.querySelector("pre") if included != null && included.nonEmpty && pre != null then { - val includesDiv = document.createElement("div") - includesDiv.classList.add("included-section") - includesDiv.classList.add("hideable") - included + val includes = included .collect { case e: html.Element => e } .toList .filter(_.hasAttribute("name")) .map(_.getAttribute("name")) .distinct .map { name => - val a = document.createElement("a").asInstanceOf[html.Anchor] - a.classList.add("unselectable") - a.href = s"#snippet-$name" - a.innerHTML = s"included $name" - a + a(cls := "unselectable", href := s"#snippet-$name")( + "included", + b(name) + ) } - .foreach(a => includesDiv.appendChild(a)) + + val includesDiv = div(cls := "included-section hideable")(includes) snippet.insertBefore(includesDiv, pre) } @@ -99,30 +89,21 @@ class CodeSnippets: private def copyRunButtons(snippet: html.Element) = { def copyButton = { - val div = document.createElement("div") - val button = document.createElement("button") - val icon = document.createElement("i") - icon.classList.add("far") - icon.classList.add("fa-clone") - button.appendChild(icon) - button.classList.add("copy-button") - button.addEventListener("click", _ => { - val code = snippet.querySelectorAll("code>span:not(.hidden)") - .map(_.textContent) - .mkString - window.navigator.clipboard.writeText(code) - }) - div.appendChild(button) - div + div( + button(cls := "copy-button")( + i(cls := "far fa-clone") + ).tap(_.addEventListener("click", _ => { + val code = snippet.querySelectorAll("code>span:not(.hidden)") + .map(_.textContent) + .mkString + window.navigator.clipboard.writeText(code) + })) + ) } def runButton = { - val div = document.createElement("div").asInstanceOf[html.Div] - val runButton = document.createElement("button").asInstanceOf[html.Button] - val runIcon = document.createElement("i") - runIcon.classList.add("fas") - runIcon.classList.add("fa-play") - runButton.classList.add("run-button") - runButton.appendChild(runIcon) + val runButton = button(cls := "run-button")( + i(cls := "fas fa-play") + ) runButton.addEventListener("click", _ => if !runButton.hasAttribute("opened") then { @@ -148,18 +129,14 @@ class CodeSnippets: } ) - div.appendChild(runButton) - div + div(runButton) } def exitButton = { - val div = document.createElement("div").asInstanceOf[html.Div] - val exitButton = document.createElement("button").asInstanceOf[html.Element] - val exitIcon = document.createElement("i") - exitIcon.classList.toggle("fas") - exitIcon.classList.toggle("fa-times") - exitButton.classList.add("exit-button") - div.style = "display:none;" - exitButton.appendChild(exitIcon) + val exitButton = button(cls := "exit-button")( + i(cls := "fas fa-times") + ) + + val bdiv = div(style := "display:none;")(exitButton) exitButton.addEventListener("click", _ => snippet.querySelector("pre") match { @@ -178,22 +155,16 @@ class CodeSnippets: case btn: html.Element => btn.parentElement.style = "display:none;" case _ => } - div.style = "display:none;" + bdiv.style = "display:none;" ) - div.appendChild(exitButton) - div + bdiv } - def toScastieButton = { - val div = document.createElement("div").asInstanceOf[html.Div] - val toScastieButton = document.createElement("button").asInstanceOf[html.Element] - val toScastieIcon = document.createElement("i").asInstanceOf[html.Image] - toScastieIcon.classList.add("fas") - toScastieIcon.classList.add("fa-external-link-alt") - toScastieButton.classList.add("to-scastie-button") - div.style = "display:none;" - toScastieButton.appendChild(toScastieIcon) + def toScastieButton = { + val toScastieButton = button(cls := "to-scastie-button")( + i(cls := "fas fa-external-link-alt") + ) toScastieButton.addEventListener("click", _ => snippet.querySelector(".embedded-menu li.logo") match { @@ -202,9 +173,9 @@ class CodeSnippets: } ) - div.appendChild(toScastieButton) - div + div("style" := "display:none;")(toScastieButton) } + val buttonsSection = getButtonsSection(snippet) buttonsSection.foreach(s => s.appendChild(copyButton) diff --git a/scaladoc-js/common/src/utils/html.scala b/scaladoc-js/common/src/utils/html.scala index 042d7f719459..fd49179752a2 100644 --- a/scaladoc-js/common/src/utils/html.scala +++ b/scaladoc-js/common/src/utils/html.scala @@ -18,7 +18,7 @@ object HTML { val elem: T = elemFactory() def unpackTags(tags: TagArg*): Unit = tags.foreach { case e: domhtml.Element => elem.appendChild(e) - case s: String => elem.appendChild(textNode(s.escapeReservedTokens)) + case s: String => elem.appendChild(textNode(s)) case elemSeq: (Seq[domhtml.Element | String] @unchecked) => unpackTags(elemSeq*) } @@ -45,7 +45,10 @@ object HTML { .replace("'", "'") case class Attr(name: String): - def :=(value: String): AppliedAttr = new AppliedAttr(name, value) + def :=(value: String): AppliedAttr = (name, value) + + extension (key: String) def :=(value: String): AppliedAttr = + (key, value) opaque type AppliedAttr = (String, String) @@ -88,6 +91,8 @@ object HTML { val th = Tag[domhtml.TableCell]("th") val tr = Tag[domhtml.TableRow]("tr") val td = Tag[domhtml.TableCell]("td") + val b = Tag[domhtml.Element]("b") + val i = Tag[domhtml.Element]("i") val cls = Attr("class") val href = Attr("href") diff --git a/scaladoc-js/contributors/src/content-contributors/ContentContributors.scala b/scaladoc-js/contributors/src/content-contributors/ContentContributors.scala index bbc9c4787dc6..28b78cef0296 100644 --- a/scaladoc-js/contributors/src/content-contributors/ContentContributors.scala +++ b/scaladoc-js/contributors/src/content-contributors/ContentContributors.scala @@ -13,6 +13,8 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.util.{Success,Failure} +import utils.HTML._ + // Contributors widget // see https://stackoverflow.com/a/19200303/4496364 // Copied from https://github.com/scala/docs.scala-lang/blob/main/resources/js/functions.js and rewritten to Scala.js @@ -90,21 +92,17 @@ class ContentContributors: getAuthorsForFilename(Globals.githubContributorsFilename.stripPrefix("/")).onComplete { case Success(authors) => val maybeDiv = Option(document.getElementById("documentation-contributors")) - maybeDiv.foreach { div => - authors.foreach { case FullAuthor(name, url, img) => - val divN = document.createElement("div") - val imgN = document.createElement("img").asInstanceOf[html.Image] - imgN.src = img - val autN = document.createElement("a").asInstanceOf[html.Anchor] - autN.href = url - autN.text = name - divN.appendChild(imgN) - divN.appendChild(autN) - div.appendChild(divN) + maybeDiv.foreach { mdiv => + authors.foreach { case FullAuthor(name, url, imgUrl) => + val inner = div( + img(src := imgUrl)(), + a(href := url)(name) + ) + mdiv.appendChild(inner) } if authors.nonEmpty then - div.asInstanceOf[html.Div].parentElement.classList.toggle("hidden") + mdiv.asInstanceOf[html.Div].parentElement.classList.toggle("hidden") } case Failure(err) => println(s"Couldn't fetch contributors. $err") diff --git a/scaladoc-js/main/src/searchbar/SearchbarComponent.scala b/scaladoc-js/main/src/searchbar/SearchbarComponent.scala index c795b4aa634a..d0080d0793ae 100644 --- a/scaladoc-js/main/src/searchbar/SearchbarComponent.scala +++ b/scaladoc-js/main/src/searchbar/SearchbarComponent.scala @@ -1,10 +1,14 @@ package dotty.tools.scaladoc +import utils.HTML._ + import org.scalajs.dom._ import org.scalajs.dom.ext._ import org.scalajs.dom.html.Input import scala.scalajs.js.timers._ -import scala.concurrent.duration._ +import scala.concurrent.duration.{span => dspan, _} + +import scala.util.chaining._ import java.net.URI @@ -13,120 +17,71 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch val resultsChunkSize = 20 extension (p: PageEntry) def toHTML = - val wrapper = document.createElement("div").asInstanceOf[html.Div] - wrapper.classList.add("scaladoc-searchbar-row") - wrapper.setAttribute("result", "") - wrapper.classList.add("monospace") - - val resultA = document.createElement("a").asInstanceOf[html.Anchor] - resultA.href = - if (p.isLocationExternal) { - p.location - } else { - Globals.pathToRoot + p.location - } - resultA.text = s"${p.fullName}" - resultA.onclick = (event: Event) => - if (document.body.contains(rootDiv)) { - document.body.removeChild(rootDiv) - } - - val location = document.createElement("span") - location.classList.add("pull-right") - location.classList.add("scaladoc-searchbar-location") - location.textContent = p.description + val location = if (p.isLocationExternal) { + p.location + } else { + Globals.pathToRoot + p.location + } - wrapper.appendChild(resultA) - resultA.appendChild(location) - wrapper.addEventListener("mouseover", { - case e: MouseEvent => handleHover(wrapper) - }) - wrapper + div(cls := "scaladoc-searchbar-row monospace", "result" := "")( + a(href := location)( + p.fullName, + span(cls := "pull-right scaladoc-searchbar-location")(p.description) + ).tap { _.onclick = (event: Event) => + if (document.body.contains(rootDiv)) { + document.body.removeChild(rootDiv) + } + } + ).tap { wrapper => wrapper.addEventListener("mouseover", { + case e: MouseEvent => handleHover(wrapper) + }) + } extension (m: InkuireMatch) def toHTML = - val wrapper = document.createElement("div").asInstanceOf[html.Div] - wrapper.classList.add("scaladoc-searchbar-row") - wrapper.setAttribute("result", "") - wrapper.setAttribute("inkuire-result", "") - wrapper.classList.add("monospace") - wrapper.setAttribute("mq", m.mq.toString) - - val resultA = document.createElement("a").asInstanceOf[html.Anchor] - // Inkuire pageLocation should start with e (external) - // or i (internal). The rest of the string is an absolute - // or relative URL - resultA.href = - if (m.pageLocation(0) == 'e') { + val location = if (m.pageLocation(0) == 'e') { m.pageLocation.substring(1) } else { Globals.pathToRoot + m.pageLocation.substring(1) } - resultA.text = m.functionName - resultA.onclick = (event: Event) => - if (document.body.contains(rootDiv)) { - document.body.removeChild(rootDiv) - } - - val packageDiv = document.createElement("div").asInstanceOf[html.Div] - packageDiv.classList.add("scaladoc-searchbar-inkuire-package") - - val packageIcon = document.createElement("span").asInstanceOf[html.Span] - packageIcon.classList.add("micon") - packageIcon.classList.add("pa") - val packageSpan = document.createElement("span").asInstanceOf[html.Span] - packageSpan.textContent = m.packageLocation - - val signature = document.createElement("span") - signature.classList.add("pull-right") - signature.classList.add("scaladoc-searchbar-inkuire-signature") - signature.textContent = m.prettifiedSignature - - wrapper.appendChild(resultA) - resultA.appendChild(signature) - wrapper.appendChild(packageDiv) - packageDiv.appendChild(packageIcon) - packageDiv.appendChild(packageSpan) - wrapper.addEventListener("mouseover", { - case e: MouseEvent => handleHover(wrapper) - }) - wrapper + div(cls := "scaladoc-searchbar-row monospace", "result" := "", "inkuire-result" := "", "mq" := m.mq.toString)( + a(href := location)( + m.functionName, + span(cls := "pull-right scaladoc-searchbar-inkuire-signature")(m.prettifiedSignature) + ).tap { _.onclick = (event: Event) => + if (document.body.contains(rootDiv)) { + document.body.removeChild(rootDiv) + } + }, + div(cls := "scaladoc-searchbar-inkuire-package")( + span(cls := "micon pa"), + span(m.packageLocation) + ) + ).tap { wrapper => wrapper.addEventListener("mouseover", { + case e: MouseEvent => handleHover(wrapper) + }) + } def createKindSeparator(kind: String) = - val kindSeparator = document.createElement("div").asInstanceOf[html.Div] - val icon = document.createElement("span").asInstanceOf[html.Span] - icon.classList.add("micon") - icon.classList.add(kind.take(2)) - val name = document.createElement("span").asInstanceOf[html.Span] - name.textContent = kind - kindSeparator.classList.add("scaladoc-searchbar-row") - kindSeparator.setAttribute("divider", "") - kindSeparator.classList.add("monospace") - kindSeparator.appendChild(icon) - kindSeparator.appendChild(name) - kindSeparator + div(cls := "scaladoc-searchbar-row monospace", "divider" := "")( + span(cls := s"micon ${kind.take(2)}"), + span(kind) + ) def handleNewFluffQuery(matchers: List[Matchers]) = val result = engine.query(matchers) val fragment = document.createDocumentFragment() def createLoadMoreElement = - val loadMoreElement = document.createElement("div").asInstanceOf[html.Div] - loadMoreElement.classList.add("scaladoc-searchbar-row") - loadMoreElement.setAttribute("loadmore", "") - loadMoreElement.classList.add("monospace") - val icon = document.createElement("a").asInstanceOf[html.Anchor] - icon.classList.add("i") - icon.classList.add("fas") - icon.classList.add("fa-arrow-down") - val text = document.createElement("span").asInstanceOf[html.Span] - text.textContent = "Show more..." - val anchor = document.createElement("a").asInstanceOf[html.Anchor] - anchor.appendChild(icon) - anchor.appendChild(text) - loadMoreElement.appendChild(anchor) - loadMoreElement.addEventListener("mouseover", _ => handleHover(loadMoreElement)) - loadMoreElement + div(cls := "scaladoc-searchbar-row monospace", "loadmore" := "")( + a( + a(cls := "i fas fa-arrow-down"), + span("Show more...") + ) + ).tap { loadMoreElement => loadMoreElement + .addEventListener("mouseover", _ => handleHover(loadMoreElement)) + } + result.groupBy(_.kind).map { case (kind, entries) => val kindSeparator = createKindSeparator(kind) @@ -158,28 +113,15 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch while (resultsDiv.hasChildNodes()) resultsDiv.removeChild(resultsDiv.lastChild) resultsDiv.appendChild(fragment) - def createLoadingAnimation: raw.HTMLElement = { - val loading = document.createElement("div").asInstanceOf[html.Div] - loading.classList.add("loading-wrapper") - val animation = document.createElement("div").asInstanceOf[html.Div] - animation.classList.add("loading") - loading.appendChild(animation) - loading -} - + def createLoadingAnimation: raw.HTMLElement = + div(cls := "loading-wrapper")( + div(cls := "loading") + ) extension (s: String) def toHTMLError = - val wrapper = document.createElement("div").asInstanceOf[html.Div] - wrapper.classList.add("scaladoc-searchbar-row") - wrapper.classList.add("scaladoc-searchbar-error") - wrapper.classList.add("monospace") - - val errorSpan = document.createElement("span").asInstanceOf[html.Span] - errorSpan.classList.add("search-error") - errorSpan.textContent = s - - wrapper.appendChild(errorSpan) - wrapper + div(cls := "scaladoc-searchbar-row monospace", "error" := "")( + span(cls := "search-error")(s) + ) var timeoutHandle: SetTimeoutHandle = null def handleNewQuery(query: String) = @@ -215,57 +157,52 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch } private val searchIcon: html.Div = - val span = document.createElement("span").asInstanceOf[html.Span] - span.innerHTML = """""" - span.id = "scaladoc-search" - span.onclick = (event: Event) => - if (document.body.contains(rootDiv)) { - document.body.removeChild(rootDiv) - } - else { - document.body.appendChild(rootDiv) - input.focus() - } - // open the search if the user hits the `s` key when not focused on a text input - document.body.addEventListener("keydown", (e: KeyboardEvent) => handleGlobalKeyDown(e)) - - val element = createNestingDiv("search-content")( - createNestingDiv("search-container")( - createNestingDiv("search")( - span + val element = div(cls := "search-content")( + div(cls := "search-container")( + div(cls := "search")( + span(id := "scaladoc-search")().tap { span => + span.innerHTML = """""" + span.onclick = (event: Event) => + if (document.body.contains(rootDiv)) { + document.body.removeChild(rootDiv) + } + else { + document.body.appendChild(rootDiv) + inputElem.focus() + } + } ) ) ) + // open the search if the user hits the `s` key when not focused on a text input + document.body.addEventListener("keydown", (e: KeyboardEvent) => handleGlobalKeyDown(e)) + document.getElementById("scaladoc-searchBar").appendChild(element) element - private val input: html.Input = - val element = document.createElement("input").asInstanceOf[html.Input] - element.id = "scaladoc-searchbar-input" - element.addEventListener("input", { e => - val inputValue = e.target.asInstanceOf[html.Input].value - if inputValue.isEmpty then showHints() - else handleNewQuery(inputValue) - }) - element.autocomplete = "off" - element + private val inputElem: html.Input = + input(id := "scaladoc-searchbar-input").tap { element => + element.addEventListener("input", { e => + val inputValue = e.target.asInstanceOf[html.Input].value + if inputValue.isEmpty then showHints() + else handleNewQuery(inputValue) + }) + + element.autocomplete = "off" + } private val resultsDiv: html.Div = - val element = document.createElement("div").asInstanceOf[html.Div] - element.id = "scaladoc-searchbar-results" - element + div(id := "scaladoc-searchbar-results") private val rootHiddenClasses = "hidden" private val rootShowClasses = "" - private def createNestingDiv(className: String)(innerElement: html.Element): html.Div = - val element = document.createElement("div").asInstanceOf[html.Div] - element.className = className - element.appendChild(innerElement) - element - private val rootDiv: html.Div = - val element = document.createElement("div").asInstanceOf[html.Div] + val element = div(id := "scaladoc-searchbar")( + inputElem, + resultsDiv + ) + element.addEventListener("mousedown", (e: Event) => e.stopPropagation()) searchIcon.addEventListener("mousedown", (e: Event) => e.stopPropagation()) document.body.addEventListener("mousedown", (e: Event) => @@ -280,9 +217,7 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch else if e.keyCode == 13 then handleEnter() else if e.keyCode == 27 then handleEscape() }) - element.id = "scaladoc-searchbar" - element.appendChild(input) - element.appendChild(resultsDiv) + element private def handleArrowUp() = { @@ -344,7 +279,7 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch } private def handleEscape() = { // clear the search input and close the search - input.value = "" + inputElem.value = "" showHints() document.body.removeChild(rootDiv) } @@ -368,63 +303,32 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch document.body.appendChild(rootDiv) // if we focus during the event handler, the `s` gets typed into the input - window.setTimeout(() => input.focus(), 1.0) + window.setTimeout(() => inputElem.focus(), 1.0) } } } } - private case class ListRoot(elems: Seq[ListNode]) - private case class ListNode(value: String, nested: ListRoot) - - private def ul(nodes: ListNode*) = ListRoot(nodes) - private def li(s: String) = ListNode(s, ListRoot(Nil)) - private def li(s: String, nested: ListRoot) = ListNode(s, nested) - - private def renderList: ListRoot => Option[html.UList] = { - case ListRoot(Nil) => None - case ListRoot(nodes) => - val list = document.createElement("ul").asInstanceOf[html.UList] - nodes.foreach { - case ListNode(txt, nested) => - val li = document.createElement("li").asInstanceOf[html.LI] - li.innerHTML = txt - renderList(nested).foreach(li.appendChild) - list.appendChild(li) - } - Some(list) - } - private def showHints() = { def clearResults() = while (resultsDiv.hasChildNodes()) resultsDiv.removeChild(resultsDiv.lastChild) - val hintsDiv = document.createElement("div").asInstanceOf[html.Div] - hintsDiv.classList.add("searchbar-hints") - val icon = document.createElement("span").asInstanceOf[html.Span] - icon.classList.add("fas") - icon.classList.add("fa-lightbulb") - icon.classList.add("fa-5x") - val header = document.createElement("h1").asInstanceOf[html.Heading] - header.textContent = "A bunch of hints to make your life easier" - val listElements: ListRoot = ul( - li("Type a phrase to search members by name and static sites by title"), - li("Type abbreviations cC, caCa, camCa to search for camelCase"), + val hintsDiv = div(cls := "searchbar-hints")( + span(cls := "fas fa-lightbulb fa-5x"), + h1("A bunch of hints to make your life easier"), + ul(cls := "searchbar-hints-list")( + li("Type a phrase to search members ", b("by name")," and static sites ", b("by title"),""), + li("Type abbreviations", b("cC, caCa, camCa")," to search for ", b("camelCase")), li( - "Type a function signature to search for members by signature using Inkuire", + "Type a function signature to search for members ", b("by signature")," using Inkuire", ul( - li("Type String => Int to find String.size, String.toInt"), - li("Type String => String => String to find String.mkString, String.stripPrefix"), - li("Inkuire also finds field accessors. Type Some[A] => A to find Some.value"), - li("For more information about Inkuire see the documentation"), + li("Type ", b("String => Int")," to find ", b("String.size"),", ", b("String.toInt"),""), + li("Type ", b("String => String => String")," to find ", b("String.mkString"),", ", b("String.stripPrefix"),""), + li("Inkuire also finds field accessors. Type ", b("Some[A] => A")," to find ", b("Some.value"),""), + li("For more information about Inkuire see ", a(href := "https://docs.scala-lang.org/scala3/guides/scaladoc/search-engine.html")("the documentation")), li("The availability of this function depends on configuration used to generate Scaladoc") ) ) + ) ) - - val list = renderList(listElements).get - list.classList.add("searchbar-hints-list") - hintsDiv.appendChild(icon) - hintsDiv.appendChild(header) - hintsDiv.appendChild(list) clearResults() resultsDiv.appendChild(hintsDiv) } diff --git a/scaladoc-js/main/src/social-links/SocialLinks.scala b/scaladoc-js/main/src/social-links/SocialLinks.scala index 5fbe42eb99e7..3e33e4065955 100644 --- a/scaladoc-js/main/src/social-links/SocialLinks.scala +++ b/scaladoc-js/main/src/social-links/SocialLinks.scala @@ -3,10 +3,12 @@ package dotty.tools.scaladoc import org.scalajs.dom._ import org.scalajs.dom.ext._ +import utils.HTML._ + class SocialLinks: def addIcon(elem: html.Element) = - val img = document.createElement("img").asInstanceOf[html.Image] - img.src = s"${Globals.pathToRoot}images/${elem.getAttribute("data-icon-path")}" - elem.appendChild(img) + elem.appendChild( + img(src := s"${Globals.pathToRoot}images/${elem.getAttribute("data-icon-path")}")() + ) document.querySelectorAll(".social-icon").collect { case e: html.Element => e }.foreach(addIcon) diff --git a/scaladoc-js/main/src/versions-dropdown/DropdownHandler.scala b/scaladoc-js/main/src/versions-dropdown/DropdownHandler.scala index 53187cf066ab..54034fb5a589 100644 --- a/scaladoc-js/main/src/versions-dropdown/DropdownHandler.scala +++ b/scaladoc-js/main/src/versions-dropdown/DropdownHandler.scala @@ -11,6 +11,8 @@ import org.scalajs.dom.ext.Ajax import scala.scalajs.js import scala.scalajs.js.JSON +import utils.HTML._ + trait Versions extends js.Object: def versions: js.Dictionary[String] @@ -23,12 +25,9 @@ class DropdownHandler: 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 + var child = a(href := v)(k) ddc.appendChild(child) - val arrow = document.createElement("span").asInstanceOf[html.Span] - arrow.classList.add("ar") + val arrow = span(cls := "ar")() document.getElementById("dropdown-button").appendChild(arrow) private def disableButton() =