diff --git a/src/pretix/control/templates/pretixcontrol/event/widget.html b/src/pretix/control/templates/pretixcontrol/event/widget.html index 5cec47144..066673c74 100644 --- a/src/pretix/control/templates/pretixcontrol/event/widget.html +++ b/src/pretix/control/templates/pretixcontrol/event/widget.html @@ -19,8 +19,8 @@ section of your website: {% endblocktrans %}

-
<link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" %}" crossorigin>
-<script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language %}" async crossorigin></script>
+
<link rel="stylesheet" type="text/css" href="{% abseventurl request.event "presale:event.widget.css" version=widget_version_default %}" crossorigin>
+<script type="text/javascript" src="{{ urlprefix }}{% url "presale:widget.js" lang=form.cleaned_data.language version=widget_version_default %}" async crossorigin></script>

{% blocktrans trimmed %} Then, copy the following code to the place of your website where you want the widget to show up: @@ -32,7 +32,7 @@ {% abseventurl request.event "presale:event.index" as indexurl %} {% endif %} {% if form.cleaned_data.compatibility_mode %} -

<div class="pretix-widget-compat" event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %} single-item-select="button"></div>
+            
<div class="pretix-widget-compat" event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}></div>
 <noscript>
    <div class="pretix-widget">
         <div class="pretix-widget-info-message">
@@ -45,7 +45,7 @@
 </noscript>
 
{% else %} -
<pretix-widget event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %} single-item-select="button"></pretix-widget>
+        
<pretix-widget event="{% abseventurl request.event "presale:event.index" %}"{% if form.cleaned_data.subevent %} subevent="{{ form.cleaned_data.subevent.pk }}"{% endif %}{% if form.cleaned_data.voucher %} voucher="{{ form.cleaned_data.voucher }}"{% endif %}></pretix-widget>
 <noscript>
    <div class="pretix-widget">
         <div class="pretix-widget-info-message">
diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py
index 4667a6b96..9ffb76eac 100644
--- a/src/pretix/control/views/event.py
+++ b/src/pretix/control/views/event.py
@@ -96,6 +96,9 @@ from pretix.control.views.user import RecentAuthenticationRequiredMixin
 from pretix.helpers.database import rolledback_transaction
 from pretix.multidomain.urlreverse import build_absolute_uri, get_event_domain
 from pretix.plugins.stripe.payment import StripeSettingsHolder
+from pretix.presale.views.widget import (
+    version_default as widget_version_default,
+)
 
 from ...base.i18n import language
 from ...base.models.items import (
@@ -1408,6 +1411,7 @@ class WidgetSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
     def get_context_data(self, **kwargs):
         ctx = super().get_context_data(**kwargs)
         ctx['urlprefix'] = settings.SITE_URL
+        ctx['widget_version_default'] = widget_version_default
         domain = get_event_domain(self.request.event, fallback=True)
         if domain:
             siteurlsplit = urlsplit(settings.SITE_URL)
diff --git a/src/pretix/presale/templates/pretixpresale/widget_dummy.v1.html b/src/pretix/presale/templates/pretixpresale/widget_dummy.v1.html
new file mode 100644
index 000000000..72f58a906
--- /dev/null
+++ b/src/pretix/presale/templates/pretixpresale/widget_dummy.v1.html
@@ -0,0 +1,5 @@
+{% load compress %}
+{% load static %}
+{% compress css %}
+    
+{% endcompress %}
diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py
index 2289c4166..18089751d 100644
--- a/src/pretix/presale/urls.py
+++ b/src/pretix/presale/urls.py
@@ -32,7 +32,7 @@
 # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations under the License.
 
-from django.urls import include, re_path
+from django.urls import include, path, re_path
 from django.views.decorators.csrf import csrf_exempt
 
 import pretix.presale.views.cart
@@ -173,7 +173,7 @@ event_patterns = [
 
     re_path(r'^widget/product_list$', pretix.presale.views.widget.WidgetAPIProductList.as_view(),
             name='event.widget.productlist'),
-    re_path(r'^widget/v1.css$', pretix.presale.views.widget.widget_css, name='event.widget.css'),
+    path('widget/v.css', pretix.presale.views.widget.widget_css, name='event.widget.css'),
     re_path(r'^(?P\d+)/widget/product_list$', pretix.presale.views.widget.WidgetAPIProductList.as_view(),
             name='event.widget.productlist'),
 
@@ -196,7 +196,7 @@ organizer_patterns = [
 
     re_path(r'^widget/product_list$', pretix.presale.views.widget.WidgetAPIProductList.as_view(),
             name='organizer.widget.productlist'),
-    re_path(r'^widget/v1.css$', pretix.presale.views.widget.widget_css, name='organizer.widget.css'),
+    path('widget/v.css', pretix.presale.views.widget.widget_css, name='organizer.widget.css'),
 
     re_path(r'^theme.css$', pretix.presale.views.theme.theme_css, name='organizer.theme.css'),
     re_path(r'^accessibility$', pretix.presale.views.organizer.AccessibilityView.as_view(), name='organizer.accessibility'),
@@ -237,5 +237,5 @@ locale_patterns = [
     re_path(r'^robots.txt$', pretix.presale.views.robots.robots_txt, name='robots.txt'),
     re_path(r'^browserconfig.xml$', pretix.presale.views.theme.browserconfig_xml, name='browserconfig.xml'),
     re_path(r'^site.webmanifest$', pretix.presale.views.theme.webmanifest, name='site.webmanifest'),
-    re_path(r'^widget/v1\.(?P[a-zA-Z0-9_\-]+)\.js$', pretix.presale.views.widget.widget_js, name='widget.js'),
+    path('widget/v..js', pretix.presale.views.widget.widget_js, name='widget.js'),
 ]
diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py
index 8ae618a6c..f16755eae 100644
--- a/src/pretix/presale/views/widget.py
+++ b/src/pretix/presale/views/widget.py
@@ -38,8 +38,10 @@ from django.core.files.base import ContentFile, File
 from django.core.files.storage import default_storage
 from django.db.models import Q
 from django.http import FileResponse, Http404, HttpResponse, JsonResponse
+from django.shortcuts import redirect
 from django.template import Context, Engine
 from django.template.loader import get_template
+from django.urls import reverse
 from django.utils.formats import date_format
 from django.utils.timezone import now
 from django.utils.translation import get_language, gettext, pgettext
@@ -81,15 +83,22 @@ logger = logging.getLogger(__name__)
 # we never change static source without restart, so we can cache this thread-wise
 _source_cache_key = None
 
+version_min = 1
+version_max = 2
+version_default = 2  # used for output in widget-embed-code
 
-def _get_source_cache_key():
+
+def _get_source_cache_key(version):
     global _source_cache_key
     checksum = hashlib.sha256()
     if not _source_cache_key:
         with open(finders.find("pretixbase/scss/_theme_variables.scss"), "r") as f:
             checksum.update(f.read().encode())
-        tpl = get_template('pretixpresale/widget_dummy.html')
-        et = html.fromstring(tpl.render({})).xpath('/html/head/link')[0].attrib['href'].replace(settings.STATIC_URL, '')
+
+        template_path = 'pretixpresale/widget_dummy.html' if version == version_max else 'pretixpresale/widget_dummy.v{}.html'.format(version)
+
+        tpl = get_template(template_path)
+        et = html.fromstring(tpl.render()).xpath('/html/head/link')[0].attrib['href'].replace(settings.STATIC_URL, '')
         checksum.update(et.encode())
         _source_cache_key = checksum.hexdigest()[:12]
     return _source_cache_key
@@ -99,29 +108,39 @@ def indent(s):
     return s.replace('\n', '\n  ')
 
 
-def widget_css_etag(request, **kwargs):
+def widget_css_etag(request, version, **kwargs):
     # This makes sure a new version of the theme is loaded whenever settings or the source files have changed
     if hasattr(request, 'event'):
-        return (f'{_get_source_cache_key()}-'
+        return (f'{_get_source_cache_key(version)}-'
                 f'{request.organizer.cache.get_or_set("css_version", default=lambda: int(time.time()))}-'
                 f'{request.event.cache.get_or_set("css_version", default=lambda: int(time.time()))}')
     else:
-        return f'{_get_source_cache_key()}-{request.organizer.cache.get_or_set("css_version", default=lambda: int(time.time()))}'
+        return f'{_get_source_cache_key(version)}-{request.organizer.cache.get_or_set("css_version", default=lambda: int(time.time()))}'
 
 
-def widget_js_etag(request, lang, **kwargs):
+def widget_js_etag(request, version, lang, **kwargs):
     gs = GlobalSettingsObject()
-    return gs.settings.get('widget_checksum_{}'.format(lang))
+    return gs.settings.get('widget_checksum_{}_{}'.format(version, lang))
 
 
 @gzip_page
 @condition(etag_func=widget_css_etag)
 @cache_page(60)
-def widget_css(request, **kwargs):
+def widget_css(request, version, **kwargs):
+    if version > version_max:
+        raise Http404()
+    if version < version_min:
+        return redirect(reverse('presale:event.widget.css' if hasattr(request, 'event') else 'organizer.widget.css', kwargs={
+            'version': version_min,
+            'organizer': request.organizer.slug,
+            'event': request.event.slug if hasattr(request, 'event') else None,
+        }))
     o = getattr(request, 'event', request.organizer)
 
-    tpl = get_template('pretixpresale/widget_dummy.html')
-    et = html.fromstring(tpl.render({})).xpath('/html/head/link')[0].attrib['href'].replace(settings.STATIC_URL, '')
+    template_path = 'pretixpresale/widget_dummy.html' if version == version_max else 'pretixpresale/widget_dummy.v{}.html'.format(version)
+
+    tpl = get_template(template_path)
+    et = html.fromstring(tpl.render()).xpath('/html/head/link')[0].attrib['href'].replace(settings.STATIC_URL, '')
     with open(finders.find(et), 'r') as f:
         widget_css = f.read()
 
@@ -134,7 +153,7 @@ def widget_css(request, **kwargs):
     return resp
 
 
-def generate_widget_js(lang):
+def generate_widget_js(version, lang):
     code = []
     with language(lang):
         # Provide isolation
@@ -169,7 +188,7 @@ def generate_widget_js(lang):
             'vuejs/vue.js' if settings.DEBUG else 'vuejs/vue.min.js',
             'pretixpresale/js/widget/docready.js',
             'pretixpresale/js/widget/floatformat.js',
-            'pretixpresale/js/widget/widget.js',
+            'pretixpresale/js/widget/widget.js' if version == version_max else 'pretixpresale/js/widget/widget.v{}.js'.format(version),
         ]
         for fname in files:
             f = finders.find(fname)
@@ -188,11 +207,17 @@ def generate_widget_js(lang):
 
 @gzip_page
 @condition(etag_func=widget_js_etag)
-def widget_js(request, lang, **kwargs):
-    if lang not in [lc for lc, ll in settings.LANGUAGES]:
+def widget_js(request, version, lang, **kwargs):
+    if version > version_max or lang not in [lc for lc, ll in settings.LANGUAGES]:
         raise Http404()
 
-    cached_js = cache.get('widget_js_data_{}'.format(lang))
+    if version < version_min:
+        return redirect(reverse('presale:widget.js', kwargs={
+            'version': version_min,
+            'lang': lang,
+        }))
+
+    cached_js = cache.get('widget_js_data_{}_{}'.format(version, lang))
     if cached_js and not settings.DEBUG:
         resp = HttpResponse(cached_js, content_type='text/javascript')
         resp._csp_ignore = True
@@ -200,7 +225,7 @@ def widget_js(request, lang, **kwargs):
         return resp
 
     gs = GlobalSettingsObject()
-    fname = gs.settings.get('widget_file_{}'.format(lang))
+    fname = gs.settings.get('widget_file_{}_{}'.format(version, lang))
     resp = None
     if fname and not settings.DEBUG:
         if isinstance(fname, File):
@@ -208,21 +233,21 @@ def widget_js(request, lang, **kwargs):
         try:
             data = default_storage.open(fname).read()
             resp = HttpResponse(data, content_type='text/javascript')
-            cache.set('widget_js_data_{}'.format(lang), data, 3600 * 4)
+            cache.set('widget_js_data_{}_{}'.format(version, lang), data, 3600 * 4)
         except:
             logger.exception('Failed to open widget.js')
 
     if not resp:
-        data = generate_widget_js(lang).encode()
+        data = generate_widget_js(version, lang).encode()
         checksum = hashlib.sha1(data).hexdigest()
         if not settings.DEBUG:
             newname = default_storage.save(
-                'widget/widget.{}.{}.js'.format(lang, checksum),
+                'widget/widget.{}.{}.{}.js'.format(version, lang, checksum),
                 ContentFile(data)
             )
-            gs.settings.set('widget_file_{}'.format(lang), 'file://' + newname)
-            gs.settings.set('widget_checksum_{}'.format(lang), checksum)
-            cache.set('widget_js_data_{}'.format(lang), data, 3600 * 4)
+            gs.settings.set('widget_file_{}_{}'.format(version, lang), 'file://' + newname)
+            gs.settings.set('widget_checksum_{}_{}'.format(version, lang), checksum)
+            cache.set('widget_js_data_{}_{}'.format(version, lang), data, 3600 * 4)
         resp = HttpResponse(data, content_type='text/javascript')
     resp._csp_ignore = True
     resp['Access-Control-Allow-Origin'] = '*'
diff --git a/src/pretix/static/pretixbase/scss/_bootstrap_vars.scss b/src/pretix/static/pretixbase/scss/_bootstrap_vars.scss
index b944766cd..9d6648d8a 100644
--- a/src/pretix/static/pretixbase/scss/_bootstrap_vars.scss
+++ b/src/pretix/static/pretixbase/scss/_bootstrap_vars.scss
@@ -14,6 +14,7 @@ $input-color-placeholder: lighten(#000, 70%) !default;
 $border-radius-base: var(--pretix-border-radius-base);
 $border-radius-large: var(--pretix-border-radius-large);
 $border-radius-small: var(--pretix-border-radius-small);
+$input-border: #949494 !default;
 
 $navbar-inverse-bg: #3b1c4a !default;
 $navbar-inverse-link-color: white;
@@ -72,7 +73,7 @@ $panel-default-heading-bg: #e5e5e5 !default;
 
 $link-hover-color: var(--pretix-brand-primary-darken-15);
 
-$btn-default-border: #CCCCCC;
+$btn-default-border: #949494;
 
 $btn-primary-border: var(--pretix-brand-primary-darken-5);
 $btn-primary-border-active: var(--pretix-brand-primary-darken-30);
@@ -122,6 +123,12 @@ $label-warning-bg-hover: var(--pretix-brand-warning-darken-10);
 $label-danger-bg: var(--pretix-brand-danger);
 $label-danger-bg-hover: var(--pretix-brand-danger-darken-10);
 
+$alert-primary-bg: var(--pretix-brand-primary-tint-90);
+$alert-primary-text: var(--pretix-brand-primary-shade-42);
+$alert-primary-border: var(--pretix-brand-primary);
+$alert-primary-hr: var(--pretix-brand-primary-darken-5);
+$alert-primary-link: var(--pretix-brand-primary-shade-42);
+
 $alert-success-bg: var(--pretix-brand-success-tint-85);
 $alert-success-text: var(--pretix-brand-success-shade-25);
 $alert-success-border: var(--pretix-brand-success);
diff --git a/src/pretix/static/pretixpresale/js/widget/widget.js b/src/pretix/static/pretixpresale/js/widget/widget.js
index 260606e94..db9eb5596 100644
--- a/src/pretix/static/pretixpresale/js/widget/widget.js
+++ b/src/pretix/static/pretixpresale/js/widget/widget.js
@@ -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: ('
' + '' + '
' @@ -227,24 +231,19 @@ Vue.component('availbox', { + '' + strings.waiting_list + '' + '
' + '
' - + '' - + '' - + '
' - + '' - + '' + + '' + + '' - + '' + + '' + '
' + '
' + '
'), @@ -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', { + '
' + '' + '
' - + '' - + '{{ item.name }}' - + '' - + '{{ item.name }}' + + '{{ item.name }}' + '
' + '

' + '{{ min_order_str }}' @@ -566,8 +534,8 @@ Vue.component('item', { // Availability + '

' - + '{{ variationsToggleLabel }}' + + '' + '' + '
' @@ -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 = ( - '
' + '' + '
' - + '' + + '' + + '

'+strings.cancel_blocked+'

' + '
' + '
' - + '' - + '' - + '
' + + '
' + + '' + '
' + + '' ); var shared_alert_fragment = ( - '
' - + '' - + '
' + '' + + '
' + '

{{ $root.error_message }}

' - + '

' - + '

' - + '
' + + '

' + + '

' + + '' + + '' + + '' + '' - + '' - + '
' + + '' ); var shared_lightbox_fragment = ( - '
' + '' + '
' + '' + '
' - + '
' + + '
' + + '
' + '
' + '' + '
{{$root.lightbox.description}}
' + '
' - + '' + '
' - + '
' + + '
' ); Vue.component('pretix-overlay', { @@ -911,6 +880,11 @@ Vue.component('pretix-overlay', { + shared_lightbox_fragment + '
' ), + 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', { + '
{{ $root.error }}
' // Resume cart - + '
' + + '
' + + '' + strings['cart_exists'] + '' + '' - + '' + strings['cart_exists'] + '' - + '
' + '
' // Seating plan @@ -1067,7 +1058,10 @@ Vue.component('pretix-widget-event-form', { // Buy button + '
' - + '' + + '' + + '' + '
' + '' @@ -1079,7 +1073,7 @@ Vue.component('pretix-widget-event-form', { + '

'+ strings['redeem_voucher'] +'

' + '
' + '
' - + '' + + '' + '
' + '' + '
' @@ -1091,14 +1085,32 @@ Vue.component('pretix-widget-event-form', { + '
' ), + 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: ('
' + '' - + '' + '' + '' + '
'), 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: ('
' - + '' - + '
'), + template: ('
' + + '
' + + '' + strings.filter_events_by + '' + + '' + + '' + + '
' + + '
'), + 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, diff --git a/src/pretix/static/pretixpresale/js/widget/widget.v1.js b/src/pretix/static/pretixpresale/js/widget/widget.v1.js new file mode 100644 index 000000000..260606e94 --- /dev/null +++ b/src/pretix/static/pretixpresale/js/widget/widget.v1.js @@ -0,0 +1,2504 @@ +/*global siteglobals, module, lang, django*/ +/* PRETIX WIDGET BEGINS HERE */ +/* This is embedded in an isolation wrapper that exposes siteglobals as the global + scope. */ + +window.PretixWidget = { + 'build_widgets': true, + 'widget_data': { + 'referer': location.href + } +}; + +var Vue = module.exports; + +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'), + 'original_price': django.pgettext('widget', 'Original price: %s'), + 'new_price': django.pgettext('widget', 'New price: %s'), + 'select': django.pgettext('widget', 'Select'), + 'select_item': django.pgettext('widget', 'Select %s'), + 'select_variant': django.pgettext('widget', 'Select variant %s'), + 'sold_out': django.pgettext('widget', 'Sold out'), + 'buy': django.pgettext('widget', 'Buy'), + 'register': django.pgettext('widget', 'Register'), + 'reserved': django.pgettext('widget', 'Reserved'), + 'free': django.pgettext('widget', 'FREE'), + 'price_from': django.pgettext('widget', 'from %(currency)s %(price)s'), + 'image_of': django.pgettext('widget', 'Image of %s'), + 'tax_incl': django.pgettext('widget', 'incl. %(rate)s% %(taxname)s'), + 'tax_plus': django.pgettext('widget', 'plus %(rate)s% %(taxname)s'), + 'tax_incl_mixed': django.pgettext('widget', 'incl. taxes'), + 'tax_plus_mixed': django.pgettext('widget', 'plus taxes'), + 'quota_left': django.pgettext('widget', 'currently available: %s'), + 'unavailable_require_voucher': django.pgettext('widget', 'Only available with a voucher'), + 'unavailable_available_from': django.pgettext('widget', 'Not yet available'), + 'unavailable_available_until': django.pgettext('widget', 'Not available anymore'), + 'unavailable_active': django.pgettext('widget', 'Currently not available'), + 'unavailable_hidden_if_item_available': django.pgettext('widget', 'Not yet available'), + 'order_min': django.pgettext('widget', 'minimum amount to order: %s'), + 'exit': django.pgettext('widget', 'Close ticket shop'), + 'loading_error': django.pgettext('widget', 'The ticket shop could not be loaded.'), + 'loading_error_429': django.pgettext('widget', 'There are currently a lot of users in this ticket shop. Please ' + + 'open the shop in a new tab to continue.'), + 'open_new_tab': django.pgettext('widget', 'Open ticket shop'), + 'checkout': django.pgettext('widget', 'Checkout'), + 'cart_error': django.pgettext('widget', 'The cart could not be created. Please try again later'), + 'cart_error_429': django.pgettext('widget', 'We could not create your cart, since there are currently too many ' + + 'users in this ticket shop. Please click "Continue" to retry in a new tab.'), + 'waiting_list': django.pgettext('widget', 'Waiting list'), + 'cart_exists': django.pgettext('widget', 'You currently have an active cart for this event. If you select more' + + ' products, they will be added to your existing cart.'), + 'resume_checkout': django.pgettext('widget', 'Resume checkout'), + 'redeem_voucher': django.pgettext('widget', 'Redeem a voucher'), + 'redeem': django.pgettext('widget', 'Redeem'), + 'voucher_code': django.pgettext('widget', 'Voucher code'), + 'close': django.pgettext('widget', 'Close'), + 'continue': django.pgettext('widget', 'Continue'), + 'variations': django.pgettext('widget', 'Show variants'), + 'hide_variations': django.pgettext('widget', 'Hide variants'), + 'back_to_list': django.pgettext('widget', 'Choose a different event'), + 'back_to_dates': django.pgettext('widget', 'Choose a different date'), + 'back': django.pgettext('widget', 'Back'), + 'next_month': django.pgettext('widget', 'Next month'), + 'previous_month': django.pgettext('widget', 'Previous month'), + 'next_week': django.pgettext('widget', 'Next week'), + 'previous_week': django.pgettext('widget', 'Previous week'), + 'show_seating': django.pgettext('widget', 'Open seat selection'), + 'seating_plan_waiting_list': django.pgettext('widget', 'Some or all ticket categories are currently sold out. If you want, you can add yourself to the waiting list. We will then notify if seats are available again.'), + 'load_more': django.pgettext('widget', 'Load more'), + 'days': { + 'MO': django.gettext('Mo'), + 'TU': django.gettext('Tu'), + 'WE': django.gettext('We'), + 'TH': django.gettext('Th'), + 'FR': django.gettext('Fr'), + 'SA': django.gettext('Sa'), + 'SU': django.gettext('Su'), + 'MONDAY': django.gettext('Monday'), + 'TUESDAY': django.gettext('Tuesday'), + 'WEDNESDAY': django.gettext('Wednesday'), + 'THURSDAY': django.gettext('Thursday'), + 'FRIDAY': django.gettext('Friday'), + 'SATURDAY': django.gettext('Saturday'), + 'SUNDAY': django.gettext('Sunday'), + }, + 'months': { + '01': django.gettext('January'), + '02': django.gettext('February'), + '03': django.gettext('March'), + '04': django.gettext('April'), + '05': django.gettext('May'), + '06': django.gettext('June'), + '07': django.gettext('July'), + '08': django.gettext('August'), + '09': django.gettext('September'), + '10': django.gettext('October'), + '11': django.gettext('November'), + '12': django.gettext('December'), + } +}; + +var setCookie = function (cname, cvalue, exdays) { + var d = new Date(); + d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); + var expires = "expires=" + d.toUTCString(); + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; +}; +var getCookie = function (name) { + var value = "; " + document.cookie; + var parts = value.split("; " + name + "="); + if (parts.length == 2) return parts.pop().split(";").shift() || null; + else return null; +}; + +var padNumber = function(number, size) { + var s = String(number); + while (s.length < (size || 2)) {s = "0" + s;} + return s; +}; + +var getISOWeeks = function (y) { + var d, isLeap; + + d = new Date(y, 0, 1); + isLeap = new Date(y, 1, 29).getMonth() === 1; + + //check for a Jan 1 that's a Thursday or a leap year that has a + //Wednesday jan 1. Otherwise it's 52 + return d.getDay() === 4 || isLeap && d.getDay() === 3 ? 53 : 52 +}; + +/* HTTP API Call helpers */ +var api = { + '_getJSON': function (endpoint, callback, err_callback) { + var xhr = new window.XMLHttpRequest(); + xhr.open("GET", endpoint, true); + xhr.onload = function (e) { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + callback(JSON.parse(xhr.responseText), xhr); + } else { + err_callback(xhr, e); + } + } + }; + xhr.onerror = function (e) { + console.error(xhr.statusText); + err_callback(xhr, e); + }; + xhr.send(null); + }, + + '_postFormJSON': function (endpoint, form, callback, err_callback) { + var params = [].filter.call(form.elements, function (el) { + return (el.type !== 'checkbox' && el.type !== 'radio') || el.checked; + }) + .filter(function (el) { + return !!el.name && !!el.value; + }) + .filter(function (el) { + return !el.disabled; + }) + .map(function (el) { + return encodeURIComponent(el.name) + '=' + encodeURIComponent(el.value); + }).join('&'); + + var xhr = new window.XMLHttpRequest(); + xhr.open("POST", endpoint, true); + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + xhr.onload = function (e) { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + callback(JSON.parse(xhr.responseText)); + } else { + err_callback(xhr, e); + } + } + }; + xhr.onerror = function (e) { + err_callback(xhr, e); + }; + xhr.send(params); + } +}; + +var makeid = function (length) { + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for (var i = 0; i < length; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + + return text; +}; + +var site_is_secure = function () { + return /https.*/.test(document.location.protocol) +}; + +var widget_id = makeid(16); + +/* Vue Components */ +Vue.component('availbox', { + template: ('
' + + '' + + '
' + + '{{unavailability_reason_message}}' + + '
' + + '
' + + strings.reserved + + '
' + + '
' + + strings.sold_out + + '
' + + '' + + '
' + + '' + + '' + + '
' + + '' + + '' + + '' + + '
' + + '
' + + '
'), + props: { + item: Object, + 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); + } + this.$root.$emit('amounts_changed') + }, + computed: { + aria_labelledby: function () { + return this.$root.html_id + '-item-label-' + this.item.id; + }, + dec_label: function () { + return '- ' + (this.item.has_variations ? this.variation.value : this.item.name) + ': ' + strings.quantity_dec; + }, + 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) { + return strings["unavailable_" + reason] || reason; + } + 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) + : strings.select_item.replace("%s", this.item.name) + }, + input_name: function () { + if (this.item.has_variations) { + return 'variation_' + this.item.id + '_' + this.variation.id; + } else { + return 'item_' + this.item.id; + } + }, + order_max: function () { + return this.item.has_variations ? this.variation.order_max : this.item.order_max; + }, + avail: function () { + return this.item.has_variations ? this.variation.avail : this.item.avail; + }, + waiting_list_show: function () { + return this.avail[0] < 100 && this.$root.waiting_list_enabled && this.item.allow_waitinglist; + }, + waiting_list_url: function () { + var u + if (this.item.has_variations) { + u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&var=' + this.variation.id + '&widget_data=' + encodeURIComponent(this.$root.widget_data_json) + this.$root.consent_parameter; + } else { + u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&widget_data=' + encodeURIComponent(this.$root.widget_data_json) + this.$root.consent_parameter; + } + if (this.$root.subevent) { + u += '&subevent=' + this.$root.subevent + } + return u + } + }, + 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)); + } + } +}); +Vue.component('pricebox', { + template: ('
' + + '' + + '' + + ' ' + + '' + + '
' + + '{{ $root.currency }} ' + + '' + + '
' + + '' + + '{{ taxline }}' + + '' + + '
'), + props: { + price: Object, + free_price: Boolean, + field_name: String, + suggested_price: Object, + original_price: String, + mandatory_priced_addons: Boolean, + item_id: Number, + }, + methods: { + stripHTML: function (s) { + var div = document.createElement('div'); + div.innerHTML = s; + return div.textContent || div.innerText || ''; + }, + }, + computed: { + aria_labelledby: function () { + return [ + this.$root.html_id + '-item-label-' + this.item_id, + this.price_box_id + ].join(" "); + }, + price_box_id: function () { + return this.$root.html_id + '-item-pricebox-' + this.item_id; + }, + price_desc_id: function () { + return this.$root.html_id + '-item-pricedesc-' + this.item_id; + }, + display_price: function () { + if (this.$root.display_net_prices) { + return floatformat(parseFloat(this.price.net), 2); + } else { + return floatformat(parseFloat(this.price.gross), 2); + } + }, + display_price_nonlocalized: function () { + if (this.$root.display_net_prices) { + return parseFloat(this.price.net).toFixed(2); + } else { + return parseFloat(this.price.gross).toFixed(2); + } + }, + suggested_price_nonlocalized: function () { + var price = this.suggested_price; + if (price === null) { + price = this.price; + } + if (this.$root.display_net_prices) { + return parseFloat(price.net).toFixed(2); + } else { + return parseFloat(price.gross).toFixed(2); + } + }, + original_price_aria_label: function () { + return django.interpolate(strings.original_price, [this.stripHTML(this.original_line)]); + }, + new_price_aria_label: function () { + return django.interpolate(strings.new_price, [this.stripHTML(this.priceline)]); + }, + original_line: function () { + return '' + this.$root.currency + " " + floatformat(parseFloat(this.original_price), 2); + }, + priceline: function () { + if (this.price.gross === "0.00") { + if (this.mandatory_priced_addons && !this.original_price) { + return "\xA0"; // nbsp, because an empty string would cause the HTML element to collapse + } + return strings.free; + } else { + return '' + this.$root.currency + " " + this.display_price; + } + }, + taxline: function () { + if (this.$root.display_net_prices) { + if (this.price.includes_mixed_tax_rate) { + return strings.tax_plus_mixed; + } else { + return django.interpolate(strings.tax_plus, { + 'rate': autofloatformat(this.price.rate, 2), + 'taxname': this.price.name + }, true); + } + } else { + if (this.price.includes_mixed_tax_rate) { + return strings.tax_incl_mixed; + } else { + return django.interpolate(strings.tax_incl, { + 'rate': autofloatformat(this.price.rate, 2), + 'taxname': this.price.name + }, true); + } + } + } + } +}); +Vue.component('variation', { + template: ('
' + + '
' + + // Variation description + + '
' + + '
' + + '{{ variation.value }}' + + '
' + + '

' + + '{{ quota_left_str }}' + + '

' + + '
' + + '
' + + // Price + + '
' + + '' + + '' + + ' ' + + '
' + + // Availability + + '
' + + '' + + '
' + + + '
' + + '
' + + '
'), + props: { + variation: Object, + item: Object, + category: Object, + }, + computed: { + orig_price: function () { + if (this.variation.original_price) { + return this.variation.original_price; + } + return this.item.original_price; + }, + quota_left_str: function () { + return django.interpolate(strings["quota_left"], [this.variation.avail[1]]); + }, + variation_label_id: function () { + return this.$root.html_id + '-variation-label-' + this.item.id + '-' + this.variation.id; + }, + variation_desc_id: function () { + return this.$root.html_id + '-variation-desc-' + this.item.id + '-' + this.variation.id; + }, + variation_price_id: function () { + return this.$root.html_id + '-variation-price-' + this.item.id + '-' + this.variation.id; + }, + aria_labelledby: function () { + return [this.variation_label_id, this.variation_price_id].join(" "); + }, + headingLevel: function () { + return this.category.name ? '5' : '4'; + }, + } +}); +Vue.component('item', { + template: ('
' + + '
' + + // Product description + + '
' + + '' + + '
' + + '' + + '{{ item.name }}' + + '' + + '{{ item.name }}' + + '
' + + '

' + + '{{ min_order_str }}' + + '

' + + '

' + + '{{ quota_left_str }}' + + '

' + + '
' + + '
' + + // Price + + '
' + + '' + + '' + + '
' + + ' ' + + '
' + + // Availability + + '
' + + '{{ variationsToggleLabel }}' + + '' + + '
' + + + '
' + + '
' + + // Variations + + '
' + + '' + + '' + + '
' + + + '
'), + props: { + item: Object, + category: Object, + }, + data: function () { + return { + expanded: this.$root.show_variations_expanded + }; + }, + mounted: function () { + if (this.$refs.variations) { + if (!this.expanded) { + var $this = this; + this.$refs.variations.hidden = true; + this.$refs.variations.addEventListener('transitionend', function (event) { + if (event.target == this) { + this.hidden = !$this.expanded; + this.style.maxHeight = 'none'; + } + }); + this.$watch('expanded', function (newValue) { + var v = this.$refs.variations; + v.hidden = false; + v.style.maxHeight = (newValue ? 0 : v.scrollHeight) + 'px'; + // Vue.nextTick does not work here + window.setTimeout(function () { + v.style.maxHeight = (!newValue ? 0 : v.scrollHeight) + 'px'; + }, 50); + }) + } + } + }, + methods: { + expand: function () { + this.expanded = !this.expanded; + }, + lightbox: function () { + this.$root.overlay.lightbox = { + image: this.item.picture_fullsize, + description: this.item.name, + } + } + }, + computed: { + classObject: function () { + return { + 'pretix-widget-item': true, + 'pretix-widget-item-with-picture': !!this.item.picture, + 'pretix-widget-item-with-variations': this.item.has_variations + } + }, + varClasses: function () { + return { + 'pretix-widget-item-variations': true, + 'pretix-widget-item-variations-expanded': this.expanded, + } + }, + picture_alt_text: function () { + return django.interpolate(strings["image_of"], [this.item.name]); + }, + headingLevel: function () { + return this.category.name ? '4' : '3'; + }, + item_label_id: function () { + return this.$root.html_id + '-item-label-' + this.item.id; + }, + item_desc_id: function () { + return this.$root.html_id + '-item-desc-' + this.item.id; + }, + item_price_id: function () { + return this.$root.html_id + '-item-price-' + this.item.id; + }, + aria_labelledby: function () { + return [this.item_label_id, this.item_price_id].join(" "); + }, + min_order_str: function () { + return django.interpolate(strings["order_min"], [this.item.order_min]); + }, + quota_left_str: function () { + return django.interpolate(strings["quota_left"], [this.item.avail[1]]); + }, + show_toggle: function () { + return this.item.has_variations && !this.$root.show_variations_expanded; + }, + pricerange: function () { + if (this.item.free_price) { + return django.interpolate(strings.price_from, { + 'currency': this.$root.currency, + 'price': floatformat(this.item.min_price, 2) + }, true).replace(this.$root.currency, '' + this.$root.currency + ''); + } else if (this.item.min_price !== this.item.max_price) { + return '' + this.$root.currency + " " + + floatformat(this.item.min_price, 2) + " – " + + floatformat(this.item.max_price, 2); + } else if (this.item.min_price === "0.00" && this.item.max_price === "0.00") { + if (this.item.mandatory_priced_addons) { + return "\xA0"; // nbsp, because an empty string would cause the HTML element to collapse + } + return strings.free; + } else { + return '' + this.$root.currency + " " + floatformat(this.item.min_price, 2); + } + }, + variationsToggleLabel: function () { + return this.expanded ? strings.hide_variations : strings.variations; + }, + } +}); +Vue.component('category', { + template: ('
' + + '

{{ category.name }}

' + + '
' + + '
' + + '
' + + '' + + '
' + + '
'), + props: { + category: Object + } +}); + +var shared_methods = { + buy: function (event) { + if (this.$root.useIframe) { + if (event) { + event.preventDefault(); + } + } else { + return; + } + if (this.$root.is_button && this.$root.items.length === 0) { + if (this.$root.voucher_code) { + this.voucher_open(this.$root.voucher_code); + } else { + this.resume(); + } + } else { + var url = this.$root.formAction + "&locale=" + lang + "&ajax=1"; + this.$root.overlay.frame_loading = true; + + this.async_task_interval = 100; + var form = this.$refs.form; + if (form === undefined) { + form = this.$refs.formcomp.$refs.form; + } + api._postFormJSON(url, form, this.buy_callback, this.buy_error_callback); + } + }, + buy_error_callback: function (xhr, data) { + if (xhr.status === 429 && typeof xhr.responseURL !== "undefined") { + this.$root.overlay.error_message = strings['cart_error_429']; + this.$root.overlay.frame_loading = false; + this.$root.overlay.error_url_after = this.$root.newTabTarget; + this.$root.overlay.error_url_after_new_tab = true; + return; + } + if (xhr.status === 405 && typeof xhr.responseURL !== "undefined") { + // Likely a redirect! + this.$root.target_url = xhr.responseURL.substr(0, xhr.responseURL.indexOf("/cart/add") - 18); + this.$root.overlay.frame_loading = false; + this.buy(); + return; + } + this.$root.overlay.error_message = strings['cart_error']; + this.$root.overlay.frame_loading = false; + }, + buy_check_error_callback: function (xhr, data) { + if (xhr.status == 200 || (xhr.status >= 400 && xhr.status < 500)) { + this.$root.overlay.error_message = strings['cart_error']; + this.$root.overlay.frame_loading = false; + } else { + this.async_task_timeout = window.setTimeout(this.buy_check, 1000); + } + }, + buy_callback: function (data) { + if (data.redirect) { + if (data.cart_id) { + this.$root.cart_id = data.cart_id; + setCookie(this.$root.cookieName, data.cart_id, 30); + } + if (data.redirect.substr(0, 1) === '/') { + data.redirect = this.$root.target_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.redirect; + } + var url = data.redirect; + if (url.indexOf('?')) { + url = url + '&iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id; + } else { + url = url + '?iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id; + } + url += this.$root.consent_parameter; + if (this.$root.additionalURLParams) { + url += '&' + this.$root.additionalURLParams; + } + if (data.success === false) { + url = url.replace(/checkout\/start/g, ""); + this.$root.overlay.error_message = data.message; + if (data.has_cart) { + this.$root.overlay.error_url_after = url; + } + this.$root.overlay.frame_loading = false; + } else { + this.$root.overlay.frame_src = url; + } + } else { + this.async_task_id = data.async_id; + if (data.check_url) { + this.async_task_check_url = this.$root.target_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.check_url; + } + this.async_task_timeout = window.setTimeout(this.buy_check, this.async_task_interval); + this.async_task_interval = 250; + } + }, + buy_check: function () { + api._getJSON(this.async_task_check_url, this.buy_callback, this.buy_check_error_callback); + }, + redeem: function (event) { + if (this.$root.useIframe) { + event.preventDefault(); + this.voucher_open(this.voucher); + } + }, + voucher_open: function (voucher) { + var redirect_url = this.$root.voucherFormTarget + '&voucher=' + encodeURIComponent(voucher); + if (this.$root.useIframe) { + this.$root.overlay.frame_src = redirect_url; + } else { + window.open(redirect_url); + } + }, + resume: function () { + var redirect_url; + redirect_url = this.$root.target_url + 'w/' + widget_id + '/'; + if (this.$root.subevent && !this.$root.cart_id) { + // button with subevent but no items + redirect_url += this.$root.subevent + '/'; + } + redirect_url += '?iframe=1&locale=' + lang; + if (this.$root.cart_id) { + redirect_url += '&take_cart_id=' + this.$root.cart_id; + } + if (this.$root.widget_data) { + redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json); + } + redirect_url += this.$root.consent_parameter; + if (this.$root.additionalURLParams) { + redirect_url += '&' + this.$root.additionalURLParams; + } + if (this.$root.useIframe) { + this.$root.overlay.frame_src = redirect_url; + } else { + window.open(redirect_url); + } + }, +}; + +var shared_widget_data = function () { + return { + async_task_id: null, + async_task_check_url: null, + async_task_timeout: null, + async_task_interval: 100, + voucher: null, + mobile: false, + } +}; + +var shared_loading_fragment = ( + '
' + + '' + + '
' +); + +var shared_iframe_fragment = ( + '
' + + '
' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' +); + +var shared_alert_fragment = ( + '
' + + '' + + '
' + + '

{{ $root.error_message }}

' + + '

' + + '

' + + '
' + + '
' + + '' + + '
' +); + +var shared_lightbox_fragment = ( + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '' + + '
{{$root.lightbox.description}}
' + + '
' + + '' + + '
' + + '
' +); + +Vue.component('pretix-overlay', { + template: ('
' + + shared_iframe_fragment + + shared_alert_fragment + + shared_lightbox_fragment + + '
' + ), + watch: { + '$root.lightbox': function (newValue, oldValue) { + if (newValue) { + if (newValue.image != oldValue?.image) { + this.$set(newValue, "loading", true); + } + if (!oldValue) { + window.addEventListener('keyup', this.lightboxCloseOnKeyup); + } + } else { + window.removeEventListener('keyup', this.lightboxCloseOnKeyup); + } + } + }, + computed: { + frameClasses: function () { + return { + 'pretix-widget-frame-holder': true, + 'pretix-widget-frame-shown': this.$root.frame_shown || this.$root.frame_loading, + }; + }, + alertClasses: function () { + return { + 'pretix-widget-alert-holder': true, + 'pretix-widget-alert-shown': this.$root.error_message, + }; + }, + lightboxClasses: function () { + return { + 'pretix-widget-lightbox-holder': true, + 'pretix-widget-lightbox-shown': this.$root.lightbox, + 'pretix-widget-lightbox-isloading': this.$root.lightbox?.loading, + }; + }, + }, + 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 () { + 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); + 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(); + }, + iframeLoaded: function () { + if (this.$root.frame_loading) { + this.$root.frame_loading = false; + if (this.$root.frame_src) { + this.$root.frame_shown = true; + } + } + }, + focusButton: function () { + this.$el.querySelector(".pretix-widget-alert-box button").focus(); + }, + + } +}); + +Vue.component('pretix-widget-event-form', { + template: ('
' + // Back navigation + + '' + + // Event name + + '
' + + '{{ $root.name }}' + + '
' + + // Date range + + '
' + + '{{ $root.date_range }}' + + '
' + + // Location + + '
' + + // Form start + + '
' + + '
' + + '' + + '' + + '' + + '' + + // Error message + + '
{{ $root.error }}
' + + // Resume cart + + '
' + + '' + + '' + strings['cart_exists'] + '' + + '
' + + '
' + + // Seating plan + + '' + + // Waiting list for seating plan + + '
' + + '
' + + strings['seating_plan_waiting_list'] + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + // Actual product list + + '' + + // Buy button + + '
' + + '' + + '
' + + + '
' + + // Voucher form + + '
' + + '
' + + '

'+ strings['redeem_voucher'] +'

' + + '
' + + '
' + + '' + + '
' + + '' + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + + '
' + ), + mounted: function() { + this.$root.$on('focus_voucher_field', this.focus_voucher_field) + }, + beforeDestroy: function() { + this.$root.$off('focus_voucher_field', this.focus_voucher_field) + }, + computed: { + aria_labelledby: function() { + return this.$root.html_id + '-voucher-headline'; + }, + display_event_info: function () { + return this.$root.display_event_info || (this.$root.display_event_info === null && (this.$root.events || this.$root.weeks || this.$root.days)); + }, + id_cart_exists_msg: function () { + return this.$root.html_id + '-cart-exists'; + }, + buy_label: function () { + var i, j, k, all_free = true; + for (i = 0; i < this.$root.categories.length; i++) { + var cat = this.$root.categories[i]; + for (j = 0; j < cat.items.length; j++) { + var item = cat.items[j]; + for (k = 0; k < item.variations.length; k++) { + var v = item.variations[k]; + if (v.price.gross !== "0.00") { + all_free = false; + break; + } + } + if ((item.variations.length === 0 && item.price.gross !== "0.00") || item.mandatory_priced_addons) { + all_free = false; + break; + } + } + if (!all_free) { + break; + } + } + if (all_free) { + return strings.register; + } else { + return strings.buy; + } + }, + hiddenParams: function () { + var params = new URL(this.$root.voucherFormTarget).searchParams; + params.delete("iframe"); + params.delete("take_cart_id"); + return [...params.entries()]; + }, + }, + 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; + if (!this.$root.subevent) { + // reset if we are not in a series + this.$root.name = null; + this.$root.frontpage_text = null; + } + this.$root.subevent = null; + this.$root.offset = 0; + this.$root.append_events = false; + this.$root.trigger_load_callback(); + if (this.$root.events !== undefined && this.$root.events !== null) { + this.$root.view = "events"; + } else if (this.$root.days !== undefined && this.$root.days !== null) { + this.$root.view = "days"; + } else { + this.$root.view = "weeks"; + } + + var $el = this.$root.$el; + this.$root.$nextTick(function() { + // wait for redraw, then focus content element for better a11y + $el.focus(); + }); + }, + } +}); + +Vue.component('pretix-widget-event-list-filter-field', { + template: ('
' + + '' + + '' + + '
'), + 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; + }, + currentValue: function () { + var filterParams = new URLSearchParams(this.$root.filter); + return filterParams.get(this.field.key) || ""; + }, + }, +}); + +Vue.component('pretix-widget-event-list-filter-form', { + template: ('
' + + '' + + '
'), +}); + +Vue.component('pretix-widget-event-list-entry', { + template: ('' + + '
{{ event.name }}
' + + '' + + '
{{ location }}
' // hidden by css for now, but + // used by a few people + + '
{{ event.availability.text }}
' + + '
'), + props: { + event: Object + }, + computed: { + classObject: function () { + var o = { + 'pretix-widget-event-list-entry': true + }; + o['pretix-widget-event-availability-' + this.event.availability.color] = true; + if (this.event.availability.reason) { + o['pretix-widget-event-availability-' + this.event.availability.reason] = true; + } + return o + }, + location: function () { + return this.event.location.replace(/\s*\n\s*/g, ', '); + } + }, + methods: { + select: function () { + this.$root.parent_stack.push(this.$root.target_url); + this.$root.target_url = this.event.event_url; + this.$root.error = null; + this.$root.subevent = this.event.subevent; + this.$root.loading++; + this.$root.reload(); + } + } +}); + +Vue.component('pretix-widget-event-list', { + template: ('
' + + '' + + '
' + + '{{ $root.name }}' + + '
' + + '
' + + '' + + '' + + '

' + + '
'), + computed: { + display_event_info: function () { + return this.$root.display_event_info || (this.$root.display_event_info === null && this.$root.parent_stack.length > 0); + }, + }, + methods: { + back_to_calendar: function () { + // make sure to always focus content element + this.$nextTick(function () { + this.$root.$el.focus(); + }); + this.$root.offset = 0; + this.$root.append_events = false; + if (this.$root.weeks) { + this.$root.events = undefined; + this.$root.view = "weeks"; + this.$root.name = null; + this.$root.frontpage_text = null; + } else { + this.$root.loading++; + this.$root.target_url = this.$root.parent_stack.pop(); + this.$root.error = null; + this.$root.reload(); + } + }, + load_more: function () { + this.$root.append_events = true; + this.$root.offset += 50; + this.$root.loading++; + this.$root.reload(); + } + } +}); + +Vue.component('pretix-widget-event-calendar-event', { + template: ('' + + '' + + '{{ event.name }}' + + '' + + '
{{ event.time }}
' + + '
{{ event.availability.text }}
' + + '
'), + props: { + event: Object, + describedby: String, + }, + computed: { + classObject: function () { + var o = { + 'pretix-widget-event-calendar-event': true + }; + o['pretix-widget-event-availability-' + this.event.availability.color] = true; + if (this.event.availability.reason) { + o['pretix-widget-event-availability-' + this.event.availability.reason] = true; + } + return o + } + }, + methods: { + select: function () { + this.$root.parent_stack.push(this.$root.target_url); + this.$root.target_url = this.event.event_url; + this.$root.error = null; + this.$root.subevent = this.event.subevent; + this.$root.loading++; + this.$root.reload(); + } + } +}); + +Vue.component('pretix-widget-event-week-cell', { + template: ('
' + + '
' + + '{{ dayhead }}' + + '
' + + '
' + + '' + + '
' + + '
'), + props: { + day: Object, + }, + methods: { + selectDay: function () { + if (!this.day || !this.day.events.length || !this.$parent.$parent.$parent.mobile) { + return; + } + if (this.day.events.length === 1) { + var ev = this.day.events[0]; + this.$root.parent_stack.push(this.$root.target_url); + this.$root.target_url = ev.event_url; + this.$root.error = null; + this.$root.subevent = ev.subevent; + this.$root.loading++; + this.$root.reload(); + } else { + this.$root.events = this.day.events; + this.$root.view = "events"; + } + } + }, + computed: { + id: function () { + return this.day ? this.$root.html_id + '-' + this.day.date : ''; + }, + dayhead: function () { + if (!this.day) { + return; + } + return this.day.day_formatted; + }, + classObject: function () { + var o = {}; + if (this.day && this.day.events.length > 0) { + o['pretix-widget-has-events'] = true; + var best = 'red'; + var all_low = true; + for (var i = 0; i < this.day.events.length; i++) { + var ev = this.day.events[i]; + if (ev.availability.color === 'green') { + best = 'green'; + if (ev.availability.reason !== 'low') { + all_low = false; + } + } else if (ev.availability.color === 'orange' && best !== 'green') { + best = 'orange' + } + } + o['pretix-widget-day-availability-' + best] = true; + if (best === 'green' && all_low) { + o['pretix-widget-day-availability-low'] = true; + } + } + return o + } + } +}); + +Vue.component('pretix-widget-event-calendar-cell', { + template: ('' + + '
' + + '{{ daynum }}' + + '
' + + '
' + + '' + + '
' + + ''), + props: { + day: Object, + }, + methods: { + selectDay: function (e) { + if (!this.day || !this.day.events.length || !this.$parent.$parent.$parent.mobile) { + return; + } + e.preventDefault(); + e.stopPropagation(); + if (this.day.events.length === 1) { + var ev = this.day.events[0]; + this.$root.parent_stack.push(this.$root.target_url); + this.$root.target_url = ev.event_url; + this.$root.error = null; + this.$root.subevent = ev.subevent; + this.$root.loading++; + this.$root.reload(); + } else { + this.$root.events = this.day.events; + this.$root.view = "events"; + } + }, + onKeyDown: function (e) { + var keyDown = e.key !== undefined ? e.key : e.keyCode; + if ( (keyDown === 'Enter' || keyDown === 13) || (['Spacebar', ' '].indexOf(keyDown) >= 0 || keyDown === 32)) { + // (prevent default so the page doesn't scroll when pressing space) + e.preventDefault(); + this.selectDay(e); + } + }, + }, + mounted: function () { + if (this.role == 'button') { + this.$el.addEventListener("click", this.selectDay); + this.$el.addEventListener("keydown", this.onKeyDown); + } + }, + watch: { + role: function (newValue) { + if (newValue == 'button') { + this.$el.addEventListener("click", this.selectDay); + this.$el.addEventListener("keydown", this.onKeyDown); + } else { + this.$el.removeEventListener("click", this.selectDay); + this.$el.removeEventListener("keydown", this.onKeyDown); + } + } + }, + computed: { + role: function () { + return (!this.day || !this.day.events.length || !this.$parent.$parent.$parent.mobile) ? 'cell' : 'button'; + }, + tabindex: function () { + return this.role == 'button' ? '0' : '-1'; + }, + daynum: function () { + if (!this.day) { + return; + } + return this.day.date.substr(8); + }, + date: function () { + return this.day ? (new Date(this.day.date)).toLocaleDateString() : ''; + }, + classObject: function () { + var o = {}; + if (this.day && this.day.events.length > 0) { + o['pretix-widget-has-events'] = true; + var best = 'red'; + var all_low = true; + for (var i = 0; i < this.day.events.length; i++) { + var ev = this.day.events[i]; + if (ev.availability.color === 'green') { + best = 'green'; + if (ev.availability.reason !== 'low') { + all_low = false; + } + } else if (ev.availability.color === 'orange' && best !== 'green') { + best = 'orange' + } + } + o['pretix-widget-day-availability-' + best] = true; + if (best === 'green' && all_low) { + o['pretix-widget-day-availability-low'] = true; + } + } + return o + } + } +}); + +Vue.component('pretix-widget-event-calendar-row', { + template: ('' + + '' + + ''), + props: { + week: Array + }, +}); + +Vue.component('pretix-widget-event-calendar', { + template: ('
' + + // Back navigation + + '' + + // Headline + + '
' + + '{{ $root.name }}' + + '
' + + '
' + + // Filter + + '' + + // Calendar navigation + + '' + + // Calendar + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + strings['days']['MO'] + '' + strings['days']['TU'] + '' + strings['days']['WE'] + '' + strings['days']['TH'] + '' + strings['days']['FR'] + '' + strings['days']['SA'] + '' + strings['days']['SU'] + '
' + + '
'), + computed: { + display_event_info: function () { + return this.$root.display_event_info || (this.$root.display_event_info === null && this.$root.parent_stack.length > 0); + }, + monthname: function () { + return strings['months'][this.$root.date.substr(5, 2)] + ' ' + this.$root.date.substr(0, 4); + }, + id: function () { + return this.$root.html_id + "-event-calendar-table"; + }, + }, + methods: { + back_to_list: function () { + this.$root.weeks = undefined; + this.$root.view = "events"; + this.$root.name = null; + this.$root.frontpage_text = null; + }, + prevmonth: function () { + var curMonth = parseInt(this.$root.date.substr(5, 2)); + var curYear = parseInt(this.$root.date.substr(0, 4)); + curMonth--; + if (curMonth < 1) { + curMonth = 12; + curYear--; + } + this.$root.date = String(curYear) + "-" + padNumber(curMonth, 2) + "-01"; + this.$root.loading++; + this.$root.reload({focus: '#'+this.id}); + }, + nextmonth: function () { + var curMonth = parseInt(this.$root.date.substr(5, 2)); + var curYear = parseInt(this.$root.date.substr(0, 4)); + curMonth++; + if (curMonth > 12) { + curMonth = 1; + curYear++; + } + this.$root.date = String(curYear) + "-" + padNumber(curMonth, 2) + "-01"; + this.$root.loading++; + this.$root.reload({focus: '#'+this.id}); + } + }, +}); + +Vue.component('pretix-widget-event-week-calendar', { + template: ('
' + // Back navigation + + '' + + // Event header + + '
' + + '{{ $root.name }}' + + '
' + + // Filter + + '' + + // Calendar navigation + + '
' + + '' + + // Actual calendar + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + + '
' + + '
'), + computed: { + display_event_info: function () { + return this.$root.display_event_info || (this.$root.display_event_info === null && this.$root.parent_stack.length > 0); + }, + weekname: function () { + var curWeek = this.$root.week[1]; + var curYear = this.$root.week[0]; + return curWeek + ' / ' + curYear; + }, + id: function () { + return this.$root.html_id + "-event-week-table"; + }, + }, + methods: { + back_to_list: function () { + this.$root.weeks = undefined; + this.$root.name = null; + this.$root.frontpage_text = null; + this.$root.view = "events"; + }, + prevweek: function () { + var curWeek = this.$root.week[1]; + var curYear = this.$root.week[0]; + curWeek--; + if (curWeek < 1) { + curYear--; + curWeek = getISOWeeks(curYear); + } + this.$root.week = [curYear, curWeek]; + this.$root.loading++; + this.$root.reload({focus: '#'+this.id}); + }, + nextweek: function () { + var curWeek = this.$root.week[1]; + var curYear = this.$root.week[0]; + curWeek++; + if (curWeek > getISOWeeks(curYear)) { + curWeek = 1; + curYear++; + } + this.$root.week = [curYear, curWeek]; + this.$root.loading++; + this.$root.reload({focus: '#'+this.id}); + } + }, +}); + +Vue.component('pretix-widget', { + template: ('
' + + '
' + + shared_loading_fragment + + '
{{ $root.error }}
' + + '' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + ), + data: shared_widget_data, + methods: shared_methods, + mounted: function () { + var thisObj = this; + if ("ResizeObserver" in window) { + var resizeObserver = new ResizeObserver(function(entries) { + thisObj.mobile = entries[0].contentRect.width <= 800; + }); + resizeObserver.observe(this.$refs.wrapper); + } else { + this.mobile = this.$refs.wrapper.clientWidth <= 800; + var debounce; + window.addEventListener("resize", function() { + if (debounce) clearTimeout(debounce); + debounce = setTimeout(function () { + thisObj.mobile = thisObj.$refs.wrapper.clientWidth <= 800; + }, 100); + }); + } + }, + computed: { + classObject: function () { + return { + 'pretix-widget': true, + 'pretix-widget-mobile': this.mobile, + 'pretix-widget-use-custom-spinners': !this.$root.use_native_spinners + }; + } + } +}); + +Vue.component('pretix-button', { + template: ('
' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + ), + data: shared_widget_data, + methods: shared_methods, +}); + +/* Function to create the actual Vue instances */ + +var shared_root_methods = { + open_link_in_frame: function (event) { + var url = event.target.attributes.href.value; + if (this.$root.additionalURLParams) { + if (url.indexOf('?')) { + url += '&' + this.$root.additionalURLParams; + } else { + url += '?' + this.$root.additionalURLParams; + } + } + if (this.$root.useIframe) { + event.preventDefault(); + if (url.indexOf('?')) { + url += '&iframe=1'; + } else { + url += '?iframe=1'; + } + this.$root.overlay.frame_src = url; + } else { + event.target.href = url; + return; + } + }, + trigger_load_callback: function () { + this.$nextTick(function () { + for (var i = 0; i < window.PretixWidget._loaded.length; i++) { + window.PretixWidget._loaded[i]() + } + }); + }, + trigger_close_callback: function () { + this.$nextTick(function () { + for (var i = 0; i < window.PretixWidget._closed.length; i++) { + window.PretixWidget._closed[i]() + } + }); + }, + reload: function (opt = {}) { + var url; + if (this.$root.is_button) { + return; + } + if (this.$root.subevent) { + url = this.$root.target_url + this.$root.subevent + '/widget/product_list?lang=' + lang; + } else { + url = this.$root.target_url + 'widget/product_list?lang=' + lang; + } + if (this.$root.offset) { + url += '&offset=' + this.$root.offset; + } + if (this.$root.filter) { + url += '&' + this.$root.filter; + } + if (this.$root.item_filter) { + url += '&items=' + encodeURIComponent(this.$root.item_filter); + } + if (this.$root.category_filter) { + url += '&categories=' + encodeURIComponent(this.$root.category_filter); + } + if (this.$root.variation_filter) { + url += '&variations=' + encodeURIComponent(this.$root.variation_filter); + } + var cart_id = getCookie(this.cookieName); + if (this.$root.voucher_code) { + url += '&voucher=' + encodeURIComponent(this.$root.voucher_code); + } + if (cart_id) { + url += "&cart_id=" + encodeURIComponent(cart_id); + } + if (this.$root.date !== null) { + url += "&date=" + this.$root.date.substr(0, 7); + } else if (this.$root.week !== null) { + url += "&date=" + this.$root.week[0] + "-W" + this.$root.week[1]; + } + if (this.$root.style !== null) { + url = url + '&style=' + encodeURIComponent(this.$root.style); + } + var root = this.$root; + api._getJSON(url, function (data, xhr) { + if (typeof xhr.responseURL !== "undefined") { + var new_url = xhr.responseURL.substr(0, xhr.responseURL.indexOf("/widget/product_list?") + 1); + var old_url = url.substr(0, url.indexOf("/widget/product_list?") + 1); + if (new_url !== old_url) { + if (root.subevent) { + new_url = new_url.substr(0, new_url.lastIndexOf("/", new_url.length - 1) + 1); + } + root.target_url = new_url; + root.reload(); + return; + } + } + root.connection_error = false; + if (data.weeks !== undefined) { + root.weeks = data.weeks; + root.date = data.date; + root.week = null; + root.events = undefined; + root.view = "weeks"; + root.name = data.name; + root.frontpage_text = data.frontpage_text; + root.meta_filter_fields = data.meta_filter_fields; + } else if (data.days !== undefined) { + root.days = data.days; + root.date = null; + root.week = data.week; + root.events = undefined; + root.view = "days"; + root.name = data.name; + root.frontpage_text = data.frontpage_text; + root.meta_filter_fields = data.meta_filter_fields; + } else if (data.events !== undefined) { + root.events = root.append_events && root.events ? root.events.concat(data.events) : data.events; + root.append_events = false; + root.weeks = undefined; + root.view = "events"; + root.name = data.name; + root.frontpage_text = data.frontpage_text; + root.has_more_events = data.has_more_events; + root.meta_filter_fields = data.meta_filter_fields; + } else { + root.view = "event"; + // Replace target_url and subevent with canonical values in case they were slightly wrong + root.target_url = data.target_url; + root.subevent = data.subevent; + // Event data + root.name = data.name; + root.frontpage_text = data.frontpage_text; + root.date_range = data.date_range; + root.location = data.location; + 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; + root.waiting_list_enabled = data.waiting_list_enabled; + root.show_variations_expanded = data.show_variations_expanded || !!root.variation_filter; + root.cart_id = cart_id; + root.cart_exists = data.cart_exists; + root.vouchers_exist = data.vouchers_exist; + root.has_seating_plan = data.has_seating_plan; + root.has_seating_plan_waitinglist = data.has_seating_plan_waitinglist; + root.itemnum = data.itemnum; + } + root.poweredby = data.poweredby; + if (root.loading > 0) { + root.loading--; + root.trigger_load_callback(); + } + if (root.parent_stack.length > 0 && root.has_seating_plan && root.categories.length === 0 && !root.frame_dismissed && root.useIframe && !root.error && !root.has_seating_plan_waitinglist) { + // If we're on desktop and someone selects a seating-only event in a calendar, let's open it right away, + // but only if the person didn't close it before. + root.startseating() + } else { + // make sure to only move focus to content element when it had focus before the reload/click + // this is needed because reload is also called on initial load and we do not want to move focus on initial load + if (root.$el.contains(document.activeElement)) { + root.$nextTick(function() { + // wait for redraw, then focus content element for better a11y + (opt.focus ? document.querySelector(opt.focus) : root.$el).focus(); + }); + } + } + }, function (error) { + root.categories = []; + root.currency = ''; + if (error.status === 429) { + root.error = strings['loading_error_429']; + root.connection_error = true; + } else { + root.error = strings['loading_error']; + root.connection_error = true; + } + if (root.loading > 0) { + root.loading--; + root.trigger_load_callback(); + } + }); + }, + startwaiting: function () { + var redirect_url = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?iframe=1&locale=' + lang; + if (this.$root.subevent){ + redirect_url += '&subevent=' + this.$root.subevent; + } + if (this.$root.additionalURLParams) { + redirect_url += '&' + this.$root.additionalURLParams; + } + if (this.$root.useIframe) { + this.$root.overlay.frame_src = redirect_url; + } else { + window.open(redirect_url); + } + }, + startseating: function () { + var redirect_url = this.$root.target_url + 'w/' + widget_id; + if (this.$root.subevent){ + redirect_url += '/' + this.$root.subevent; + } + redirect_url += '/seatingframe/?iframe=1&locale=' + lang; + if (this.$root.voucher_code) { + redirect_url += '&voucher=' + encodeURIComponent(this.$root.voucher_code); + } + if (this.$root.cart_id) { + redirect_url += '&take_cart_id=' + this.$root.cart_id; + } + if (this.$root.widget_data) { + redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json); + } + if (this.$root.additionalURLParams) { + redirect_url += '&' + this.$root.additionalURLParams; + } + redirect_url += this.$root.consent_parameter; + if (this.$root.useIframe) { + this.$root.overlay.frame_src = redirect_url; + } else { + window.open(redirect_url); + } + }, + choose_event: function (event) { + this.$root.target_url = event.event_url; + this.$root.error = null; + this.$root.connection_error = false; + this.$root.subevent = event.subevent; + this.$root.loading++; + this.$root.reload(); + } +}; + +var shared_root_computed = { + cookieName: function () { + return "pretix_widget_" + this.target_url.replace(/[^a-zA-Z0-9]+/g, "_"); + }, + formTarget: function () { + var is_firefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; + var is_android = navigator.userAgent.toLowerCase().indexOf("android") > -1; + if (is_android && is_firefox) { + // Opening a POST form in a new browser fails in Firefox. This is supposed to be fixed since FF 76 + // but for some reason, it is still the case in FF for Android. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1629441 + // https://github.com/pretix/pretix/issues/1040 + return "_top"; + } else { + return "_blank"; + } + }, + voucherFormTarget: function () { + var form_target = this.target_url + 'w/' + widget_id + '/redeem?iframe=1&locale=' + lang; + var cookie = getCookie(this.cookieName); + if (cookie) { + form_target += "&take_cart_id=" + cookie; + } + if (this.subevent) { + form_target += "&subevent=" + this.subevent; + } + if (this.$root.widget_data) { + form_target += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json); + } + form_target += this.$root.consent_parameter; + if (this.$root.additionalURLParams) { + form_target += '&' + this.$root.additionalURLParams; + } + return form_target; + }, + formMethod: function () { + if (!this.useIframe && this.is_button && this.items.length === 0) { + return 'get'; + } + return 'post'; + }, + formAction: function () { + if (!this.useIframe && this.is_button && this.items.length === 0) { + var target; + if (this.voucher_code) { + target = this.target_url + 'redeem'; + } else if (this.subevent) { + target = this.target_url + this.subevent + '/'; + } else { + target = this.target_url; + } + return target; + } + var checkout_url = "/" + this.target_url.replace(/^[^\/]+:\/\/([^\/]+)\//, "") + "w/" + widget_id + "/"; + if (!this.$root.cart_exists) { + checkout_url += "checkout/start"; + } + if (this.$root.additionalURLParams) { + checkout_url += '?' + this.$root.additionalURLParams; + } + var form_target = this.target_url + 'w/' + widget_id + '/cart/add?iframe=1&next=' + encodeURIComponent(checkout_url); + var cookie = getCookie(this.cookieName); + if (cookie) { + form_target += "&take_cart_id=" + cookie; + } + form_target += this.$root.consent_parameter + return form_target + }, + newTabTarget: function () { + var target = this.target_url; + if (this.subevent) { + target = this.target_url + this.subevent + '/'; + } + return target; + }, + useIframe: function () { + if (window.crossOriginIsolated === true) { + console.warn("pretix Widget cannot use iframe due to Cross-Origin-Embed-Policy") + return false; + } + return !this.disable_iframe && (this.skip_ssl || site_is_secure()); + }, + showPrices: function () { + var has_priced = false; + var cnt_items = 0; + for (var i = 0; i < this.categories.length; i++) { + for (var j = 0; j < this.categories[i].items.length; j++) { + var item = this.categories[i].items[j]; + if (item.has_variations) { + cnt_items += item.variations.length; + has_priced = true; + } else { + cnt_items++; + has_priced = has_priced || item.price.gross != "0.00" || item.free_price; + } + } + } + return has_priced || cnt_items > 1; + }, + consent_parameter_value: function () { + if (typeof this.widget_data["consent"] !== "undefined") { + return encodeURIComponent(this.widget_data["consent"]); + } + return ""; + }, + consent_parameter: function () { + if (typeof this.widget_data["consent"] !== "undefined") { + return "&consent=" + encodeURIComponent(this.widget_data["consent"]); + } + return ""; + }, + widget_data_json: function () { + var cloned_data = Object.assign({}, this.widget_data); + if (typeof cloned_data["consent"] !== "undefined") { + // Remove consent as we pass it differently. We still keep it as widget_data in the input to avoid breaking + // the JS API of the widget. + delete cloned_data["consent"]; + } + return JSON.stringify(cloned_data); + }, + additionalURLParams: function () { + if (!window.location.search.indexOf('utm_')) { + return ''; + } + var params = new URLSearchParams(window.location.search); + for (var [key, value] of params.entries()) { + if (!key.startsWith('utm_')) { + params.delete(key); + } + } + return params.toString(); + }, +}; + +var create_overlay = function (app) { + var elem = document.createElement('pretix-overlay'); + document.body.appendChild(elem); + + var framechild = new Vue({ + el: elem, + data: function () { + return { + parent: app, + frame_loading: false, + frame_shown: false, + error_url_after: null, + error_url_after_new_tab: true, + error_message: null, + lightbox: null, + prevActiveElement: null, + } + }, + props: { + frame_src: String, + }, + methods: { + }, + watch: { + frame_src: function (newValue, oldValue) { + // show loading spinner only when previously no frame_src was set + if (newValue && !oldValue) { + this.frame_loading = true; + } + // 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; +}; + +function get_ga_client_id(tracking_id) { + if (typeof ga === "undefined") { + return null; + } + try { + var trackers = ga.getAll(); + var i, len; + for (i = 0, len = trackers.length; i < len; i += 1) { + if (trackers[i].get('trackingId') === tracking_id) { + return trackers[i].get('clientId'); + } + } + } catch (e) { + } + return null; +} + +var create_widget = function (element, html_id=null) { + var target_url = element.attributes.event.value; + if (!target_url.match(/\/$/)) { + target_url += "/"; + } + var voucher = element.attributes.voucher ? element.attributes.voucher.value : null; + var subevent = element.attributes.subevent ? element.attributes.subevent.value : null; + var style = element.attributes["list-type"] ? element.attributes["list-type"].value : (element.attributes.style ? element.attributes.style.value : null); + var skip_ssl = element.attributes["skip-ssl-check"] ? true : false; + var disable_iframe = element.attributes["disable-iframe"] ? true : false; + var disable_vouchers = element.attributes["disable-vouchers"] ? true : false; + var disable_filters = element.attributes["disable-filters"] ? true : false; + var display_event_info = element.getAttribute("display-event-info"); // null means "auto" (as before), everything other than "false" is true + if (display_event_info !== null && display_event_info !== "auto") { + display_event_info = display_event_info !== "false"; + } else { + display_event_info = null; + } + var widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data)); + var filter = element.attributes.filter ? element.attributes.filter.value : 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-.*$/)) { + widget_data[attrib.name.replace(/^data-/, '')] = attrib.value; + } + } + html_id = html_id || element.id || makeid(16); + + var observer = new MutationObserver((mutationList) => { + mutationList.forEach((mutation) => { + if (mutation.type == "attributes" && mutation.attributeName.startsWith("data-")) { + Vue.set(app.widget_data, mutation.attributeName.substring(5), mutation.target.getAttribute(mutation.attributeName)); + } + }); + }); + var observerOptions = { attributes: true }; + + if (element.tagName !== "pretix-widget") { + element.innerHTML = ""; + // we need to watch the container as well as the replaced root-node (see mounted()) + observer.observe(element, observerOptions); + } + + var app = new Vue({ + el: element, + data: function () { + return { + target_url: target_url, + parent_stack: [], + subevent: subevent, + is_button: false, + categories: null, + currency: null, + name: null, + date_range: null, + location: null, + offset: 0, + has_more_events: false, + append_events: false, + frontpage_text: null, + filter: filter, + item_filter: items, + category_filter: categories, + 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, + disable_iframe: disable_iframe, + style: style, + connection_error: false, + error: null, + weeks: null, + days: null, + date: null, + week: null, + frame_dismissed: false, + events: null, + view: null, + display_add_to_cart: false, + widget_data: widget_data, + loading: 1, + widget_id: 'pretix-widget-' + widget_id, + html_id: html_id, + vouchers_exist: false, + disable_vouchers: disable_vouchers, + disable_filters: disable_filters, + display_event_info: display_event_info, + cart_exists: false, + itemcount: 0, + overlay: null, + poweredby: "", + has_seating_plan: false, + has_seating_plan_waitinglist: false, + meta_filter_fields: [], + } + }, + created: function () { + this.reload(); + }, + mounted: function () { + observer.observe(this.$el, observerOptions); + }, + computed: shared_root_computed, + methods: shared_root_methods, + watch: { + 'view': function (newValue, oldValue) { + if (oldValue) { + // always make sure the widget is scrolled to the top + // as we only check top, we do not need to wait for a redraw + var rect = this.$el.getBoundingClientRect(); + if (rect.top < 0) { + this.$el.scrollIntoView(); + } + } + } + } + }); + create_overlay(app); + return app; +}; + +var create_button = function (element, html_id=null) { + var target_url = element.attributes.event.value; + if (!target_url.match(/\/$/)) { + target_url += "/"; + } + var voucher = element.attributes.voucher ? element.attributes.voucher.value : null; + var subevent = element.attributes.subevent ? element.attributes.subevent.value : null; + var raw_items = element.attributes.items ? element.attributes.items.value : ""; + var skip_ssl = element.attributes["skip-ssl-check"] ? true : false; + var disable_iframe = element.attributes["disable-iframe"] ? true : false; + var button_text = element.innerHTML; + var widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data)); + for (var i = 0; i < element.attributes.length; i++) { + var attrib = element.attributes[i]; + if (attrib.name.match(/^data-.*$/)) { + widget_data[attrib.name.replace(/^data-/, '')] = attrib.value; + } + } + html_id = html_id || element.id || makeid(16); + + var observer = new MutationObserver((mutationList) => { + mutationList.forEach((mutation) => { + if (mutation.type == "attributes" && mutation.attributeName.startsWith("data-")) { + Vue.set(app.widget_data, mutation.attributeName.substring(5), mutation.target.getAttribute(mutation.attributeName)); + } + }); + }); + var observerOptions = { attributes: true }; + + if (element.tagName !== "pretix-button") { + element.innerHTML = "" + element.innerHTML + ""; + // Vue does not replace the container, so watch container as well + observer.observe(element, observerOptions); + } + + var itemsplit = raw_items.split(","); + var items = []; + for (var i = 0; i < itemsplit.length; i++) { + if (itemsplit[i].indexOf("=") > 0 ) { + var splitthis = itemsplit[i].split("="); + items.push({'item': splitthis[0], 'count': splitthis[1]}) + } + } + + var app = new Vue({ + el: element, + data: function () { + return { + target_url: target_url, + subevent: subevent, + is_button: true, + skip_ssl: skip_ssl, + disable_iframe: disable_iframe, + voucher_code: voucher, + items: items, + error: null, + filter: null, + frame_dismissed: false, + widget_data: widget_data, + widget_id: 'pretix-widget-' + widget_id, + html_id: html_id, + button_text: button_text + } + }, + created: function () { + }, + mounted: function () { + observer.observe(this.$el, observerOptions); + }, + computed: shared_root_computed, + methods: shared_root_methods + }); + create_overlay(app); + return app; +}; + +/* Find all widgets on the page and render them */ +widgetlist = []; +buttonlist = []; +window.PretixWidget._loaded = []; +window.PretixWidget._closed = []; +window.PretixWidget.addLoadListener = function (f) { + window.PretixWidget._loaded.push(f); +} +window.PretixWidget.addCloseListener = function (f) { + window.PretixWidget._closed.push(f); +} +window.PretixWidget.buildWidgets = function () { + document.createElement("pretix-widget"); + document.createElement("pretix-button"); + docReady(function () { + var widgets = document.querySelectorAll("pretix-widget, div.pretix-widget-compat"); + var wlength = widgets.length; + for (var i = 0; i < wlength; i++) { + var widget = widgets[i]; + widgetlist.push(create_widget(widget, widget.id || "pretix-widget-"+i)); + } + + var buttons = document.querySelectorAll("pretix-button, div.pretix-button-compat"); + var blength = buttons.length; + for (var i = 0; i < blength; i++) { + var button = buttons[i]; + buttonlist.push(create_button(button, button.id || "pretix-button-"+i)); + } + }); +}; + +window.PretixWidget.open = function (target_url, voucher, subevent, items, widget_data, skip_ssl_check, disable_iframe) { + if (!target_url.match(/\/$/)) { + target_url += "/"; + } + + var all_widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data)); + if (widget_data) { + Object.keys(widget_data).forEach(function(key) { all_widget_data[key] = widget_data[key]; }); + } + var root = document.createElement("div"); + document.body.appendChild(root); + root.classList.add("pretix-widget-hidden"); + root.innerHTML = ""; + var app = new Vue({ + el: root, + data: function () { + return { + target_url: target_url, + subevent: subevent || null, + is_button: true, + skip_ssl: skip_ssl_check || false, + disable_iframe: disable_iframe || false, + voucher_code: voucher || null, + items: items || [], + error: null, + filter: null, + frame_dismissed: false, + widget_data: all_widget_data, + widget_id: 'pretix-widget-' + widget_id, + button_text: "" + } + }, + created: function () { + }, + computed: shared_root_computed, + methods: shared_root_methods + }); + create_overlay(app); + app.$nextTick(function () { + if (this.$root.useIframe) { + this.$refs.btn.buy(); + } else { + this.$refs.btn.$refs.form.submit(); + } + }) +}; + +if (typeof window.pretixWidgetCallback !== "undefined") { + window.pretixWidgetCallback(); +} +if (window.PretixWidget.build_widgets) { + window.PretixWidget.buildWidgets(); +} + +/* Set a global variable for debugging. In DEBUG mode, siteglobals will be window, otherwise it will be something + unnamed. */ +siteglobals.pretixwidget_debug = { + 'Vue': Vue, + 'widgets': widgetlist, + 'buttons': buttonlist +}; diff --git a/src/pretix/static/pretixpresale/scss/main.scss b/src/pretix/static/pretixpresale/scss/main.scss index 17acd4dcc..b047b1827 100644 --- a/src/pretix/static/pretixpresale/scss/main.scss +++ b/src/pretix/static/pretixpresale/scss/main.scss @@ -1,6 +1,5 @@ // before variables.scss because it only affects presale, not control $body-bg: #f5f5f5 !default; -$input-border: #949494; $font-size-base: 0.875rem !default;/* 14px/16px = 0.875rem */ diff --git a/src/pretix/static/pretixpresale/scss/widget.scss b/src/pretix/static/pretixpresale/scss/widget.scss index 900b15369..ebd59a945 100644 --- a/src/pretix/static/pretixpresale/scss/widget.scss +++ b/src/pretix/static/pretixpresale/scss/widget.scss @@ -9,7 +9,7 @@ .pretix-widget, .pretix-widget-alert-box { a { color: $link-color; - text-decoration: none; + text-decoration: underline; &:hover, &:focus { @@ -17,9 +17,9 @@ text-decoration: $link-hover-decoration; } &:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; + outline: 2px solid $brand-primary; + outline-offset: 2px; + z-index: 999; } } img { @@ -57,6 +57,9 @@ &.focus { text-decoration: none; @include tab-focus; + outline: 2px solid $brand-primary; + outline-offset: 2px; + z-index: 999; } } &.disabled, @@ -72,6 +75,7 @@ } label.pretix-widget-btn-checkbox { @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border, darken($btn-default-bg, 10%), darken($btn-default-border, 25%), darken($btn-default-border, 12%)); + border-radius: $input-border-radius; border-width: 1px; border-style: solid; position: relative; @@ -88,6 +92,11 @@ background-color: #e6e6e6; border-color: #adadad; } + &:focus-within { + outline: 2px solid $brand-primary; + outline-offset: 2px; + z-index: 999; + } } .pretix-widget-icon-cart { display: inline-block; @@ -114,9 +123,9 @@ $color-rgba: rgba(red($input-border-focus), green($input-border-focus), blue($input-border-focus), .6); &:focus { - border-color: $input-border-focus; - outline: 0; - @include box-shadow(inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px $color-rgba); + outline: 2px solid $brand-primary; + outline-offset: 2px; + z-index: 999; } } input[type=number] { @@ -151,7 +160,6 @@ border-radius: $input-border-radius; .pretix-widget-resume-button { - float: right; margin-left: 10px; } @@ -160,6 +168,9 @@ } .pretix-widget-info-message { + display: flex; + justify-content: space-between; + align-items: flex-end; padding: 10px; text-align: left; margin: 10px 0; @@ -168,6 +179,15 @@ color: $state-info-text; border-radius: $alert-border-radius; } + &.pretix-widget-mobile { + .pretix-widget-info-message { + flex-direction: column; + } + .pretix-widget-resume-button { + margin-top: 10px; + margin-left: 0; + } + } .pretix-widget-error-message { padding: 10px; @@ -352,12 +372,15 @@ font-size: 12px; } - .pretix-widget-item-picture { + .pretix-widget-item-picture-link { width: 60px; height: 60px; margin-right: 10px; float: left; } + .pretix-widget-item-picture { + max-width: 100%; + } .pretix-widget-action { margin-left: 75%; @@ -431,6 +454,17 @@ .pretix-widget-item-availability-col { text-align: center; + .pretix-widget-collapse-indicator { + width: 100%; + + border: 1px solid $input-border; + border-radius: $input-border-radius; + height: $input-height-base; + padding: $padding-base-vertical $padding-base-horizontal; + color: $input-color; + background-color: $input-bg; + } + .pretix-widget-collapse-indicator::before { content: ""; display: inline-block; @@ -514,6 +548,10 @@ flex-wrap: wrap; color: $text-color; + &:has(.pretix-widget-event-list-entry-availability) { + text-decoration: none; + } + &:hover, &:active, &:focus { background: $gray-lighter; text-decoration: none; @@ -540,41 +578,59 @@ padding: 7px 5px 3px; box-sizing: border-box; span { + position: relative; display: inline; - padding: 2px 6px 3px; + padding: 6px 6px 4px 17px; font-size: 75%; font-weight: bold; line-height: 1; - color: #fff; + color: var(--status-text-color, #000); + background-color: var(--status-bg-color, #fff); + border: 1px solid var(--status-border-color, #000); text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: 4px; } + span:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 11px; + height: 100%; + background: var(--status-border-color, #000); + } } } - .pretix-widget-event-availability-orange .pretix-widget-event-list-entry-availability span, - .pretix-widget-event-availability-orange.pretix-widget-event-calendar-event { - background-color: $brand-warning; + .pretix-widget-event-availability-orange, + .pretix-widget-day-availability-orange { + --status-bg-color: #{$alert-warning-bg}; + --status-text-color: #{$alert-warning-text}; + --status-border-color: #{$alert-warning-border}; } - .pretix-widget-event-availability-none .pretix-widget-event-list-entry-availability span, - .pretix-widget-event-availability-none.pretix-widget-event-calendar-event { - background-color: $brand-primary; + .pretix-widget-event-availability-none, + .pretix-widget-day-availability-none { + --status-bg-color: #{$alert-primary-bg}; + --status-text-color: #{$alert-primary-text}; + --status-border-color: #{$alert-primary-border}; } - .pretix-widget-event-availability-green .pretix-widget-event-list-entry-availability span, - .pretix-widget-event-availability-green.pretix-widget-event-calendar-event { - background-color: $brand-success; + .pretix-widget-event-availability-green, + .pretix-widget-day-availability-green { + --status-bg-color: #{$alert-success-bg}; + --status-text-color: #{$alert-success-text}; + --status-border-color: #{$alert-success-border}; } - .pretix-widget-event-availability-red .pretix-widget-event-list-entry-availability span, - .pretix-widget-event-availability-red.pretix-widget-event-calendar-event { - background-color: $brand-danger; + .pretix-widget-event-availability-red, + .pretix-widget-day-availability-red { + --status-bg-color: #{$alert-danger-bg}; + --status-text-color: #{$alert-danger-text}; + --status-border-color: #{$alert-danger-border}; } - .pretix-widget-event-availability-low .pretix-widget-event-list-entry-availability span { - border-left: 10px solid $brand-warning; - } - .pretix-widget-event-availability-low.pretix-widget-event-calendar-event { - border-right: 10px solid $brand-warning; + .pretix-widget-event-list .pretix-widget-event-availability-low .pretix-widget-event-list-entry-availability span:before, + .pretix-widget-event-calendar .pretix-widget-event-availability-low.pretix-widget-event-calendar-event:before { + background: linear-gradient(to bottom, var(--pretix-brand-warning) 1em, var(--status-border-color) 2.5em); } .pretix-widget-event-calendar { @@ -587,7 +643,7 @@ .pretix-widget-event-week-col { flex: 1; - margin: 0 5px; + margin: 0; &:first-child { margin-left: 0; @@ -595,6 +651,12 @@ &:last-child { margin-right: 0; } + &:nth-child(even) { + background-color: $table-bg-accent; + } + .pretix-widget-event-calendar-events { + margin: 4px; + } } } @@ -616,31 +678,62 @@ } } .pretix-widget-event-calendar-event { + position: relative; display: block; border-radius: 4px; + border: 1px solid var(--status-border-color, #000); + background-color: var(--status-bg-color, #fff); + color: var(--status-text-color, #000); padding: 5px; - color: white; + padding-left: 17px; cursor: pointer; margin-bottom: 5px; + + &:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 11px; + height: 100%; + background: var(--status-border-color, #000); + } + &:last-child { margin-bottom: 0; } - &:hover { - text-decoration: none; - } + text-decoration: none; } .pretix-widget-event-calendar-table { width: 100%; + border-spacing: 0; th, td { width: 14.285714285714286%; vertical-align: top; - padding: 10px 5px; + padding: 4px; + border-bottom: 1px solid $table-border-color; + } + th { + border-bottom-width: 2px; + color: $text-muted; + } + td:has(.pretix-widget-event-calendar-day):nth-child(even) { + background: $table-bg-accent; } } .pretix-widget-event-calendar-day { font-weight: bold; + font-size: 86%; + padding: 5px 5px 1em; + } + .pretix-widget-event-week-table .pretix-widget-event-calendar-day { + padding-bottom: 5px; + background-color: #fff; + border-bottom: 2px solid $table-border-color; + color: $text-muted; + font-size: 100%; } } @@ -656,9 +749,24 @@ .pretix-widget-event-list-filter-form { - display: flex; - flex-direction: row; - align-items: end; + .pretix-widget-event-list-filter-fieldset { + display: flex; + flex-direction: row; + align-items: end; + border: none; + padding: 0; + margin: 0; + > legend { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; + } + } margin-bottom: 15px; .pretix-widget-event-list-filter-field { @@ -667,7 +775,7 @@ margin: 0 15px 0 0; label { - display: inline-block; + display: block; font-weight: bold; margin-bottom: 5px; } @@ -682,7 +790,9 @@ } } .pretix-widget.pretix-widget-mobile .pretix-widget-event-list-filter-form { - display: block; + .pretix-widget-event-list-filter-fieldset { + display: block; + } .pretix-widget-event-list-filter-field { display: block; @@ -701,24 +811,120 @@ transform: scale(1); } } -.pretix-widget-alert-holder { - position: fixed; - left: 0; - top: 0; - width: 100%; - height: 100%; - background: rgba(255, 255, 255, 0.8); - z-index: 16777271; + +.pretix-widget-visibility-hidden { visibility: hidden; - opacity: 0; - transition: opacity 0.5s; /* do not animate visibility or we'll have a flashing thing on load */ +} +.pretix-widget-shake-once { + animation: pretix-widget-shake .2s; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; +} - &.pretix-widget-alert-shown { - visibility: visible; - opacity: 1; - transition: opacity 0.5s, visibility 0.5s; +@keyframes pretix-widget-shake { + 0% { transform: skewX(0deg); } + 20% { transform: skewX(-5deg); } + 40% { transform: skewX(5deg); } + 60% { transform: skewX(-5deg); } + 80% { transform: skewX(5deg); } + 100% { transform: skewX(0deg); } +} + + +.pretix-widget-alert-holder, +.pretix-widget-frame-holder, +.pretix-widget-lightbox-holder { + border: none; + background: transparent; + overflow: visible; + &::backdrop { + background: rgba(255, 255, 255, 0.8); } + &:focus { + outline: 2px solid $brand-primary; + outline-offset: 2px; + } +} +.pretix-widget-frame-isloading:focus { + outline: none; + svg { + outline: 2px solid $brand-primary; + border-radius: 100%; + } +} +.pretix-widget-frame-loading p, +.pretix-widget-lightbox-loading p { + text-align: center; + width: 256px; + margin: 0 auto; + color: $brand-danger; +} +.pretix-widget-frame-loading svg, +.pretix-widget-lightbox-loading svg { + margin: 40px; + -webkit-animation: pretix-widget-spin 6s linear infinite; + -moz-animation: pretix-widget-spin 6s linear infinite; + animation: pretix-widget-spin 6s linear infinite; +} +.pretix-widget-frame-close, +.pretix-widget-lightbox-close { + position: absolute; + top: -12px; + right: -12px; + z-index: 2; +} + +.pretix-widget-frame-close button, +.pretix-widget-lightbox-close button { + color: white; + cursor: pointer; + font-weight: bold; + font-family: sans-serif; + text-decoration: none; + padding: 4px 0; + display: inline-block; + line-height: 16px; + border: none; + background: none; + width: 24px; + height: 24px; + background: $brand-primary; + border-radius: 12px; + -moz-border-radius: 12px; + -webkit-border-radius: 12px; + text-align: center; + + &:focus { + outline: 2px solid $brand-primary; + outline-offset: 2px; + } +} + +.pretix-widget-frame-close svg, +.pretix-widget-lightbox-close svg { + display: inline-block; + border: none; +} + +.pretix-widget-frame-inner, +.pretix-widget-lightbox-inner, +.pretix-widget-alert-box { + position: relative; + background: white; + border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + -webkit-border-radius: 5px 5px 5px 5px; + box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); + -webkit-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); + -moz-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); + box-sizing: border-box; + padding: 10px; +} + + + +.pretix-widget-alert-holder { .bounce-enter-active { animation: pretix-widget-bounce-in .5s; } @@ -727,19 +933,6 @@ } .pretix-widget-alert-box { - position: fixed; - left: 50%; - width: 600px; - margin-left: -300px; - top: 100px; - background: white; - border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - -webkit-border-radius: 5px 5px 5px 5px; - box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); - -webkit-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); - -moz-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); - box-sizing: border-box; padding: 42px 20px 20px 20px; text-align: center; font-size: 20px; @@ -752,197 +945,47 @@ } } .pretix-widget-alert-icon { - position: fixed; + position: absolute; left: 50%; width: 64px; margin-left: -32px; - top: 68px; + top: -20px; } } -.pretix-widget-frame-holder { - position: fixed; - left: 0; - top: 0; - width: 100%; - height: 100%; - background: rgba(255, 255, 255, 0.8); - z-index: 16777271; + + + +.pretix-widget-frame-inner { + width: 80vw; + height: 80vh; +} +.pretix-widget-frame-inner iframe { + width: 100% !important; + height: 100% !important; +} + + +.pretix-widget-lightbox-inner { + max-width: 90vw; + max-height: 90vh; +} +.pretix-widget-lightbox-isloading .pretix-widget-lightbox-inner { visibility: hidden; - opacity: 0; - transition: opacity 0.5s, visibility 0.5s; - - .pretix-widget-frame-loading { - text-align: center; - display: flex; - align-items: center; - justify-content: center; - height: 100%; - width: 100%; - position: fixed; - left: 0; - top: 0; - } - - .pretix-widget-frame-loading svg { - margin: 40px; - -webkit-animation: pretix-widget-spin 6s linear infinite; - -moz-animation: pretix-widget-spin 6s linear infinite; - animation: pretix-widget-spin 6s linear infinite; - } - - &.pretix-widget-frame-shown { - visibility: visible; - opacity: 1; - transition: opacity 0.5s, visibility 0.5s; - } - - .pretix-widget-frame-inner { - position: fixed; - left: 10%; - width: 80%; - height: 80%; - top: 10%; - background: white; - border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - -webkit-border-radius: 5px 5px 5px 5px; - box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); - -webkit-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); - -moz-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); - box-sizing: border-box; - padding: 10px; - } - - .pretix-widget-frame-close { - position: fixed; - right: 10%; - top: 10%; - width: 24px; - height: 24px; - background: $brand-primary; - margin: -12px -12px 0 0; - border-radius: 12px; - -moz-border-radius: 12px; - -webkit-border-radius: 12px; - text-align: center; - } - - .pretix-widget-frame-close a { - color: white; - font-weight: bold; - font-family: sans-serif; - text-decoration: none; - padding: 4px 0; - display: inline-block; - line-height: 16px; - } - - .pretix-widget-frame-close svg { - display: inline-block; - border: none; - } - - .pretix-widget-frame-inner iframe { - width: 100% !important; - height: 100% !important; - } +} +.pretix-widget-lightbox-image { + margin: 0; + padding: 0; + text-align: center; +} +.pretix-widget-lightbox-image img { + max-width: 80vw; + max-height: 80vh; + object-fit: scale-down; +} +.pretix-widget-lightbox-image figcaption { + margin: 0.5em 0 0; } -.pretix-widget-lightbox-holder { - position: fixed; - left: 0; - top: 0; - width: 100%; - height: 100%; - background: rgba(255, 255, 255, 0.8); - z-index: 16777271; - visibility: hidden; - opacity: 0; - transition: opacity 0.5s, visibility 0.5s; - display: flex; - align-items: center; - justify-content: center; - - .pretix-widget-lightbox-loading svg { - margin: 40px; - -webkit-animation: pretix-widget-spin 6s linear infinite; - -moz-animation: pretix-widget-spin 6s linear infinite; - animation: pretix-widget-spin 6s linear infinite; - } - - &.pretix-widget-lightbox-shown { - visibility: visible; - opacity: 1; - transition: opacity 0.5s, visibility 0.5s; - } - - .pretix-widget-lightbox-inner { - position: relative; - background: white; - border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - -webkit-border-radius: 5px 5px 5px 5px; - box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); - -webkit-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); - -moz-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); - box-sizing: border-box; - padding: 10px; - max-width: 90%; - max-height: 90%; - } - &.pretix-widget-lightbox-isloading .pretix-widget-lightbox-inner { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0,0,0,0); - border: 0; - } - - .pretix-widget-lightbox-image { - margin: 0; - padding: 0; - text-align: center; - } - .pretix-widget-lightbox-image img { - max-width: 80vw; - max-height: 80vh; - object-fit: scale-down; - } - .pretix-widget-lightbox-image figcaption { - margin: 0.5em 0 0; - } - - .pretix-widget-lightbox-close { - position: absolute; - right: -12px; - top: -12px; - width: 24px; - height: 24px; - background: $brand-primary; - margin: 0; - border: none; - border-radius: 12px; - -moz-border-radius: 12px; - -webkit-border-radius: 12px; - text-align: center; - color: white; - font-weight: bold; - font-family: sans-serif; - text-decoration: none; - padding: 4px 0; - display: inline-block; - line-height: 16px; - cursor: pointer; - } - - .pretix-widget-lightbox-close svg { - display: inline-block; - border: none; - } -} .pretix-widget-primary-color { /* in SVG */ @@ -1015,22 +1058,14 @@ display: block; } } - td.pretix-widget-has-events { - background: $brand-primary; - color: white; + td.pretix-widget-has-events .pretix-widget-event-calendar-day { + background: var(--status-bg-color, #fff); + color: var(--status-text-color, #000); + border: 1px solid var(--status-border-color, inherit); + border-top-width: 11px; + padding-bottom: 5px; + border-radius: $input-border-radius; cursor: pointer; - &.pretix-widget-day-availability-red { - background: $brand-danger; - } - &.pretix-widget-day-availability-green { - background: $brand-success; - } - &.pretix-widget-day-availability-low { - border-right: 5px solid $brand-warning; - } - &.pretix-widget-day-availability-orange { - background: $brand-warning; - } } .pretix-widget-event-calendar-head { @@ -1048,41 +1083,3 @@ } } -@media (min-width: 1200px) { - .pretix-widget-frame-holder { - .pretix-widget-frame-inner { - left: 50%; - margin-left: -540px; - width: 1080px; - } - .pretix-widget-frame-close { - left: 50%; - margin-left: 528px; - } - } -} - -@media (max-width: 800px) { - .pretix-widget-frame-holder .pretix-widget-frame-inner { - left: 0; - width: 100%; - height: 100%; - top: 0; - background: $brand-primary; - border-radius: 0; - -moz-border-radius: 0; - -webkit-border-radius: 0; - box-shadow: none; - -webkit-box-shadow: none; - -moz-box-shadow: none; - padding: 40px 0 0 0; - } - .pretix-widget-frame-holder .pretix-widget-frame-close { - right: 20px; - top: 20px; - background: white; - svg path { - fill: $brand-primary; - } - } -} diff --git a/src/pretix/static/pretixpresale/scss/widget.v1.scss b/src/pretix/static/pretixpresale/scss/widget.v1.scss new file mode 100644 index 000000000..900b15369 --- /dev/null +++ b/src/pretix/static/pretixpresale/scss/widget.v1.scss @@ -0,0 +1,1088 @@ +// not included, will be dynamically prepended @import "../../pretixbase/scss/_theme_variables.scss"; +@import "../../pretixbase/scss/_bootstrap_vars.scss"; +@import "../../bootstrap/scss/bootstrap/variables"; +@import "../../bootstrap/scss/bootstrap/mixins"; + +.pretix-widget-hidden { + display: none; +} +.pretix-widget, .pretix-widget-alert-box { + a { + color: $link-color; + text-decoration: none; + + &:hover, + &:focus { + color: $link-hover-color; + text-decoration: $link-hover-decoration; + } + &:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; + } + } + img { + border: 0; + } + b, strong { + font-weight: bold; + } + h3 { + font-size: $font-size-h3; + font-weight: bold; + padding: 0 15px; + } + button, input[type="button"], a.pretix-widget-button { + overflow: visible; + text-transform: none; + cursor: pointer; + display: inline-block; + margin-bottom: 0; + text-align: center; + vertical-align: middle; + touch-action: manipulation; + background-image: none; + border: 1px solid transparent; + white-space: nowrap; + text-decoration: none; + @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $btn-border-radius-base); + @include user-select(none); + @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border, $btn-primary-background-active, $btn-primary-border-active, $btn-primary-border-hover); + + &, + &:active, + &.active { + &:focus, + &.focus { + text-decoration: none; + @include tab-focus; + } + } + &.disabled, + &[disabled], + fieldset[disabled] & { + cursor: $cursor-disabled; + @include opacity(.65); + @include box-shadow(none); + } + &.pretix-widget-btn-default { + @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border, darken($btn-default-bg, 10%), darken($btn-default-border, 25%), darken($btn-default-border, 12%)); + } + } + label.pretix-widget-btn-checkbox { + @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border, darken($btn-default-bg, 10%), darken($btn-default-border, 25%), darken($btn-default-border, 12%)); + border-width: 1px; + border-style: solid; + position: relative; + cursor: pointer; + padding: 6px 24px; + min-height: 32px; + box-sizing: border-box; + color: #333; + input { + position: absolute; + left: 10px; + } + &:has(input:checked) { + background-color: #e6e6e6; + border-color: #adadad; + } + } + .pretix-widget-icon-cart { + display: inline-block; + width: 1em; + height: 1em; + vertical-align: text-bottom; + fill: #333; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2.267 6.756c0-.312-.202-.563-.453-.563-.252 0-.454.251-.454.563 0 .312.202.563.454.563.251 0 .453-.251.453-.563Zm3.174 0c0-.312-.202-.563-.454-.563-.251 0-.453.251-.453.563 0 .312.202.563.453.563.252 0 .454-.251.454-.563Zm.453-4.785c0-.154-.103-.282-.227-.282H1.413c-.035-.211-.039-.563-.28-.563H.227c-.124 0-.227.128-.227.282 0 .153.103.281.227.281h.722l.627 3.62c-.049.127-.216.466-.216.603 0 .153.103.281.227.281h3.627c.124 0 .227-.128.227-.281 0-.154-.103-.282-.227-.282H1.955c.036-.088.085-.18.085-.281 0-.102-.032-.212-.046-.308l3.698-.537c.117-.018.202-.141.202-.281V1.971Z' transform='matrix(2.52069 0 0 2.02994 -.035 -.523)'/%3E%3C/svg%3E%0A"); + } + input:checked + .pretix-widget-icon-cart { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.534 3.097a.317.317 0 0 1-.067.197L3.56 4.42a.207.207 0 0 1-.16.084.207.207 0 0 1-.159-.084l-.907-1.126a.317.317 0 0 1-.067-.197c0-.154.103-.282.227-.282.06 0 .117.031.159.084l.521.642V2.252c0-.154.102-.281.226-.281.124 0 .227.127.227.281v1.289l.521-.642a.205.205 0 0 1 .159-.084c.124 0 .227.128.227.282ZM2.267 6.756c0-.312-.202-.563-.453-.563-.252 0-.454.251-.454.563 0 .312.202.563.454.563.251 0 .453-.251.453-.563Zm3.174 0c0-.312-.202-.563-.454-.563-.251 0-.453.251-.453.563 0 .312.202.563.453.563.252 0 .454-.251.454-.563Zm.453-4.785c0-.154-.103-.282-.227-.282H1.413c-.035-.211-.039-.563-.28-.563H.227c-.124 0-.227.128-.227.282 0 .153.103.281.227.281h.722l.627 3.62c-.049.127-.216.466-.216.603 0 .153.103.281.227.281h3.627c.124 0 .227-.128.227-.281 0-.154-.103-.282-.227-.282H1.955c.036-.088.085-.18.085-.281 0-.102-.032-.212-.046-.308l3.698-.537c.117-.018.202-.141.202-.281V1.971Z' transform='matrix(2.52069 0 0 2.02994 -.035 -.523)'/%3E%3C/svg%3E%0A"); + } + input[type="text"], input[type="number"], select { + line-height: normal; + border: 1px solid $input-border; + border-radius: $input-border-radius; + height: $input-height-base; + padding: $padding-base-vertical $padding-base-horizontal; + color: $input-color; + background-color: $input-bg; + @include box-shadow(inset 0 1px 1px rgba(0, 0, 0, .075)); + @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s); + @include placeholder; + $color-rgba: rgba(red($input-border-focus), green($input-border-focus), blue($input-border-focus), .6); + + &:focus { + border-color: $input-border-focus; + outline: 0; + @include box-shadow(inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px $color-rgba); + } + } + input[type=number] { + padding-right: 0; // Setting the padding-right to zero, as some versions Firefox render the arrow-buttons on number inputs useless. + } + input[type="checkbox"], + input[type="radio"] { + box-sizing: border-box; // 1 + padding: 0; // 2 + &:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; + } + } +} +.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; + border: 1px solid #ccc; + position: relative; + min-height: 208px; + border-radius: $input-border-radius; + + .pretix-widget-resume-button { + float: right; + margin-left: 10px; + } + + .pretix-widget-clickable { + cursor: pointer; + } + + .pretix-widget-info-message { + padding: 10px; + text-align: left; + margin: 10px 0; + background-color: white; + border: 2px solid $brand-info; + color: $state-info-text; + border-radius: $alert-border-radius; + } + + .pretix-widget-error-message { + padding: 10px; + text-align: center; + margin: 10px 0; + background-color: white; + border: 2px solid $brand-danger; + color: $state-danger-text; + border-radius: $alert-border-radius; + } + + .pretix-widget-error-action { + padding: 10px; + text-align: center; + } + + .pretix-widget-loading { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, .8); + text-align: center; + } + + @-moz-keyframes pretix-widget-spin { + 100% { + -moz-transform: rotate(360deg); + } + } + + @-webkit-keyframes pretix-widget-spin { + 100% { + -webkit-transform: rotate(360deg); + } + } + + @keyframes pretix-widget-spin { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } + + .pretix-widget-loading svg { + margin: 40px; + /*Fallback*/ + position: absolute; + top: 50%; + margin-top: -64px; + /*Sticky*/ + position: -webkit-sticky; + position: sticky; + top: Min(50vh, 50%);/* use uppercase M to use CSS-min and not SASS-min*/ + + -webkit-animation: pretix-widget-spin 6s linear infinite; + -moz-animation: pretix-widget-spin 6s linear infinite; + animation: pretix-widget-spin 6s linear infinite; + } + + .pretix-widget-item-row, .pretix-widget-category { + clear: both; + } + + .pretix-widget-item-title { + font-weight: bold; + } + + .pretix-widget-item-row { + padding: 10px 0; + } + + .pretix-widget-category { + margin: 10px 0; + } + + .pretix-widget-category-description { + padding: 0 15px; + } + + .pretix-widget-category-name { + margin: 10px 0 0 0; + } + + .pretix-widget-item-info-col { + width: 50%; + float: left; + padding: 0 15px; + box-sizing: border-box; + } + + .pretix-widget-item-price-col, .pretix-widget-item-availability-col { + width: 25%; + float: left; + padding: 0 15px; + box-sizing: border-box; + } + + .pretix-widget-item-description p, .pretix-widget-item-meta { + margin: 0; + } + + .pretix-widget-item-price-col { + text-align: right; + } + + del.pretix-widget-pricebox-original-price { + color: $text-muted; + } + + ins.pretix-widget-pricebox-new-price { + font-size: 120%; + font-weight: bold; + text-decoration: none; + } + + .pretix-widget-clear { + clear: both; + } + + .pretix-widget-category-description p { + margin: 0 0 10px; + } + + .pretix-widget-pricebox-tax { + 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%; + box-sizing: border-box; + padding: 5px; + text-align: center; + } + + .pretix-widget-pricebox-price-input { + display: inline; + width: 100px; + box-sizing: border-box; + text-align: right; + } + + .pretix-widget-item-count-single-label { + display: block; + text-align: center; + width: 100%; + } + + .pretix-widget-attribution { + padding: 10px 15px; + text-align: center; + font-size: 12px; + } + + .pretix-widget-item-picture { + width: 60px; + height: 60px; + margin-right: 10px; + float: left; + } + + .pretix-widget-action { + margin-left: 75%; + width: 25%; + padding: 0 15px; + box-sizing: border-box; + } + + .pretix-widget-action button { + width: 100%; + } + + .pretix-widget-voucher-text { + margin: 10px 0; + padding: 0 15px; + } + + .pretix-widget-voucher-headline { + margin: 10px 0 0 0; + } + + .pretix-widget-voucher-input-wrap { + padding: 0 15px; + width: 75%; + box-sizing: border-box; + float: left; + } + + .pretix-widget-voucher input { + width: 100%; + box-sizing: border-box; + } + + .pretix-widget-voucher-button-wrap { + padding: 0 15px; + width: 25%; + box-sizing: border-box; + float: left; + } + + .pretix-widget-voucher button { + width: 100%; + } + + .pretix-widget-seating-waitinglist { + margin: 15px 0; + } + + .pretix-widget-seating-waitinglist-text { + padding: 0 15px; + width: 75%; + box-sizing: border-box; + float: left; + } + + .pretix-widget-seating-waitinglist-button-wrap { + padding: 0 15px; + width: 25%; + box-sizing: border-box; + float: left; + } + + .pretix-widget-seating-waitinglist-button { + width: 100%; + } + + .pretix-widget-item-with-picture .pretix-widget-main-item-row .pretix-widget-item-title-and-description { + margin-left: 70px; + } + + .pretix-widget-item-availability-col { + text-align: center; + + .pretix-widget-collapse-indicator::before { + content: ""; + display: inline-block; + width: $font-size-base; + height: $font-size-base; + background: url("data:image/svg+xml,%3Csvg viewBox='0 0 14 14' xmlns='http://www.w3.org/2000/svg' xml:space='preserve'%3E%3Cpath fill='#{url-friendly-colour($link-color)}' d='M6.395 4.151a.268.268 0 0 0-.177.077l-.386.386a.259.259 0 0 0-.077.177c.002.067.029.13.077.179l3.033 3.031-3.033 3.032a.255.255 0 0 0-.077.177.253.253 0 0 0 .077.178l.386.385a.268.268 0 0 0 .177.077.27.27 0 0 0 .178-.077l3.595-3.595a.259.259 0 0 0 .077-.177.255.255 0 0 0-.077-.176L6.573 4.228a.257.257 0 0 0-.178-.077Z'/%3E%3C/svg%3E"); + transition: transform .5s; + } + + .pretix-widget-collapse-indicator[aria-expanded=true]::before { + transform: rotate(90deg); + } + } + + .pretix-widget-availability-gone { + font-weight: bold; + color: $brand-danger; + text-transform: uppercase; + } + + .pretix-widget-availability-unavailable { + color: $brand-danger; + } + + .pretix-widget-item-variations { + overflow: hidden; + padding-top: 0; + padding-bottom: 0; + margin-top: 0; + margin-bottom: 0; + -moz-transition-duration: 0.5s; + -webkit-transition-duration: 0.5s; + -o-transition-duration: 0.5s; + transition-duration: 0.5s; + -moz-transition-timing-function: ease-in-out; + -webkit-transition-timing-function: ease-in-out; + -o-transition-timing-function: ease-in-out; + transition-timing-function: ease-in-out; + } + .pretix-widget-event-header { + padding-top: 10px; + text-align: center; + } + .pretix-widget-event-details { + padding-top: 10px; + text-align: center; + } + .pretix-widget-event-location { + display: none; + padding-top: 10px; + text-align: center; + } + .pretix-widget-event-description { + padding: 0 15px; + } + .pretix-widget-event-list-back { + padding-top: 10px; + text-align: center; + display: block; + a { + display: block; + } + } + .pretix-widget-back { + padding-bottom: 10px; + text-align: center; + display: block; + a { + display: block; + } + } + + .pretix-widget-event-list { + padding: 10px 0; + cursor: pointer; + } + .pretix-widget-event-list-entry { + display: flex; + flex-direction: row; + padding: 5px 0; + flex-wrap: wrap; + color: $text-color; + + &:hover, &:active, &:focus { + background: $gray-lighter; + text-decoration: none; + } + + .pretix-widget-event-list-entry-name { + width: 50%; + padding: 5px; + box-sizing: border-box; + } + .pretix-widget-event-list-entry-location { + padding: 5px; + box-sizing: border-box; + display: none; + } + .pretix-widget-event-list-entry-date { + width: 25%; + padding: 5px; + box-sizing: border-box; + } + .pretix-widget-event-list-entry-availability { + width: 25%; + text-align: right; + padding: 7px 5px 3px; + box-sizing: border-box; + span { + display: inline; + padding: 2px 6px 3px; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 4px; + } + } + } + + .pretix-widget-event-availability-orange .pretix-widget-event-list-entry-availability span, + .pretix-widget-event-availability-orange.pretix-widget-event-calendar-event { + background-color: $brand-warning; + } + .pretix-widget-event-availability-none .pretix-widget-event-list-entry-availability span, + .pretix-widget-event-availability-none.pretix-widget-event-calendar-event { + background-color: $brand-primary; + } + .pretix-widget-event-availability-green .pretix-widget-event-list-entry-availability span, + .pretix-widget-event-availability-green.pretix-widget-event-calendar-event { + background-color: $brand-success; + } + .pretix-widget-event-availability-red .pretix-widget-event-list-entry-availability span, + .pretix-widget-event-availability-red.pretix-widget-event-calendar-event { + background-color: $brand-danger; + } + .pretix-widget-event-availability-low .pretix-widget-event-list-entry-availability span { + border-left: 10px solid $brand-warning; + } + .pretix-widget-event-availability-low.pretix-widget-event-calendar-event { + border-right: 10px solid $brand-warning; + } + + .pretix-widget-event-calendar { + padding-top: 10px; + word-break: break-word; + + .pretix-widget-event-week-table { + display: flex; + flex-direction: row; + + .pretix-widget-event-week-col { + flex: 1; + margin: 0 5px; + + &:first-child { + margin-left: 0; + } + &:last-child { + margin-right: 0; + } + } + } + + .pretix-widget-event-calendar-head { + display: flex; + flex-direction: row; + + strong { + width: 50%; + text-align: center; + display: block; + } + .pretix-widget-event-calendar-next-month, .pretix-widget-event-calendar-previous-month { + display: block; + width: 25%; + } + .pretix-widget-event-calendar-next-month { + text-align: right; + } + } + .pretix-widget-event-calendar-event { + display: block; + border-radius: 4px; + padding: 5px; + color: white; + cursor: pointer; + margin-bottom: 5px; + &:last-child { + margin-bottom: 0; + } + &:hover { + text-decoration: none; + } + } + + .pretix-widget-event-calendar-table { + width: 100%; + + th, td { + width: 14.285714285714286%; + vertical-align: top; + padding: 10px 5px; + } + } + .pretix-widget-event-calendar-day { + font-weight: bold; + } + } + + .pretix-widget-seating-link-wrapper { + padding: 0 15px; + margin: 15px 0 10px; + } + .pretix-widget-seating-link { + display: block; + width: 100%; + } +} + + +.pretix-widget-event-list-filter-form { + display: flex; + flex-direction: row; + align-items: end; + margin-bottom: 15px; + + .pretix-widget-event-list-filter-field { + display: block; + width: 100%; + margin: 0 15px 0 0; + + label { + display: inline-block; + font-weight: bold; + margin-bottom: 5px; + } + + select { + display: block; + width: 100%; + } + } + .pretix-widget-event-list-filter-field:last-child { + margin: 0; + } +} +.pretix-widget.pretix-widget-mobile .pretix-widget-event-list-filter-form { + display: block; + + .pretix-widget-event-list-filter-field { + display: block; + margin: 0 0 5px; + } +} + +@keyframes pretix-widget-bounce-in { + 0% { + transform: scale(0); + } + 50% { + transform: scale(1.5); + } + 100% { + transform: scale(1); + } +} +.pretix-widget-alert-holder { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.8); + z-index: 16777271; + visibility: hidden; + opacity: 0; + transition: opacity 0.5s; /* do not animate visibility or we'll have a flashing thing on load */ + + &.pretix-widget-alert-shown { + visibility: visible; + opacity: 1; + transition: opacity 0.5s, visibility 0.5s; + } + + .bounce-enter-active { + animation: pretix-widget-bounce-in .5s; + } + .bounce-leave-active { + animation: pretix-widget-bounce-in .5s reverse; + } + + .pretix-widget-alert-box { + position: fixed; + left: 50%; + width: 600px; + margin-left: -300px; + top: 100px; + background: white; + border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + -webkit-border-radius: 5px 5px 5px 5px; + box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); + -webkit-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); + -moz-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); + box-sizing: border-box; + padding: 42px 20px 20px 20px; + text-align: center; + font-size: 20px; + + p:first-child { + margin-top: 0; + } + p:last-child { + margin-bottom: 0; + } + } + .pretix-widget-alert-icon { + position: fixed; + left: 50%; + width: 64px; + margin-left: -32px; + top: 68px; + } +} +.pretix-widget-frame-holder { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.8); + z-index: 16777271; + visibility: hidden; + opacity: 0; + transition: opacity 0.5s, visibility 0.5s; + + .pretix-widget-frame-loading { + text-align: center; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + position: fixed; + left: 0; + top: 0; + } + + .pretix-widget-frame-loading svg { + margin: 40px; + -webkit-animation: pretix-widget-spin 6s linear infinite; + -moz-animation: pretix-widget-spin 6s linear infinite; + animation: pretix-widget-spin 6s linear infinite; + } + + &.pretix-widget-frame-shown { + visibility: visible; + opacity: 1; + transition: opacity 0.5s, visibility 0.5s; + } + + .pretix-widget-frame-inner { + position: fixed; + left: 10%; + width: 80%; + height: 80%; + top: 10%; + background: white; + border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + -webkit-border-radius: 5px 5px 5px 5px; + box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); + -webkit-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); + -moz-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); + box-sizing: border-box; + padding: 10px; + } + + .pretix-widget-frame-close { + position: fixed; + right: 10%; + top: 10%; + width: 24px; + height: 24px; + background: $brand-primary; + margin: -12px -12px 0 0; + border-radius: 12px; + -moz-border-radius: 12px; + -webkit-border-radius: 12px; + text-align: center; + } + + .pretix-widget-frame-close a { + color: white; + font-weight: bold; + font-family: sans-serif; + text-decoration: none; + padding: 4px 0; + display: inline-block; + line-height: 16px; + } + + .pretix-widget-frame-close svg { + display: inline-block; + border: none; + } + + .pretix-widget-frame-inner iframe { + width: 100% !important; + height: 100% !important; + } +} + +.pretix-widget-lightbox-holder { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.8); + z-index: 16777271; + visibility: hidden; + opacity: 0; + transition: opacity 0.5s, visibility 0.5s; + display: flex; + align-items: center; + justify-content: center; + + .pretix-widget-lightbox-loading svg { + margin: 40px; + -webkit-animation: pretix-widget-spin 6s linear infinite; + -moz-animation: pretix-widget-spin 6s linear infinite; + animation: pretix-widget-spin 6s linear infinite; + } + + &.pretix-widget-lightbox-shown { + visibility: visible; + opacity: 1; + transition: opacity 0.5s, visibility 0.5s; + } + + .pretix-widget-lightbox-inner { + position: relative; + background: white; + border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + -webkit-border-radius: 5px 5px 5px 5px; + box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); + -webkit-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); + -moz-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09); + box-sizing: border-box; + padding: 10px; + max-width: 90%; + max-height: 90%; + } + &.pretix-widget-lightbox-isloading .pretix-widget-lightbox-inner { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; + } + + .pretix-widget-lightbox-image { + margin: 0; + padding: 0; + text-align: center; + } + .pretix-widget-lightbox-image img { + max-width: 80vw; + max-height: 80vh; + object-fit: scale-down; + } + .pretix-widget-lightbox-image figcaption { + margin: 0.5em 0 0; + } + + .pretix-widget-lightbox-close { + position: absolute; + right: -12px; + top: -12px; + width: 24px; + height: 24px; + background: $brand-primary; + margin: 0; + border: none; + border-radius: 12px; + -moz-border-radius: 12px; + -webkit-border-radius: 12px; + text-align: center; + color: white; + font-weight: bold; + font-family: sans-serif; + text-decoration: none; + padding: 4px 0; + display: inline-block; + line-height: 16px; + cursor: pointer; + } + + .pretix-widget-lightbox-close svg { + display: inline-block; + border: none; + } +} + +.pretix-widget-primary-color { + /* in SVG */ + fill: $brand-primary; +} + +.pretix-widget-event-list-load-more { + text-align: center; +} + +.pretix-widget.pretix-widget-mobile { + .pretix-widget-event-week-table { + display: block; + + .pretix-widget-event-week-col { + flex: 1; + margin: 10px 0; + } + } + + .pretix-widget-item-info-col { + width: 100%; + float: none; + margin-bottom: 5px; + } + .pretix-widget-item-info-col:after { + display: block; + content: ""; + clear: both; + } + .pretix-widget-item-price-col, .pretix-widget-item-availability-col { + width: 50%; + min-width: 140px; + } + .pretix-widget-action { + width: 100%; + margin-left: 0; + } + .pretix-widget-voucher-input-wrap { + width: 100%; + float: none; + } + .pretix-widget-voucher-button-wrap { + width: 100%; + float: none; + margin-top: 10px; + } + + .pretix-widget-event-list-entry { + .pretix-widget-event-list-entry-name { + width: 100%; + } + .pretix-widget-event-list-entry-location { + width: 100%; + } + .pretix-widget-event-list-entry-date { + width: 50%; + } + .pretix-widget-event-list-entry-availability { + width: 50%; + } + } + + .pretix-widget-event-calendar { + .pretix-widget-event-calendar-events { + display: none; + } + .pretix-widget-event-week-table { + .pretix-widget-event-calendar-events { + display: block; + } + } + td.pretix-widget-has-events { + background: $brand-primary; + color: white; + cursor: pointer; + &.pretix-widget-day-availability-red { + background: $brand-danger; + } + &.pretix-widget-day-availability-green { + background: $brand-success; + } + &.pretix-widget-day-availability-low { + border-right: 5px solid $brand-warning; + } + &.pretix-widget-day-availability-orange { + background: $brand-warning; + } + } + + .pretix-widget-event-calendar-head { + display: block; + strong { + width: 100%; + display: block; + } + .pretix-widget-event-calendar-next-month, .pretix-widget-event-calendar-previous-month { + display: block; + width: 100%; + text-align: center; + } + } + } +} + +@media (min-width: 1200px) { + .pretix-widget-frame-holder { + .pretix-widget-frame-inner { + left: 50%; + margin-left: -540px; + width: 1080px; + } + .pretix-widget-frame-close { + left: 50%; + margin-left: 528px; + } + } +} + +@media (max-width: 800px) { + .pretix-widget-frame-holder .pretix-widget-frame-inner { + left: 0; + width: 100%; + height: 100%; + top: 0; + background: $brand-primary; + border-radius: 0; + -moz-border-radius: 0; + -webkit-border-radius: 0; + box-shadow: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + padding: 40px 0 0 0; + } + .pretix-widget-frame-holder .pretix-widget-frame-close { + right: 20px; + top: 20px; + background: white; + svg path { + fill: $brand-primary; + } + } +}