@@ -140,6 +140,10 @@ export class APISelect {
140
140
*/
141
141
private queryUrl : string = '' ;
142
142
143
+ /**
144
+ * Interal state variable used to remember search key entered by user for "Filter" search box
145
+ */
146
+ private searchKey : Nullable < string > = null ;
143
147
/**
144
148
* Scroll position of options is at the bottom of the list, or not. Used to determine if
145
149
* additional options should be fetched from the API.
@@ -359,30 +363,41 @@ export class APISelect {
359
363
this . slim . enable ( ) ;
360
364
}
361
365
366
+ private setSearchKey ( event : Event ) {
367
+ const { value : q } = event . target as HTMLInputElement ;
368
+ this . searchKey = q
369
+ }
370
+
362
371
/**
363
372
* Add event listeners to this element and its dependencies so that when dependencies change
364
373
* this element's options are updated.
365
374
*/
366
375
private addEventListeners ( ) : void {
367
376
// Create a debounced function to fetch options based on the search input value.
368
- const fetcher = debounce ( ( event : Event ) => this . handleSearch ( event ) , 300 , false ) ;
377
+ const fetcher = debounce ( ( action : ApplyMethod , url : Nullable < string > ) => this . handleSearch ( action , url ) , 300 , false ) ;
369
378
370
379
// Query the API when the input value changes or a value is pasted.
371
380
this . slim . slim . search . input . addEventListener ( 'keyup' , event => {
372
381
// Only search when necessary keys are pressed.
373
382
if ( ! event . key . match ( / ^ ( A r r o w | E n t e r | T a b ) .* / ) ) {
374
- return fetcher ( event ) ;
383
+ this . setSearchKey ( event ) ;
384
+ return fetcher ( 'replace' , null ) ;
375
385
}
376
386
} ) ;
377
- this . slim . slim . search . input . addEventListener ( 'paste' , event => fetcher ( event ) ) ;
387
+ this . slim . slim . search . input . addEventListener ( 'paste' , event => {
388
+ this . setSearchKey ( event ) ;
389
+ return fetcher ( 'replace' , null ) ; ;
390
+ } ) ;
378
391
379
392
// Watch every scroll event to determine if the scroll position is at bottom.
380
393
this . slim . slim . list . addEventListener ( 'scroll' , ( ) => this . handleScroll ( ) ) ;
381
394
382
395
// When the scroll position is at bottom, fetch additional options.
383
- this . base . addEventListener ( `netbox.select.atbottom.${ this . name } ` , ( ) =>
384
- this . fetchOptions ( this . more , 'merge' ) ,
385
- ) ;
396
+ this . base . addEventListener ( `netbox.select.atbottom.${ this . name } ` , ( ) => {
397
+ if ( this . more != null ) {
398
+ return fetcher ( 'merge' , this . more , )
399
+ }
400
+ } ) ;
386
401
387
402
// When the base select element is disabled or enabled, properly disable/enable this instance.
388
403
this . base . addEventListener ( `netbox.select.disabled.${ this . name } ` , event =>
@@ -551,6 +566,14 @@ export class APISelect {
551
566
}
552
567
}
553
568
569
+ private getUrl ( ) {
570
+ var url = this . queryUrl
571
+ if ( this . searchKey != null ) {
572
+ url = queryString . stringifyUrl ( { url : this . queryUrl , query : { q : this . searchKey } } )
573
+ }
574
+ return url
575
+ }
576
+
554
577
/**
555
578
* Query the NetBox API for this element's options.
556
579
*/
@@ -559,21 +582,25 @@ export class APISelect {
559
582
this . resetOptions ( ) ;
560
583
return ;
561
584
}
562
- await this . fetchOptions ( this . queryUrl , action ) ;
585
+ const url = this . getUrl ( )
586
+ await this . fetchOptions ( url , action ) ;
563
587
}
564
588
565
589
/**
566
590
* Query the API for a specific search pattern and add the results to the available options.
567
591
*/
568
- private async handleSearch ( event : Event ) {
569
- const { value : q } = event . target as HTMLInputElement ;
570
- const url = queryString . stringifyUrl ( { url : this . queryUrl , query : { q } } ) ;
571
- if ( ! url . includes ( `{{` ) ) {
572
- await this . fetchOptions ( url , 'merge' ) ;
573
- this . slim . data . search ( q ) ;
574
- this . slim . render ( ) ;
592
+ private async handleSearch ( action : ApplyMethod = 'merge' , url : Nullable < string > ) {
593
+ if ( url == null ) {
594
+ url = this . getUrl ( )
575
595
}
576
- return ;
596
+ if ( url . includes ( `{{` ) ) {
597
+ return
598
+ }
599
+ await this . fetchOptions ( url , action ) ;
600
+ if ( this . searchKey != null ) {
601
+ this . slim . data . search ( this . searchKey ) ;
602
+ }
603
+ this . slim . render ( ) ;
577
604
}
578
605
579
606
/**
@@ -586,13 +613,11 @@ export class APISelect {
586
613
Math . floor ( this . slim . slim . list . scrollTop ) + this . slim . slim . list . offsetHeight ===
587
614
this . slim . slim . list . scrollHeight ;
588
615
589
- if ( this . atBottom && ! atBottom ) {
590
- this . atBottom = false ;
591
- this . base . dispatchEvent ( this . bottomEvent ) ;
592
- } else if ( ! this . atBottom && atBottom ) {
593
- this . atBottom = true ;
616
+ this . atBottom = atBottom
617
+
618
+ if ( this . atBottom ) {
594
619
this . base . dispatchEvent ( this . bottomEvent ) ;
595
- }
620
+ }
596
621
}
597
622
598
623
/**
@@ -994,7 +1019,9 @@ export class APISelect {
994
1019
[ 'btn' , 'btn-sm' , 'btn-ghost-dark' ] ,
995
1020
[ createElement ( 'i' , null , [ 'mdi' , 'mdi-reload' ] ) ] ,
996
1021
) ;
997
- refreshButton . addEventListener ( 'click' , ( ) => this . loadData ( ) ) ;
1022
+ // calling this.loadData() will prevent first page of returned items
1023
+ // with non-null search key inplace not selectable
1024
+ refreshButton . addEventListener ( 'click' , ( ) => this . handleSearch ( 'replace' , null ) ) ;
998
1025
refreshButton . type = 'button' ;
999
1026
this . slim . slim . search . container . appendChild ( refreshButton ) ;
1000
1027
}
0 commit comments