Add proper delete button and fix item ordering

This commit is contained in:
Antoine Bartuccio 2024-10-20 16:57:38 +02:00
parent bb3f277ba5
commit be5ce414ba
2 changed files with 49 additions and 32 deletions

View File

@ -25,10 +25,6 @@
.ts-control { .ts-control {
.item { .item {
.fa-times {
margin-left: 5px;
margin-right: 5px;
}
cursor: pointer; cursor: pointer;
background-color: #e4e4e4; background-color: #e4e4e4;
@ -39,6 +35,7 @@
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px; margin-bottom: 5px;
padding-right: 10px; padding-right: 10px;
padding-left: 10px;
} }
} }

View File

@ -3,7 +3,6 @@ import { inheritHtmlElement, registerComponent } from "#core:utils/web-component
import TomSelect from "tom-select"; import TomSelect from "tom-select";
import type { import type {
RecursivePartial, RecursivePartial,
TomItem,
TomLoadCallback, TomLoadCallback,
TomOption, TomOption,
TomSettings, TomSettings,
@ -71,14 +70,25 @@ class AutocompleteSelect extends inheritHtmlElement("select") {
this.attachBehaviors(); this.attachBehaviors();
} }
protected shouldLoad(query: string) {
console.log(this);
return query.length >= this.minCharNumberForSearch; // Avoid launching search with less than setup number of characters
}
protected tomSelectSettings(): RecursivePartial<TomSettings> { protected tomSelectSettings(): RecursivePartial<TomSettings> {
return { return {
plugins: {
// biome-ignore lint/style/useNamingConvention: this is required by the api
remove_button: {
title: gettext("Remove"),
},
},
persist: false,
maxItems: this.node.multiple ? this.max : 1, maxItems: this.node.multiple ? this.max : 1,
closeAfterSelect: true,
loadThrottle: this.delay, loadThrottle: this.delay,
placeholder: this.placeholder, placeholder: this.placeholder,
shouldLoad: (query: string) => { shouldLoad: (query: string) => this.shouldLoad(query), // wraps the method to avoid shadowing `this` by the one from tom-select
return query.length >= this.minCharNumberForSearch; // Avoid launching search with less than 2 characters
},
render: { render: {
option: (item: TomOption, sanitize: typeof escape_html) => { option: (item: TomOption, sanitize: typeof escape_html) => {
return `<div class="select-item"> return `<div class="select-item">
@ -86,7 +96,7 @@ class AutocompleteSelect extends inheritHtmlElement("select") {
</div>`; </div>`;
}, },
item: (item: TomOption, sanitize: typeof escape_html) => { item: (item: TomOption, sanitize: typeof escape_html) => {
return `<span><i class="fa fa-times"></i>${sanitize(item.text)}</span>`; return `<span>${sanitize(item.text)}</span>`;
}, },
// biome-ignore lint/style/useNamingConvention: that's how it's defined // biome-ignore lint/style/useNamingConvention: that's how it's defined
not_loading: (data: TomOption, _sanitize: typeof escape_html) => { not_loading: (data: TomOption, _sanitize: typeof escape_html) => {
@ -101,14 +111,7 @@ class AutocompleteSelect extends inheritHtmlElement("select") {
} }
protected attachBehaviors() { protected attachBehaviors() {
// Allow removing selected items by clicking on them /* Called once the widget has been initialized */
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("");
});
} }
} }
@ -118,6 +121,8 @@ abstract class AjaxSelect extends AutocompleteSelect {
protected abstract valueField: string; protected abstract valueField: string;
protected abstract labelField: string; protected abstract labelField: string;
protected abstract searchField: string[];
protected abstract renderOption( protected abstract renderOption(
item: TomOption, item: TomOption,
sanitize: typeof escape_html, sanitize: typeof escape_html,
@ -129,16 +134,28 @@ abstract class AjaxSelect extends AutocompleteSelect {
this.filter = filter; this.filter = filter;
} }
protected getLoadFunction() { protected shouldLoad(query: string) {
// this will be replaced by TomSelect if we don't wrap it that way const resp = super.shouldLoad(query);
return async (query: string, callback: TomLoadCallback) => { /* Force order sync with backend if no client side filtering is set */
const resp = await this.search(query); if (!resp && this.searchField.length === 0) {
if (this.filter) { this.widget.clearOptions();
callback(this.filter(resp), []); }
} else { return resp;
callback(resp, []); }
}
}; protected async loadFunction(query: string, callback: TomLoadCallback) {
/* Force order sync with backend if no client side filtering is set */
if (this.searchField.length === 0) {
this.widget.clearOptions();
}
const resp = await this.search(query);
if (this.filter) {
callback(this.filter(resp), []);
} else {
callback(resp, []);
}
} }
protected tomSelectSettings(): RecursivePartial<TomSettings> { protected tomSelectSettings(): RecursivePartial<TomSettings> {
@ -149,8 +166,9 @@ abstract class AjaxSelect extends AutocompleteSelect {
duplicates: false, duplicates: false,
valueField: this.valueField, valueField: this.valueField,
labelField: this.labelField, labelField: this.labelField,
searchField: [], // Disable local search filter and rely on tested backend searchField: this.searchField,
load: this.getLoadFunction(), load: (query: string, callback: TomLoadCallback) =>
this.loadFunction(query, callback), // wraps the method to avoid shadowing `this` by the one from tom-select
render: { render: {
...super.tomSelectSettings().render, ...super.tomSelectSettings().render,
option: this.renderOption, option: this.renderOption,
@ -166,7 +184,7 @@ abstract class AjaxSelect extends AutocompleteSelect {
for (const value of Array.from(this.children) for (const value of Array.from(this.children)
.filter((child) => child.tagName.toLowerCase() === "slot") .filter((child) => child.tagName.toLowerCase() === "slot")
.map((slot) => JSON.parse(slot.innerHTML))) { .map((slot) => JSON.parse(slot.innerHTML))) {
this.widget.addOption(value, true); this.widget.addOption(value, false);
this.widget.addItem(value[this.valueField]); this.widget.addItem(value[this.valueField]);
} }
} }
@ -176,6 +194,7 @@ abstract class AjaxSelect extends AutocompleteSelect {
export class UserAjaxSelect extends AjaxSelect { export class UserAjaxSelect extends AjaxSelect {
protected valueField = "id"; protected valueField = "id";
protected labelField = "display_name"; protected labelField = "display_name";
protected searchField: string[] = []; // Disable local search filter and rely on tested backend
protected async search(query: string): Promise<TomOption[]> { protected async search(query: string): Promise<TomOption[]> {
const resp = await userSearchUsers({ query: { search: query } }); const resp = await userSearchUsers({ query: { search: query } });
@ -197,7 +216,7 @@ export class UserAjaxSelect extends AjaxSelect {
} }
protected renderItem(item: UserProfileSchema, sanitize: typeof escape_html) { protected renderItem(item: UserProfileSchema, sanitize: typeof escape_html) {
return `<span><i class="fa fa-times"></i>${sanitize(item.display_name)}</span>`; return `<span>${sanitize(item.display_name)}</span>`;
} }
} }
@ -205,6 +224,7 @@ export class UserAjaxSelect extends AjaxSelect {
export class GroupsAjaxSelect extends AjaxSelect { export class GroupsAjaxSelect extends AjaxSelect {
protected valueField = "id"; protected valueField = "id";
protected labelField = "name"; protected labelField = "name";
protected searchField = ["name"];
protected async search(query: string): Promise<TomOption[]> { protected async search(query: string): Promise<TomOption[]> {
const resp = await groupSearchGroup({ query: { search: query } }); const resp = await groupSearchGroup({ query: { search: query } });
@ -221,6 +241,6 @@ export class GroupsAjaxSelect extends AjaxSelect {
} }
protected renderItem(item: GroupSchema, sanitize: typeof escape_html) { protected renderItem(item: GroupSchema, sanitize: typeof escape_html) {
return `<span><i class="fa fa-times"></i>${sanitize(item.name)}</span>`; return `<span>${sanitize(item.name)}</span>`;
} }
} }