Skip to content

Replace default dokka searchbar with new implemented in Scala.js #11021

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 19, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ val `tasty-core-bootstrapped` = Build.`tasty-core-bootstrapped`
val `tasty-core-scala2` = Build.`tasty-core-scala2`
val scala3doc = Build.scala3doc
val `scala3doc-testcases` = Build.`scala3doc-testcases`
val `scala3doc-js` = Build.`scala3doc-js`
val `scala3-bench-run` = Build.`scala3-bench-run`
val dist = Build.dist
val `community-build` = Build.`community-build`
Expand Down
28 changes: 28 additions & 0 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import sbtbuildinfo.BuildInfoPlugin.autoImport._

import scala.util.Properties.isJavaAtLeast

import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._

object MyScalaJSPlugin extends AutoPlugin {
import Build._

Expand Down Expand Up @@ -1228,6 +1230,8 @@ object Build {
lazy val `scala3doc` = project.in(file("scala3doc")).asScala3doc
lazy val `scala3doc-testcases` = project.in(file("scala3doc-testcases")).asScala3docTestcases

lazy val `scala3doc-js` = project.in(file("scala3doc-js")).asScala3docJs

// sbt plugin to use Dotty in your own build, see
// https://github.com/lampepfl/scala3-example-project for usage.
lazy val `sbt-dotty` = project.in(file("sbt-dotty")).
Expand Down Expand Up @@ -1640,6 +1644,19 @@ object Build {
),
Compile / buildInfoKeys := Seq[BuildInfoKey](version),
Compile / buildInfoPackage := "dotty.dokka",
Compile / resourceGenerators += Def.task {
val jsDestinationFile = (Compile / resourceManaged).value / "dotty_res" / "scripts" / "searchbar.js"
sbt.IO.copyFile((fullOptJS in Compile in `scala3doc-js`).value.data, jsDestinationFile)
Seq(jsDestinationFile)
}.taskValue,
Compile / resourceGenerators += Def.task {
val cssDesitnationFile = (Compile / resourceManaged).value / "dotty_res" / "styles" / "scala3doc-searchbar.css"
val cssSourceFile = (resourceDirectory in Compile in `scala3doc-js`).value / "scala3doc-searchbar.css"
FileFunction.cached(streams.value.cacheDirectory / "css-cache") { (in: Set[File]) =>
in.headOption.map(sbt.IO.copyFile(_, cssDesitnationFile))
Set(cssDesitnationFile)
}.apply(Set(cssSourceFile)).toSeq
}.taskValue,
testDocumentationRoot := (baseDirectory.value / "test-documentations").getAbsolutePath,
buildInfoPackage in Test := "dotty.dokka.test",
BuildInfoPlugin.buildInfoScopedSettings(Test),
Expand All @@ -1653,6 +1670,17 @@ object Build {
def asScala3docTestcases: Project =
project.dependsOn(`scala3-compiler-bootstrapped`).settings(commonBootstrappedSettings)

def asScala3docJs: Project =
project.
enablePlugins(MyScalaJSPlugin).
dependsOn(`scala3-library-bootstrappedJS`).
settings(
fork in Test := false,
scalaJSUseMainModuleInitializer := true,
libraryDependencies += ("org.scala-js" %%% "scalajs-dom" % "1.1.0").withDottyCompat(scalaVersion.value)
)


def asDist(implicit mode: Mode): Project = project.
enablePlugins(PackPlugin).
withCommonSettings.
Expand Down
102 changes: 102 additions & 0 deletions scala3doc-js/resources/scala3doc-searchbar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* button */
.search span {
background: #ED3522;
fill: #fff;
cursor: pointer;
border: none;
padding: 9px;
border-radius: 24px;
box-shadow: 0 0 16px #F27264;
}
.search span:hover {
fill: #F27264;
}

@media(max-width: 576px) {
.search span {
background: none;
fill: var(--icon-color);
cursor: pointer;
border: none;
padding: 0;
box-shadow: none;
margin-top: 2px;
}
.search span:hover {
fill: var(--link-hover-fg);
}
}

#scala3doc-search {
margin-top: 10px;
cursor: pointer;
position: fixed;
top: 0;
right: 20px;
z-index: 5;
}

#scala3doc-searchbar.hidden {
display: none;
}

#scala3doc-searchbar {
position: absolute;
top: 50px;
right: 40px;
width: calc(100% - 360px);
box-shadow: 0 2px 16px 0 rgba(0, 42, 76, 0.15);
font-size: 13px;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Arial, sans-serif;
}

#scala3doc-searchbar-input {
width: 100%;
min-height: 32px;
border: none;
border-bottom: 1px solid #bbb;
padding: 10px;
}

#scala3doc-searchbar-input:focus {
outline: none;
}

#scala3doc-searchbar-results {
background: white;
display: flex;
flex-direction: column;
max-height: 500px;
overflow: auto;
}

.scala3doc-searchbar-result {
line-height: 32px;
padding-left: 10px;
padding-right: 10px;
}

.scala3doc-searchbar-result:first-of-type {
margin-top: 10px;
}

.scala3doc-searchbar-result:hover {
background-color: #d4edff;
}

.scala3doc-searchbar-result a {
color: #1f2326;
}

.scala3doc-searchbar-result .scala3doc-searchbar-location {
color: gray;
}

#searchBar {
display: inline-flex;
}

.pull-right {
float: right;
margin-left: auto
}
10 changes: 10 additions & 0 deletions scala3doc-js/src/Globals.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dotty.dokka

import scala.scalajs.js
import scala.scalajs.js.annotation.JSGlobalScope

@js.native
@JSGlobalScope
object Globals extends js.Object {
val pathToRoot: String = js.native
}
7 changes: 7 additions & 0 deletions scala3doc-js/src/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dotty.dokka

object Main extends App {
def initializeSearchbar(): Unit = Searchbar()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we simply call Searchbar() instead of defining method and calling it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed


initializeSearchbar()
}
29 changes: 29 additions & 0 deletions scala3doc-js/src/searchbar/PageEntry.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package dotty.dokka

import scala.scalajs.js

@js.native
trait PageEntryJS extends js.Object {
val name: String = js.native
val description: String = js.native
val location: String = js.native
val searchKeys: js.Array[String] = js.native
}

case class PageEntry(
fullName: String,
description: String,
location: String,
shortName: String,
acronym: Option[String],
)

object PageEntry {
def apply(jsObj: PageEntryJS): PageEntry = PageEntry(
jsObj.name,
jsObj.description,
jsObj.location,
jsObj.searchKeys.head.toLowerCase,
Option.when(jsObj.searchKeys.size > 1)(jsObj.searchKeys.last)
)
}
8 changes: 8 additions & 0 deletions scala3doc-js/src/searchbar/Searchbar.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dotty.dokka

class Searchbar {
val pages = SearchbarGlobals.pages.toList.map(PageEntry.apply)
val engine = SearchbarEngine(pages)
val parser = QueryParser()
val component = SearchbarComponent(q => engine.query(parser.parse(q)))
}
101 changes: 101 additions & 0 deletions scala3doc-js/src/searchbar/SearchbarComponent.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package dotty.dokka

import org.scalajs.dom._
import org.scalajs.dom.html.Input

class SearchbarComponent(val callback: (String) => List[PageEntry]):
extension (p: PageEntry)
def toHTML =
val wrapper = document.createElement("div").asInstanceOf[html.Div]
wrapper.classList.add("scala3doc-searchbar-result")
wrapper.classList.add("monospace")

val resultA = document.createElement("a").asInstanceOf[html.Anchor]
resultA.href = Globals.pathToRoot + p.location
resultA.text = s"${p.fullName}"

val location = document.createElement("span")
location.classList.add("pull-right")
location.classList.add("scala3doc-searchbar-location")
location.textContent = p.description

wrapper.appendChild(resultA)
wrapper.appendChild(location)
wrapper

def handleNewQuery(query: String) =
val result = callback(query).map(_.toHTML)
resultsDiv.scrollTop = 0
while (resultsDiv.hasChildNodes()) resultsDiv.removeChild(resultsDiv.lastChild)
val fragment = document.createDocumentFragment()
result.take(100).foreach(fragment.appendChild)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

magic number

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored

resultsDiv.appendChild(fragment)
def loadMoreResults(result: List[raw.HTMLElement]): Unit = {
resultsDiv.onscroll = (event: Event) => {
if (resultsDiv.scrollHeight - resultsDiv.scrollTop == resultsDiv.clientHeight)
{
val fragment = document.createDocumentFragment()
result.take(100).foreach(fragment.appendChild)
resultsDiv.appendChild(fragment)
loadMoreResults(result.drop(100))
}
}
}
loadMoreResults(result.drop(100))

private val logoClick: html.Div =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logoClick seems a bit misleading, maybe searchIcon is better name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

val span = document.createElement("span").asInstanceOf[html.Span]
span.innerHTML = """<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><path d="M19.64 18.36l-6.24-6.24a7.52 7.52 0 10-1.28 1.28l6.24 6.24zM7.5 13.4a5.9 5.9 0 115.9-5.9 5.91 5.91 0 01-5.9 5.9z"></path></svg>"""
span.id = "scala3doc-search"
span.onclick = (event: Event) =>
if (document.body.contains(rootDiv)) {
document.body.removeChild(rootDiv)
}
else document.body.appendChild(rootDiv)

val element = createNestingDiv("search-content")(
createNestingDiv("search-container")(
createNestingDiv("search")(
span
)
)
)
document.getElementById("scala3doc-searchBar").appendChild(element)
element


private val input: html.Input =
val element = document.createElement("input").asInstanceOf[html.Input]
element.id = "scala3doc-searchbar-input"
element.addEventListener("input", (e) => handleNewQuery(e.target.asInstanceOf[html.Input].value))
element

private val resultsDiv: html.Div =
val element = document.createElement("div").asInstanceOf[html.Div]
element.id = "scala3doc-searchbar-results"
element

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]
element.addEventListener("mousedown", (e: Event) => e.stopPropagation())
logoClick.addEventListener("mousedown", (e: Event) => e.stopPropagation())
document.body.addEventListener("mousedown", (e: Event) =>
if (document.body.contains(element)) {
document.body.removeChild(element)
}
)
element.id = "scala3doc-searchbar"
element.appendChild(input)
element.appendChild(resultsDiv)
element

handleNewQuery("")
10 changes: 10 additions & 0 deletions scala3doc-js/src/searchbar/SearchbarGlobals.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dotty.dokka

import scala.scalajs.js
import scala.scalajs.js.annotation.JSGlobalScope

@js.native
@JSGlobalScope
object SearchbarGlobals extends js.Object {
val pages: js.Array[PageEntryJS] = js.native
}
19 changes: 19 additions & 0 deletions scala3doc-js/src/searchbar/engine/Matchers.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dotty.dokka

enum Matchers(func: (PageEntry) => Int) extends Function1[PageEntry, Int]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be more readable if func is implemented in this way:

 enum Matchers extends Function1[PageEntry, Int]:
   case ByName(query: String)
   case ByKind(kind: String)
   
  def apply(p: PageEntry): Int = this match
    case ByName(query) =>
       val nameOption = Option(p.shortName)
       val acronym = p.acronym
       //Edge case for empty query string
       if query == "" then 1 else
         val results = List(
           nameOption.filter(_.contains(query.toLowerCase)).fold(-1)(_.size - query.size),
           acronym.filter(_.contains(query)).fold(-1)(_.size - query.size + 1)
         )
          if results.forall(_ == -1) then -1 else results.filter(_ != -1).min
   case ByKind(kind) =>
      p.fullName.split(" ").headOption match
        case world if world.equalsIgnoreCase(kind) => 1 // kind matches
        case _ => -1



Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to try here some Scala3 tricks, but apparently it didn't make things clearer

export func.apply
case ByName(query: String) extends Matchers( (p) => {
val nameOption = Option(p.shortName)
val acronym = p.acronym
//Edge case for empty query string
if query == "" then 1
else {
val results = List(
nameOption.filter(_.contains(query.toLowerCase)).fold(-1)(_.size - query.size),
acronym.filter(_.contains(query)).fold(-1)(_.size - query.size + 1)
)
if results.forall(_ == -1) then -1 else results.filter(_ != -1).min
}
})
case ByKind(kind: String) extends Matchers((p) => p.fullName.split(" ").headOption.filter(_.equalsIgnoreCase(kind)).fold(-1)(_ => 1))

28 changes: 28 additions & 0 deletions scala3doc-js/src/searchbar/engine/QueryParser.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dotty.dokka

import scala.util.matching.Regex._
import scala.util.matching._

class QueryParser:
val kinds = Seq(
"class",
"trait",
"enum",
"object",
"def",
"val",
"var",
"package",
"given",
"type"
)
val kindRegex = ("(?i)" + kinds.mkString("(","|",")") + " (.*)").r
val restRegex = raw"(.*)".r
val escapedRegex = raw"`(.*)`".r

def parse(query: String): List[Matchers] = query match {
case escapedRegex(rest) => List(Matchers.ByName(rest))
case kindRegex(kind, rest) => List(Matchers.ByKind(kind)) ++ parse(rest)
case restRegex(name) => List(Matchers.ByName(name))
case _ => List()
}
Loading