Skip to content

Commit 542ee05

Browse files
committed
Add grouping in searchbar. Unify default and Inkuire html representation.
1 parent 5a4a571 commit 542ee05

File tree

5 files changed

+139
-82
lines changed

5 files changed

+139
-82
lines changed

scaladoc-js/common/css/searchbar.css

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -206,41 +206,42 @@ div[selected] > .scaladoc-searchbar-inkuire-package {
206206
overflow: auto;
207207
}
208208

209-
.scaladoc-searchbar-result {
209+
.scaladoc-searchbar-row {
210+
display: flex;
210211
background-color: var(--leftbar-bg);
211212
color: var(--leftbar-fg);
212213
line-height: 24px;
213214
padding: 4px 10px 4px 10px;
214215
}
215216

216-
.scaladoc-searchbar-result-row {
217-
display: flex;
217+
.scaladoc-searchbar-row.hidden {
218+
display: none;
219+
}
220+
221+
.scaladoc-searchbar-row[divider] {
222+
border-top: solid 1px var(--leftbar-border);
218223
}
219224

220-
.scaladoc-searchbar-result .micon {
225+
.scaladoc-searchbar-row .micon {
221226
height: 16px;
222227
width: 16px;
223228
margin: 4px 8px 0px 0px;
224229
}
225230

226-
.scaladoc-searchbar-result:first-of-type {
231+
.scaladoc-searchbar-row:first-of-type {
227232
margin-top: 10px;
228233
}
229234

230-
.scaladoc-searchbar-result[selected] {
235+
.scaladoc-searchbar-row[selected] {
231236
background-color: var(--leftbar-hover-bg);
232237
color: var(--leftbar-hover-fg);
233238
}
234239

235-
.scaladoc-searchbar-result a {
236-
/* for some reason, with display:block if there's a wrap between the
237-
* search result text and the location span, the dead space to the
238-
* left of the location span doesn't get treated as part of the block,
239-
* which defeats the purpose of making the <a> a block element.
240-
* But inline-block with width:100% works as desired.
241-
*/
242-
display: inline-block;
243-
width: 100%;
240+
.scaladoc-searchbar-row[result] {
241+
flex-direction: column;
242+
}
243+
244+
.scaladoc-searchbar-row[result] a {
244245
text-indent: -20px;
245246
padding-left: 20px;
246247
}

scaladoc-js/main/src/searchbar/SearchbarComponent.scala

Lines changed: 116 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,14 @@ import scala.concurrent.duration._
99
import java.net.URI
1010

1111
class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearchEngine, parser: QueryParser):
12-
val resultsChunkSize = 100
12+
val resultsChunkSize = 5
1313
extension (p: PageEntry)
1414
def toHTML =
1515
val wrapper = document.createElement("div").asInstanceOf[html.Div]
16-
wrapper.classList.add("scaladoc-searchbar-result")
17-
wrapper.classList.add("scaladoc-searchbar-result-row")
16+
wrapper.classList.add("scaladoc-searchbar-row")
17+
wrapper.setAttribute("result", "")
1818
wrapper.classList.add("monospace")
1919

20-
val icon = document.createElement("span").asInstanceOf[html.Span]
21-
icon.classList.add("micon")
22-
icon.classList.add(p.kind.take(2))
23-
2420
val resultA = document.createElement("a").asInstanceOf[html.Anchor]
2521
resultA.href =
2622
if (p.isLocationExternal) {
@@ -39,7 +35,6 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
3935
location.classList.add("scaladoc-searchbar-location")
4036
location.textContent = p.description
4137

42-
wrapper.appendChild(icon)
4338
wrapper.appendChild(resultA)
4439
resultA.appendChild(location)
4540
wrapper.addEventListener("mouseover", {
@@ -50,27 +45,22 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
5045
extension (m: InkuireMatch)
5146
def toHTML =
5247
val wrapper = document.createElement("div").asInstanceOf[html.Div]
53-
wrapper.classList.add("scaladoc-searchbar-result")
48+
wrapper.classList.add("scaladoc-searchbar-row")
49+
wrapper.setAttribute("result", "")
50+
wrapper.setAttribute("inkuire-result", "")
5451
wrapper.classList.add("monospace")
5552
wrapper.setAttribute("mq", m.mq.toString)
5653

57-
val resultDiv = document.createElement("div").asInstanceOf[html.Div]
58-
resultDiv.classList.add("scaladoc-searchbar-result-row")
59-
60-
val icon = document.createElement("span").asInstanceOf[html.Span]
61-
icon.classList.add("micon")
62-
icon.classList.add(m.entryType.take(2))
63-
6454
val resultA = document.createElement("a").asInstanceOf[html.Anchor]
6555
// Inkuire pageLocation should start with e (external)
6656
// or i (internal). The rest of the string is an absolute
6757
// or relative URL
68-
resultA.href =
58+
resultA.href =
6959
if (m.pageLocation(0) == 'e') {
7060
m.pageLocation.substring(1)
7161
} else {
7262
Globals.pathToRoot + m.pageLocation.substring(1)
73-
}
63+
}
7464
resultA.text = m.functionName
7565
resultA.onclick = (event: Event) =>
7666
if (document.body.contains(rootDiv)) {
@@ -92,9 +82,7 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
9282
signature.classList.add("scaladoc-searchbar-inkuire-signature")
9383
signature.textContent = m.prettifiedSignature
9484

95-
wrapper.appendChild(resultDiv)
96-
resultDiv.appendChild(icon)
97-
resultDiv.appendChild(resultA)
85+
wrapper.appendChild(resultA)
9886
resultA.appendChild(signature)
9987
wrapper.appendChild(packageDiv)
10088
packageDiv.appendChild(packageIcon)
@@ -104,29 +92,78 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
10492
})
10593
wrapper
10694

95+
def createKindSeparator(kind: String) =
96+
val kindSeparator = document.createElement("div").asInstanceOf[html.Div]
97+
val icon = document.createElement("span").asInstanceOf[html.Span]
98+
icon.classList.add("micon")
99+
icon.classList.add(kind.take(2))
100+
val name = document.createElement("span").asInstanceOf[html.Span]
101+
name.textContent = kind
102+
kindSeparator.classList.add("scaladoc-searchbar-row")
103+
kindSeparator.setAttribute("divider", "")
104+
kindSeparator.classList.add("monospace")
105+
kindSeparator.appendChild(icon)
106+
kindSeparator.appendChild(name)
107+
kindSeparator
108+
107109
def handleNewFluffQuery(matchers: List[Matchers]) =
108-
val result = engine.query(matchers).map(_.toHTML)
109-
resultsDiv.scrollTop = 0
110-
while (resultsDiv.hasChildNodes()) resultsDiv.removeChild(resultsDiv.lastChild)
110+
val result = engine.query(matchers)
111111
val fragment = document.createDocumentFragment()
112-
result.take(resultsChunkSize).foreach(fragment.appendChild)
113-
resultsDiv.appendChild(fragment)
114-
def loadMoreResults(result: List[raw.HTMLElement]): Unit = {
115-
resultsDiv.onscroll = (event: Event) => {
116-
if (resultsDiv.scrollHeight - resultsDiv.scrollTop == resultsDiv.clientHeight) {
117-
val fragment = document.createDocumentFragment()
118-
result.take(resultsChunkSize).foreach(fragment.appendChild)
119-
resultsDiv.appendChild(fragment)
120-
loadMoreResults(result.drop(resultsChunkSize))
112+
def createLoadMoreElement =
113+
val loadMoreElement = document.createElement("div").asInstanceOf[html.Div]
114+
loadMoreElement.classList.add("scaladoc-searchbar-row")
115+
loadMoreElement.setAttribute("loadmore", "")
116+
loadMoreElement.classList.add("monospace")
117+
val anchor = document.createElement("a").asInstanceOf[html.Anchor]
118+
anchor.text = "Show more..."
119+
loadMoreElement.appendChild(anchor)
120+
loadMoreElement.addEventListener("mouseover", _ => handleHover(loadMoreElement))
121+
loadMoreElement
122+
result.groupBy(_.kind).map {
123+
case (kind, entries) =>
124+
val kindSeparator = createKindSeparator(kind)
125+
val htmlEntries = entries.map(_.toHTML)
126+
val loadMoreElement = createLoadMoreElement
127+
def loadMoreResults(entries: List[raw.HTMLElement]): Unit = {
128+
loadMoreElement.onclick = (event: Event) => {
129+
entries.take(resultsChunkSize).foreach(_.classList.remove("hidden"))
130+
val nextElems = entries.drop(resultsChunkSize)
131+
if nextElems.nonEmpty then loadMoreResults(nextElems) else loadMoreElement.classList.add("hidden")
132+
}
121133
}
122-
}
134+
135+
fragment.appendChild(kindSeparator)
136+
htmlEntries.foreach(fragment.appendChild)
137+
fragment.appendChild(loadMoreElement)
138+
139+
val nextElems = htmlEntries.drop(resultsChunkSize)
140+
if nextElems.nonEmpty then {
141+
nextElems.foreach(_.classList.add("hidden"))
142+
loadMoreResults(nextElems)
143+
} else {
144+
loadMoreElement.classList.add("hidden")
145+
}
146+
123147
}
124-
loadMoreResults(result.drop(resultsChunkSize))
148+
149+
resultsDiv.scrollTop = 0
150+
while (resultsDiv.hasChildNodes()) resultsDiv.removeChild(resultsDiv.lastChild)
151+
resultsDiv.appendChild(fragment)
152+
153+
def createLoadingAnimation: raw.HTMLElement = {
154+
val loading = document.createElement("div").asInstanceOf[html.Div]
155+
loading.classList.add("loading-wrapper")
156+
val animation = document.createElement("div").asInstanceOf[html.Div]
157+
animation.classList.add("loading")
158+
loading.appendChild(animation)
159+
loading
160+
}
125161

126162
extension (s: String)
127163
def toHTMLError =
128164
val wrapper = document.createElement("div").asInstanceOf[html.Div]
129-
wrapper.classList.add("scaladoc-searchbar-result")
165+
wrapper.classList.add("scaladoc-searchbar-row")
166+
wrapper.classList.add("scaladoc-searchbar-error")
130167
wrapper.classList.add("monospace")
131168

132169
val errorSpan = document.createElement("span").asInstanceOf[html.Span]
@@ -141,35 +178,30 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
141178
clearTimeout(timeoutHandle)
142179
resultsDiv.scrollTop = 0
143180
resultsDiv.onscroll = (event: Event) => { }
144-
while (resultsDiv.hasChildNodes()) resultsDiv.removeChild(resultsDiv.lastChild)
181+
def clearResults() = while (resultsDiv.hasChildNodes()) resultsDiv.removeChild(resultsDiv.lastChild)
145182
val fragment = document.createDocumentFragment()
146183
parser.parse(query) match {
147184
case EngineMatchersQuery(matchers) =>
185+
clearResults()
148186
handleNewFluffQuery(matchers)
149187
case BySignature(signature) =>
150188
timeoutHandle = setTimeout(1.second) {
151-
val properResultsDiv = document.createElement("div").asInstanceOf[html.Div]
152-
resultsDiv.appendChild(properResultsDiv)
153-
val loading = document.createElement("div").asInstanceOf[html.Div]
154-
loading.classList.add("loading-wrapper")
155-
val animation = document.createElement("div").asInstanceOf[html.Div]
156-
animation.classList.add("loading")
157-
loading.appendChild(animation)
158-
properResultsDiv.appendChild(loading)
189+
val loading = createLoadingAnimation
190+
val kindSeparator = createKindSeparator("inkuire")
191+
clearResults()
192+
resultsDiv.appendChild(loading)
193+
resultsDiv.appendChild(kindSeparator)
159194
inkuireEngine.query(query) { (m: InkuireMatch) =>
160-
val next = properResultsDiv.children.foldLeft[Option[Element]](None) {
161-
case (acc, child) if !acc.isEmpty => acc
162-
case (_, child) =>
163-
Option.when(child.hasAttribute("mq") && Integer.parseInt(child.getAttribute("mq")) > m.mq)(child)
164-
}
195+
val next = resultsDiv.children
196+
.find(child => child.hasAttribute("mq") && Integer.parseInt(child.getAttribute("mq")) > m.mq)
165197
next.fold {
166-
properResultsDiv.appendChild(m.toHTML)
198+
resultsDiv.appendChild(m.toHTML)
167199
} { next =>
168-
properResultsDiv.insertBefore(m.toHTML, next)
200+
resultsDiv.insertBefore(m.toHTML, next)
169201
}
170202
} { (s: String) =>
171-
animation.classList.remove("loading")
172-
properResultsDiv.appendChild(s.toHTMLError)
203+
resultsDiv.removeChild(loading)
204+
resultsDiv.appendChild(s.toHTMLError)
173205
}
174206
}
175207
}
@@ -226,7 +258,7 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
226258
searchIcon.addEventListener("mousedown", (e: Event) => e.stopPropagation())
227259
document.body.addEventListener("mousedown", (e: Event) =>
228260
if (document.body.contains(element)) {
229-
document.body.removeChild(element)
261+
handleEscape()
230262
}
231263
)
232264
element.addEventListener("keydown", {
@@ -245,32 +277,50 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
245277
val selectedElement = resultsDiv.querySelector("[selected]")
246278
if selectedElement != null then {
247279
selectedElement.removeAttribute("selected")
248-
val sibling = selectedElement.previousElementSibling
249-
if sibling != null && sibling.classList.contains("scaladoc-searchbar-result") then {
280+
def recur(elem: raw.Element): raw.Element = {
281+
val prev = elem.previousElementSibling
282+
if prev == null then null
283+
else {
284+
if !prev.classList.contains("hidden") &&
285+
prev.classList.contains("scaladoc-searchbar-row") &&
286+
(prev.hasAttribute("result") || prev.hasAttribute("loadmore"))
287+
then prev
288+
else recur(prev)
289+
}
290+
}
291+
val sibling = recur(selectedElement)
292+
if sibling != null then {
250293
sibling.setAttribute("selected", "")
251294
resultsDiv.scrollTop = sibling.asInstanceOf[html.Element].offsetTop - (2 * sibling.asInstanceOf[html.Element].clientHeight)
252295
}
253296
}
254297
}
255298
private def handleArrowDown() = {
256299
val selectedElement = resultsDiv.querySelector("[selected]")
300+
def recur(elem: raw.Element): raw.Element = {
301+
val next = elem.nextElementSibling
302+
if next == null then null
303+
else {
304+
if !next.classList.contains("hidden") &&
305+
next.classList.contains("scaladoc-searchbar-row") &&
306+
(next.hasAttribute("result") || next.hasAttribute("loadmore"))
307+
then next
308+
else recur(next)
309+
}
310+
}
257311
if selectedElement != null then {
258-
val sibling = selectedElement.nextElementSibling
312+
val sibling = recur(selectedElement)
259313
if sibling != null then {
260314
selectedElement.removeAttribute("selected")
261315
sibling.setAttribute("selected", "")
262316
resultsDiv.scrollTop = sibling.asInstanceOf[html.Element].offsetTop - (2 * sibling.asInstanceOf[html.Element].clientHeight)
263317
}
264318
} else {
265319
val firstResult = resultsDiv.firstElementChild
266-
if firstResult != null && firstResult.classList.contains("scaladoc-searchbar-result") then {
267-
firstResult.setAttribute("selected", "")
268-
resultsDiv.scrollTop = firstResult.asInstanceOf[html.Element].offsetTop - (2 * firstResult.asInstanceOf[html.Element].clientHeight)
269-
} else if firstResult != null && firstResult.firstElementChild != null && firstResult.firstElementChild.nextElementSibling != null then {
270-
// for Inkuire there is another wrapper to avoid displaying old results + the first (child) div is a loading animation wrapper | should be resolved in #12995
271-
val properFirstResult = firstResult.firstElementChild.nextElementSibling
272-
properFirstResult.setAttribute("selected", "")
273-
resultsDiv.scrollTop = properFirstResult.asInstanceOf[html.Element].offsetTop - (2 * properFirstResult.asInstanceOf[html.Element].clientHeight)
320+
if firstResult != null then {
321+
val toSelect = if firstResult.classList.contains("scaladoc-searchbar-row") && firstResult.hasAttribute("result") then firstResult else recur(firstResult)
322+
toSelect.setAttribute("selected", "")
323+
resultsDiv.scrollTop = toSelect.asInstanceOf[html.Element].offsetTop - (2 * toSelect.asInstanceOf[html.Element].clientHeight)
274324
}
275325
}
276326
}
Lines changed: 1 addition & 0 deletions
Loading

scaladoc/resources/dotty_res/styles/scalastyle.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,10 @@ footer .mode {
848848
content: url("../images/method.svg")
849849
}
850850

851+
.micon.in {
852+
content: url("../images/inkuire.svg")
853+
}
854+
851855
#leftColumn .socials {
852856
display: none;
853857
}

scaladoc/src/dotty/tools/scaladoc/renderers/Resources.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ trait Resources(using ctx: DocContext) extends Locations, Writer:
147147
val descr = m.dri.asFileLocation
148148
def processMember(member: Member): Seq[JSON] =
149149
val signatureBuilder = ScalaSignatureProvider.rawSignature(member, InlineSignatureBuilder())().asInstanceOf[InlineSignatureBuilder]
150-
val sig = Signature(Plain(s"${member.kind.name} "), Plain(member.name)) ++ signatureBuilder.names.reverse
150+
val sig = Signature(Plain(member.name)) ++ signatureBuilder.names.reverse
151151
val entry = mkEntry(member.dri, member.name, flattenToText(sig), descr, member.kind.name)
152152
val children = member
153153
.membersBy(m => m.kind != Kind.Package && !m.kind.isInstanceOf[Classlike])
@@ -189,6 +189,7 @@ trait Resources(using ctx: DocContext) extends Locations, Writer:
189189
dottyRes("images/val.svg"),
190190
dottyRes("images/package.svg"),
191191
dottyRes("images/static.svg"),
192+
dottyRes("images/inkuire.svg"),
192193
dottyRes("images/github-icon-black.png"),
193194
dottyRes("images/github-icon-white.png"),
194195
dottyRes("images/discord-icon-black.png"),

0 commit comments

Comments
 (0)