UNPKG

13.9 kBJavaScriptView Raw
1// @ts-check
2
3/* eslint-disable fp/no-mutating-methods */
4
5/**
6 * @typedef { (x: VNode ) => VNode } VNodeTransform
7 * @param { VNodeTransform } f
8 */
9function $root(f){
10
11 /**
12 * @param { VNode } o
13 */
14 function action(o){
15 var r =
16 f(o)
17
18 return typeof r !== 'undefined'
19 ? r
20 : o
21 }
22 return action
23}
24
25/**
26 * @param { VNodeTransform } f
27 */
28function $ul(f){
29
30 /**
31 * @param { VNode } div
32 */
33 function action(div){
34 var r =
35 f(div.children[1])
36
37 if( typeof r !== 'undefined' ){
38 // eslint-disable-next-line fp/no-mutation
39 div.children[1] = r
40 }
41 return div
42 }
43 return action
44}
45
46/**
47 * @param { ( o: VNode[]) => VNode[] } f
48 */
49function $li( f ){
50
51 return $ul(function(ul){
52 var r =
53 f(ul.children)
54
55
56 if( typeof r !== 'undefined' ){
57 // eslint-disable-next-line fp/no-mutation
58 ul.children = r
59 }
60
61 return ul
62 })
63}
64
65/**
66 * @param { VNodeTransform } f
67 */
68function $input(f){
69
70 /**
71 * @param { VNode } div
72 */
73 function action(div){
74 var r =
75 f(div.children[0])
76
77 if( typeof r !== 'undefined' ){
78 // eslint-disable-next-line fp/no-mutation
79 div.children[0] = r
80 }
81
82 return div
83 }
84 return action
85}
86
87/*
88* The following utilities are all adapted from
89* https://github.com/LeaVerou/awesomplete
90*/
91
92 /**
93 * @type { (input: string, text: string ) => boolean }
94 */
95 var contains = function contains(input, text){
96 return input.trim().length
97 ? RegExp(regExpEscape(input.trim()), "i").test(text)
98 : true
99 }
100
101 /**
102 * @type { (s: string ) => string }
103 */
104 var regExpEscape = function regExpEscape(s) {
105 return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
106 }
107
108 /**
109 * @type { {(a: string, b: string ) : number} }
110 */
111 var sortByLength = function sortByLength(a, b) {
112
113 if (a.length !== b.length) {
114 return a.length - b.length;
115 }
116
117 return a < b ? -1 : 1;
118 };
119
120/**
121 @type {
122 (p: { [k: string]: any }, b: [string, any] ) => { [k:string] : any }
123 }
124 */
125var assignPair = function assignPair(p, pair){
126 // eslint-disable-next-line fp/no-mutation
127 p[ pair[0] ] = pair[1]
128 return p
129}
130
131var keyboard = {
132 submit:
133 function submit(
134 /** @type {number} */code
135 ,/** @type {string} */ highlighted
136 ) {
137 return code == 13 && highlighted
138 ? [highlighted]
139 : []
140 }
141
142 ,dismiss:
143
144 function dismiss(
145 /** @type {number} */ code
146 ){
147 return code == 27
148 ? [true]
149 : []
150 }
151
152 ,navigate:
153 function navigate(
154 /** @type {boolean} */ showingDrawer
155 ,/** @type {string} */ highlighted
156 ,/** @type {string[]} */ renderedList
157 ,/** @type {number} */ code
158 ){
159 var KEY_UP = 38
160 var KEY_DOWN = 40
161 var i = renderedList.indexOf(highlighted)
162 var NO_MATCH = i == -1
163 var LOWER_BOUND = 0
164 var UPPER_BOUND = renderedList.length -1
165 var MATCH = i >= -1
166 var NEXT = i+1
167 var PREV = i-1
168
169 if( showingDrawer && (code == KEY_UP || code == KEY_DOWN) ){
170 if( code == KEY_UP && (NO_MATCH || i == LOWER_BOUND) ){
171 return [renderedList[UPPER_BOUND]]
172 } else if(code == KEY_UP && MATCH) {
173 return [renderedList[PREV]]
174 } else if (
175 code == KEY_DOWN && (
176 NO_MATCH || i == UPPER_BOUND
177 )
178 ) {
179 return [renderedList[LOWER_BOUND]]
180 } else { // ( code == KEY_DOWN && MATCH )
181 return [renderedList[NEXT]]
182 }
183 } else {
184 return []
185 }
186 }
187 }
188
189/**
190
191 @typedef {any} VNode
192 @typedef {
193 (tagName: string, attrs: object, children: VNode[] ) => VNode
194 } HyperscriptConstructor
195 @typedef { ( key: any ) => any } FrameworkGet
196 @typedef { ( key: any, value: any ) => any } FrameworkSet
197 @typedef {{
198 get: FrameworkGet
199 set: FrameworkSet
200 hyperscript: HyperscriptConstructor
201 }} Framework
202
203 @typedef {{
204 list: any
205 input: any
206 chosen: any
207 open: any
208 highlighted: any
209 }} Model
210
211 @typedef { Event & { currentTarget: { value: String }} } InputEvent
212 @typedef {{
213 minChars: number
214 maxItems: number
215 sort: { (input: string, text: string ): number }
216 filter: { (value: string, s: string): boolean }
217 filteredList: string[]
218 eventNames: { [k:string]: string }
219 showingDrawer: boolean
220 choose: { (x:string) : void }
221 clickItem: { (x:string) : void }
222 PATTERN_INPUT: RegExp | null
223 mark: { (x:string) : VNode }
224 highlight: { (x:string) : VNode }
225 oninput: { (e: InputEvent ): void }
226 onfocus: { (e: FocusEvent ): void }
227 close: { (): void }
228 onblur: { (e: FocusEvent ): void }
229 renderInput: { (config:Overrides) : VNode }
230 itemClassNames: { (x:string, config: Overrides) : string }
231 renderItem: { (x:string, config: Overrides): VNode }
232 renderItems: { (config: Overrides): VNode }
233 classNames: { () : string }
234 renderRoot: { (config: Overrides): VNode }
235 keyboardSubmit: { (code: number, highlighted: string ): string[] }
236 keyboardDismiss: { (code: number ): boolean[] }
237 keyboardNavigate: {
238 ( showingDrawer: boolean
239 , highlighted: string
240 , renderedList: string[]
241 , code: number
242 ) : string[]
243 }
244 onkeydown: { (e: KeyboardEvent ): void }
245
246 }} Overrides
247
248 @param {Framework} framework
249 */
250function BaseAutocomplete(framework){
251
252 var h = framework.hyperscript
253 var get = framework.get
254 var set = framework.set
255
256 /**
257 *
258 * @param {Model} model
259 * @param { Partial<Overrides> } nullableOverrides
260 */
261 function Autocomplete(model, nullableOverrides){
262 var overrides = nullableOverrides || {}
263 var list = model.list
264 var input = model.input
265 var chosen = model.chosen
266 var open = model.open
267
268 /** @type { () => string[] } */
269 var getList = function(){
270 return get(list)
271 }
272
273 /** @type { () => string } */
274 var getInput = function(){
275 return get(input)
276 }
277
278 /** @type { () => string } */
279 var getChosen = function(){
280 return get(chosen)
281 }
282
283 /** @type { () => boolean } */
284 var getOpen = function(){
285 return get(open)
286 }
287
288 /** @type { () => string } */
289 var getHighlighted = function(){
290 return get(highlighted)
291 }
292
293 var highlighted = model.highlighted
294
295 var value = getInput()
296
297 var minChars = typeof overrides.minChars != 'undefined'
298 ? overrides.minChars
299 : 2
300
301 var maxItems = typeof overrides.maxItems != 'undefined'
302 ? overrides.maxItems
303 : 10
304
305 var sort = typeof overrides.sort != 'undefined'
306 ? overrides.sort
307 : sortByLength
308
309 var filter = typeof overrides.filter != 'undefined'
310 ? overrides.filter
311 : contains
312
313 var filteredList =
314 typeof overrides.filteredList != 'undefined'
315 ? overrides.filteredList
316 : getList()
317 .filter(function (s){
318 return filter(value, s)
319 })
320 .sort( sort )
321 .slice(0, maxItems)
322
323 if( getHighlighted() != null
324 && filteredList.indexOf( get( highlighted ) ) == -1
325 ){
326 set(highlighted, null)
327 }
328
329 if( getChosen() != null
330 && getChosen() != value
331 ){
332 set(chosen, null)
333 }
334
335 /** @type {Overrides} */
336 var config = {
337
338 filteredList: filteredList
339 ,minChars: minChars
340 ,maxItems: maxItems
341 ,sort: sort
342 ,filter: filter
343 ,eventNames:
344 typeof overrides.eventNames != 'undefined'
345 ? overrides.eventNames
346 : { oninput: 'oninput'
347 , onfocus: 'onfocus'
348 , onblur: 'onblur'
349 , onkeydown: 'onkeydown'
350 , onmousedown: 'onmousedown'
351 }
352 ,showingDrawer:
353 typeof overrides.showingDrawer != 'undefined'
354 ? overrides.showingDrawer
355 : getOpen()
356 && value.length >= minChars
357 && filteredList.length > 0
358
359 ,choose:
360 typeof overrides.choose != 'undefined'
361 ? overrides.choose
362 : function choose(x){
363
364
365 if( getInput() != x ){
366 set(input, x)
367 }
368
369 if( getChosen() != x ){
370 set(chosen, x)
371 }
372
373 config.close()
374 }
375 ,clickItem:
376 typeof overrides.clickItem != 'undefined'
377 ? overrides.clickItem
378 : function clickItem(x){
379 return config.choose(x)
380
381 }
382 ,PATTERN_INPUT:
383 typeof overrides.PATTERN_INPUT != 'undefined'
384 ? overrides.PATTERN_INPUT
385 : value
386 ? new RegExp(value, 'gi')
387 : null
388 ,mark:
389 typeof overrides.mark != 'undefined'
390 ? overrides.mark
391 : function(x){
392 return h('mark', {}, [x])
393 }
394
395 ,highlight:
396 typeof overrides.highlight != 'undefined'
397 ? overrides.highlight
398 : function highlight( x ){
399
400 /** @type {string[] | null} */
401 var matches =
402 config.PATTERN_INPUT != null
403 ? x.match( config.PATTERN_INPUT )
404 : null
405
406 /** @type {{ buffer: string, output: string[] }} */
407 var initial = {
408 buffer: x
409 ,output: []
410 }
411
412 var processed =
413 matches != null
414 ? matches
415 .reduce(function(p, n){
416 var i = p.buffer.indexOf(n)
417
418 return {
419 buffer: p.buffer.slice(i+n.length)
420 ,output: p.output.concat(
421 i === 0
422 ? []
423 : p.buffer.slice(0, i)
424 ,[ config.mark(
425 p.buffer.slice(i, i+n.length)
426 )
427 ]
428 )
429 }
430 }, initial )
431 : { output: [x], buffer: '' }
432
433 return processed.output.concat(
434 processed.buffer || []
435 )
436 }
437 ,oninput:
438 typeof overrides.oninput != 'undefined'
439 ? overrides.oninput
440 : function oninput(e){
441
442 var v = e.currentTarget.value
443
444 if( getInput() != v ){
445 set(input, v)
446 }
447
448 if( !getOpen() ){
449 set(open, true)
450 }
451 }
452
453 ,onfocus:
454 typeof overrides.onfocus != 'undefined'
455 ? overrides.onfocus
456 : function onfocus(){
457 if( !getOpen() ){
458 set(open, true)
459 }
460 }
461 ,close:
462 typeof overrides.close != 'undefined'
463 ? overrides.close
464 : function close(){
465 //eslint-disable-next-line fp/no-mutation
466 if( getOpen() ){
467 set(open, false)
468 }
469 }
470 ,onblur:
471 typeof overrides.onblur != 'undefined'
472 ? overrides.onblur
473 : function onblur(){
474 config.close()
475 }
476 ,renderInput:
477 typeof overrides.renderInput != 'undefined'
478 ? overrides.renderInput
479 : function renderInput(){
480 return h('input'
481 ,[ ['value', value]
482 , [config.eventNames.oninput, config.oninput]
483 , [config.eventNames.onfocus, config.onfocus]
484 , [config.eventNames.onblur, config.onblur]
485 ]
486 .reduce(assignPair, {})
487 , []
488 )
489 }
490
491 ,itemClassNames:
492 typeof overrides.itemClassNames != 'undefined'
493 ? overrides.itemClassNames
494 : function itemClassNames(x){
495 return x == get(highlighted)
496 ? 'highlight'
497 : ''
498 }
499
500 ,renderItem:
501 typeof overrides.renderItem != 'undefined'
502 ? overrides.renderItem
503 : function renderItem(x, config){
504 return h(
505 'li'
506 ,[ ['className', config.itemClassNames(x, config) ]
507 , [ config.eventNames.onmousedown, function(
508 /** @type {Event} */ e
509 ){
510 config.clickItem(x)
511 e.stopPropagation()
512 }]
513 ]
514 .reduce(assignPair, {})
515
516 , config.highlight(x)
517 )
518 }
519 ,renderItems:
520 typeof overrides.renderItems != 'undefined'
521 ? overrides.renderItems
522 : function renderItems(config){
523 return h(
524 'ul'
525 , {}
526 , config.filteredList.map(
527 function filteredList$map(x){
528 return config.renderItem(x, config)
529 }
530 )
531 )
532 }
533 ,classNames:
534 typeof overrides.classNames != 'undefined'
535 ? overrides.classNames
536 : function classNames(){
537 return ['manuel-complete']
538 .concat(
539 config.showingDrawer ? ['open'] : []
540 ,value.length > 0 ? ['not-empty'] : []
541 ,getList().length > 0 ? ['loaded'] : []
542 )
543 .join(' ')
544 }
545 ,renderRoot:
546 typeof overrides.renderRoot != 'undefined'
547 ? overrides.renderRoot
548 : function renderRoot(config){
549 return h('div'
550 ,[[ 'className', config.classNames() ]
551 , [config.eventNames.onkeydown, config.onkeydown]
552 ]
553 .reduce(assignPair, {})
554 ,[ config.renderInput(config)
555 , config.renderItems(config)
556 ]
557 )
558 }
559 ,keyboardSubmit:
560 typeof overrides.keyboardSubmit != 'undefined'
561 ? overrides.keyboardSubmit
562 : keyboard.submit
563 ,keyboardDismiss:
564 typeof overrides.keyboardDismiss != 'undefined'
565 ? overrides.keyboardDismiss
566 : keyboard.dismiss
567 ,keyboardNavigate:
568 typeof overrides.keyboardNavigate != 'undefined'
569 ? overrides.keyboardNavigate
570 : keyboard.navigate
571 ,onkeydown:
572 typeof overrides.onkeydown != 'undefined'
573 ? overrides.onkeydown
574 : function onkeydown(e){
575 var new_chosen =
576 config.keyboardSubmit(
577 e.keyCode
578 , get(highlighted)
579 )
580
581 var dismiss = config.keyboardDismiss(
582 e.keyCode
583 )
584
585 var new_highlighted =
586 e.shiftKey
587 ? []
588 : config.keyboardNavigate(
589 config.showingDrawer
590 , get(highlighted)
591 , config.filteredList
592 , e.keyCode
593 )
594
595 new_chosen.map(
596 config.choose
597 )
598
599 new_highlighted.map(
600 function new_highlighted$map(v){
601 return set(highlighted, v)
602 }
603 )
604
605 dismiss.map( config.close )
606
607 new_chosen.length
608 + dismiss.length
609 + new_highlighted.length
610 > 0
611
612 && e.preventDefault()
613 }
614 }
615
616 return config.renderRoot(config)
617 }
618
619 return Autocomplete
620}
621
622module.exports = BaseAutocomplete
623
624// eslint-disable-next-line fp/no-mutation
625BaseAutocomplete.queries = {
626 listItems: $li
627 , list: $ul
628 , root: $root
629 , input: $input
630}
\No newline at end of file