From df74cbf5fc466d25b83e848a9e0169234635283f Mon Sep 17 00:00:00 2001 From: Richard Schreiber Date: Tue, 29 Sep 2026 10:14:21 +0200 Subject: [PATCH] Remove Vue2-based widget (#6610) * Remove Vue2-based widget * move floatformat.js * Delete docready.js * Update widget.py --- src/pretix/base/settings.py | 4 - src/pretix/control/forms/global_settings.py | 7 - .../templates/pretixpresale/fragment_js.html | 2 +- src/pretix/presale/views/widget.py | 69 +- src/pretix/settings.py | 1 - .../js/{widget => }/floatformat.js | 0 .../pretixpresale/js/widget/docready.js | 81 - .../static/pretixpresale/js/widget/widget.js | 2592 ----------------- src/tests/e2e/conftest.py | 2 - 9 files changed, 18 insertions(+), 2740 deletions(-) rename src/pretix/static/pretixpresale/js/{widget => }/floatformat.js (100%) delete mode 100644 src/pretix/static/pretixpresale/js/widget/docready.js delete mode 100644 src/pretix/static/pretixpresale/js/widget/widget.js diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index aa14716cb6..7af6886f5e 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -3767,10 +3767,6 @@ Your {organizer} team""")) # noqa: W291 'default': "7B227073704964223A2239373943394538343346343131343044463144313834343232393232313734313034353044314339464446394437384337313531303944334643463542433731222C2276657273696F6E223A312C22637265617465644F6E223A313536363233343735303036312C227369676E6174757265223A22333038303036303932613836343838366637306430313037303261303830333038303032303130313331306633303064303630393630383634383031363530333034303230313035303033303830303630393261383634383836663730643031303730313030303061303830333038323033653333303832303338386130303330323031303230323038346333303431343935313964353433363330306130363038326138363438636533643034303330323330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333031653137306433313339333033353331333833303331333333323335333735613137306433323334333033353331333633303331333333323335333735613330356633313235333032333036303335353034303330633163363536333633326437333664373032643632373236663662363537323264373336393637366535663535343333343264353035323466343433313134333031323036303335353034306230633062363934663533323035333739373337343635366437333331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333035393330313330363037326138363438636533643032303130363038326138363438636533643033303130373033343230303034633231353737656465626436633762323231386636386464373039306131323138646337623062643666326332383364383436303935643934616634613534313162383334323065643831316633343037653833333331663163353463336637656233323230643662616435643465666634393238393839336537633066313361333832303231313330383230323064333030633036303335353164313330313031666630343032333030303330316630363033353531643233303431383330313638303134323366323439633434663933653465663237653663346636323836633366613262626664326534623330343530363038326230363031303530353037303130313034333933303337333033353036303832623036303130353035303733303031383632393638373437343730336132663266366636333733373032653631373037303663363532653633366636643266366636333733373033303334326436313730373036633635363136393633363133333330333233303832303131643036303335353164323030343832303131343330383230313130333038323031306330363039326138363438383666373633363430353031333038316665333038316333303630383262303630313035303530373032303233303831623630633831623335323635366336393631366536333635323036663665323037343638363937333230363336353732373436393636363936333631373436353230363237393230363136653739323037303631373237343739323036313733373337353664363537333230363136333633363537303734363136653633363532303666363632303734363836353230373436383635366532303631373037303663363936333631363236633635323037333734363136653634363137323634323037343635373236643733323036313665363432303633366636653634363937343639366636653733323036663636323037353733363532633230363336353732373436393636363936333631373436353230373036663663363936333739323036313665363432303633363537323734363936363639363336313734363936663665323037303732363136333734363936333635323037333734363137343635366436353665373437333265333033363036303832623036303130353035303730323031313632613638373437343730336132663266373737373737326536313730373036633635326536333666366432663633363537323734363936363639363336313734363536313735373436383666373236393734373932663330333430363033353531643166303432643330326233303239613032376130323538363233363837343734373033613266326636333732366332653631373037303663363532653633366636643266363137303730366336353631363936333631333332653633373236633330316430363033353531643065303431363034313439343537646236666435373438313836383938393736326637653537383530376537396235383234333030653036303335353164306630313031666630343034303330323037383033303066303630393261383634383836663736333634303631643034303230353030333030613036303832613836343863653364303430333032303334393030333034363032323130306265303935373166653731653165373335623535653561666163623463373266656234343566333031383532323263373235313030326236316562643666353530323231303064313862333530613564643664643665623137343630333562313165623263653837636661336536616636636264383338303839306463383263646461613633333038323032656533303832303237356130303330323031303230323038343936643266626633613938646139373330306130363038326138363438636533643034303330323330363733313162333031393036303335353034303330633132343137303730366336353230353236663666373432303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343137303730366336353230343936653633326533313062333030393036303335353034303631333032353535333330316531373064333133343330333533303336333233333334333633333330356131373064333233393330333533303336333233333334333633333330356133303761333132653330326330363033353530343033306332353431373037303663363532303431373037303663363936333631373436393666366532303439366537343635363737323631373436393666366532303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343137303730366336353230343936653633326533313062333030393036303335353034303631333032353535333330353933303133303630373261383634386365336430323031303630383261383634386365336430333031303730333432303030346630313731313834313964373634383564353161356532353831303737366538383061326566646537626165346465303864666334623933653133333536643536363562333561653232643039373736306432323465376262613038666437363137636538386362373662623636373062656338653832393834666635343435613338316637333038316634333034363036303832623036303130353035303730313031303433613330333833303336303630383262303630313035303530373330303138363261363837343734373033613266326636663633373337303265363137303730366336353265363336663664326636663633373337303330333432643631373037303663363537323666366637343633363136373333333031643036303335353164306530343136303431343233663234396334346639336534656632376536633466363238366333666132626266643265346233303066303630333535316431333031303166663034303533303033303130316666333031663036303335353164323330343138333031363830313462626230646561313538333338383961613438613939646562656264656261666461636232346162333033373036303335353164316630343330333032653330326361303261613032383836323636383734373437303361326632663633373236633265363137303730366336353265363336663664326636313730373036633635373236663666373436333631363733333265363337323663333030653036303335353164306630313031666630343034303330323031303633303130303630613261383634383836663736333634303630323065303430323035303033303061303630383261383634386365336430343033303230333637303033303634303233303361636637323833353131363939623138366662333563333536636136326266663431376564643930663735346461323865626566313963383135653432623738396638393866373962353939663938643534313064386639646539633266653032333033323264643534343231623061333035373736633564663333383362393036376664313737633263323136643936346663363732363938323132366635346638376137643162393963623962303938393231363130363939306630393932316430303030333138323031386233303832303138373032303130313330383138363330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533303230383463333034313439353139643534333633303064303630393630383634383031363530333034303230313035303061303831393533303138303630393261383634383836663730643031303930333331306230363039326138363438383666373064303130373031333031633036303932613836343838366637306430313039303533313066313730643331333933303338333133393331333733313332333333303561333032613036303932613836343838366637306430313039333433313164333031623330306430363039363038363438303136353033303430323031303530306131306130363038326138363438636533643034303330323330326630363039326138363438383666373064303130393034333132323034323062303731303365313430613462386231376262613230316130336163643036396234653431366232613263383066383661383338313435633239373566633131333030613036303832613836343863653364303430333032303434363330343430323230343639306264636637626461663833636466343934396534633035313039656463663334373665303564373261313264376335666538633033303033343464663032323032363764353863393365626233353031333836363062353730373938613064643731313734316262353864626436613138363633353038353431656565393035303030303030303030303030227D", # NoQA 'type': str, }, - 'widget_vue2_origins': { - 'default': '', - 'type': str, - }, } PERSON_NAME_TITLE_GROUPS = OrderedDict([ ('english_common', (_('Most common English titles'), ( diff --git a/src/pretix/control/forms/global_settings.py b/src/pretix/control/forms/global_settings.py index f35753a027..2a710574da 100644 --- a/src/pretix/control/forms/global_settings.py +++ b/src/pretix/control/forms/global_settings.py @@ -105,13 +105,6 @@ class GlobalSettingsForm(SettingsForm): domain=settings.SITE_URL ) )), - ('widget_vue2_origins', forms.CharField( - widget=forms.Textarea(attrs={'rows': '3'}), - required=False, - # Not translated on purpose, this is a temporary feature and contains too many special case words - label="Vue2 widget origins", - help_text="One origin per line (e.g. https://example.com). Requests from these origins will be served the old vue2-based widget.", - )) ]) responses = register_global_settings.send(self) for r, response in sorted(responses, key=lambda r: str(r[0])): diff --git a/src/pretix/presale/templates/pretixpresale/fragment_js.html b/src/pretix/presale/templates/pretixpresale/fragment_js.html index cde66ed807..0d5b0ea32c 100644 --- a/src/pretix/presale/templates/pretixpresale/fragment_js.html +++ b/src/pretix/presale/templates/pretixpresale/fragment_js.html @@ -12,7 +12,7 @@ - + diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py index bc1b8c42d7..eccea871f2 100644 --- a/src/pretix/presale/views/widget.py +++ b/src/pretix/presale/views/widget.py @@ -122,26 +122,9 @@ def widget_css_etag(request, version, **kwargs): return f'{_get_source_cache_key(version)}-{request.organizer.cache.get_or_set("css_version", default=lambda: int(time.time()))}' -# use vite by default, serve old vue2-based widget only for widget_vue2_origins -def _use_vite(request): - if getattr(settings, 'PRETIX_WIDGET_VUE', False) or "legacy" in request.GET: - return False - origin = request.META.get('HTTP_ORIGIN', '') - gs = GlobalSettingsObject() - vue_origins = gs.settings.get('widget_vue2_origins', as_type=str, default='') - if vue_origins and not origin: - referer = request.META.get('HTTP_REFERER', '') - origin = '/'.join(referer.split('/', 3)[:3]) - if origin and vue_origins: - origins_list = [o.strip() for o in vue_origins.strip().splitlines() if o.strip()] - return origin not in origins_list - return True - - def widget_js_etag(request, version, lang, **kwargs): gs = GlobalSettingsObject() - variant = 'vite' if _use_vite(request) else 'legacy' - return gs.settings.get('widget_checksum_{}_{}_{}'.format(version, lang, variant)) + return gs.settings.get('widget_checksum_{}_{}'.format(version, lang)) @gzip_page @@ -169,16 +152,13 @@ def widget_css(request, version, **kwargs): return resp -def generate_widget_js(version, lang, use_vite=False): +def generate_widget_js(version, lang): code = [] with language(lang): # Provide isolation code.append('(function (siteglobals) {\n') code.append('var module = {}, exports = {};\n') - if use_vite: - code.append('const LANG = "%s";\n' % lang) - else: - code.append('var lang = "%s";\n' % lang) + code.append('const LANG = "%s";\n' % lang) c = JavaScriptCatalog() c.translation = DjangoTranslation(lang, domain='djangojs') @@ -202,23 +182,11 @@ def generate_widget_js(version, lang, use_vite=False): i18n_js = template.render(context) code.append(i18n_js) - if use_vite: - vite_js = finders.find('vite/widget/widget.js') - if not vite_js: - raise FileNotFoundError('Vite widget build not found. Run: npm run build:widget') - with open(vite_js, 'r', encoding='utf-8') as fp: - code.append(fp.read()) - else: - files = [ - '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' if version == version_max else 'pretixpresale/js/widget/widget.v{}.js'.format(version), - ] - for fname in files: - f = finders.find(fname) - with open(f, 'r', encoding='utf-8') as fp: - code.append(fp.read()) + vite_js = finders.find('vite/widget/widget.js') + if not vite_js: + raise FileNotFoundError('Vite widget build not found. Run: npm run build:widget') + with open(vite_js, 'r', encoding='utf-8') as fp: + code.append(fp.read()) if settings.DEBUG: code.append('})(this);\n') @@ -230,14 +198,13 @@ def generate_widget_js(version, lang, use_vite=False): return f"/* v{version} */\n" + code -def get_widget_js(version, lang, use_vite, force_regenerate=False): +def get_widget_js(version, lang, force_regenerate=False): if settings.DEBUG: - return generate_widget_js(version, lang, use_vite=use_vite).encode() + return generate_widget_js(version, lang).encode() - variant = 'vite' if use_vite else 'legacy' - cache_prefix = 'widget_js_data_v{}_{}_{}'.format(version, lang, variant) - settings_key = 'widget_file_v{}_{}_{}'.format(version, lang, variant) - checksum_key = 'widget_checksum_v{}_{}_{}'.format(version, lang, variant) + cache_prefix = 'widget_js_data_v{}_{}'.format(version, lang) + settings_key = 'widget_file_v{}_{}'.format(version, lang) + checksum_key = 'widget_checksum_v{}_{}'.format(version, lang) gs = GlobalSettingsObject() if not force_regenerate: @@ -259,7 +226,7 @@ def get_widget_js(version, lang, use_vite, force_regenerate=False): else: fname = gs.settings.get(settings_key) - data = generate_widget_js(version, lang, use_vite=use_vite).encode() + data = generate_widget_js(version, lang).encode() checksum = hashlib.sha1(data).hexdigest() should_save = ( not fname @@ -267,7 +234,7 @@ def get_widget_js(version, lang, use_vite, force_regenerate=False): ) if should_save: newname = default_storage.save( - 'widget/widget.{}.{}.{}.{}.js'.format(version, lang, variant, checksum), + 'widget/widget.{}.{}.{}.js'.format(version, lang, checksum), ContentFile(data) ) gs.settings.set(settings_key, 'file://' + newname) @@ -284,8 +251,7 @@ def get_widget_js(version, lang, use_vite, force_regenerate=False): def regenerate_all_widget_js(): for lc, ll in settings.LANGUAGES: for version in range(version_min, version_max + 1): - for use_vite in [True, False]: - get_widget_js(version, lc, use_vite, force_regenerate=True) + get_widget_js(version, lc, force_regenerate=True) @gzip_page @@ -297,8 +263,7 @@ def widget_js(request, version, lang, **kwargs): if version < version_min: version = version_min - use_vite = _use_vite(request) - data = get_widget_js(version, lang, use_vite) + data = get_widget_js(version, lang) resp = HttpResponse(data, content_type='text/javascript') resp['Access-Control-Allow-Origin'] = '*' diff --git a/src/pretix/settings.py b/src/pretix/settings.py index c358b91a10..e32ca6e926 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -913,7 +913,6 @@ VITE_DEV_SERVER_PORT = 5173 VITE_DEV_SERVER = f"http://localhost:{VITE_DEV_SERVER_PORT}" VITE_DEV_MODE = DEBUG VITE_IGNORE = False # Used to ignore `collectstatic`/`rebuild` -PRETIX_WIDGET_VUE = os.environ.get('PRETIX_WIDGET_VUE', '') not in ('', '0') if DEBUG: # Reload if settings file changes diff --git a/src/pretix/static/pretixpresale/js/widget/floatformat.js b/src/pretix/static/pretixpresale/js/floatformat.js similarity index 100% rename from src/pretix/static/pretixpresale/js/widget/floatformat.js rename to src/pretix/static/pretixpresale/js/floatformat.js diff --git a/src/pretix/static/pretixpresale/js/widget/docready.js b/src/pretix/static/pretixpresale/js/widget/docready.js deleted file mode 100644 index f23aeef508..0000000000 --- a/src/pretix/static/pretixpresale/js/widget/docready.js +++ /dev/null @@ -1,81 +0,0 @@ -// Pure-js replacement for $.ready -// by John Friend, https://github.com/jfriend00/docReady -// MIT License - -(function (funcName, baseObj) { - 'use strict' - // The public function name defaults to window.docReady - // but you can modify the last line of this function to pass in a different object or method name - // if you want to put them in a different namespace and those will be used instead of - // window.docReady(...) - funcName = funcName || 'docReady' - baseObj = baseObj || window - let readyList = [] - let readyFired = false - let readyEventHandlersInstalled = false - - // call this when the document is ready - // this function protects itself against being called more than once - function ready () { - if (!readyFired) { - // this must be set to true before we start calling callbacks - readyFired = true - for (let i = 0; i < readyList.length; i++) { - // if a callback here happens to add new ready handlers, - // the docReady() function will see that it already fired - // and will schedule the callback to run right after - // this event loop finishes so all handlers will still execute - // in order and no new ones will be added to the readyList - // while we are processing the list - readyList[i].fn.call(window, readyList[i].ctx) - } - // allow any closures held by these functions to free - readyList = [] - } - } - - function readyStateChange () { - if (document.readyState === 'complete') { - ready() - } - } - - // This is the one public interface - // docReady(fn, context); - // the context argument is optional - if present, it will be passed - // as an argument to the callback - baseObj[funcName] = function (callback, context) { - if (typeof callback !== 'function') { - throw new TypeError('callback for docReady(fn) must be a function') - } - // if ready has already fired, then just schedule the callback - // to fire asynchronously, but right away - if (readyFired) { - setTimeout(function () { callback(context) }, 1) - return - } else { - // add the function and context to the list - readyList.push({ fn: callback, ctx: context }) - } - // if document already ready to go, schedule the ready function to run - // IE only safe when readyState is "complete", others safe when readyState is "interactive" - if (document.readyState === 'complete' || (!document.attachEvent && document.readyState === 'interactive')) { - setTimeout(ready, 1) - } else if (!readyEventHandlersInstalled) { - // otherwise if we don't have event handlers installed, install them - if (document.addEventListener) { - // first choice is DOMContentLoaded event - document.addEventListener('DOMContentLoaded', ready, false) - // backup is window load event - window.addEventListener('load', ready, false) - } else { - // must be IE - document.attachEvent('onreadystatechange', readyStateChange) - window.attachEvent('onload', ready) - } - readyEventHandlersInstalled = true - } - } -})('docReady', window) -// modify this previous line to pass in your own method name -// and object for the method to be attached to diff --git a/src/pretix/static/pretixpresale/js/widget/widget.js b/src/pretix/static/pretixpresale/js/widget/widget.js deleted file mode 100644 index 0073f6ca14..0000000000 --- a/src/pretix/static/pretixpresale/js/widget/widget.js +++ /dev/null @@ -1,2592 +0,0 @@ -/* 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 - } -} - -let Vue = module.exports - -let 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'), - 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'), - 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'), - 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'), - } -} - -let setCookie = function (cname, cvalue, exdays) { - let d = new Date() - d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)) - var expires = 'expires=' + d.toUTCString() - if (!cvalue) { - var expires = 'expires=Thu, 01 Jan 1970 00:00:00 GMT' - cvalue = '' - } - let same_site = '' - if (site_is_secure()) { - same_site = ';SameSite=None;Secure' - } - document.cookie = cname + '=' + cvalue + ';' + expires + same_site + ';path=/' -} - -let getCookie = function (name) { - let value = '; ' + document.cookie - let parts = value.split('; ' + name + '=') - if (parts.length == 2) return parts.pop().split(';').shift() || null - else return null -} - -let padNumber = function (number, size) { - let s = String(number) - while (s.length < (size || 2)) { s = '0' + s } - return s -} - -let getISOWeeks = function (y) { - let 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 */ -let api = { - _getJSON: function (endpoint, callback, err_callback) { - let 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) { - let 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('&') - - let 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) - } -} - -let makeid = function (length) { - let text = '' - let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - - for (let 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) -} - -let widget_id = makeid(16) - -/* Vue Components */ -Vue.component('Availbox', { - props: { - item: Object, - variation: Object - }, - 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 - }, - 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 - }, - unavailability_reason_message: function () { - let reason = this.item.current_unavailability_reason || this.variation?.current_unavailability_reason - if (reason) { - return strings['unavailable_' + reason] || reason - } - return '' - }, - 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 () { - let u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?locale=' + lang + '&item=' + this.item.id - if (this.item.has_variations) { - u += '&var=' + this.variation.id - } - if (this.$root.subevent) { - u += '&subevent=' + this.$root.subevent - } - u += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json) + this.$root.consent_parameter - return u - } - }, - mounted: function () { - if (!this.$root.cart_exists && this.$root.itemnum === 1 && (!this.$root.categories[0].items[0].has_variations || this.$root.categories[0].items[0].variations.length < 2) && !this.$root.has_seating_plan ? 1 : 0) { - this.$refs.quantity.value = 1 - if (this.order_max === 1) { - this.$refs.quantity.checked = true - } - } - }, - methods: { - on_step: function (e) { - let t = e.target.tagName == 'BUTTON' ? e.target : e.target.closest('button') - let step = parseFloat(t.getAttribute('data-step')) - let controls = document.getElementById(t.getAttribute('data-controls')) - 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, - })) - } - }, - template: ('
' - + '' - + '
' - + '{{unavailability_reason_message}}' - + '
' - + '
' - + strings.reserved - + '
' - + '
' - + strings.sold_out - + '
' - + '' - + '
' - + '' - + '
' - + '' - + '' - + '' - + '
' - + '
' - + '
') -}) -Vue.component('Pricebox', { - props: { - price: Object, - free_price: Boolean, - field_name: String, - suggested_price: Object, - original_price: String, - mandatory_priced_addons: Boolean, - item_id: Number, - }, - 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(this.price.net, this.$root.currency_places) - } else { - return floatformat(this.price.gross, this.$root.currency_places) - } - }, - display_price_nonlocalized: function () { - if (this.$root.display_net_prices) { - return parseFloat(this.price.net).toFixed(this.$root.currency_places) - } else { - return parseFloat(this.price.gross).toFixed(this.$root.currency_places) - } - }, - suggested_price_nonlocalized: function () { - let price = this.suggested_price - if (price === null) { - price = this.price - } - if (this.$root.display_net_prices) { - return parseFloat(price.net).toFixed(this.$root.currency_places) - } else { - return parseFloat(price.gross).toFixed(this.$root.currency_places) - } - }, - 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(this.original_price, this.$root.currency_places) - }, - 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 - } - }, - show_taxline: function () { - // rate can either be "0.00" or "0" => parseFloat to check - return Number.parseFloat(this.price.rate) && Number.parseFloat(this.price.gross) - }, - 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) - } - } - } - }, - methods: { - stripHTML: function (s) { - let div = document.createElement('div') - div.innerHTML = s - return div.textContent || div.innerText || '' - }, - }, - template: ('
' - + '' - + '' - + ' ' - + '' - + '
' - + '{{ $root.currency }} ' - + '' - + '
' - + '' - + '{{ taxline }}' - + '' - + '
') -}) -Vue.component('Variation', { - 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' - }, - }, - template: ('
' - + '
' - - // Variation description - + '
' - + '
' - + '{{ variation.value }}' - + '
' - + '

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

' - + '
' - + '
' - - // Price - + '
' - + '' - + '' - + ' ' - + '
' - - // Availability - + '
' - + '' - + '
' - - + '
' - + '
' - + '
') -}) -Vue.component('Item', { - props: { - item: Object, - category: Object, - }, - data: function () { - return { - expanded: this.$root.show_variations_expanded - } - }, - 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, this.$root.currency_places) - }, 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, this.$root.currency_places) + ' – ' - + floatformat(this.item.max_price, this.$root.currency_places) - } 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, this.$root.currency_places) - } - }, - variationsToggleLabel: function () { - return this.expanded ? strings.hide_variations : strings.variations - }, - }, - mounted: function () { - if (this.$refs.variations) { - if (!this.expanded) { - let $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) { - let 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, - } - }, - }, - template: ('
' - + '
' - - // Product description - + '
' - + '' - + '
' - + '{{ item.name }}' - + '
' - + '

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

' - + '

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

' - + '
' - + '
' - - // Price - + '
' - + '' - + '' - + '
' - + ' ' - + '
' - - // Availability - + '
' - + '' - + '' - + '
' - - + '
' - + '
' - - // Variations - + '
' - + '' - + '' - + '
' - - + '
') -}) -Vue.component('Category', { - props: { - category: Object - }, - template: ('
' - + '

{{ category.name }}

' - + '
' - + '
' - + '
' - + '' - + '
' - + '
') -}) - -let 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 { - let url = this.$root.formAction + '&locale=' + lang + '&ajax=1' - this.$root.overlay.frame_loading = true - - this.async_task_interval = 100 - let 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.set_cart_id(data.cart_id) - } - if (data.redirect.substr(0, 1) === '/') { - data.redirect = this.$root.target_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, '$1') + data.redirect - } - let url = data.redirect - if (url.indexOf('?')) { - url = url + '&iframe=1&locale=' + lang + '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id()) - } else { - url = url + '?iframe=1&locale=' + lang + '&take_cart_id=' + encodeURIComponent(this.$root.get_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) { - let 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 () { - if (!this.$root.get_cart_id() && this.$root.keep_cart) { - // create an empty cart whose id we can persist - this.$root.create_cart(this.resume) - return - } - let redirect_url - redirect_url = this.$root.target_url + 'w/' + widget_id + '/' - if (this.$root.subevent && this.$root.is_button && this.$root.items.length === 0) { - // button with subevent but no items - redirect_url += this.$root.subevent + '/' - } - redirect_url += '?iframe=1&locale=' + lang - if (this.$root.get_cart_id()) { - redirect_url += '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id()) - if (this.$root.keep_cart) { - // make sure the cart-id is used, even if the cart is currently empty - redirect_url += '&ajax=1' - } - } - 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) - } - }, -} - -let 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, - } -} - -let shared_loading_fragment = ( - '
' - + '' - + '
' -) - -let shared_iframe_fragment = ( - '' - + '
' - + '' - + '

' + strings.cancel_blocked + '

' - + '
' - + '
' - + '
' - + '' - + '
' - + '
' -) - -let shared_alert_fragment = ( - '' - + '
' - + '

{{ $root.error_message }}

' - + '

' - + '

' - + '
' - + '' - + '' - + '' - + '
' -) - -let shared_lightbox_fragment = ( - '' - + '
' - + '' - + '
' - + '
' - + '
' - + '
' - + '' - + '
{{$root.lightbox.description}}
' - + '
' - + '
' - + '
' -) - -Vue.component('PretixOverlay', { - data: function () { - return { - cancelBlocked: false, - } - }, - 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 () { - 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, - } - }, - cancelBlockedClasses: function () { - return { - 'pretix-widget-visibility-hidden': !this.cancelBlocked, - } - }, - }, - watch: { - '$root.lightbox': function (newValue, oldValue) { - if (newValue) { - if (newValue.image != oldValue?.image) { - this.$set(newValue, 'loading', true) - } - if (!oldValue) { - this.$el?.querySelector('.pretix-widget-lightbox-holder').showModal() - } - } - }, - '$root.error_message': function (newValue, oldValue) { - if (newValue) { - if (!oldValue) { - this.$el?.querySelector('.pretix-widget-alert-holder').showModal() - } - } - }, - '$root.frame_shown': function (newValue) { - if (newValue) { - let btn = this.$el?.querySelector('.pretix-widget-frame-close button') - this.$nextTick(function () { - btn.focus() - }) - } - }, - }, - mounted () { - window.addEventListener('message', this.onMessage, false) - }, - unmounted () { - window.removeEventListener('message', this.onMessage, false) - }, - methods: { - onMessage: function (e) { - if (e.data.type && e.data.type == 'pretix:widget:title') { - this.$el.querySelector('iframe').title = e.data.title - } - }, - lightboxClose: function () { - this.$root.lightbox = null - }, - lightboxLoaded: function () { - this.$root.lightbox.loading = false - }, - errorClose: function (e) { - 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) - } else if (this.$root.overlay) { - this.$root.overlay.frame_src = this.$root.error_url_after - this.$root.frame_loading = true - } - }, - 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.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 - } - } - }, - }, - template: ('
' - + shared_iframe_fragment - + shared_alert_fragment - + shared_lightbox_fragment - + '
' - ) -}) - -Vue.component('PretixWidgetEventForm', { - data: function () { - return { - is_items_selected: false, - } - }, - computed: { - 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 () { - 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 () { - let i, j, k, all_free = true - for (i = 0; i < this.$root.categories.length; i++) { - let cat = this.$root.categories[i] - for (j = 0; j < cat.items.length; j++) { - let item = cat.items[j] - for (k = 0; k < item.variations.length; k++) { - let 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 () { - let params = new URL(this.$root.voucherFormTarget).searchParams - params.delete('iframe') - params.delete('take_cart_id') - return [...params.entries()] - }, - }, - 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.$refs.form.addEventListener('change', this.calc_items_selected) - }, - beforeUnmount: function () { - this.$root.$off('focus_voucher_field', this.focus_voucher_field) - this.$refs.form.removeEventListener('change', this.calc_items_selected) - }, - methods: { - 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' - } - - let $el = this.$root.$el - this.$root.$nextTick(function () { - // wait for redraw, then focus content element for better a11y - $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 - }) - }, - }, - 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'] + '

' - + '
' - + '
' - + '' - + '
' - + '' - + '
' - + '' - + '
' - + '
' - + '
' - + '
' - - + '
' - ) -}) - -Vue.component('PretixWidgetEventListFilterField', { - props: { - field: Object - }, - computed: { - id: function () { - return widget_id + '_' + this.field.key - }, - currentValue: function () { - let filterParams = new URLSearchParams(this.$root.filter) - return filterParams.get(this.field.key) || '' - }, - }, - template: ('
' - + '' - + '' - + '
'), -}) - -Vue.component('PretixWidgetEventListFilterForm', { - methods: { - onSubmit: function (e) { - e.preventDefault() - let formData = new FormData(this.$refs.filterform) - let 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() - }, - }, - template: ('
' - + '
' - + '' + strings.filter_events_by + '' - + '' - + '' - + '
' - + '
'), -}) - -Vue.component('PretixWidgetEventListEntry', { - props: { - event: Object - }, - computed: { - classObject: function () { - let 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() - } - }, - template: ('' - + '
{{ event.name }}
' - + '' - + '
{{ location }}
' // hidden by css for now, but - // used by a few people - + '
{{ event.availability.text }}
' - + '
') -}) - -Vue.component('PretixWidgetEventList', { - 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() - } - }, - template: ('
' - + '' - + '
' - + '{{ $root.name }}' - + '
' - + '
' - + '' - + '' - + '

' - + '
') -}) - -Vue.component('PretixWidgetEventCalendarEvent', { - props: { - event: Object, - describedby: String, - }, - computed: { - classObject: function () { - let 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() - } - }, - template: ('' - + '' - + '{{ event.name }}' - + '' - + '
{{ event.time }}
' - + '
{{ event.availability.text }}
' - + '
') -}) - -Vue.component('PretixWidgetEventWeekCell', { - props: { - day: Object, - }, - 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 () { - let o = {} - if (this.day && this.day.events.length > 0) { - o['pretix-widget-has-events'] = true - let best = 'red' - let all_low = true - for (let i = 0; i < this.day.events.length; i++) { - let 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 - } - }, - methods: { - selectDay: function () { - if (!this.day || !this.day.events.length || !this.$parent.$parent.$parent.mobile) { - return - } - if (this.day.events.length === 1) { - let 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' - } - } - }, - template: ('
' - + '
' - + '{{ dayhead }}' - + '
' - + '
' - + '' - + '
' - + '
') -}) - -Vue.component('PretixWidgetEventCalendarCell', { - props: { - day: Object, - }, - 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 () { - let o = {} - if (this.day && this.day.events.length > 0) { - o['pretix-widget-has-events'] = true - let best = 'red' - let all_low = true - for (let i = 0; i < this.day.events.length; i++) { - let 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 - } - }, - 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) - } - } - }, - mounted: function () { - if (this.role == 'button') { - this.$el.addEventListener('click', this.selectDay) - this.$el.addEventListener('keydown', this.onKeyDown) - } - }, - 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) { - let 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) { - let 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) - } - }, - }, - template: ('' - + '
' - + '{{ daynum }}' - + '
' - + '
' - + '' - + '
' - + '') -}) - -Vue.component('PretixWidgetEventCalendarRow', { - props: { - week: Array - }, - template: ('' - + '' - + ''), -}) - -Vue.component('PretixWidgetEventCalendar', { - 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' - }, - aria_labelledby: function () { - return this.$root.html_id + '-event-calendar-table-label' - }, - }, - methods: { - back_to_list: function () { - this.$root.weeks = undefined - this.$root.view = 'events' - this.$root.name = null - this.$root.frontpage_text = null - }, - prevmonth: function () { - let curMonth = parseInt(this.$root.date.substr(5, 2)) - let 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 () { - let curMonth = parseInt(this.$root.date.substr(5, 2)) - let 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 }) - } - }, - 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'] + '
' - + '
'), -}) - -Vue.component('PretixWidgetEventWeekCalendar', { - 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 () { - let curWeek = this.$root.week[1] - let 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 () { - let curWeek = this.$root.week[1] - let 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 () { - let curWeek = this.$root.week[1] - let 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 }) - } - }, - template: ('
' - // Back navigation - + '' - - // Event header - + '
' - + '{{ $root.name }}' - + '
' - - // Filter - + '' - - // Calendar navigation - + '
' - + '' - - // Actual calendar - + '
' - + '
' - + '' - + '' - + '
' - + '
' - - + '
' - + ''), -}) - -Vue.component('PretixWidget', { - data: shared_widget_data, - computed: { - classObject: function () { - return { - 'pretix-widget': true, - 'pretix-widget-mobile': this.mobile, - 'pretix-widget-use-custom-spinners': true, - } - } - }, - mounted: function () { - let thisObj = this - let resizeObserver = new ResizeObserver(function (entries) { - thisObj.mobile = entries[0].contentRect.width <= 800 - }) - resizeObserver.observe(this.$refs.wrapper) - }, - methods: shared_methods, - template: ('
' - + '
' - + shared_loading_fragment - + '
{{ $root.error }}
' - + '' - + '' - + '' - + '' - + '' - + '
' - + '
' - + '
' - + '
' - + '
' - + '' - ) -}) - -Vue.component('PretixButton', { - data: shared_widget_data, - methods: shared_methods, - template: ('
' - + '
' - + '
' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + '
' - + '
' - + '
' - + '
' - + '' - ), -}) - -/* Function to create the actual Vue instances */ - -let shared_root_methods = { - open_link_in_frame: function (event) { - let 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 (let i = 0; i < window.PretixWidget._loaded.length; i++) { - window.PretixWidget._loaded[i]() - } - }) - }, - trigger_close_callback: function () { - this.$nextTick(function () { - for (let i = 0; i < window.PretixWidget._closed.length; i++) { - window.PretixWidget._closed[i]() - } - }) - }, - reload: function (opt = {}) { - let 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) - } - if (this.$root.voucher_code) { - url += '&voucher=' + encodeURIComponent(this.$root.voucher_code) - } - if (this.$root.get_cart_id()) { - url += '&cart_id=' + encodeURIComponent(this.$root.get_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) - } - let root = this.$root - api._getJSON(url, function (data, xhr) { - if (typeof xhr.responseURL !== 'undefined') { - let new_url = xhr.responseURL.substr(0, xhr.responseURL.indexOf('/widget/product_list?') + 1) - let 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.currency_places = data.currency_places - root.display_net_prices = data.display_net_prices - 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_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 = '' - root.currency_places = 2 - 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 () { - let 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 () { - let 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.get_cart_id()) { - redirect_url += '&take_cart_id=' + encodeURIComponent(this.$root.get_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() - }, - create_cart: function (callback) { - let url = this.$root.target_url + 'w/' + widget_id + '/cart/create?ajax=1' - - this.$root.overlay.frame_loading = true - api._getJSON(url, (data) => { - this.$root.set_cart_id(data.cart_id) - this.$root.overlay.frame_loading = false - callback() - }, (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 - } else { - this.$root.overlay.error_message = strings['cart_error'] - this.$root.overlay.frame_loading = false - } - }) - }, - get_cart_id: function () { - if (!this.$root.keep_cart) { - return null - } - if (this.$root.cart_id) { - return this.$root.cart_id - } - return getCookie(this.$root.cookieName) - }, - set_cart_id: function (newValue) { - this.$root.cart_id = newValue - setCookie(this.$root.cookieName, newValue, 30) - }, -} - -let shared_root_computed = { - cookieName: function () { - return 'pretix_widget_' + this.target_url.replace(/[^a-zA-Z0-9]+/g, '_') - }, - formTarget: function () { - let is_firefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 - let 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 () { - let form_target = this.target_url + 'w/' + widget_id + '/redeem?iframe=1&locale=' + lang - if (this.get_cart_id()) { - form_target += '&take_cart_id=' + encodeURIComponent(this.get_cart_id()) - } - 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) { - let 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 - } - let 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 - } - let form_target = this.target_url + 'w/' + widget_id + '/cart/add?iframe=1&next=' + encodeURIComponent(checkout_url) - if (this.get_cart_id()) { - form_target += '&take_cart_id=' + encodeURIComponent(this.get_cart_id()) - } - form_target += this.$root.consent_parameter - return form_target - }, - newTabTarget: function () { - let target = this.target_url - if (this.subevent) { - target = this.target_url + this.subevent + '/' - } - let parameters = this.$root.consent_parameter - if (this.$root.additionalURLParams) { - parameters += `&${this.$root.additionalURLParams}` - } - if (parameters) { - target += '?' + parameters.replace(/^&/, '') - } - 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 () { - let has_priced = false - let cnt_items = 0 - for (let i = 0; i < this.categories.length; i++) { - for (let j = 0; j < this.categories[i].items.length; j++) { - let 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 () { - let 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 '' - } - let params = new URLSearchParams(window.location.search) - for (let [key, value] of params.entries()) { - if (!key.startsWith('utm_')) { - params.delete(key) - } - } - return params.toString() - }, -} - -let create_overlay = function (app) { - let elem = document.createElement('pretix-overlay') - document.body.appendChild(elem) - - let framechild = new Vue({ - el: elem, - props: { - frame_src: String, - }, - 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, - } - }, - 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_loading: function (newValue) { - let dialog = this.$el?.querySelector('dialog.pretix-widget-frame-holder') - if (newValue) { - if (!dialog.open) { - dialog.showModal() - } - } else { - if (!this.frame_src && dialog.open) { // finished loading, but no iframe to display => close - dialog.close() - } - } - }, - }, - methods: { - } - }) - app.$root.overlay = framechild -} - -function get_ga_client_id (tracking_id) { - if (typeof ga === 'undefined') { - return null - } - try { - let trackers = ga.getAll() - let 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 -} - -let create_widget = function (element, html_id = null) { - let target_url = element.attributes.event.value - if (!target_url.match(/\/$/)) { - target_url += '/' - } - let voucher = element.attributes.voucher ? element.attributes.voucher.value : null - let subevent = element.attributes.subevent ? element.attributes.subevent.value : null - let style = element.attributes['list-type'] ? element.attributes['list-type'].value : (element.attributes.style ? element.attributes.style.value : null) - let skip_ssl = element.attributes['skip-ssl-check'] ? true : false - let disable_iframe = element.attributes['disable-iframe'] ? true : false - let disable_vouchers = element.attributes['disable-vouchers'] ? true : false - let disable_filters = element.attributes['disable-filters'] ? true : false - let 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 - } - let widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data)) - let filter = element.attributes.filter ? element.attributes.filter.value : null - let items = element.attributes.items ? element.attributes.items.value : null - let variations = element.attributes.variations ? element.attributes.variations.value : null - let categories = element.attributes.categories ? element.attributes.categories.value : null - for (let i = 0; i < element.attributes.length; i++) { - let 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) - - let 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)) - } - }) - }) - let 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, - currency_places: 2, - 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, - 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: [], - keep_cart: true, - cart_id: null - } - }, - computed: shared_root_computed, - 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 - let rect = this.$el.getBoundingClientRect() - if (rect.top < 0) { - this.$el.scrollIntoView() - } - } - } - }, - created: function () { - this.reload() - }, - mounted: function () { - observer.observe(this.$el, observerOptions) - }, - methods: shared_root_methods - }) - create_overlay(app) - return app -} - -let create_button = function (element, html_id = null) { - let target_url = element.attributes.event.value - if (!target_url.match(/\/$/)) { - target_url += '/' - } - let voucher = element.attributes.voucher ? element.attributes.voucher.value : null - let subevent = element.attributes.subevent ? element.attributes.subevent.value : null - let raw_items = element.attributes.items ? element.attributes.items.value : '' - let skip_ssl = element.attributes['skip-ssl-check'] ? true : false - let disable_iframe = element.attributes['disable-iframe'] ? true : false - let keep_cart = element.attributes['keep-cart'] ? true : false - let button_text = element.innerHTML - let widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data)) - for (var i = 0; i < element.attributes.length; i++) { - let 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) - - let 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)) - } - }) - }) - let 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) - } - - let itemsplit = raw_items.split(',') - let items = [] - for (var i = 0; i < itemsplit.length; i++) { - if (itemsplit[i].indexOf('=') > 0) { - let 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, - keep_cart: keep_cart || items.length > 0, - cart_id: null - } - }, - computed: shared_root_computed, - created: function () { - }, - mounted: function () { - observer.observe(this.$el, observerOptions) - }, - 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 () { - let widgets = document.querySelectorAll('pretix-widget, div.pretix-widget-compat') - let wlength = widgets.length - for (var i = 0; i < wlength; i++) { - let widget = widgets[i] - widgetlist.push(create_widget(widget, widget.id || 'pretix-widget-' + i)) - } - - let buttons = document.querySelectorAll('pretix-button, div.pretix-button-compat') - let blength = buttons.length - for (var i = 0; i < blength; i++) { - let 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 += '/' - } - - let 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] }) - } - let root = document.createElement('div') - document.body.appendChild(root) - root.classList.add('pretix-widget-hidden') - root.innerHTML = '' - let 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: '', - keep_cart: true, - cart_id: null - } - }, - computed: shared_root_computed, - created: function () { - }, - 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/tests/e2e/conftest.py b/src/tests/e2e/conftest.py index 7c223083a1..f0308e838a 100644 --- a/src/tests/e2e/conftest.py +++ b/src/tests/e2e/conftest.py @@ -89,8 +89,6 @@ def _widget_assets(): f'Start it with: npm run dev:widget' ) yield - elif os.environ.get("PRETIX_WIDGET_VUE"): - yield # Old widget, no build needed else: subprocess.check_call(['npm', 'run', 'build:widget'], cwd=PROJECT_ROOT) yield