From 304d290f2258e8347335da7cf6021fd28e33c817 Mon Sep 17 00:00:00 2001 From: Mira Date: Thu, 14 Mar 2024 09:17:42 +0100 Subject: [PATCH] Presale: improve clientside handling of max-count for add-on products * Fix typo in error message * Use exclusive checkboxes for addon items with max_count == 1 and !multi_allowed * combine exclusive items + variations * move exclusive to containing fieldset * fix add-on-exclusive * add max_count check * fix plus/minus-stepper buttons bubbling * Update src/pretix/static/pretixpresale/js/ui/main.js --------- Co-authored-by: Richard Schreiber --- src/pretix/base/services/cart.py | 2 +- src/pretix/base/services/orders.py | 2 +- .../event/fragment_addon_choice.html | 9 ++-- src/pretix/static/pretixpresale/js/ui/main.js | 42 ++++++++++++++++--- 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 7f96ea74d3..ca1864690e 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -203,7 +203,7 @@ error_messages = { 'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.', 'min' ), - 'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'), + 'addon_no_multi': gettext_lazy('You can select every add-on from the category %(cat)s for the product %(base)s at most once.'), 'addon_only': gettext_lazy('One of the products you selected can only be bought as an add-on to another product.'), 'bundled_only': gettext_lazy('One of the products you selected can only be bought part of a bundle.'), 'seat_required': gettext_lazy('You need to select a specific seat.'), diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 9c192ac3bb..cb3dfc54a6 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -197,7 +197,7 @@ error_messages = { 'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.', 'min' ), - 'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'), + 'addon_no_multi': gettext_lazy('You can select every add-on from the category %(cat)s for the product %(base)s at most once.'), 'addon_already_checked_in': gettext_lazy('You cannot remove the position %(addon)s since it has already been checked in.'), } diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html b/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html index e8eab738f1..b0b5bab565 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html @@ -6,13 +6,13 @@ {% load eventsignal %} {% load rich_text %} {% for c in form.categories %} -
+
{{ c.category.name }} {% if c.category.description %} {{ c.category.description|rich_text }} {% endif %} {% if c.min_count == c.max_count %} -

+

{% blocktrans trimmed count min_count=c.min_count %} You need to choose exactly one option from this category. {% plural %} @@ -21,7 +21,7 @@

{% elif c.min_count == 0 and c.max_count >= c.items|length and not c.multi_allowed %} {% elif c.min_count == 0 %} -

+

{% blocktrans trimmed count max_count=c.max_count %} You can choose {{ max_count }} option from this category. {% plural %} @@ -29,7 +29,7 @@ {% endblocktrans %}

{% else %} -

+

{% blocktrans trimmed with min_count=c.min_count max_count=c.max_count %} You can choose between {{ min_count }} and {{ max_count }} options from this category. @@ -196,7 +196,6 @@ {% endif %} id="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}" name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}" - data-exclusive-prefix="cp_{{ form.pos.pk }}_variation_{{ item.id }}_" aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}"> {% trans "Select" context "checkbox" %} diff --git a/src/pretix/static/pretixpresale/js/ui/main.js b/src/pretix/static/pretixpresale/js/ui/main.js index 28f9a62a1e..26ad321213 100644 --- a/src/pretix/static/pretixpresale/js/ui/main.js +++ b/src/pretix/static/pretixpresale/js/ui/main.js @@ -123,7 +123,7 @@ var form_handlers = function (el) { var controls = document.getElementById(this.getAttribute("data-controls")); var currentValue = parseFloat(controls.value); controls.value = Math.max(controls.min, Math.min(controls.max || Number.MAX_SAFE_INTEGER, (currentValue || 0) + step)); - controls.dispatchEvent(new Event("change")); + controls.dispatchEvent(new Event("change", { bubbles: true })); }); el.find(".btn-checkbox input").on("change", function (e) { $(this).closest(".btn-checkbox") @@ -149,11 +149,41 @@ var form_handlers = function (el) { ).find("canvas").attr("role", "img").attr("aria-label", this.getAttribute("data-desc")); }); - el.find("input[data-exclusive-prefix]").each(function () { - var $others = $("input[name^=" + $(this).attr("data-exclusive-prefix") + "]:not([name=" + $(this).attr("name") + "])"); - $(this).on('click change', function () { - if ($(this).prop('checked')) { - $others.prop('checked', false).trigger('change'); + + el.find("fieldset[data-addon-max-count]").each(function() { + // usually addons are only allowed once one per item + var multipleAllowed = this.hasAttribute("data-addon-multi-allowed"); + var $inputs = $(".availability-box input", this); + var max = parseInt(this.getAttribute("data-addon-max-count")); + var desc = $(".addon-count-desc", this).text().trim(); + this.addEventListener("change", function (e) { + var variations = e.target.closest(".variations"); + if (variations && !multipleAllowed && e.target.checked) { + // uncheck all other checkboxes inside this variations + $(".availability-box input:checked", variations).not(e.target).prop("checked", false).trigger("change"); + } + + if (max === 1) { + if (e.target.checked) { + $inputs.filter(":checked").not(e.target).prop("checked", false).trigger("change"); + } + return; + } + var total = $inputs.toArray().reduce(function(a, e) { + return a + (e.type == "checkbox" ? (e.checked ? parseInt(e.value) : 0) : parseInt(e.value) || 0); + }, 0); + if (total > max) { + if (e.target.type == "checkbox") { + e.target.checked = false; + } else { + e.target.value = e.target.value - (total - max); + } + $(e.target).trigger("change").closest(".availability-box").tooltip({ + "title": desc, + }).tooltip('show'); + e.preventDefault(); + } else { + $(".availability-box", this).tooltip('destroy') } }); });