2024-10-15 20:06:22 +00:00
import "tom-select/dist/css/tom-select.css" ;
2024-10-17 21:14:54 +00:00
import { inheritHtmlElement , registerComponent } from "#core:utils/web-components" ;
2024-10-15 20:06:22 +00:00
import TomSelect from "tom-select" ;
2024-10-18 21:26:04 +00:00
import type {
RecursivePartial ,
TomItem ,
TomLoadCallback ,
TomOption ,
TomSettings ,
} from "tom-select/dist/types/types" ;
2024-10-15 20:06:22 +00:00
import type { escape_html } from "tom-select/dist/types/utils" ;
import { type UserProfileSchema , userSearchUsers } from "#openapi" ;
2024-10-18 21:26:04 +00:00
abstract class AjaxSelectBase extends inheritHtmlElement ( "select" ) {
static observedAttributes = [ "delay" , "placeholder" , "max" ] ;
2024-10-17 21:14:54 +00:00
public widget : TomSelect ;
2024-10-18 21:26:04 +00:00
private delay : number | null = null ;
private placeholder = "" ;
private max : number | null = null ;
2024-10-18 21:34:37 +00:00
protected attributeChangedCallback (
name : string ,
_oldValue? : string ,
newValue? : string ,
) {
2024-10-18 21:26:04 +00:00
switch ( name ) {
case "delay" : {
this . delay = Number . parseInt ( newValue ) ? ? null ;
break ;
}
case "placeholder" : {
this . placeholder = newValue ? ? "" ;
break ;
}
case "max" : {
this . max = Number . parseInt ( newValue ) ? ? null ;
break ;
}
default : {
return ;
}
}
}
2024-10-15 20:06:22 +00:00
constructor ( ) {
2024-10-17 21:14:54 +00:00
super ( ) ;
2024-10-15 20:06:22 +00:00
window . addEventListener ( "DOMContentLoaded" , ( ) = > {
2024-10-18 21:26:04 +00:00
this . configureTomSelect ( this . defaultTomSelectSettings ( ) ) ;
this . setDefaultTomSelectBehaviors ( ) ;
2024-10-15 20:06:22 +00:00
} ) ;
}
2024-10-18 21:26:04 +00:00
private defaultTomSelectSettings ( ) : RecursivePartial < TomSettings > {
return {
maxItems : this.max ,
loadThrottle : this.delay ,
placeholder : this.placeholder ,
} ;
}
2024-10-15 20:06:22 +00:00
2024-10-18 21:26:04 +00:00
private setDefaultTomSelectBehaviors() {
// Allow removing selected items by clicking on them
this . widget . on ( "item_select" , ( item : TomItem ) = > {
this . widget . removeItem ( item ) ;
} ) ;
// Remove typed text once an item has been selected
this . widget . on ( "item_add" , ( ) = > {
this . widget . setTextboxValue ( "" ) ;
} ) ;
}
abstract configureTomSelect ( defaultSettings : RecursivePartial < TomSettings > ) : void ;
}
@registerComponent ( "user-ajax-select" )
export class UserAjaxSelect extends AjaxSelectBase {
public filter ? : < T > ( items : T [ ] ) = > T [ ] ;
2024-10-18 21:34:37 +00:00
static observedAttributes = [
"min-characters-for-search" ,
. . . AjaxSelectBase . observedAttributes ,
] ;
private minCharNumberForSearch = 2 ;
protected attributeChangedCallback (
name : string ,
_oldValue? : string ,
newValue? : string ,
) {
super . attributeChangedCallback ( name , _oldValue , newValue ) ;
if ( name === "min-characters-for-search" ) {
this . minCharNumberForSearch = Number . parseInt ( newValue ) ? ? 0 ;
}
}
2024-10-15 20:06:22 +00:00
2024-10-18 21:26:04 +00:00
configureTomSelect ( defaultSettings : RecursivePartial < TomSettings > ) {
2024-10-16 12:59:02 +00:00
this . widget = new TomSelect ( this . node , {
2024-10-18 21:26:04 +00:00
. . . defaultSettings ,
2024-10-15 20:06:22 +00:00
hideSelected : true ,
2024-10-16 00:22:27 +00:00
diacritics : true ,
duplicates : false ,
2024-10-15 20:06:22 +00:00
valueField : "id" ,
labelField : "display_name" ,
2024-10-18 21:26:04 +00:00
searchField : [ ] , // Disable local search filter and rely on tested backend
2024-10-16 00:22:27 +00:00
shouldLoad : ( query : string ) = > {
2024-10-18 21:34:37 +00:00
return query . length >= this . minCharNumberForSearch ; // Avoid launching search with less than 2 characters
2024-10-16 00:22:27 +00:00
} ,
2024-10-15 20:06:22 +00:00
load : ( query : string , callback : TomLoadCallback ) = > {
userSearchUsers ( {
query : {
search : query ,
} ,
} ) . then ( ( response ) = > {
if ( response . data ) {
if ( this . filter ) {
callback ( this . filter ( response . data . results ) , [ ] ) ;
} else {
callback ( response . data . results , [ ] ) ;
}
return ;
}
callback ( [ ] , [ ] ) ;
} ) ;
} ,
render : {
option : ( item : UserProfileSchema , sanitize : typeof escape_html ) = > {
return ` <div class="select-item">
< img
src = "${sanitize(item.profile_pict)}"
alt = "${sanitize(item.display_name)}"
onerror = "this.src = '/static/core/img/unknown.jpg'"
/ >
< span class = "select-item-text" > $ { sanitize ( item . display_name ) } < / span >
< / div > ` ;
} ,
item : ( item : UserProfileSchema , sanitize : typeof escape_html ) = > {
return ` <span><i class="fa fa-times"></i> ${ sanitize ( item . display_name ) } </span> ` ;
} ,
2024-10-16 00:22:27 +00:00
// biome-ignore lint/style/useNamingConvention: that's how it's defined
not_loading : ( data : TomOption , _sanitize : typeof escape_html ) = > {
2024-10-18 21:34:37 +00:00
return ` <div class="no-results"> ${ interpolate ( gettext ( "You need to type %(number)s more characters" ) , { number : this . minCharNumberForSearch - data . input . length } , true)}</div> ` ;
2024-10-16 00:22:27 +00:00
} ,
// biome-ignore lint/style/useNamingConvention: that's how it's defined
no_results : ( _data : TomOption , _sanitize : typeof escape_html ) = > {
return ` <div class="no-results"> ${ gettext ( "No results found" ) } </div> ` ;
} ,
2024-10-15 20:06:22 +00:00
} ,
} ) ;
}
}