1
+ /// <reference lib="dom" />
2
+
1
3
/* eslint-env browser */
2
4
5
+ /**
6
+ * @import {ElementContent} from 'hast'
7
+ * @import {Data} from '../generate/data.js'
8
+ * @import {Index as FlexSearch} from 'flexsearch'
9
+ */
10
+
11
+ /**
12
+ * @typedef Search
13
+ * @property {HTMLElement } $scope
14
+ * @property {(search: Search) => Promise<undefined> } create
15
+ * @property {(data: Data, query: string) => ElementContent } empty
16
+ * @property {((data: Data, result: ReadonlyArray<string>) => Array<string>) | undefined } [filter]
17
+ * @property {FlexSearch } index
18
+ * @property {(data: Data, result: ReadonlyArray<string>) => ElementContent } results
19
+ * @property {(data: Data) => ElementContent } preview
20
+ * @property {string } selector
21
+ * @property {(item: string) => number } weight
22
+ */
23
+
24
+ import { ok as assert } from 'devlop'
3
25
import flexsearch from 'flexsearch'
4
- import mean from 'compute-mean'
5
26
import { toDom } from 'hast-util-to-dom'
6
27
import { data } from '../generate/data.js'
7
28
import { search as searchForm } from '../generate/molecule/search.js'
@@ -41,50 +62,83 @@ function init() {
41
62
const keywords = Object . keys ( data . packagesByKeyword )
42
63
const topics = Object . keys ( data . projectsByTopic )
43
64
const $root = document . querySelector ( '#' + id )
65
+ assert ( $root )
44
66
const $form = toDom ( searchForm ( data , parameter ) )
67
+ assert ( $form instanceof HTMLElement )
45
68
const $input = $form . querySelector ( '[name=' + parameter + ']' )
69
+ assert ( $input )
46
70
47
71
$root . prepend ( $form )
48
72
49
73
const promises = [
50
74
{
51
75
selector : '#root-keyword' ,
52
- create : ( search ) =>
53
- new Promise ( ( resolve ) => {
76
+ /**
77
+ * @param {Search } search
78
+ * @returns {Promise<undefined> }
79
+ */
80
+ create ( search ) {
81
+ return new Promise ( ( resolve ) => {
54
82
window . requestAnimationFrame ( ( ) => {
55
83
keywords . forEach ( ( d ) => search . index . add ( d , d ) )
56
- resolve ( )
84
+ resolve ( undefined )
57
85
} )
58
- } ) ,
59
- weight : ( d ) => data . packagesByKeyword [ d ] . length ,
86
+ } )
87
+ } ,
88
+ /**
89
+ * @param {string } d
90
+ * @returns {number }
91
+ */
92
+ weight ( d ) {
93
+ return data . packagesByKeyword [ d ] . length
94
+ } ,
60
95
filter : keywordFilter ,
61
96
preview : keywordPreview ,
62
97
empty : keywordEmpty ,
63
98
results : keywordResults
64
99
} ,
65
100
{
66
101
selector : '#root-topic' ,
67
- create : ( search ) =>
68
- new Promise ( ( resolve ) => {
102
+ /**
103
+ * @param {Search } search
104
+ * @returns {Promise<undefined> }
105
+ */
106
+ create ( search ) {
107
+ return new Promise ( ( resolve ) => {
69
108
window . requestAnimationFrame ( ( ) => {
70
109
topics . forEach ( ( d ) => search . index . add ( d , d ) )
71
- resolve ( )
110
+ resolve ( undefined )
72
111
} )
73
- } ) ,
74
- weight : ( d ) => data . projectsByTopic [ d ] . length ,
112
+ } )
113
+ } ,
114
+ /**
115
+ * @param {string } d
116
+ * @returns {number }
117
+ */
118
+ weight ( d ) {
119
+ return data . projectsByTopic [ d ] . length
120
+ } ,
75
121
filter : topicFilter ,
76
122
preview : topicPreview ,
77
123
empty : topicEmpty ,
78
124
results : topicResults
79
125
} ,
80
126
{
81
127
selector : '#root-package' ,
82
- create : ( search ) =>
83
- new Promise ( ( resolve ) => {
128
+ /**
129
+ * @param {Search } search
130
+ * @returns {Promise<undefined> }
131
+ */
132
+ create ( search ) {
133
+ return new Promise ( ( resolve ) => {
84
134
const size = 100
85
135
86
136
window . requestAnimationFrame ( ( ) => next ( 0 ) )
87
137
138
+ /**
139
+ * @param {number } start
140
+ * @returns {undefined }
141
+ */
88
142
function next ( start ) {
89
143
const end = start + size
90
144
const slice = names . slice ( start , end )
@@ -97,25 +151,40 @@ function init() {
97
151
)
98
152
99
153
if ( slice . length === 0 ) {
100
- resolve ( )
154
+ resolve ( undefined )
101
155
} else {
102
156
window . requestAnimationFrame ( ( ) => next ( end ) )
103
157
}
104
158
}
105
- } ) ,
106
- weight : ( d ) => data . packageByName [ d ] . score ,
159
+ } )
160
+ } ,
161
+ /**
162
+ * @param {string } d
163
+ * @returns {number }
164
+ */
165
+ weight ( d ) {
166
+ return data . packageByName [ d ] . score
167
+ } ,
107
168
preview : packagePreview ,
108
169
empty : packageEmpty ,
109
170
results : packageResults
110
171
} ,
111
172
{
112
173
selector : '#root-project' ,
113
- create : ( search ) =>
114
- new Promise ( ( resolve ) => {
174
+ /**
175
+ * @param {Search } search
176
+ * @returns {Promise<undefined> }
177
+ */
178
+ create ( search ) {
179
+ return new Promise ( ( resolve ) => {
115
180
const size = 100
116
181
117
182
window . requestAnimationFrame ( ( ) => next ( 0 ) )
118
183
184
+ /**
185
+ * @param {number } start
186
+ * @returns {undefined }
187
+ */
119
188
function next ( start ) {
120
189
const end = start + size
121
190
const slice = repos . slice ( start , end )
@@ -128,46 +197,73 @@ function init() {
128
197
} )
129
198
130
199
if ( slice . length === 0 ) {
131
- resolve ( )
200
+ resolve ( undefined )
132
201
} else {
133
202
window . requestAnimationFrame ( ( ) => next ( end ) )
134
203
}
135
204
}
136
- } ) ,
137
- weight : ( d ) => helperReduceScore ( data , d ) ,
205
+ } )
206
+ } ,
207
+ /**
208
+ * @param {string } d
209
+ * @returns {number }
210
+ */
211
+ weight ( d ) {
212
+ return helperReduceScore ( data , d )
213
+ } ,
138
214
preview : projectPreview ,
139
215
empty : projectEmpty ,
140
216
results : projectResults
141
217
}
142
- ] . map ( ( d ) => {
143
- const $scope = document . querySelector ( d . selector )
144
- const index = new Index ( { preset : 'score' , tokenize : 'full' } )
145
- const view = { ...d , index, $scope}
146
-
147
- return view . create ( view ) . then ( ( ) => view )
148
- } )
218
+ ] . map (
219
+ /**
220
+ *
221
+ * @param {Omit<Search, '$scope' | 'index'> } d
222
+ * @returns
223
+ */
224
+ function ( d ) {
225
+ const $scope = document . querySelector ( d . selector )
226
+ assert ( $scope instanceof HTMLElement )
227
+ const index = new Index ( { preset : 'score' , tokenize : 'full' } )
228
+ /** @type {Search } */
229
+ const view = { ...d , index, $scope}
230
+
231
+ return view . create ( view ) . then ( ( ) => view )
232
+ }
233
+ )
149
234
150
235
Promise . all ( promises ) . then ( ( searches ) => {
151
236
start ( )
152
237
153
238
$form . addEventListener ( 'submit' , onsubmit )
154
239
window . addEventListener ( 'popstate' , onpopstate )
155
240
241
+ /**
242
+ * @returns {undefined }
243
+ */
156
244
function start ( ) {
157
- const query = clean ( new URL ( loc ) . searchParams . get ( parameter ) )
245
+ const query = clean ( new URL ( loc . href ) . searchParams . get ( parameter ) )
158
246
159
247
if ( query ) {
160
248
onpopstate ( )
161
249
}
162
250
}
163
251
252
+ /**
253
+ * @returns {undefined }
254
+ */
164
255
function onpopstate ( ) {
165
- search ( clean ( new URL ( loc ) . searchParams . get ( parameter ) ) )
256
+ search ( clean ( new URL ( loc . href ) . searchParams . get ( parameter ) ) )
166
257
}
167
258
259
+ /**
260
+ * @param {HTMLElementEventMap['submit'] } ev
261
+ * @returns {undefined }
262
+ */
168
263
function onsubmit ( ev ) {
169
- const url = new URL ( loc )
264
+ const url = new URL ( loc . href )
170
265
const current = clean ( url . searchParams . get ( parameter ) )
266
+ assert ( $input instanceof HTMLInputElement )
171
267
const value = clean ( $input . value )
172
268
173
269
ev . preventDefault ( )
@@ -182,38 +278,49 @@ function init() {
182
278
url . searchParams . delete ( parameter )
183
279
}
184
280
185
- history . pushState (
186
- { } ,
187
- null ,
188
- url . pathname + url . search . replace ( / % 2 0 / g, '+' )
189
- )
281
+ history . pushState ( { } , '' , url . pathname + url . search . replace ( / % 2 0 / g, '+' ) )
190
282
191
283
search ( value )
192
284
}
193
285
286
+ /**
287
+ * @param {string } query
288
+ * @returns {undefined }
289
+ */
194
290
function search ( query ) {
291
+ const $release = document . querySelector ( '#root-release' )
292
+ assert ( $release instanceof HTMLElement )
293
+ assert ( $input instanceof HTMLInputElement )
195
294
$input . value = query
196
295
197
296
if ( ! query ) {
198
- document . querySelector ( '#root- release' ) . style = ''
297
+ $ release. style . removeProperty ( 'display' )
199
298
searches . forEach ( ( search ) => replace ( search , [ ] , query ) )
200
299
return
201
300
}
202
301
203
- document . querySelector ( '#root- release' ) . style = 'display:none '
302
+ $ release. style . display = 'block '
204
303
205
304
searches . forEach ( ( search ) => {
206
305
search . index . searchAsync ( query , { suggest : true } , ( result ) => {
207
- const clean = result . filter ( unique )
306
+ const clean = /** @type { Array<string> } */ ( result . filter ( unique ) )
208
307
const weighted = desc ( clean , weight )
209
308
210
309
replace ( search , asc ( clean , combined ) , query )
211
310
311
+ /**
312
+ * @param {string } d
313
+ * @returns {number }
314
+ */
212
315
function combined ( d ) {
213
- return mean ( [ clean . indexOf ( d ) , weighted . indexOf ( d ) ] )
316
+ return ( clean . indexOf ( d ) + weighted . indexOf ( d ) ) / 2
214
317
}
215
318
} )
216
319
320
+ /**
321
+ * @param {string } d
322
+ * @returns {number }
323
+ */
217
324
function weight ( d ) {
218
325
return search . weight ( d )
219
326
}
@@ -222,6 +329,11 @@ function init() {
222
329
} )
223
330
}
224
331
332
+ /**
333
+ * @param {Search } search
334
+ * @param {ReadonlyArray<string> } result
335
+ * @param {string } query
336
+ */
225
337
function replace ( search , result , query ) {
226
338
const { $scope, filter, preview, empty, results} = search
227
339
@@ -244,6 +356,10 @@ function replace(search, result, query) {
244
356
$scope . append ( $next )
245
357
}
246
358
359
+ /**
360
+ * @param {string | null } value
361
+ * @returns {string }
362
+ */
247
363
function clean ( value ) {
248
364
return ( value || '' ) . trim ( ) . toLowerCase ( )
249
365
}
0 commit comments