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 all 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
}
5 changes: 5 additions & 0 deletions scala3doc-js/src/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dotty.dokka

object Main extends App {
Searchbar()
}
32 changes: 32 additions & 0 deletions scala3doc-js/src/searchbar/PageEntry.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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 {
private def createAcronym(s: String): Option[String] =
s.headOption.map(firstLetter => firstLetter.toString ++ s.tail.filter(_.isUpper))

def apply(jsObj: PageEntryJS): PageEntry = PageEntry(
jsObj.name,
jsObj.description,
jsObj.location,
jsObj.searchKeys.head.toLowerCase,
createAcronym(jsObj.searchKeys.head)
)
}
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)))
}
102 changes: 102 additions & 0 deletions scala3doc-js/src/searchbar/SearchbarComponent.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package dotty.dokka

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

class SearchbarComponent(val callback: (String) => List[PageEntry]):
val resultsChunkSize = 100
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(resultsChunkSize).foreach(fragment.appendChild)
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(resultsChunkSize).foreach(fragment.appendChild)
resultsDiv.appendChild(fragment)
loadMoreResults(result.drop(resultsChunkSize))
}
}
}
loadMoreResults(result.drop(resultsChunkSize))

private val searchIcon: html.Div =
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())
searchIcon.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
}
23 changes: 23 additions & 0 deletions scala3doc-js/src/searchbar/engine/Matchers.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dotty.dokka

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.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