From 57522d89c2c7b15169b6615ac165052da4fabed6 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 16 Mar 2025 18:43:43 +0100 Subject: [PATCH] feat: add x-limited-choices directive --- core/static/bundled/alpine-index.js | 3 +- core/static/bundled/alpine/limited-choices.ts | 69 +++++++++++++++++++ .../templates/election/election_detail.jinja | 35 ++-------- 3 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 core/static/bundled/alpine/limited-choices.ts diff --git a/core/static/bundled/alpine-index.js b/core/static/bundled/alpine-index.js index 211600a5..4e04e7dd 100644 --- a/core/static/bundled/alpine-index.js +++ b/core/static/bundled/alpine-index.js @@ -1,7 +1,8 @@ +import { limitedChoices } from "#core:alpine/limited-choices"; import sort from "@alpinejs/sort"; import Alpine from "alpinejs"; -Alpine.plugin(sort); +Alpine.plugin([sort, limitedChoices]); window.Alpine = Alpine; window.addEventListener("DOMContentLoaded", () => { diff --git a/core/static/bundled/alpine/limited-choices.ts b/core/static/bundled/alpine/limited-choices.ts new file mode 100644 index 00000000..211441d0 --- /dev/null +++ b/core/static/bundled/alpine/limited-choices.ts @@ -0,0 +1,69 @@ +import type { Alpine as AlpineType } from "alpinejs"; + +export function limitedChoices(Alpine: AlpineType) { + /** + * Directive to limit the number of elements + * that can be selected in a group of checkboxes. + * + * When the max numbers of selectable elements is reached, + * new elements will still be inserted, but oldest ones will be deselected. + * For example, if checkboxes A, B and C have been selected and the max + * number of selections is 3, then selecting D will result in having + * B, C and D selected. + * + * # Example in template + * ```html + *
+ * + * + * + * + * + *
+ * ``` + */ + Alpine.directive( + "limited-choices", + (el, { expression }, { evaluateLater, effect }) => { + const getMaxChoices = evaluateLater(expression); + let maxChoices: number; + const inputs: HTMLInputElement[] = Array.from( + el.querySelectorAll("input[type='checkbox']"), + ); + const checked = [] as HTMLInputElement[]; + + const manageDequeue = () => { + if (checked.length <= maxChoices) { + // There isn't too many checkboxes selected. Nothing to do + return; + } + const popped = checked.splice(0, checked.length - maxChoices); + for (const p of popped) { + p.checked = false; + } + }; + + for (const input of inputs) { + input.addEventListener("change", (_e) => { + if (input.checked) { + checked.push(input); + } else { + checked.splice(checked.indexOf(input), 1); + } + manageDequeue(); + }); + } + effect(() => { + getMaxChoices((value: string) => { + const previousValue = maxChoices; + maxChoices = Number.parseInt(value); + if (maxChoices < previousValue) { + // The maximum number of selectable items has been lowered. + // Some currently selected elements may need to be removed + manageDequeue(); + } + }); + }); + }, + ); +} diff --git a/election/templates/election/election_detail.jinja b/election/templates/election/election_detail.jinja index 28b2a956..2c628211 100644 --- a/election/templates/election/election_detail.jinja +++ b/election/templates/election/election_detail.jinja @@ -63,7 +63,13 @@ {%- for role in role_list %} {%- set count = [0] %} {%- set role_data = election_form.data.getlist(role.title) if role.title in election_form.data else [] %} - + + 1 -%} + x-data x-limited-choices="{{ role.max_choice }}" + {%- endif %} + class="role {% if role.title in election_form.errors %}role_error{% endif %}" + >
@@ -197,30 +203,3 @@ {%- endif %} {% endblock %} - -{% block script %} - {{ super() }} - -{% endblock %}