Skip to content

Commit 26e4f07

Browse files
committed
Add grouping in searchbar. Unify default and Inkuire html representation.
1 parent dbfe1b5 commit 26e4f07

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 = Globals.pathToRoot + p.location
2622
resultA.text = s"${p.fullName}"
@@ -34,7 +30,6 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
3430
location.classList.add("scaladoc-searchbar-location")
3531
location.textContent = p.description
3632

37-
wrapper.appendChild(icon)
3833
wrapper.appendChild(resultA)
3934
resultA.appendChild(location)
4035
wrapper.addEventListener("mouseover", {
@@ -45,24 +40,19 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
4540
extension (m: InkuireMatch)
4641
def toHTML =
4742
val wrapper = document.createElement("div").asInstanceOf[html.Div]
48-
wrapper.classList.add("scaladoc-searchbar-result")
43+
wrapper.classList.add("scaladoc-searchbar-row")
44+
wrapper.setAttribute("result", "")
45+
wrapper.setAttribute("inkuire-result", "")
4946
wrapper.classList.add("monospace")
5047
wrapper.setAttribute("mq", m.mq.toString)
5148

52-
val resultDiv = document.createElement("div").asInstanceOf[html.Div]
53-
resultDiv.classList.add("scaladoc-searchbar-result-row")
54-
55-
val icon = document.createElement("span").asInstanceOf[html.Span]
56-
icon.classList.add("micon")
57-
icon.classList.add(m.entryType.take(2))
58-
5949
val resultA = document.createElement("a").asInstanceOf[html.Anchor]
60-
resultA.href =
50+
resultA.href =
6151
if(new URI(m.pageLocation).isAbsolute()) {
6252
m.pageLocation
6353
} else {
6454
Globals.pathToRoot + m.pageLocation
65-
}
55+
}
6656
resultA.text = m.functionName
6757
resultA.onclick = (event: Event) =>
6858
if (document.body.contains(rootDiv)) {
@@ -84,9 +74,7 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
8474
signature.classList.add("scaladoc-searchbar-inkuire-signature")
8575
signature.textContent = m.prettifiedSignature
8676

87-
wrapper.appendChild(resultDiv)
88-
resultDiv.appendChild(icon)
89-
resultDiv.appendChild(resultA)
77+
wrapper.appendChild(resultA)
9078
resultA.appendChild(signature)
9179
wrapper.appendChild(packageDiv)
9280
packageDiv.appendChild(packageIcon)
@@ -96,29 +84,78 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
9684
})
9785
wrapper
9886

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

118154
extension (s: String)
119155
def toHTMLError =
120156
val wrapper = document.createElement("div").asInstanceOf[html.Div]
121-
wrapper.classList.add("scaladoc-searchbar-result")
157+
wrapper.classList.add("scaladoc-searchbar-row")
158+
wrapper.classList.add("scaladoc-searchbar-error")
122159
wrapper.classList.add("monospace")
123160

124161
val errorSpan = document.createElement("span").asInstanceOf[html.Span]
@@ -133,35 +170,30 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
133170
clearTimeout(timeoutHandle)
134171
resultsDiv.scrollTop = 0
135172
resultsDiv.onscroll = (event: Event) => { }
136-
while (resultsDiv.hasChildNodes()) resultsDiv.removeChild(resultsDiv.lastChild)
173+
def clearResults() = while (resultsDiv.hasChildNodes()) resultsDiv.removeChild(resultsDiv.lastChild)
137174
val fragment = document.createDocumentFragment()
138175
parser.parse(query) match {
139176
case EngineMatchersQuery(matchers) =>
177+
clearResults()
140178
handleNewFluffQuery(matchers)
141179
case BySignature(signature) =>
142180
timeoutHandle = setTimeout(1.second) {
143-
val properResultsDiv = document.createElement("div").asInstanceOf[html.Div]
144-
resultsDiv.appendChild(properResultsDiv)
145-
val loading = document.createElement("div").asInstanceOf[html.Div]
146-
loading.classList.add("loading-wrapper")
147-
val animation = document.createElement("div").asInstanceOf[html.Div]
148-
animation.classList.add("loading")
149-
loading.appendChild(animation)
150-
properResultsDiv.appendChild(loading)
181+
val loading = createLoadingAnimation
182+
val kindSeparator = createKindSeparator("inkuire")
183+
clearResults()
184+
resultsDiv.appendChild(loading)
185+
resultsDiv.appendChild(kindSeparator)
151186
inkuireEngine.query(query) { (m: InkuireMatch) =>
152-
val next = properResultsDiv.children.foldLeft[Option[Element]](None) {
153-
case (acc, child) if !acc.isEmpty => acc
154-
case (_, child) =>
155-
Option.when(child.hasAttribute("mq") && Integer.parseInt(child.getAttribute("mq")) > m.mq)(child)
156-
}
187+
val next = resultsDiv.children
188+
.find(child => child.hasAttribute("mq") && Integer.parseInt(child.getAttribute("mq")) > m.mq)
157189
next.fold {
158-
properResultsDiv.appendChild(m.toHTML)
190+
resultsDiv.appendChild(m.toHTML)
159191
} { next =>
160-
properResultsDiv.insertBefore(m.toHTML, next)
192+
resultsDiv.insertBefore(m.toHTML, next)
161193
}
162194
} { (s: String) =>
163-
animation.classList.remove("loading")
164-
properResultsDiv.appendChild(s.toHTMLError)
195+
resultsDiv.removeChild(loading)
196+
resultsDiv.appendChild(s.toHTMLError)
165197
}
166198
}
167199
}
@@ -218,7 +250,7 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
218250
searchIcon.addEventListener("mousedown", (e: Event) => e.stopPropagation())
219251
document.body.addEventListener("mousedown", (e: Event) =>
220252
if (document.body.contains(element)) {
221-
document.body.removeChild(element)
253+
handleEscape()
222254
}
223255
)
224256
element.addEventListener("keydown", {
@@ -237,32 +269,50 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
237269
val selectedElement = resultsDiv.querySelector("[selected]")
238270
if selectedElement != null then {
239271
selectedElement.removeAttribute("selected")
240-
val sibling = selectedElement.previousElementSibling
241-
if sibling != null && sibling.classList.contains("scaladoc-searchbar-result") then {
272+
def recur(elem: raw.Element): raw.Element = {
273+
val prev = elem.previousElementSibling
274+
if prev == null then null
275+
else {
276+
if !prev.classList.contains("hidden") &&
277+
prev.classList.contains("scaladoc-searchbar-row") &&
278+
(prev.hasAttribute("result") || prev.hasAttribute("loadmore"))
279+
then prev
280+
else recur(prev)
281+
}
282+
}
283+
val sibling = recur(selectedElement)
284+
if sibling != null then {
242285
sibling.setAttribute("selected", "")
243286
resultsDiv.scrollTop = sibling.asInstanceOf[html.Element].offsetTop - (2 * sibling.asInstanceOf[html.Element].clientHeight)
244287
}
245288
}
246289
}
247290
private def handleArrowDown() = {
248291
val selectedElement = resultsDiv.querySelector("[selected]")
292+
def recur(elem: raw.Element): raw.Element = {
293+
val next = elem.nextElementSibling
294+
if next == null then null
295+
else {
296+
if !next.classList.contains("hidden") &&
297+
next.classList.contains("scaladoc-searchbar-row") &&
298+
(next.hasAttribute("result") || next.hasAttribute("loadmore"))
299+
then next
300+
else recur(next)
301+
}
302+
}
249303
if selectedElement != null then {
250-
val sibling = selectedElement.nextElementSibling
304+
val sibling = recur(selectedElement)
251305
if sibling != null then {
252306
selectedElement.removeAttribute("selected")
253307
sibling.setAttribute("selected", "")
254308
resultsDiv.scrollTop = sibling.asInstanceOf[html.Element].offsetTop - (2 * sibling.asInstanceOf[html.Element].clientHeight)
255309
}
256310
} else {
257311
val firstResult = resultsDiv.firstElementChild
258-
if firstResult != null && firstResult.classList.contains("scaladoc-searchbar-result") then {
259-
firstResult.setAttribute("selected", "")
260-
resultsDiv.scrollTop = firstResult.asInstanceOf[html.Element].offsetTop - (2 * firstResult.asInstanceOf[html.Element].clientHeight)
261-
} else if firstResult != null && firstResult.firstElementChild != null && firstResult.firstElementChild.nextElementSibling != null then {
262-
// 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
263-
val properFirstResult = firstResult.firstElementChild.nextElementSibling
264-
properFirstResult.setAttribute("selected", "")
265-
resultsDiv.scrollTop = properFirstResult.asInstanceOf[html.Element].offsetTop - (2 * properFirstResult.asInstanceOf[html.Element].clientHeight)
312+
if firstResult != null then {
313+
val toSelect = if firstResult.classList.contains("scaladoc-searchbar-row") && firstResult.hasAttribute("result") then firstResult else recur(firstResult)
314+
toSelect.setAttribute("selected", "")
315+
resultsDiv.scrollTop = toSelect.asInstanceOf[html.Element].offsetTop - (2 * toSelect.asInstanceOf[html.Element].clientHeight)
266316
}
267317
}
268318
}
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
@@ -146,7 +146,7 @@ trait Resources(using ctx: DocContext) extends Locations, Writer:
146146
val descr = m.dri.asFileLocation
147147
def processMember(member: Member): Seq[JSON] =
148148
val signatureBuilder = ScalaSignatureProvider.rawSignature(member, InlineSignatureBuilder())().asInstanceOf[InlineSignatureBuilder]
149-
val sig = Signature(Plain(s"${member.kind.name} "), Plain(member.name)) ++ signatureBuilder.names.reverse
149+
val sig = Signature(Plain(member.name)) ++ signatureBuilder.names.reverse
150150
val entry = mkEntry(member.dri, member.name, flattenToText(sig), descr, member.kind.name)
151151
val children = member
152152
.membersBy(m => m.kind != Kind.Package && !m.kind.isInstanceOf[Classlike])
@@ -188,6 +188,7 @@ trait Resources(using ctx: DocContext) extends Locations, Writer:
188188
dottyRes("images/val.svg"),
189189
dottyRes("images/package.svg"),
190190
dottyRes("images/static.svg"),
191+
dottyRes("images/inkuire.svg"),
191192
dottyRes("images/github-icon-black.png"),
192193
dottyRes("images/github-icon-white.png"),
193194
dottyRes("images/discord-icon-black.png"),

0 commit comments

Comments
 (0)