Widget: add versioning support and add v2 with improved a11y-support (#5136)

* Add support for versioning widget.js

* add versionable css

* add version deprecation + redirect

* use dynamic template_path instead of dynamic css_path

* remove dummy code from widget.v1.scss

* fix typo

* [A11y] fix input border & focus style (#5149)

* [A11y] fix input border & focus style

* Fix double semi-colon

* [A11y] make collapse-indicator a button (#5150)

* Fix source order for cart-exists-message (#5152)

* [A11y] underline links (#5151)

* [A11y] Move modal-dialogs to HTMLDialogElement (#5147)

* [A11y] move widget/iframe to html-dialog

* make lightbox a dialog

* move error-alert to dialog

* re-add crossorigin

* fix esc-handling and move animation to icon to enable focusing the button

* fix code-style issues

* block canceling loading iframe

* Escape/cancel blocking fix for Chrome

* add round focus-outline when dialog is loading

* Widget v2: change voucher-link to hash-based link (#5161)

* Fix variants toggle-button being submit-button

* Widget v2: make single-item-select button and always show custom-spinners (#5165)

* Widget v2: make single-item-select=button default

* remove native-spinners and single_item_select

* Stop suggesting old parameter

---------

Co-authored-by: Raphael Michel <michel@rami.io>

* Widget v2: add filter button to events metadata-filter (#5162)

* Widget v2: do not underline events in list and calendar (#5163)

* Fix checkbox button missing border radius (#5158)

* Widget v2: turn add-to-cart-button into resume-button if cart-exists and no items selected (#5160)

* Widget v2: make cart-alert live=polite

* Add resume-button if cart-exists and no items selected

* fix error handling with new-tab and later returning to old window

* Fix cart-message button being full height

* fix amount_selected recalc

* Fix broken v-model

* fix merge

* Widget v2: Remove link from variation-product title (#5159)

* Remove link from variation-product, focus associated input

* open variations onclick on product-title

* clickable elements should be focussable and interactive, so better remove click-handler on product-title

* Widget v2: Fix calendar events color contrast (#5164)

* Widget v2: Fix calendar events color contrast

* fix status-bubbles in list-view

* fix color in mobile

* add striped-background to calendar and week

* improve display of calendar for super small screens

* Fix meta-filter legend not being screen-reader accessible

* update version_default to 2

Co-authored-by: Raphael Michel <michel@rami.io>

---------

Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
Richard Schreiber
2025-05-28 15:02:39 +02:00
committed by GitHub
parent e46e689f01
commit 92f7456eca
11 changed files with 4130 additions and 500 deletions

View File

@@ -16,6 +16,8 @@ var strings = {
'quantity': django.pgettext('widget', 'Quantity'),
'quantity_dec': django.pgettext('widget', 'Decrease quantity'),
'quantity_inc': django.pgettext('widget', 'Increase quantity'),
'filter_events_by': django.pgettext('widget', 'Filter events by'),
'filter': django.pgettext('widget', 'Filter'),
'price': django.pgettext('widget', 'Price'),
'original_price': django.pgettext('widget', 'Original price: %s'),
'new_price': django.pgettext('widget', 'New price: %s'),
@@ -57,6 +59,8 @@ var strings = {
'redeem': django.pgettext('widget', 'Redeem'),
'voucher_code': django.pgettext('widget', 'Voucher code'),
'close': django.pgettext('widget', 'Close'),
'close_checkout': django.pgettext('widget', 'Close checkout'),
'cancel_blocked': django.pgettext('widget', 'You cannot cancel this operation. Please wait for loading to finish.'),
'continue': django.pgettext('widget', 'Continue'),
'variations': django.pgettext('widget', 'Show variants'),
'hide_variations': django.pgettext('widget', 'Hide variants'),
@@ -208,7 +212,7 @@ Vue.component('availbox', {
template: ('<div class="pretix-widget-availability-box">'
+ '<div class="pretix-widget-availability-unavailable"'
+ ' v-if="item.current_unavailability_reason === \'require_voucher\'">'
+ '<small><a @click.prevent.stop="focus_voucher_field" role="button" tabindex="0">{{unavailability_reason_message}}</a></small>'
+ '<small><a :href="voucher_jump_link" v-bind:aria-describedby="aria_labelledby">{{unavailability_reason_message}}</a></small>'
+ '</div>'
+ '<div class="pretix-widget-availability-unavailable"'
+ ' v-else-if="unavailability_reason_message">'
@@ -227,24 +231,19 @@ Vue.component('availbox', {
+ '<a :href="waiting_list_url" target="_blank" @click="$root.open_link_in_frame">' + strings.waiting_list + '</a>'
+ '</div>'
+ '<div class="pretix-widget-availability-available" v-if="!unavailability_reason_message && avail[0] === 100">'
+ '<label class="pretix-widget-item-count-single-label pretix-widget-btn-checkbox" v-if="order_max === 1 && $root.single_item_select == \'button\'">'
+ '<input type="checkbox" value="1" :checked="!!amount_selected" @change="amount_selected = $event.target.checked" :name="input_name"'
+ '<label class="pretix-widget-item-count-single-label pretix-widget-btn-checkbox" v-if="order_max === 1">'
+ '<input ref="quantity" type="checkbox" value="1" :name="input_name"'
+ ' v-bind:aria-label="label_select_item"'
+ '>'
+ '<span class="pretix-widget-icon-cart" aria-hidden="true"></span> ' + strings.select
+ '</label>'
+ '<label class="pretix-widget-item-count-single-label" v-else-if="order_max === 1">'
+ '<input type="checkbox" value="1" :checked="!!amount_selected" @change="amount_selected = $event.target.checked" :name="input_name"'
+ ' v-bind:aria-label="label_select_item"'
+ '>'
+ '</label>'
+ '<div :class="count_group_classes" v-else role="group" v-bind:aria-label="item.name">'
+ '<button v-if="!$root.use_native_spinners" type="button" @click.prevent.stop="on_step" data-step="-1" v-bind:data-controls="\'input_\' + input_name" class="pretix-widget-btn-default pretix-widget-item-count-dec" v-bind:aria-label="dec_label"><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"'
+ ' v-bind:aria-labelledby="aria_labelledby" ref="quantity"'
+ '<div class="pretix-widget-item-count-group" v-else role="group" v-bind:aria-label="item.name">'
+ '<button type="button" @click.prevent.stop="on_step" data-step="-1" v-bind:data-controls="\'input_\' + input_name" class="pretix-widget-btn-default pretix-widget-item-count-dec" v-bind:aria-label="dec_label"><span>-</span></button>'
+ '<input ref="quantity" type="number" inputmode="numeric" pattern="\d*" class="pretix-widget-item-count-multiple" placeholder="0" min="0"'
+ ' :max="order_max" :name="input_name" :id="\'input_\' + input_name"'
+ ' v-bind:aria-labelledby="aria_labelledby"'
+ ' >'
+ '<button v-if="!$root.use_native_spinners" type="button" @click.prevent.stop="on_step" data-step="1" v-bind:data-controls="\'input_\' + input_name" class="pretix-widget-btn-default pretix-widget-item-count-inc" v-bind:aria-label="inc_label"><span>+</span></button>'
+ '<button type="button" @click.prevent.stop="on_step" data-step="1" v-bind:data-controls="\'input_\' + input_name" class="pretix-widget-btn-default pretix-widget-item-count-inc" v-bind:aria-label="inc_label"><span>+</span></button>'
+ '</div>'
+ '</div>'
+ '</div>'),
@@ -253,15 +252,17 @@ Vue.component('availbox', {
variation: Object
},
mounted: function() {
if (this.item.has_variations) {
this.$set(this.variation, 'amount_selected', 0);
} else {
// Automatically set the only available item to be selected.
this.$set(this.item, 'amount_selected', this.$root.itemnum === 1 && !this.$root.has_seating_plan ? 1 : 0);
if (this.$root.itemnum === 1 && !this.$root.has_seating_plan ? 1 : 0) {
this.$refs.quantity.value = 1;
if (this.order_max === 1) {
this.$refs.quantity.checked = true;
}
}
this.$root.$emit('amounts_changed')
},
computed: {
voucher_jump_link: function () {
return '#' + this.$root.html_id + '-voucher-input';
},
aria_labelledby: function () {
return this.$root.html_id + '-item-label-' + this.item.id;
},
@@ -271,11 +272,6 @@ Vue.component('availbox', {
inc_label: function () {
return '+ ' + (this.item.has_variations ? this.variation.value : this.item.name) + ': ' + strings.quantity_inc;
},
count_group_classes: function () {
return {
'pretix-widget-item-count-group': !this.$root.use_native_spinners
}
},
unavailability_reason_message: function () {
var reason = this.item.current_unavailability_reason || this.variation?.current_unavailability_reason;
if (reason) {
@@ -283,29 +279,6 @@ Vue.component('availbox', {
}
return "";
},
amount_selected: {
cache: false,
get: function () {
var selected = this.item.has_variations ? this.variation.amount_selected : this.item.amount_selected
if (selected === 0) return undefined;
return selected
},
set: function (value) {
// Unary operator to force boolean to integer conversion, as the HTML form submission
// needs the value to be integer for all products.
value = (+value);
if (this.item.has_variations) {
this.variation.amount_selected = value;
} else {
this.item.amount_selected = value;
}
if (this.$refs.quantity) {
// manually set value on quantity as on reload somehow v-model binding breaks
this.$refs.quantity.value = value;
}
this.$root.$emit("amounts_changed")
}
},
label_select_item: function () {
return this.item.has_variations
? strings.select_variant.replace("%s", this.variation.value)
@@ -341,14 +314,14 @@ Vue.component('availbox', {
}
},
methods: {
focus_voucher_field: function () {
this.$root.$emit('focus_voucher_field')
},
on_step: function (e) {
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 || Number.MAX_SAFE_INTEGER, (this.amount_selected || 0) + step));
this.$refs.quantity.value = Math.max(controls.min, Math.min(controls.max || Number.MAX_SAFE_INTEGER, (parseInt(this.$refs.quantity.value || "0")) + step));
this.$refs.quantity.dispatchEvent(new CustomEvent("change", {
bubbles: true,
}));
}
}
});
@@ -537,12 +510,7 @@ Vue.component('item', {
+ '<div class="pretix-widget-item-info-col">'
+ '<a :href="item.picture_fullsize" v-if="item.picture" class="pretix-widget-item-picture-link" @click.prevent.stop="lightbox"><img :src="item.picture" class="pretix-widget-item-picture" :alt="picture_alt_text"></a>'
+ '<div class="pretix-widget-item-title-and-description">'
+ '<a v-if="item.has_variations && show_toggle" :id="item_label_id" role="heading" v-bind:aria-level="headingLevel" class="pretix-widget-item-title" :href="\'#\' + item.id + \'-variants\'"'
+ ' @click.prevent.stop="expand"'
+ '>'
+ '{{ item.name }}'
+ '</a>'
+ '<strong v-else class="pretix-widget-item-title" :id="item_label_id" role="heading" v-bind:aria-level="headingLevel">{{ item.name }}</strong>'
+ '<strong class="pretix-widget-item-title" :id="item_label_id" role="heading" v-bind:aria-level="headingLevel">{{ item.name }}</strong>'
+ '<div class="pretix-widget-item-description" :id="item_desc_id" v-if="item.description" v-html="item.description"></div>'
+ '<p class="pretix-widget-item-meta" v-if="item.order_min && item.order_min > 1">'
+ '<small>{{ min_order_str }}</small>'
@@ -566,8 +534,8 @@ Vue.component('item', {
// Availability
+ '<div class="pretix-widget-item-availability-col">'
+ '<a class="pretix-widget-collapse-indicator" v-if="show_toggle" :href="\'#\' + item.id + \'-variants\'" @click.prevent.stop="expand" role="button" tabindex="0"'
+ ' v-bind:aria-expanded="expanded ? \'true\': \'false\'" v-bind:aria-controls="item.id + \'-variants\'">{{ variationsToggleLabel }}</a>'
+ '<button type="button" class="pretix-widget-collapse-indicator" v-if="show_toggle" @click.prevent.stop="expand"'
+ ' v-bind:aria-expanded="expanded ? \'true\': \'false\'" v-bind:aria-controls="item.id + \'-variants\'" v-bind:aria-describedby="item_desc_id">{{ variationsToggleLabel }}</button>'
+ '<availbox v-if="!item.has_variations" :item="item"></availbox>'
+ '</div>'
@@ -622,7 +590,7 @@ Vue.component('item', {
image: this.item.picture_fullsize,
description: this.item.name,
}
}
},
},
computed: {
classObject: function () {
@@ -855,53 +823,54 @@ var shared_loading_fragment = (
);
var shared_iframe_fragment = (
'<div :class="frameClasses" role="dialog" aria-modal="true" aria-label="'+strings.checkout+'">'
'<dialog :class="frameClasses" role="alertdialog" aria-label="'+strings.checkout+'" @close="close" @cancel="cancel">'
+ '<div class="pretix-widget-frame-loading" v-show="$root.frame_loading">'
+ '<svg width="256" height="256" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path class="pretix-widget-primary-color" d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z"/></svg>'
+ '<svg width="256" height="256" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path class="pretix-widget-primary-color" d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z"/></svg>'
+ '<p :class="cancelBlockedClasses"><strong>'+strings.cancel_blocked+'</strong></p>'
+ '</div>'
+ '<div class="pretix-widget-frame-inner" ref="frame-container" v-show="$root.frame_shown">'
+ '<div class="pretix-widget-frame-close"><a href="#" @click.prevent.stop="close" role="button" aria-label="'+strings.close+'">'
+ '<svg alt="'+strings.close+'" height="16" viewBox="0 0 512 512" width="16" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z"/></svg>'
+ '</a></div>'
+ '<iframe frameborder="0" width="650" height="650" @load="iframeLoaded" '
+ ' :name="$root.parent.widget_id" src="about:blank" v-once'
+ ' allow="autoplay *; camera *; fullscreen *; payment *"'
+ ' title="'+strings.checkout+'"'
+ ' referrerpolicy="origin">'
+ 'Please enable frames in your browser!'
+ '</iframe>'
+ '</div>'
+ '<form class="pretix-widget-frame-close" method="dialog"><button aria-label="'+strings.close_checkout+'" autofocus="autofocus">'
+ '<svg alt="'+strings.close+'" height="16" viewBox="0 0 512 512" width="16" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z"/></svg>'
+ '</button></form>'
+ '<iframe frameborder="0" width="650" height="650" @load="iframeLoaded" '
+ ' :name="$root.parent.widget_id" src="about:blank" v-once'
+ ' allow="autoplay *; camera *; fullscreen *; payment *"'
+ ' title="'+strings.checkout+'"'
+ ' referrerpolicy="origin">'
+ 'Please enable frames in your browser!'
+ '</iframe>'
+ '</div>'
+ '</dialog>'
);
var shared_alert_fragment = (
'<div :class="alertClasses" role="alertdialog" v-bind:aria-labelledby="$root.parent.html_id + \'-error-message\'">'
+ '<transition name="bounce" @after-enter="focusButton">'
+ '<div class="pretix-widget-alert-box" v-if="$root.error_message">'
'<dialog :class="alertClasses" role="alertdialog" v-bind:aria-labelledby="$root.parent.html_id + \'-error-message\'" @close="errorClose">'
+ '<form class="pretix-widget-alert-box" method="dialog">'
+ '<p :id="$root.parent.html_id + \'-error-message\'">{{ $root.error_message }}</p>'
+ '<p><button v-if="$root.error_url_after" @click.prevent.stop="errorContinue">' + strings.continue + '</button>'
+ '<button v-else @click.prevent.stop="errorClose">' + strings.close + '</button></p>'
+ '</div>'
+ '<p><button v-if="$root.error_url_after" value="continue" autofocus v-bind:aria-describedby="$root.parent.html_id + \'-error-message\'">' + strings.continue + '</button>'
+ '<button v-else autofocus v-bind:aria-describedby="$root.parent.html_id + \'-error-message\'">' + strings.close + '</button></p>'
+ '</form>'
+ '<transition name="bounce">'
+ '<svg v-if="$root.error_message" width="64" height="64" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" class="pretix-widget-alert-icon"><path style="fill:#ffffff;" d="M 599.86438,303.72882 H 1203.5254 V 1503.4576 H 599.86438 Z" /><path class="pretix-widget-primary-color" d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5-103 385.5-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103zm128 1247v-190q0-14-9-23.5t-22-9.5h-192q-13 0-23 10t-10 23v190q0 13 10 23t23 10h192q13 0 22-9.5t9-23.5zm-2-344l18-621q0-12-10-18-10-8-24-8h-220q-14 0-24 8-10 6-10 18l17 621q0 10 10 17.5t24 7.5h185q14 0 23.5-7.5t10.5-17.5z"/></svg>'
+ '</transition>'
+ '<svg width="64" height="64" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" class="pretix-widget-alert-icon"><path style="fill:#ffffff;" d="M 599.86438,303.72882 H 1203.5254 V 1503.4576 H 599.86438 Z" /><path class="pretix-widget-primary-color" d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5-103 385.5-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103zm128 1247v-190q0-14-9-23.5t-22-9.5h-192q-13 0-23 10t-10 23v190q0 13 10 23t23 10h192q13 0 22-9.5t9-23.5zm-2-344l18-621q0-12-10-18-10-8-24-8h-220q-14 0-24 8-10 6-10 18l17 621q0 10 10 17.5t24 7.5h185q14 0 23.5-7.5t10.5-17.5z"/></svg>'
+ '</div>'
+ '</dialog>'
);
var shared_lightbox_fragment = (
'<div :class="lightboxClasses" role="dialog" aria-modal="true" v-if="$root.lightbox" @click="lightboxClose">'
'<dialog :class="lightboxClasses" role="alertdialog" @close="lightboxClose">'
+ '<div class="pretix-widget-lightbox-loading" v-if="$root.lightbox?.loading">'
+ '<svg width="256" height="256" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path class="pretix-widget-primary-color" d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z"/></svg>'
+ '</div>'
+ '<div class="pretix-widget-lightbox-inner" @click.stop="">'
+ '<div class="pretix-widget-lightbox-inner" v-if="$root.lightbox">'
+ '<form class="pretix-widget-lightbox-close" method="dialog"><button aria-label="'+strings.close+'" autofocus="autofocus">'
+ '<svg alt="'+strings.close+'" height="16" viewBox="0 0 512 512" width="16" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z"/></svg>'
+ '</button></form>'
+ '<figure class="pretix-widget-lightbox-image">'
+ '<img :src="$root.lightbox.image" :alt="$root.lightbox.description" @load="lightboxLoaded" ref="lightboxImage" crossorigin>'
+ '<figcaption v-if="$root.lightbox.description">{{$root.lightbox.description}}</figcaption>'
+ '</figure>'
+ '<button type="button" class="pretix-widget-lightbox-close" @click="lightboxClose" aria-label="'+strings.close+'">'
+ '<svg alt="'+strings.close+'" height="16" viewBox="0 0 512 512" width="16" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z"/></svg>'
+ '</button>'
+ '</div>'
+ '</div>'
+ '</dialog>'
);
Vue.component('pretix-overlay', {
@@ -911,6 +880,11 @@ Vue.component('pretix-overlay', {
+ shared_lightbox_fragment
+ '</div>'
),
data: function () {
return {
cancelBlocked: false,
}
},
watch: {
'$root.lightbox': function (newValue, oldValue) {
if (newValue) {
@@ -918,18 +892,24 @@ Vue.component('pretix-overlay', {
this.$set(newValue, "loading", true);
}
if (!oldValue) {
window.addEventListener('keyup', this.lightboxCloseOnKeyup);
this.$el?.querySelector(".pretix-widget-lightbox-holder").showModal();
}
} else {
window.removeEventListener('keyup', this.lightboxCloseOnKeyup);
}
}
},
'$root.error_message': function (newValue, oldValue) {
if (newValue) {
if (!oldValue) {
this.$el?.querySelector(".pretix-widget-alert-holder").showModal();
}
}
},
},
computed: {
frameClasses: function () {
return {
'pretix-widget-frame-holder': true,
'pretix-widget-frame-shown': this.$root.frame_shown || this.$root.frame_loading,
'pretix-widget-frame-isloading': this.$root.frame_loading,
};
},
alertClasses: function () {
@@ -945,54 +925,67 @@ Vue.component('pretix-overlay', {
'pretix-widget-lightbox-isloading': this.$root.lightbox?.loading,
};
},
cancelBlockedClasses: function () {
return {
'pretix-widget-visibility-hidden': !this.cancelBlocked,
}
},
},
methods: {
lightboxCloseOnKeyup: function (event) {
if (event.keyCode === 27) {
// abort on ESC-key
this.lightboxClose();
}
},
lightboxClose: function () {
this.$root.lightbox = null;
},
lightboxLoaded: function () {
this.$root.lightbox.loading = false;
},
errorClose: function () {
errorClose: function (e) {
var dialog = e.target;
if (dialog.returnValue == "continue" && this.$root.error_url_after) {
if (this.$root.error_url_after_new_tab) {
window.open(this.$root.error_url_after);
} else if (this.$root.overlay) {
this.$root.overlay.frame_src = this.$root.error_url_after;
this.$root.frame_loading = true;
}
}
this.$root.error_message = null;
this.$root.error_url_after = null;
this.$root.error_url_after_new_tab = false;
},
errorContinue: function () {
if (this.$root.error_url_after_new_tab) {
window.open(this.$root.error_url_after);
close: function (e) {
if (this.$root.frame_loading) {
// Chrome does not allow blocking dialog.cancel event more than once
// => wiggle the loading-element and re-open the modal
this.cancel(e);
e.target.showModal();
return;
}
this.$root.overlay.frame_src = this.$root.error_url_after;
this.$root.frame_loading = true;
this.$root.error_message = null;
this.$root.error_url_after = null;
},
close: function () {
this.$root.frame_shown = false;
this.$root.parent.frame_dismissed = true;
this.$root.frame_src = "";
this.$root.parent.reload();
this.$root.parent.trigger_close_callback();
},
cancel: function (e) {
// do not allow to cancel while frame is loading as we cannot abort the operation
if (this.$root.frame_loading) {
e.preventDefault();
e.target.addEventListener("animationend", function () {
e.target.classList.remove("pretix-widget-shake-once");
}, {once: true});
e.target.classList.add("pretix-widget-shake-once");
this.cancelBlocked = true;
}
},
iframeLoaded: function () {
if (this.$root.frame_loading) {
this.$root.frame_loading = false;
this.cancelBlocked = false;
if (this.$root.frame_src) {
this.$root.frame_shown = true;
}
}
},
focusButton: function () {
this.$el.querySelector(".pretix-widget-alert-box button").focus();
},
}
});
@@ -1033,13 +1026,11 @@ Vue.component('pretix-widget-event-form', {
+ '<div class="pretix-widget-error-message" v-if="$root.error">{{ $root.error }}</div>'
// Resume cart
+ '<div class="pretix-widget-info-message pretix-widget-clickable"'
+ ' v-if="$root.cart_exists">'
+ '<div class="pretix-widget-info-message pretix-widget-clickable" v-if="$root.cart_exists">'
+ '<span :id="id_cart_exists_msg">' + strings['cart_exists'] + '</span>'
+ '<button @click.prevent.stop="$parent.resume" class="pretix-widget-resume-button" type="button" v-bind:aria-describedby="id_cart_exists_msg">'
+ strings['resume_checkout']
+ '</button>'
+ '<span :id="id_cart_exists_msg">' + strings['cart_exists'] + '</span>'
+ '<div class="pretix-widget-clear"></div>'
+ '</div>'
// Seating plan
@@ -1067,7 +1058,10 @@ Vue.component('pretix-widget-event-form', {
// Buy button
+ '<div class="pretix-widget-action" v-if="$root.display_add_to_cart">'
+ '<button type="submit">{{ this.buy_label }}</button>'
+ '<button v-if="!this.$root.cart_exists || this.is_items_selected" type="submit" v-bind:aria-describedby="id_cart_exists_msg">{{ buy_label }}</button>'
+ '<button v-else @click.prevent.stop="$parent.resume" type="button" v-bind:aria-describedby="id_cart_exists_msg">'
+ strings['resume_checkout']
+ '</button>'
+ '</div>'
+ '</form>'
@@ -1079,7 +1073,7 @@ Vue.component('pretix-widget-event-form', {
+ '<h3 class="pretix-widget-voucher-headline" :id="aria_labelledby">'+ strings['redeem_voucher'] +'</h3>'
+ '<div v-if="$root.voucher_explanation_text" class="pretix-widget-voucher-text" v-html="$root.voucher_explanation_text"></div>'
+ '<div class="pretix-widget-voucher-input-wrap">'
+ '<input class="pretix-widget-voucher-input" ref="voucherinput" type="text" v-model="$parent.voucher" name="voucher" placeholder="'+strings.voucher_code+'" v-bind:aria-labelledby="aria_labelledby">'
+ '<input :id="id_voucher_input" class="pretix-widget-voucher-input" ref="voucherinput" type="text" v-model="$parent.voucher" name="voucher" placeholder="'+strings.voucher_code+'" v-bind:aria-labelledby="aria_labelledby">'
+ '</div>'
+ '<input type="hidden" v-for="p in hiddenParams" :name="p[0]" :value="p[1]" />'
+ '<div class="pretix-widget-voucher-button-wrap">'
@@ -1091,14 +1085,32 @@ Vue.component('pretix-widget-event-form', {
+ '</div>'
),
data: function () {
return {
is_items_selected: false,
}
},
watch: {
'$root.overlay.frame_shown': function (newValue) {
if (!newValue) {
this.$refs.form.reset();
this.calc_items_selected();
}
}
},
mounted: function() {
this.$root.$on('focus_voucher_field', this.focus_voucher_field)
this.$root.$on('focus_voucher_field', this.focus_voucher_field);
this.$refs.form.addEventListener("change", this.calc_items_selected);
},
beforeDestroy: function() {
this.$root.$off('focus_voucher_field', this.focus_voucher_field)
this.$root.$off('focus_voucher_field', this.focus_voucher_field);
this.$refs.form.removeEventListener("change", this.calc_items_selected);
},
computed: {
aria_labelledby: function() {
id_voucher_input: function () {
return this.$root.html_id + '-voucher-input';
},
aria_labelledby: function () {
return this.$root.html_id + '-voucher-headline';
},
display_event_info: function () {
@@ -1143,10 +1155,6 @@ Vue.component('pretix-widget-event-form', {
},
},
methods: {
focus_voucher_field: function() {
this.$refs.voucherinput.scrollIntoView(false)
this.$refs.voucherinput.focus()
},
back_to_list: function() {
this.$root.target_url = this.$root.parent_stack.pop();
this.$root.error = null;
@@ -1173,32 +1181,26 @@ Vue.component('pretix-widget-event-form', {
$el.focus();
});
},
calc_items_selected: function () {
this.is_items_selected = [...this.$refs.form.querySelectorAll("input[type=checkbox], input[type=radio]")].some(function(element) {
return element.checked;
}) || [...this.$refs.form.querySelectorAll(".pretix-widget-item-count-group input")].some(function(element) {
return parseInt(element.value || "0") > 0;
});
},
}
});
Vue.component('pretix-widget-event-list-filter-field', {
template: ('<div class="pretix-widget-event-list-filter-field">'
+ '<label :for="id">{{ field.label }}</label>'
+ '<select :id="id" :name="field.key" @change="onChange($event)" :value="currentValue">'
+ '<select :id="id" :name="field.key" :value="currentValue">'
+ '<option v-for="choice in field.choices" :value="choice[0]">{{ choice[1] }}</option>'
+ '</select>'
+ '</div>'),
props: {
field: Object
},
methods: {
onChange: function(event) {
var filterParams = new URLSearchParams(this.$root.filter);
if (event.target.value) {
filterParams.set(this.field.key, event.target.value);
} else {
filterParams.delete(this.field.key);
}
this.$root.filter = filterParams.toString();
this.$root.loading++;
this.$root.reload();
},
},
computed: {
id: function () {
return widget_id + "_" + this.field.key;
@@ -1211,9 +1213,29 @@ Vue.component('pretix-widget-event-list-filter-field', {
});
Vue.component('pretix-widget-event-list-filter-form', {
template: ('<div class="pretix-widget-event-list-filter-form">'
+ '<pretix-widget-event-list-filter-field v-for="field in $root.meta_filter_fields" :field="field" :key="field.key"></pretix-widget-event-list-filter-field>'
+ '</div>'),
template: ('<form ref="filterform" class="pretix-widget-event-list-filter-form" @submit="onSubmit">'
+ '<fieldset class="pretix-widget-event-list-filter-fieldset">'
+ '<legend>' + strings.filter_events_by + '</legend>'
+ '<pretix-widget-event-list-filter-field v-for="field in $root.meta_filter_fields" :field="field" :key="field.key"></pretix-widget-event-list-filter-field>'
+ '<button>' + strings.filter + '</button>'
+ '</fieldset>'
+ '</form>'),
methods: {
onSubmit: function(e) {
e.preventDefault();
var formData = new FormData(this.$refs.filterform);
var filterParams = new URLSearchParams(formData);
formData.forEach(function (value, key) {
if (value == "") {
filterParams.delete(key);
}
});
this.$root.filter = filterParams.toString();
this.$root.loading++;
this.$root.reload();
},
},
});
Vue.component('pretix-widget-event-list-entry', {
@@ -1740,7 +1762,7 @@ Vue.component('pretix-widget', {
return {
'pretix-widget': true,
'pretix-widget-mobile': this.mobile,
'pretix-widget-use-custom-spinners': !this.$root.use_native_spinners
'pretix-widget-use-custom-spinners': true,
};
}
}
@@ -1902,7 +1924,6 @@ 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;
@@ -2161,28 +2182,11 @@ var create_overlay = function (app) {
// show loading spinner only when previously no frame_src was set
if (newValue && !oldValue) {
this.frame_loading = true;
this.$el?.querySelector('dialog.pretix-widget-frame-holder').showModal();
}
// to close and unload the iframe, frame_src can be empty -> make it valid HTML with about:blank
this.$el.querySelector("iframe").src = newValue || "about:blank";
},
frame_shown: function (newValue) {
if (newValue) {
this.prevActiveElement = document.activeElement;
var btn = this.$el?.querySelector(".pretix-widget-frame-close a");
this.$nextTick(function () {
btn?.focus();
});
} else {
this.prevActiveElement?.focus();
}
},
error_message: function (newValue) {
if (newValue) {
this.prevActiveElement = document.activeElement;
} else {
this.prevActiveElement?.focus();
}
},
}
});
app.$root.overlay = framechild;
@@ -2228,7 +2232,6 @@ var create_widget = function (element, html_id=null) {
var items = element.attributes.items ? element.attributes.items.value : null;
var variations = element.attributes.variations ? element.attributes.variations.value : null;
var categories = element.attributes.categories ? element.attributes.categories.value : null;
var single_item_select = element.getAttribute("single-item-select") || "checkbox";
for (var i = 0; i < element.attributes.length; i++) {
var attrib = element.attributes[i];
if (attrib.name.match(/^data-.*$/)) {
@@ -2275,8 +2278,6 @@ var create_widget = function (element, html_id=null) {
variation_filter: variations,
voucher_code: voucher,
display_net_prices: false,
use_native_spinners: false,
single_item_select: single_item_select,
voucher_explanation_text: null,
show_variations_expanded: !!variations,
skip_ssl: skip_ssl,