mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-10-31 09:03:06 +00:00 
			
		
		
		
	Create select widget based on tomselect on django backend
Replace make_ajax in elections by the new widget
This commit is contained in:
		| @@ -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<TomSettings> { | ||||
|     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 `<div class="select-item"> | ||||
|             <span class="select-item-text">${sanitize(item.text)}</span> | ||||
|           </div>`; | ||||
|         }, | ||||
|         item: (item: TomOption, sanitize: typeof escape_html) => { | ||||
|           return `<span><i class="fa fa-times"></i>${sanitize(item.text)}</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>`; | ||||
|         }, | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
| @@ -76,23 +115,8 @@ class AutocompleteSelect extends inheritHtmlElement("select") { | ||||
| @registerComponent("user-ajax-select") | ||||
| export class UserAjaxSelect extends AutocompleteSelect { | ||||
|   public filter?: <T>(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<TomSettings> { | ||||
|     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 `<div class="select-item"> | ||||
|             <img | ||||
| @@ -137,14 +159,6 @@ export class UserAjaxSelect extends AutocompleteSelect { | ||||
|         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>`; | ||||
|         }, | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
|   | ||||
							
								
								
									
										10
									
								
								core/templates/core/widgets/autocomplete_select.jinja
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								core/templates/core/widgets/autocomplete_select.jinja
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| <script-once src="{{ statics.js }}" defer></script-once> | ||||
| {% for css in statics.csss %} | ||||
|   <link-once rel="stylesheet" type="text/css" href="{{ css }}" defer></link-once> | ||||
| {% endfor %} | ||||
|  | ||||
| <autocomplete-select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %} | ||||
|   <optgroup label="{{ group_name }}">{% endif %}{% for widget in group_choices %} | ||||
|     {% include widget.template_name %}{% endfor %}{% if group_name %} | ||||
|       </optgroup>{% endif %}{% endfor %} | ||||
| </autocomplete-select> | ||||
| @@ -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" | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user