diff --git a/core/static/webpack/core/components/ajax-select-index.ts b/core/static/webpack/core/components/ajax-select-index.ts index 37915f7a..1db8f8b8 100644 --- a/core/static/webpack/core/components/ajax-select-index.ts +++ b/core/static/webpack/core/components/ajax-select-index.ts @@ -13,12 +13,18 @@ import { type UserProfileSchema, userSearchUsers } from "#openapi"; @registerComponent("autocomplete-select") class AutocompleteSelect extends inheritHtmlElement("select") { - static observedAttributes = ["delay", "placeholder", "max"]; + static observedAttributes = [ + "delay", + "placeholder", + "max", + "min-characters-for-search", + ]; public widget: TomSelect; - private delay: number | null = null; - private placeholder = ""; - private max: number | null = null; + protected minCharNumberForSearch = 0; + protected delay: number | null = null; + protected placeholder = ""; + protected max: number | null = null; protected attributeChangedCallback( name: string, @@ -38,26 +44,59 @@ class AutocompleteSelect extends inheritHtmlElement("select") { this.max = Number.parseInt(newValue) ?? null; break; } + case "min-characters-for-search": { + this.minCharNumberForSearch = Number.parseInt(newValue) ?? 0; + break; + } default: { return; } } } - constructor() { - super(); - - window.addEventListener("DOMContentLoaded", () => { - this.widget = new TomSelect(this.node, this.tomSelectSettings()); - this.attachBehaviors(); - }); + connectedCallback() { + super.connectedCallback(); + // Collect all options nodes and put them into the select node + const options: Element[] = []; // We need to make a copy to delete while iterating + for (const child of this.children) { + if (child.tagName.toLowerCase() === "option") { + options.push(child); + } + } + for (const option of options) { + this.removeChild(option); + this.node.appendChild(option); + } + this.widget = new TomSelect(this.node, this.tomSelectSettings()); + this.attachBehaviors(); } protected tomSelectSettings(): RecursivePartial { return { - maxItems: this.max, + maxItems: this.node.multiple ? this.max : 1, loadThrottle: this.delay, placeholder: this.placeholder, + shouldLoad: (query: string) => { + return query.length >= this.minCharNumberForSearch; // Avoid launching search with less than 2 characters + }, + render: { + option: (item: TomOption, sanitize: typeof escape_html) => { + return `
+ ${sanitize(item.text)} +
`; + }, + item: (item: TomOption, sanitize: typeof escape_html) => { + return `${sanitize(item.text)}`; + }, + // biome-ignore lint/style/useNamingConvention: that's how it's defined + not_loading: (data: TomOption, _sanitize: typeof escape_html) => { + return `
${interpolate(gettext("You need to type %(number)s more characters"), { number: this.minCharNumberForSearch - data.input.length }, true)}
`; + }, + // biome-ignore lint/style/useNamingConvention: that's how it's defined + no_results: (_data: TomOption, _sanitize: typeof escape_html) => { + return `
${gettext("No results found")}
`; + }, + }, }; } @@ -76,23 +115,8 @@ class AutocompleteSelect extends inheritHtmlElement("select") { @registerComponent("user-ajax-select") export class UserAjaxSelect extends AutocompleteSelect { public filter?: (items: T[]) => T[]; - static observedAttributes = [ - "min-characters-for-search", - ...AutocompleteSelect.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; - } - } + protected minCharNumberForSearch = 2; protected tomSelectSettings(): RecursivePartial { return { @@ -103,9 +127,6 @@ export class UserAjaxSelect extends AutocompleteSelect { valueField: "id", labelField: "display_name", 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 - }, load: (query: string, callback: TomLoadCallback) => { userSearchUsers({ query: { @@ -124,6 +145,7 @@ export class UserAjaxSelect extends AutocompleteSelect { }); }, render: { + ...super.tomSelectSettings().render, option: (item: UserProfileSchema, sanitize: typeof escape_html) => { return `
{ return `${sanitize(item.display_name)}`; }, - // biome-ignore lint/style/useNamingConvention: that's how it's defined - not_loading: (data: TomOption, _sanitize: typeof escape_html) => { - return `
${interpolate(gettext("You need to type %(number)s more characters"), { number: this.minCharNumberForSearch - data.input.length }, true)}
`; - }, - // biome-ignore lint/style/useNamingConvention: that's how it's defined - no_results: (_data: TomOption, _sanitize: typeof escape_html) => { - return `
${gettext("No results found")}
`; - }, }, }; } diff --git a/core/static/webpack/easymde-index.ts b/core/static/webpack/core/components/easymde-index.ts similarity index 100% rename from core/static/webpack/easymde-index.ts rename to core/static/webpack/core/components/easymde-index.ts diff --git a/core/templates/core/widgets/autocomplete_select.jinja b/core/templates/core/widgets/autocomplete_select.jinja new file mode 100644 index 00000000..19f1deae --- /dev/null +++ b/core/templates/core/widgets/autocomplete_select.jinja @@ -0,0 +1,10 @@ + +{% for css in statics.csss %} + +{% endfor %} + +{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %} + {% endif %}{% for widget in group_choices %} + {% include widget.template_name %}{% endfor %}{% if group_name %} + {% endif %}{% endfor %} + \ No newline at end of file diff --git a/core/views/forms.py b/core/views/forms.py index 232c938b..228e4cfe 100644 --- a/core/views/forms.py +++ b/core/views/forms.py @@ -36,9 +36,11 @@ from django.forms import ( CheckboxSelectMultiple, DateInput, DateTimeInput, + SelectMultiple, Textarea, TextInput, ) +from django.forms.widgets import Select from django.utils.translation import gettext from django.utils.translation import gettext_lazy as _ from phonenumber_field.widgets import RegionalPhoneNumberWidget @@ -72,12 +74,37 @@ class MarkdownInput(Textarea): context = super().get_context(name, value, attrs) context["statics"] = { - "js": staticfiles_storage.url("webpack/easymde-index.ts"), - "css": staticfiles_storage.url("webpack/easymde-index.css"), + "js": staticfiles_storage.url("webpack/core/components/easymde-index.ts"), + "css": staticfiles_storage.url("webpack/core/components/easymde-index.css"), } return context +class AutoCompleteSelectMixin: + template_name = "core/widgets/autocomplete_select.jinja" + + def get_context(self, name, value, attrs): + context = super().get_context(name, value, attrs) + context["statics"] = { + "js": staticfiles_storage.url( + "webpack/core/components/ajax-select-index.ts" + ), + "csss": [ + staticfiles_storage.url( + "webpack/core/components/ajax-select-index.css" + ), + staticfiles_storage.url("core/components/ajax-select.scss"), + ], + } + return context + + +class AutoCompleteSelect(AutoCompleteSelectMixin, Select): ... + + +class AutoCompleteSelectMultiple(AutoCompleteSelectMixin, SelectMultiple): ... + + class NFCTextInput(TextInput): template_name = "core/widgets/nfc.jinja" diff --git a/election/views.py b/election/views.py index 65aaf363..c24e044b 100644 --- a/election/views.py +++ b/election/views.py @@ -1,6 +1,5 @@ from typing import TYPE_CHECKING -from ajax_select import make_ajax_field from ajax_select.fields import AutoCompleteSelectField from django import forms from django.core.exceptions import PermissionDenied @@ -13,7 +12,11 @@ from django.views.generic import DetailView, ListView from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView from core.views import CanCreateMixin, CanEditMixin, CanViewMixin -from core.views.forms import MarkdownInput, SelectDateTime +from core.views.forms import ( + AutoCompleteSelectMultiple, + MarkdownInput, + SelectDateTime, +) from election.models import Candidature, Election, ElectionList, Role, Vote if TYPE_CHECKING: @@ -146,23 +149,12 @@ class ElectionForm(forms.ModelForm): "vote_groups", "candidature_groups", ] - - edit_groups = make_ajax_field( - Election, "edit_groups", "groups", help_text="", label=_("edit groups") - ) - view_groups = make_ajax_field( - Election, "view_groups", "groups", help_text="", label=_("view groups") - ) - vote_groups = make_ajax_field( - Election, "vote_groups", "groups", help_text="", label=_("vote groups") - ) - candidature_groups = make_ajax_field( - Election, - "candidature_groups", - "groups", - help_text="", - label=_("candidature groups"), - ) + widgets = { + "edit_groups": AutoCompleteSelectMultiple, + "view_groups": AutoCompleteSelectMultiple, + "vote_groups": AutoCompleteSelectMultiple, + "candidature_groups": AutoCompleteSelectMultiple, + } start_date = forms.DateTimeField( label=_("Start date"), widget=SelectDateTime, required=True