Sith/core/static/webpack/ajax-select-index.ts

153 lines
4.7 KiB
TypeScript
Raw Normal View History

2024-10-15 20:06:22 +00:00
import "tom-select/dist/css/tom-select.css";
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"];
public widget: TomSelect;
2024-10-18 21:26:04 +00:00
private delay: number | null = null;
private placeholder = "";
private max: number | null = null;
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() {
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[];
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>) {
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,
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
shouldLoad: (query: string) => {
return query.length >= this.minCharNumberForSearch; // Avoid launching search with less than 2 characters
},
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>`;
},
// biome-ignore lint/style/useNamingConvention: that's how it's defined
not_loading: (data: TomOption, _sanitize: typeof escape_html) => {
return `<div class="no-results">${interpolate(gettext("You need to type %(number)s more characters"), { number: this.minCharNumberForSearch - data.input.length }, true)}</div>`;
},
// 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
},
});
}
}