Widget & Cart: Add custom number spinners for item quantity

This commit is contained in:
Richard Schreiber
2023-05-08 11:38:44 +02:00
committed by GitHub
parent f97effd0b7
commit 1d0eb81659
10 changed files with 175 additions and 32 deletions

View File

@@ -2608,6 +2608,15 @@ Your {organizer} team"""))
label=_("Use round edges"),
)
},
'widget_use_native_spinners': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Use native spinners in the widget instead of custom ones for numeric inputs such as quantity."),
)
},
'primary_font': {
'default': 'Open Sans',
'type': str,

View File

@@ -190,7 +190,9 @@
<i class="fa fa-cart-plus fa-lg" aria-hidden="true"></i>
</label>
{% else %}
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
<div class="input-item-count-group">
<button type="button" data-step="-1" data-controls="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}">-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if var.initial %}value="{{ var.initial }}"{% endif %}
{% if item.free_price %}
data-checked-onchange="price-variation-{{form.pos.pk}}-{{ item.pk }}-{{ var.pk }}"
@@ -199,6 +201,8 @@
id="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
aria-label="{% blocktrans with item=item.name var=var %}Quantity of {{ item }}, {{ var }} to order{% endblocktrans %}">
<button type="button" data-step="1" data-controls="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}">+</button>
</div>
{% endif %}
</div>
{% else %}
@@ -319,7 +323,9 @@
<i class="fa fa-cart-plus fa-lg" aria-hidden="true"></i>
</label>
{% else %}
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
<div class="input-item-count-group">
<button type="button" data-step="-1" data-controls="cp_{{ form.pos.pk }}_item_{{ item.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}">-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if item.free_price %}
data-checked-onchange="price-item-{{ form.pos.pk }}-{{ item.pk }}"
{% endif %}
@@ -329,6 +335,8 @@
id="cp_{{ form.pos.pk }}_item_{{ item.id }}"
aria-label="{% blocktrans with item=item.name %}Quantity of {{ item }} to order{% endblocktrans %}"
{% if item.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.id }}-description"{% endif %}>
<button type="button" data-step="1" data-controls="cp_{{ form.pos.pk }}_item_{{ item.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Increase quantity" %}">+</button>
</div>
{% endif %}
</div>
{% else %}

View File

@@ -197,15 +197,19 @@
<i class="fa fa-cart-plus fa-lg" aria-hidden="true"></i>
</label>
{% else %}
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if not ev.presale_is_running %}disabled{% endif %}
{% if item.free_price %}
data-checked-onchange="price-variation-{{ item.pk }}-{{ var.pk }}"
{% endif %}
max="{{ var.order_max }}"
id="variation_{{ item.id }}_{{ var.id }}"
name="variation_{{ item.id }}_{{ var.id }}"
aria-label="{% blocktrans with item=item.name var=var.name %}Quantity of {{ item }}, {{ var }} to order{% endblocktrans %}">
<div class="input-item-count-group">
<button type="button" data-step="-1" data-controls="variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}">-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if not ev.presale_is_running %}disabled{% endif %}
{% if item.free_price %}
data-checked-onchange="price-variation-{{ item.pk }}-{{ var.pk }}"
{% endif %}
max="{{ var.order_max }}"
id="variation_{{ item.id }}_{{ var.id }}"
name="variation_{{ item.id }}_{{ var.id }}"
aria-label="{% blocktrans with item=item.name var=var.name %}Quantity of {{ item }}, {{ var }} to order{% endblocktrans %}">
<button type="button" data-step="1" data-controls="variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}">+</button>
</div>
{% endif %}
</div>
{% else %}
@@ -334,17 +338,21 @@
<i class="fa fa-cart-plus fa-lg" aria-hidden="true"></i>
</label>
{% else %}
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if not ev.presale_is_running %}disabled{% endif %}
{% if itemnum == 1 %}value="1"{% endif %}
{% if item.free_price %}
data-checked-onchange="price-item-{{ item.pk }}"
{% endif %}
max="{{ item.order_max }}"
name="item_{{ item.id }}"
id="item_{{ item.id }}"
aria-label="{% blocktrans with item=item.name %}Quantity of {{ item }} to order{% endblocktrans %}"
{% if item.description %} aria-describedby="item-{{ item.id }}-description"{% endif %}>
<div class="input-item-count-group">
<button type="button" data-step="-1" data-controls="item_{{ item.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}">-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if not ev.presale_is_running %}disabled{% endif %}
{% if itemnum == 1 %}value="1"{% endif %}
{% if item.free_price %}
data-checked-onchange="price-item-{{ item.pk }}"
{% endif %}
max="{{ item.order_max }}"
name="item_{{ item.id }}"
id="item_{{ item.id }}"
aria-label="{% blocktrans with item=item.name %}Quantity of {{ item }} to order{% endblocktrans %}"
{% if item.description %} aria-describedby="item-{{ item.id }}-description"{% endif %}>
<button type="button" data-step="1" data-controls="item_{{ item.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}">+</button>
</div>
{% endif %}
</div>
{% else %}

View File

@@ -254,12 +254,16 @@
<i class="fa fa-cart-plus fa-lg" aria-hidden="true"></i>
</label>
{% else %}
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
<div class="input-item-count-group">
<button type="button" data-step="-1" data-controls="variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}">-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
max="{{ item.order_max }}"
id="variation_{{ item.id }}_{{ var.id }}"
name="variation_{{ item.id }}_{{ var.id }}"
{% if options == 1 %}value="1"{% endif %}
aria-label="{% blocktrans with item=item.name var=var.name %}Quantity of {{ item }}, {{ var }} to order{% endblocktrans %}">
<button type="button" data-step="1" data-controls="variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}">+</button>
</div>
{% endif %}
{% else %}
<label>
@@ -392,7 +396,9 @@
<i class="fa fa-cart-plus fa-lg" aria-hidden="true"></i>
</label>
{% else %}
<input type="number" class="form-control input-item-count"
<div class="input-item-count-group">
<button type="button" data-step="-1" data-controls="item_{{ item.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}">-</button>
<input type="number" class="form-control input-item-count"
placeholder="0" min="0"
max="{{ item.order_max }}"
id="item_{{ item.id }}"
@@ -400,6 +406,8 @@
{% if options == 1 %}value="1"{% endif %}
aria-label="{% blocktrans with item=item.name %}Quantity of {{ item }} to order{% endblocktrans %}"
{% if item.description %} aria-describedby="item-{{ item.id }}-description"{% endif %}>
<button type="button" data-step="1" data-controls="item_{{ item.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}">+</button>
</div>
{% endif %}
{% else %}
<label>

View File

@@ -668,6 +668,7 @@ class WidgetAPIProductList(EventListMixin, View):
data = {
'currency': request.event.currency,
'display_net_prices': request.event.settings.display_net_prices,
'use_native_spinners': request.event.settings.widget_use_native_spinners,
'show_variations_expanded': request.event.settings.show_variations_expanded,
'waiting_list_enabled': request.event.settings.waiting_list_enabled,
'voucher_explanation_text': str(rich_text(request.event.settings.voucher_explanation_text, safelinks=False)),

View File

@@ -117,6 +117,14 @@ var form_handlers = function (el) {
$(this).datetimepicker(opts);
});
el.find(".input-item-count-dec, .input-item-count-inc").on("click", function (e) {
e.preventDefault();
var step = parseFloat(this.getAttribute("data-step"));
var controls = document.getElementById(this.getAttribute("data-controls"));
var currentValue = parseFloat(controls.value);
controls.value = Math.max(controls.min, Math.min(controls.max, (currentValue || 0) + step));
});
el.find("script[data-replace-with-qr]").each(function () {
var $div = $("<div>");
$div.insertBefore($(this));

View File

@@ -15,6 +15,8 @@ Vue.component('resize-observer', VueResize.ResizeObserver)
var strings = {
'quantity': django.pgettext('widget', 'Quantity'),
'quantity_dec': django.pgettext('widget', 'Decrease quantity'),
'quantity_inc': django.pgettext('widget', 'Increase quantity'),
'price': django.pgettext('widget', 'Price'),
'select_item': django.pgettext('widget', 'Select %s'),
'select_variant': django.pgettext('widget', 'Select variant %s'),
@@ -218,10 +220,14 @@ Vue.component('availbox', {
+ ' v-bind:aria-label="label_select_item"'
+ '>'
+ '</label>'
+ '<input type="number" class="pretix-widget-item-count-multiple" placeholder="0" min="0"'
+ ' v-model="amount_selected" :max="order_max" :name="input_name"'
+ '<div :class="count_group_classes" v-else>'
+ '<button v-if="!$root.use_native_spinners" type="button" @click="on_step" data-step="-1" v-bind:data-controls="\'input_\' + input_name" class="pretix-widget-btn-default pretix-widget-item-count-dec" aria-label="' + strings.quantity_dec + '"><span>-</span></button>'
+ '<input type="number" inputmode="numeric" pattern="\d*" class="pretix-widget-item-count-multiple" placeholder="0" min="0"'
+ ' v-model="amount_selected" :max="order_max" :name="input_name" :id="\'input_\' + input_name"'
+ ' aria-label="' + strings.quantity + '"'
+ ' v-if="order_max !== 1">'
+ ' >'
+ '<button v-if="!$root.use_native_spinners" type="button" @click="on_step" data-step="1" v-bind:data-controls="\'input_\' + input_name" class="pretix-widget-btn-default pretix-widget-item-count-inc" aria-label="' + strings.quantity_inc + '"><span>+</span></button>'
+ '</div>'
+ '</div>'
+ '</div>'),
props: {
@@ -238,6 +244,11 @@ Vue.component('availbox', {
this.$root.$emit('amounts_changed')
},
computed: {
count_group_classes: function () {
return {
'pretix-widget-item-count-group': !this.$root.use_native_spinners
}
},
require_voucher: function () {
return this.item.require_voucher && !this.$root.voucher_code
},
@@ -296,6 +307,13 @@ Vue.component('availbox', {
methods: {
focus_voucher_field: function () {
this.$root.$emit('focus_voucher_field')
},
on_step: function (e) {
e.preventDefault();
var t = e.target.tagName == 'BUTTON' ? e.target : e.target.closest('button');
var step = parseFloat(t.getAttribute("data-step"));
var controls = document.getElementById(t.getAttribute("data-controls"));
this.amount_selected = Math.max(controls.min, Math.min(controls.max, (this.amount_selected || 0) + step));
}
}
});
@@ -1438,11 +1456,11 @@ Vue.component('pretix-widget', {
},
computed: {
classObject: function () {
var o = {'pretix-widget': true};
if (this.mobile) {
o['pretix-widget-mobile'] = true;
}
return o;
return {
'pretix-widget': true,
'pretix-widget-mobile': this.mobile,
'pretix-widget-use-custom-spinners': !this.$root.use_native_spinners
};
}
}
});
@@ -1588,6 +1606,7 @@ var shared_root_methods = {
root.categories = data.items_by_category;
root.currency = data.currency;
root.display_net_prices = data.display_net_prices;
root.use_native_spinners = data.use_native_spinners;
root.voucher_explanation_text = data.voucher_explanation_text;
root.error = data.error;
root.display_add_to_cart = data.display_add_to_cart;
@@ -1833,6 +1852,7 @@ var create_widget = function (element) {
variation_filter: variations,
voucher_code: voucher,
display_net_prices: false,
use_native_spinners: false,
voucher_explanation_text: null,
show_variations_expanded: !!variations,
skip_ssl: skip_ssl,

View File

@@ -54,6 +54,43 @@ a.btn, button.btn {
}
}
.input-item-count-group {
display: flex;
}
.input-item-count-group input {
border-radius: 0;
border-left: none;
border-right: none;
padding-right: 12px;
-moz-appearance: textfield;
}
.input-item-count-group input::-webkit-outer-spin-button,
.input-item-count-group input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.input-item-count-dec {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
width: 2.5em;
z-index: 2;
}
.input-item-count-inc {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
width: 2.5em;
}
.input-group-price {
input {
-moz-appearance: textfield;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
.required-legend span {
color: $brand-primary;
font-weight: bold;

View File

@@ -65,6 +65,9 @@
@include opacity(.65);
@include box-shadow(none);
}
&.pretix-widget-btn-default {
@include button-variant($btn-default-color, $btn-default-bg, $btn-default-border);
}
}
input[type="text"], input[type="number"] {
line-height: normal;
@@ -99,6 +102,15 @@
}
}
}
.pretix-widget-use-custom-spinners input[type=number] {
padding-right: $padding-base-horizontal;
-moz-appearance: textfield;
}
.pretix-widget-use-custom-spinners input[type=number]::-webkit-outer-spin-button,
.pretix-widget-use-custom-spinners input[type=number]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.pretix-widget {
margin: 10px 0;
padding: 0 10px;
@@ -254,6 +266,34 @@
display: block;
}
.pretix-widget-item-count-group {
display: flex;
}
.pretix-widget-item-count-group input {
border-radius: 0;
border-left: none;
border-right: none;
}
.pretix-widget-item-count-group button span {
vertical-align: 25%;
line-height: 0.5;
}
.pretix-widget-item-count-dec {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
width: 2.5em;
z-index: 2;
}
.pretix-widget-item-count-inc {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
width: 2.5em;
}
.pretix-widget-item-count-multiple {
display: block;
width: 100%;

View File

@@ -169,6 +169,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"currency": "EUR",
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
"vouchers_exist": False,
"waiting_list_enabled": False,
"error": None,
@@ -354,6 +355,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"currency": "EUR",
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
"has_seating_plan": False,
"has_seating_plan_waitinglist": False,
"vouchers_exist": True,
@@ -409,6 +411,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"currency": "EUR",
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
"vouchers_exist": True,
"has_seating_plan": False,
"has_seating_plan_waitinglist": False,
@@ -481,6 +484,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
'poweredby': '<a href="https://pretix.eu" target="_blank" rel="noopener">ticketing powered by pretix</a>',
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
"has_seating_plan": False,
"has_seating_plan_waitinglist": False,
"vouchers_exist": True,