Dialog for cart renewal, async task without page refresh (#5148)

* async_task: deduplicate response handling code

* extend cart without full page reload

* update dialog markup

* fix error response from CartExtend

* refactor asynctask, make sure waitingDialog.show() re-initializes dialog contents

* add cart expiry notification

* add aria references to other dialogs

* improve error handling

* fix error if max_extend=None

* different message for expiring soon and expired carts

* refactor dialog css

* add classes to further dialog elements

* switch extend-cart-dialog and loadingmodal to <dialog>

* Backport simple_block_tag from Django 5.2

* Use simple_block_tag for {% dialog %} tag

* add alertdialog role

* Update src/pretix/static/pretixbase/scss/_dialogs.scss

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* fix mobile dialog styles not being overwritten

* asynctask dialog: prevent close by escape on chrome

* remove dynamic aria-live from #cart-deadline

dynamic aria-live is generally not well supported and as we have the dialog now anyways, we can remove it

* move continue-button to right

* Update src/pretix/static/pretixpresale/js/ui/cart.js

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Fix CSS for old-style dialog

* fix heading display/level

* align dialogs at the top as they originally were

* fix </div> from merge-conflict

* fix missing grow for dialog-content

* improve cart-extend-button ui

* do not show cart-extend-dialog onload

* improve message if 0 minutes

* do not save messae in session if ajax_dont_redirect

* add ajax_dont_redirect to async_task_check_url

* improve draw_deadline to only update #cart-deadline if necessary

* add renew-confirmation-message

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
luelista
2025-05-27 07:17:50 +02:00
committed by GitHub
parent fdbcffd5fd
commit 5962536a11
19 changed files with 621 additions and 556 deletions

View File

@@ -1655,7 +1655,7 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,)) @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def extend_cart_reservation(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None: def extend_cart_reservation(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> dict:
""" """
Resets the expiry time of a cart to the configured reservation time of this event. Resets the expiry time of a cart to the configured reservation time of this event.
Limited to 11x the reservation time. Limited to 11x the reservation time.
@@ -1672,7 +1672,7 @@ def extend_cart_reservation(self, event: Event, cart_id: str=None, locale='en',
try: try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel) cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
cm.commit() cm.commit()
return cm.num_extended_positions return {"success": cm.num_extended_positions, "expiry": cm._expiry, "max_expiry_extend": cm._max_expiry_extend}
except LockTimeoutException: except LockTimeoutException:
self.retry() self.retry()
except (MaxRetriesExceededError, LockTimeoutException): except (MaxRetriesExceededError, LockTimeoutException):

View File

@@ -23,38 +23,39 @@ from django import template
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from pretix.helpers.templatetags.simple_block_tag import (
register_simple_block_tag,
)
from django.utils.translation import gettext_lazy as _ # NOQA from django.utils.translation import gettext_lazy as _ # NOQA
register = template.Library() register = template.Library()
@register.simple_tag @register_simple_block_tag(register)
def dialog(html_id, label, description, *args, **kwargs): def dialog(content, html_id, title, description, *args, **kwargs):
format_kwargs = { format_kwargs = {
"id": html_id, "id": html_id,
"label": label, "title": title,
"description": description, "description": description,
"icon": format_html('<div class="modal-card-icon"><span class="fa fa-{}" aria-hidden="true"></span></div>', kwargs["icon"]) if "icon" in kwargs else "", "icon": format_html('<div class="modal-card-icon"><span class="fa fa-{}" aria-hidden="true"></span></div>', kwargs["icon"]) if "icon" in kwargs else "",
"alert": mark_safe('role="alertdialog"') if kwargs.get("alert", "False") != "False" else "", "alert": mark_safe('role="alertdialog"') if kwargs.get("alert", "False") != "False" else "",
"content": content,
} }
result = """ result = """
<dialog {alert} <dialog {alert}
id="{id}" id="{id}" class="modal-card"
aria-labelledby="{id}-label" aria-labelledby="{id}-title"
aria-describedby="{id}-description"> aria-describedby="{id}-description">
<form method="dialog" class="modal-card form-horizontal"> <form method="dialog" class="modal-card-inner form-horizontal">
{icon} {icon}
<div class="modal-card-content"> <div class="modal-card-content">
<h2 id="{id}-label">{label}</h2> <h2 id="{id}-title" class="modal-card-title h3">{title}</h2>
<p id="{id}-description">{description}</p> <p id="{id}-description" class="modal-card-description">{description}</p>
""" {content}
return format_html(result, **format_kwargs)
@register.simple_tag
def enddialog(*args, **kwargs):
return mark_safe("""
</div> </div>
</form> </form>
</dialog> </dialog>
""") """
return format_html(result, **format_kwargs)

View File

@@ -68,7 +68,7 @@ class AsyncMixin:
def get_check_url(self, task_id, ajax): def get_check_url(self, task_id, ajax):
return self.request.path + '?async_id=%s' % task_id + ('&ajax=1' if ajax else '') return self.request.path + '?async_id=%s' % task_id + ('&ajax=1' if ajax else '')
def _ajax_response_data(self): def _ajax_response_data(self, value):
return {} return {}
def _return_ajax_result(self, res, timeout=.5): def _return_ajax_result(self, res, timeout=.5):
@@ -85,7 +85,7 @@ class AsyncMixin:
logger.warning('Ignored ResponseError in AsyncResult.get()') logger.warning('Ignored ResponseError in AsyncResult.get()')
except ConnectionError: except ConnectionError:
# Redis probably just restarted, let's just report not ready and retry next time # Redis probably just restarted, let's just report not ready and retry next time
data = self._ajax_response_data() data = self._ajax_response_data(None)
data.update({ data.update({
'async_id': res.id, 'async_id': res.id,
'ready': False 'ready': False
@@ -93,7 +93,7 @@ class AsyncMixin:
return data return data
state, info = res.state, res.info state, info = res.state, res.info
data = self._ajax_response_data() data = self._ajax_response_data(info)
data.update({ data.update({
'async_id': res.id, 'async_id': res.id,
'ready': ready, 'ready': ready,
@@ -102,23 +102,21 @@ class AsyncMixin:
if ready: if ready:
if state == states.SUCCESS and not isinstance(info, Exception): if state == states.SUCCESS and not isinstance(info, Exception):
smes = self.get_success_message(info) smes = self.get_success_message(info)
if smes: if smes and 'ajax_dont_redirect' not in self.request.GET and 'ajax_dont_redirect' not in self.request.POST:
messages.success(self.request, smes) messages.success(self.request, smes)
# TODO: Do not store message if the ajax client states that it will not redirect
# but handle the message itself
data.update({ data.update({
'redirect': self.get_success_url(info), 'redirect': self.get_success_url(info),
'success': True, 'success': True,
'message': str(self.get_success_message(info)) 'message': str(smes)
}) })
else: else:
messages.error(self.request, self.get_error_message(info)) smes = self.get_error_message(info)
# TODO: Do not store message if the ajax client states that it will not redirect if smes and 'ajax_dont_redirect' not in self.request.GET and 'ajax_dont_redirect' not in self.request.POST:
# but handle the message itself messages.error(self.request, smes)
data.update({ data.update({
'redirect': self.get_error_url(), 'redirect': self.get_error_url(),
'success': False, 'success': False,
'message': str(self.get_error_message(info)) 'message': str(smes)
}) })
elif state == 'PROGRESS': elif state == 'PROGRESS':
data.update({ data.update({

View File

@@ -4,6 +4,8 @@
{% load statici18n %} {% load statici18n %}
{% load eventsignal %} {% load eventsignal %}
{% load eventurl %} {% load eventurl %}
{% load dialog %}
{% load icon %}
<!DOCTYPE html> <!DOCTYPE html>
<html{% if rtl %} dir="rtl" class="rtl"{% endif %}> <html{% if rtl %} dir="rtl" class="rtl"{% endif %}>
<head> <head>
@@ -463,16 +465,9 @@
</div> </div>
</div> </div>
</div> </div>
<div id="ajaxerr"> <div id="ajaxerr" class="modal-wrapper" hidden>
</div> </div>
<div id="loadingmodal"> {% dialog "loadingmodal" "" "" icon="cog rotating" %}
<div class="modal-card">
<div class="modal-card-icon">
<i class="fa fa-cog big-rotating-icon"></i>
</div>
<div class="modal-card-content">
<h3></h3>
<p class="text"></p>
<p class="status">{% trans "If this takes longer than a few minutes, please contact us." %}</p> <p class="status">{% trans "If this takes longer than a few minutes, please contact us." %}</p>
<div class="progress"> <div class="progress">
<div class="progress-bar progress-bar-success"> <div class="progress-bar progress-bar-success">
@@ -480,8 +475,6 @@
</div> </div>
<div class="steps"> <div class="steps">
</div> </div>
</div> {% enddialog %}
</div>
</div>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,133 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
#
# backport from Django 5.2 (django/django/template/library.py)
#
# TODO: remove once we upgrade to Django 5.2
from functools import wraps
from inspect import getfullargspec, unwrap
from django.template.exceptions import TemplateSyntaxError
from django.template.library import SimpleNode, parse_bits
class SimpleBlockNode(SimpleNode):
def __init__(self, nodelist, *args, **kwargs):
super().__init__(*args, **kwargs)
self.nodelist = nodelist
def get_resolved_arguments(self, context):
resolved_args, resolved_kwargs = super().get_resolved_arguments(context)
# Restore the "content" argument.
# It will move depending on whether takes_context was passed.
resolved_args.insert(
1 if self.takes_context else 0, self.nodelist.render(context)
)
return resolved_args, resolved_kwargs
def register_simple_block_tag(library, func=None, takes_context=None, name=None, end_name=None):
"""
Register a callable as a compiled block template tag. Example:
@register_simple_block_tag(register)
def hello(content):
return 'world'
"""
def dec(func):
nonlocal end_name
(
params,
varargs,
varkw,
defaults,
kwonly,
kwonly_defaults,
_,
) = getfullargspec(unwrap(func))
function_name = name or func.__name__
if end_name is None:
end_name = f"end{function_name}"
@wraps(func)
def compile_func(parser, token):
tag_params = params.copy()
if takes_context:
if len(tag_params) >= 2 and tag_params[1] == "content":
del tag_params[1]
else:
raise TemplateSyntaxError(
f"{function_name!r} is decorated with takes_context=True so"
" it must have a first argument of 'context' and a second "
"argument of 'content'"
)
elif tag_params and tag_params[0] == "content":
del tag_params[0]
else:
raise TemplateSyntaxError(
f"'{function_name}' must have a first argument of 'content'"
)
bits = token.split_contents()[1:]
target_var = None
if len(bits) >= 2 and bits[-2] == "as":
target_var = bits[-1]
bits = bits[:-2]
nodelist = parser.parse((end_name,))
parser.delete_first_token()
args, kwargs = parse_bits(
parser,
bits,
tag_params,
varargs,
varkw,
defaults,
kwonly,
kwonly_defaults,
takes_context,
function_name,
)
return SimpleBlockNode(
nodelist, func, takes_context, args, kwargs, target_var
)
library.tag(function_name, compile_func)
return func
if func is None:
# @register.simple_block_tag(...)
return dec
elif callable(func):
# @register.simple_block_tag
return dec(func)
else:
raise ValueError("Invalid arguments provided to simple_block_tag")

View File

@@ -495,7 +495,7 @@
<form class="text-muted" id="cart-extend-form" data-asynctask data-asynctask-no-redirect <form class="text-muted" id="cart-extend-form" data-asynctask data-asynctask-no-redirect
method="post" action="{% eventurl request.event "presale:event.cart.extend" cart_namespace=cart_namespace %}"> method="post" action="{% eventurl request.event "presale:event.cart.extend" cart_namespace=cart_namespace %}">
{% csrf_token %} {% csrf_token %}
<span id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}" data-max-expiry-extend="{{ cart.max_expiry_extend|date:"Y-m-d H:i:sO" }}"> <p id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}" data-max-expiry-extend="{{ cart.max_expiry_extend|date:"Y-m-d H:i:sO" }}">
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %} {% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
{% blocktrans trimmed with minutes=cart.minutes_left %} {% blocktrans trimmed with minutes=cart.minutes_left %}
The items in your cart are reserved for you for {{ minutes }} minutes. The items in your cart are reserved for you for {{ minutes }} minutes.
@@ -503,9 +503,12 @@
{% else %} {% else %}
{% trans "The items in your cart are no longer reserved for you. You can still complete your order as long as theyre available." %} {% trans "The items in your cart are no longer reserved for you. You can still complete your order as long as theyre available." %}
{% endif %} {% endif %}
</span> </p>
<button class="btn btn-link" type="submit" id="cart-extend-button"> <p>
<i class="fa fa-refresh" aria-hidden="true"></i> {% trans "Extend" %}</button> <button class="btn btn-default" type="submit" id="cart-extend-button" aria-describedby="cart-deadline">
<i class="fa fa-refresh" aria-hidden="true"></i> {% trans "Extend reservation" %}
</button>
</p>
</form> </form>
{% else %} {% else %}
<p class="sr-only" id="cart-description">{% trans "Overview of your ordered products." %}</p> <p class="sr-only" id="cart-description">{% trans "Overview of your ordered products." %}</p>

View File

@@ -4,7 +4,7 @@
{% load rich_text %} {% load rich_text %}
{% load money %} {% load money %}
<details class="panel {% if open %}panel-primary{% else %}panel-default{% endif %} cart" {% if open %}open{% endif %}> <details class="panel {% if open %}panel-primary{% else %}panel-default{% endif %} cart" {% if open %}open{% endif %}>
<summary class="panel-heading"> <summary class="panel-heading" aria-describedby="cart-deadline">
<h2 class="panel-title"> <h2 class="panel-title">
<span> <span>
<i class="fa fa-shopping-cart" aria-hidden="true"></i> <i class="fa fa-shopping-cart" aria-hidden="true"></i>

View File

@@ -4,18 +4,19 @@
{% load escapejson %} {% load escapejson %}
{% load icon %} {% load icon %}
{% load dialog %} {% load dialog %}
<div id="ajaxerr"> <div id="ajaxerr" class="modal-wrapper" hidden>
</div> </div>
<div id="popupmodal" hidden aria-live="polite"> <div id="popupmodal" class="modal-wrapper" hidden aria-live="polite" role="dialog"
aria-labelledby="popupmodal-title">
<div class="modal-card"> <div class="modal-card">
<div class="modal-card-icon"> <div class="modal-card-icon">
<i class="fa fa-window-restore big-icon" aria-hidden="true"></i> <i class="fa fa-window-restore big-icon" aria-hidden="true"></i>
</div> </div>
<div class="modal-card-content"> <div class="modal-card-content">
<div> <div>
<h3> <h2 id="popupmodal-title" class="h3">
{% trans "We've started the requested process in a new window." %} {% trans "We've started the requested process in a new window." %}
</h3> </h2>
<p class="text"> <p class="text">
{% trans "If you do not see the new window, we can help you launch it again." %} {% trans "If you do not see the new window, we can help you launch it again." %}
</p> </p>
@@ -32,20 +33,21 @@
</div> </div>
</div> </div>
</div> </div>
<div id="loadingmodal" hidden aria-live="polite">
<div class="modal-card"> {% dialog "loadingmodal" "" "" icon="cog rotating" %}
<div class="modal-card-icon">
<i class="fa fa-cog big-rotating-icon" aria-hidden="true"></i>
</div>
<div class="modal-card-content">
<h3 id="loadingmodal-label"></h3>
<div id="loadingmodal-description">
<p class="text"></p>
<p class="status">{% trans "If this takes longer than a few minutes, please contact us." %}</p> <p class="status">{% trans "If this takes longer than a few minutes, please contact us." %}</p>
<div class="progress">
<div class="progress-bar progress-bar-success">
</div> </div>
</div> </div>
<div class="steps">
</div> </div>
</div> {% enddialog %}
{% trans "Please let us know you're still there." as label_cart_extend_dialog %}
{% dialog "dialog-cart-extend" label_cart_extend_dialog "" icon="clock-o" alert=true %}
<p class="modal-card-confirm"><button class="btn btn-lg btn-primary">{% trans "Continue" %}</button></p>
{% enddialog %}
<dialog id="lightbox-dialog" role="alertdialog" aria-labelledby="lightbox-label"> <dialog id="lightbox-dialog" role="alertdialog" aria-labelledby="lightbox-label">
<form method="dialog" class="modal-card"> <form method="dialog" class="modal-card">

View File

@@ -229,11 +229,13 @@ class CartMixin:
try: try:
first_expiry = min(p.expires for p in positions) if positions else now() first_expiry = min(p.expires for p in positions) if positions else now()
max_expiry_extend = min((p.max_extend for p in positions if p.max_extend), default=None)
total_seconds_left = max(first_expiry - now(), timedelta()).total_seconds() total_seconds_left = max(first_expiry - now(), timedelta()).total_seconds()
minutes_left = int(total_seconds_left // 60) minutes_left = int(total_seconds_left // 60)
seconds_left = int(total_seconds_left % 60) seconds_left = int(total_seconds_left % 60)
except AttributeError: except AttributeError:
first_expiry = None first_expiry = None
max_expiry_extend = None
minutes_left = None minutes_left = None
seconds_left = None seconds_left = None
@@ -250,6 +252,7 @@ class CartMixin:
'minutes_left': minutes_left, 'minutes_left': minutes_left,
'seconds_left': seconds_left, 'seconds_left': seconds_left,
'first_expiry': first_expiry, 'first_expiry': first_expiry,
'max_expiry_extend': max_expiry_extend,
'is_ordered': bool(order), 'is_ordered': bool(order),
'itemcount': sum(c.count for c in positions if not c.addon_to), 'itemcount': sum(c.count for c in positions if not c.addon_to),
'current_selected_payments': [p for p in self.current_selected_payments(total) if p.get('multi_use_supported')] 'current_selected_payments': [p for p in self.current_selected_payments(total) if p.get('multi_use_supported')]

View File

@@ -542,8 +542,14 @@ class CartExtendReservation(EventViewMixin, CartActionMixin, AsyncAction, View):
task = extend_cart_reservation task = extend_cart_reservation
known_errortypes = ['CartError'] known_errortypes = ['CartError']
def _ajax_response_data(self, value):
if isinstance(value, dict):
return value
else:
return {}
def get_success_message(self, value): def get_success_message(self, value):
if value > 0: if value['success'] > 0:
return _('Your cart timeout was extended.') return _('Your cart timeout was extended.')
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@@ -561,7 +567,7 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
def get_success_message(self, value): def get_success_message(self, value):
return _('The products have been successfully added to your cart.') return _('The products have been successfully added to your cart.')
def _ajax_response_data(self): def _ajax_response_data(self, value):
cart_id = get_or_create_cart_id(self.request) cart_id = get_or_create_cart_id(self.request)
return { return {
'cart_id': cart_id, 'cart_id': cart_id,

View File

@@ -5,74 +5,94 @@ var async_task_check_url = null;
var async_task_old_url = null; var async_task_old_url = null;
var async_task_is_download = false; var async_task_is_download = false;
var async_task_is_long = false; var async_task_is_long = false;
var async_task_dont_redirect = false;
function async_task_check() { var async_task_status_messages = {
long_task_started: gettext(
'Your request is currently being processed. Depending on the size of your event, this might take up to ' +
'a few minutes.'
),
long_task_pending: gettext(
'Your request has been queued on the server and will soon be ' +
'processed.'
),
short_task: gettext(
'Your request arrived on the server but we still wait for it to be ' +
'processed. If this takes longer than two minutes, please contact us or go ' +
'back in your browser and try again.'
)
};
function async_task_schedule_check(context, timeout) {
"use strict"; "use strict";
async_task_timeout = window.setTimeout(function() {
$.ajax( $.ajax(
{ {
'type': 'GET', 'type': 'GET',
'url': async_task_check_url, 'url': async_task_check_url,
'success': async_task_check_callback, 'success': async_task_check_callback,
'error': async_task_check_error, 'error': async_task_check_error,
'context': this, 'context': context,
'dataType': 'json' 'dataType': 'json'
} }
); );
}, timeout);
} }
function async_task_check_callback(data, textStatus, jqXHR) { function async_task_on_success(data) {
"use strict"; "use strict";
if (data.ready && data.redirect) { if ((async_task_is_download && data.success) || async_task_dont_redirect) {
if (async_task_is_download && data.success) {
waitingDialog.hide(); waitingDialog.hide();
if (location.href.indexOf("async_id") !== -1) { if (location.href.indexOf("async_id") !== -1) {
history.replaceState({}, "pretix", async_task_old_url); history.replaceState({}, "pretix", async_task_old_url);
} }
} }
if (!async_task_dont_redirect)
location.href = data.redirect; location.href = data.redirect;
return; $(this).trigger('pretix:async-task-success', data);
} else if (typeof data.percentage === "number") {
$("#loadingmodal .progress").show();
$("#loadingmodal .progress .progress-bar").css("width", data.percentage + "%");
if (typeof data.steps === "object" && Array.isArray(data.steps)) {
var $steps = $("#loadingmodal .steps");
$steps.html("").show()
for (var step of data.steps) {
$steps.append(
$("<span>").addClass("fa fa-fw")
.toggleClass("fa-check text-success", step.done)
.toggleClass("fa-cog fa-spin text-muted", !step.done)
).append(
$("<span>").text(step.label)
).append(
$("<br>")
)
} }
}
}
async_task_timeout = window.setTimeout(async_task_check, 250);
function async_task_check_callback(data, textStatus, jqXHR) {
"use strict";
if (data.ready && data.redirect) {
async_task_on_success.call(this, data);
return;
}
if (typeof data.percentage === "number") {
waitingDialog.setProgress(data.percentage);
}
if (typeof data.steps === "object" && Array.isArray(data.steps)) {
waitingDialog.setSteps(data.steps);
}
async_task_schedule_check(this, 250);
async_task_update_status(data);
}
function async_task_update_status(data) {
if (async_task_is_long) { if (async_task_is_long) {
if (data.started) { if (data.started) {
$("#loadingmodal p.status").text(gettext( waitingDialog.setStatus(async_task_status_messages.long_task_started);
'Your request is currently being processed. Depending on the size of your event, this might take up to ' +
'a few minutes.'
));
} else { } else {
$("#loadingmodal p.status").text(gettext( waitingDialog.setStatus(async_task_status_messages.long_task_pending);
'Your request has been queued on the server and will soon be ' +
'processed.'
));
} }
} else { } else {
$("#loadingmodal p.status").text(gettext( waitingDialog.setStatus(async_task_status_messages.short_task);
'Your request arrived on the server but we still wait for it to be ' +
'processed. If this takes longer than two minutes, please contact us or go ' +
'back in your browser and try again.'
));
} }
} }
function async_task_replace_page(target, new_html) {
"use strict";
waitingDialog.hide();
$(target).html(new_html);
setup_basics($(target));
form_handlers($(target));
setup_collapsible_details($(target));
window.setTimeout(function () { $(window).scrollTop(0) }, 200)
$(document).trigger("pretix:bind-forms");
}
function async_task_check_error(jqXHR, textStatus, errorThrown) { function async_task_check_error(jqXHR, textStatus, errorThrown) {
"use strict"; "use strict";
var respdom = $(jqXHR.responseText); var respdom = $(jqXHR.responseText);
@@ -80,16 +100,10 @@ function async_task_check_error(jqXHR, textStatus, errorThrown) {
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) { if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
// This is a failed form validation, let's just use it // This is a failed form validation, let's just use it
$("body").data('ajaxing', false); $("body").data('ajaxing', false);
waitingDialog.hide(); async_task_replace_page("body", jqXHR.responseText.substring(
$("body").html(jqXHR.responseText.substring(
jqXHR.responseText.indexOf("<body"), jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body") jqXHR.responseText.indexOf("</body")
)); ));
setup_basics($("body"));
form_handlers($("body"));
setup_collapsible_details($("body"));
window.setTimeout(function () { $(window).scrollTop(0) }, 200)
$(document).trigger("pretix:bind-forms");
} else if (c.length > 0) { } else if (c.length > 0) {
// This is some kind of 500/404/403 page, show it in an overlay // This is some kind of 500/404/403 page, show it in an overlay
$("body").data('ajaxing', false); $("body").data('ajaxing', false);
@@ -105,9 +119,9 @@ function async_task_check_error(jqXHR, textStatus, errorThrown) {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status)); alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
} else { } else {
// 500 can be an application error or overload in some cases :( // 500 can be an application error or overload in some cases :(
$("#loadingmodal p.status").text(gettext('We currently cannot reach the server, but we keep trying.' + waitingDialog.setStatus(gettext('We currently cannot reach the server, but we keep trying.' +
' Last error code: {code}').replace(/\{code\}/, jqXHR.status)); ' Last error code: {code}').replace(/\{code\}/, jqXHR.status));
async_task_timeout = window.setTimeout(async_task_check, 5000); async_task_schedule_check(this, 5000);
} }
} }
} }
@@ -116,38 +130,19 @@ function async_task_callback(data, jqXHR, status) {
"use strict"; "use strict";
$("body").data('ajaxing', false); $("body").data('ajaxing', false);
if (data.redirect) { if (data.redirect) {
if (async_task_is_download && data.success) { async_task_on_success.call(this, data);
waitingDialog.hide();
if (location.href.indexOf("async_id") !== -1) {
history.replaceState({}, "pretix", async_task_old_url);
}
}
location.href = data.redirect;
return; return;
} }
var check_url = new URL(data.check_url);
if (async_task_dont_redirect) {
check_url.searchParams.set('ajax_dont_redirect', '1');
}
async_task_id = data.async_id; async_task_id = data.async_id;
async_task_check_url = data.check_url; async_task_check_url = check_url.toString();
async_task_timeout = window.setTimeout(async_task_check, 100); async_task_schedule_check(this, 100);
async_task_update_status(data);
if (async_task_is_long) {
if (data.started) {
$("#loadingmodal p.status").text(gettext(
'Your request is currently being processed. Depending on the size of your event, this might take up to ' +
'a few minutes.'
));
} else {
$("#loadingmodal p.status").text(gettext(
'Your request has been queued on the server and will soon be ' +
'processed.'
));
}
} else {
$("#loadingmodal p.status").text(gettext(
'Your request arrived on the server but we still wait for it to be ' +
'processed. If this takes longer than two minutes, please contact us or go ' +
'back in your browser and try again.'
));
}
if (location.href.indexOf("async_id") === -1) { if (location.href.indexOf("async_id") === -1) {
history.pushState({}, "Waiting", async_task_check_url.replace(/ajax=1/, '')); history.pushState({}, "Waiting", async_task_check_url.replace(/ajax=1/, ''));
} }
@@ -156,48 +151,34 @@ function async_task_callback(data, jqXHR, status) {
function async_task_error(jqXHR, textStatus, errorThrown) { function async_task_error(jqXHR, textStatus, errorThrown) {
"use strict"; "use strict";
$("body").data('ajaxing', false); $("body").data('ajaxing', false);
waitingDialog.hide();
if (textStatus === "timeout") { if (textStatus === "timeout") {
alert(gettext("The request took too long. Please try again.")); alert(gettext("The request took too long. Please try again."));
waitingDialog.hide();
} else if (jqXHR.responseText.indexOf('<html') > 0) { } else if (jqXHR.responseText.indexOf('<html') > 0) {
var respdom = $(jqXHR.responseText); var respdom = $(jqXHR.responseText);
var c = respdom.filter('.container'); var c = respdom.filter('.container');
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) { if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
// This is a failed form validation, let's just use it // This is a failed form validation, let's just use it
waitingDialog.hide();
if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) { if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) {
$("#page-wrapper").html(respdom.find("#page-wrapper").html()); async_task_replace_page("#page-wrapper", respdom.find("#page-wrapper").html());
setup_basics($("#page-wrapper"));
form_handlers($("#page-wrapper"));
setup_collapsible_details($("#page-wrapper"));
$(document).trigger("pretix:bind-forms");
window.setTimeout(function () { $(window).scrollTop(0) }, 200)
} else { } else {
$("body").html(jqXHR.responseText.substring( async_task_replace_page("body", jqXHR.responseText.substring(
jqXHR.responseText.indexOf("<body"), jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body") jqXHR.responseText.indexOf("</body")
)); ));
setup_basics($("body"));
form_handlers($("body"));
setup_collapsible_details($("body"));
$(document).trigger("pretix:bind-forms");
window.setTimeout(function () { $(window).scrollTop(0) }, 200)
} }
} else if (c.length > 0) { } else if (c.length > 0) {
waitingDialog.hide(); // This is some kind of 500/404/403 page, show it in an overlay
ajaxErrDialog.show(c.first().html()); ajaxErrDialog.show(c.first().html());
} else { } else {
waitingDialog.hide();
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status)); alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
} }
} else { } else {
if (jqXHR.status >= 400 && jqXHR.status < 500) { if (jqXHR.status >= 400 && jqXHR.status < 500) {
waitingDialog.hide();
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status)); alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
} else { } else {
waitingDialog.hide();
alert(gettext('We currently cannot reach the server. Please try again. ' + alert(gettext('We currently cannot reach the server. Please try again. ' +
'Error code: {code}').replace(/\{code\}/, jqXHR.status)); 'Error code: {code}').replace(/\{code\}/, jqXHR.status));
} }
@@ -221,28 +202,26 @@ $(function () {
} }
async_task_id = null; async_task_id = null;
async_task_is_download = $(this).is("[data-asynctask-download]"); async_task_is_download = $(this).is("[data-asynctask-download]");
async_task_dont_redirect = $(this).is("[data-asynctask-no-redirect]");
async_task_is_long = $(this).is("[data-asynctask-long]"); async_task_is_long = $(this).is("[data-asynctask-long]");
async_task_old_url = location.href; async_task_old_url = location.href;
$("body").data('ajaxing', true); $("body").data('ajaxing', true);
if ($(this).is("[data-asynctask-headline]")) { waitingDialog.show(
waitingDialog.show($(this).attr("data-asynctask-headline")); $(this).attr("data-asynctask-headline") || gettext('We are processing your request …'),
} else { $(this).attr("data-asynctask-text") || '',
waitingDialog.show(gettext('We are processing your request …')); gettext(
}
if ($(this).is("[data-asynctask-text]")) {
$("#loadingmodal p.text").text($(this).attr("data-asynctask-text")).show();
} else {
$("#loadingmodal p.text").hide();
}
$("#loadingmodal p.status").text(gettext(
'We are currently sending your request to the server. If this takes longer ' + 'We are currently sending your request to the server. If this takes longer ' +
'than one minute, please check your internet connection and then reload ' + 'than one minute, please check your internet connection and then reload ' +
'this page and try again.' 'this page and try again.'
)); )
);
var action = this.action; var action = this.action;
var formData = new FormData(this); var formData = new FormData(this);
formData.append('ajax', '1'); formData.append('ajax', '1');
if (async_task_dont_redirect) {
formData.append('ajax_dont_redirect', '1');
}
if (submitter && submitter.name) { if (submitter && submitter.name) {
formData.append(submitter.name, submitter.value); formData.append(submitter.name, submitter.value);
} }
@@ -275,21 +254,66 @@ $(function () {
}, 10); }, 10);
} }
}, false); }, false);
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
$("#loadingmodal").on("cancel", function() {
return false;
});
$("#loadingmodal").prop("closedBy", "none");
}); });
var waitingDialog = { var waitingDialog = {
show: function (message) { show: function (title, text, status) {
"use strict"; "use strict";
$("#loadingmodal h3").html(message); this.setTitle(title);
$("#loadingmodal .progress").hide(); this.setText(text);
$("#loadingmodal .steps").hide(); this.setStatus(status || gettext('If this takes longer than a few minutes, please contact us.'));
$("body").addClass("loading"); this.setProgress(null);
$("#loadingmodal").removeAttr("hidden"); this.setSteps(null);
document.getElementById("loadingmodal").showModal();
}, },
hide: function () { hide: function () {
"use strict"; "use strict";
$("body").removeClass("loading"); document.getElementById("loadingmodal").close();
$("#loadingmodal").attr("hidden", true); },
setTitle: function(title) {
$("#loadingmodal .modal-card-title").text(title);
},
setStatus: function(statusText) {
$("#loadingmodal p.status").text(statusText);
},
setText: function(text) {
if (text)
$("#loadingmodal .modal-card-description").text(text).show();
else
$("#loadingmodal .modal-card-description").hide();
},
setProgress: function(percentage) {
if (typeof percentage === 'number') {
$("#loadingmodal .progress").show();
$("#loadingmodal .progress .progress-bar").css("width", percentage + "%");
} else {
$("#loadingmodal .progress").hide();
}
},
setSteps: function(steps) {
var $steps = $("#loadingmodal .steps");
if (steps) {
$steps.html("").show()
for (var step of steps) {
$steps.append(
$("<span>").addClass("fa fa-fw")
.toggleClass("fa-check text-success", step.done)
.toggleClass("fa-cog fa-spin text-muted", !step.done)
).append(
$("<span>").text(step.label)
).append(
$("<br>")
)
}
} else {
$steps.hide();
}
} }
}; };
@@ -299,10 +323,10 @@ var ajaxErrDialog = {
$("#ajaxerr").html(c); $("#ajaxerr").html(c);
$("#ajaxerr .links").html("<a class='btn btn-default ajaxerr-close'>" $("#ajaxerr .links").html("<a class='btn btn-default ajaxerr-close'>"
+ gettext("Close message") + "</a>"); + gettext("Close message") + "</a>");
$("body").addClass("ajaxerr"); $("body").addClass("ajaxerr has-modal-dialog");
}, },
hide: function () { hide: function () {
"use strict"; "use strict";
$("body").removeClass("ajaxerr"); $("body").removeClass("ajaxerr has-modal-dialog");
} },
}; };

View File

@@ -0,0 +1,174 @@
/* Modal dialogs using HTML5 dialog tags for accessibility */
dialog.modal-card {
border: none;
width: 80%;
max-width: 43em;
padding: 0;
margin-top: 60px;
box-shadow: 0 7px 14px 0 rgba(78, 50, 92, 0.1),0 3px 6px 0 rgba(0,0,0,.07);
background: white;
border-radius: $border-radius-large;
opacity: 0;
transition: opacity .5s allow-discrete;
}
.modal-card-inner {
display: flex;
flex-direction: column;
align-content: stretch;
}
dialog.modal-card .modal-card-icon {
background: $brand-primary;
font-size: 2em;
color: white;
text-align: center;
padding: 3px;
.rotating {
-webkit-animation: fa-spin 8s infinite linear;
animation: fa-spin 8s infinite linear;
}
}
dialog.modal-card .modal-card-content {
padding: 1.5em;
flex-grow: 1;
}
.modal-card-content>*:last-child {
margin-bottom: 0;
}
.modal-card-content>*:first-child {
margin-top: 0;
}
.modal-card-confirm {
margin-top: 2em;
display: flex;
justify-content: flex-end;
gap: 1em;
align-items: center;
}
.modal-card-confirm-spread {
justify-content: space-between;
}
dialog::backdrop {
background-color: rgba(255, 255, 255, .5);
opacity: 0;
transition: opacity .5s allow-discrete;
backdrop-filter: blur(2px);
}
dialog[open], dialog[open]::backdrop {
opacity: 1;
}
@starting-style {
dialog[open], dialog[open]::backdrop {
opacity: 0;
}
}
@media screen and (min-width: $screen-sm-min) {
dialog.modal-card:has(.modal-card-icon) .modal-card-inner {
flex-direction: row;
}
dialog.modal-card .modal-card-content {
padding: 2em;
}
dialog.modal-card .modal-card-icon {
font-size: 4em;
padding: 6px 16px;
}
}
.shake-once {
animation: shake .2s;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
}
@keyframes 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); }
}
/* Legacy dialogs (still used for #ajaxerr and #popupmodal) */
body.has-modal-dialog .container, body.has-modal-dialog #wrapper {
-webkit-filter: blur(2px);
-moz-filter: blur(2px);
-ms-filter: blur(2px);
-o-filter: blur(2px);
filter: blur(2px);
}
.modal-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(255, 255, 255, .7);
z-index: 900000;
padding: 10px;
.modal-card {
margin: 50px auto 0;
width: 90%;
max-width: 600px;
max-height: calc(100vh - 100px);
overflow-y: auto;
background: white;
border-radius: $border-radius-large;
box-shadow: 0 7px 14px 0 rgba(78, 50, 92, 0.1),0 3px 6px 0 rgba(0,0,0,.07);
padding: 20px;
min-height: 160px;
border: 0;
.modal-card-icon {
float: left;
width: 150px;
text-align: center;
.big-icon {
margin-top: 10px;
font-size: 100px;
color: $brand-primary;
}
}
.modal-card-content {
margin-left: 160px;
text-align: left;
h3 {
margin-top: 0;
}
}
}
}
@media (max-width: 700px) {
.modal-wrapper .modal-card {
margin: 25px auto 0;
max-height: calc(100vh - 50px - 20px);
.modal-card-icon {
float: none;
width: 100%;
}
.modal-card-content {
text-align: center;
margin-left: 0;
margin-right: 0;
margin-top: 10px;
}
}
}
#ajaxerr {
background: rgba(236, 236, 236, .9);
.big-icon {
margin-top: 50px;
font-size: 200px;
color: $brand-primary;
}
}

View File

@@ -263,117 +263,3 @@ svg.svg-icon {
@include table-row-variant('warning', var(--pretix-brand-warning-lighten-40), var(--pretix-brand-warning-lighten-35)); @include table-row-variant('warning', var(--pretix-brand-warning-lighten-40), var(--pretix-brand-warning-lighten-35));
@include table-row-variant('danger', var(--pretix-brand-danger-lighten-30), var(--pretix-brand-danger-lighten-25)); @include table-row-variant('danger', var(--pretix-brand-danger-lighten-30), var(--pretix-brand-danger-lighten-25));
dialog {
border: none;
width: 80%;
max-width: 43em;
padding: 0;
box-shadow: 0 7px 14px 0 rgba(78, 50, 92, 0.1),0 3px 6px 0 rgba(0,0,0,.07);
background: white;
border-radius: $border-radius-large;
opacity: 0;
transition: opacity .5s allow-discrete;
.modal-card {
display: flex;
flex-direction: column;
align-content: stretch;
}
.modal-card-icon {
background: $brand-primary;
font-size: 2em;
color: white;
text-align: center;
padding: 3px;
}
.modal-card-content {
padding: 1.5em;
}
.modal-card-content>*:last-child {
margin-bottom: 0;
}
.modal-card-content>*:first-child {
margin-top: 0;
}
.modal-card-confirm {
margin-top: 2em;
display: flex;
justify-content: flex-end;
gap: 1em;
align-items: center;
}
.modal-card-confirm-spread {
justify-content: space-between;
}
}
dialog::backdrop {
background-color: rgba(255, 255, 255, .5);
opacity: 0;
transition: opacity .5s allow-discrete;
}
dialog[open], dialog[open]::backdrop {
opacity: 1;
}
@starting-style {
dialog[open], dialog[open]::backdrop {
opacity: 0;
}
}
@media screen and (min-width: $screen-sm-min) {
dialog {
.modal-card:has(.modal-card-icon) {
flex-direction: row;
}
.modal-card-content {
padding: 2em;
}
.modal-card-icon {
font-size: 4em;
padding: 6px 16px;
}
}
}
.shake-once {
animation: shake .2s;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
}
@keyframes 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); }
}
#lightbox-dialog {
width: fit-content;
max-width: 80%;
min-width: 24em;
.modal-card-content {
padding: 2.5em;
}
img {
max-width: 100%;
}
button {
position: absolute;
top: 1em;
right: 1em;
}
figcaption {
margin-top: 1em;
}
}

View File

@@ -51,32 +51,6 @@ function formatPrice(price, currency, locale) {
} }
} }
var waitingDialog = {
show: function (message) {
"use strict";
$("#loadingmodal").find("h1").html(message);
$("body").addClass("loading");
},
hide: function () {
"use strict";
$("body").removeClass("loading");
}
};
var ajaxErrDialog = {
show: function (c) {
"use strict";
$("#ajaxerr").html(c);
$("#ajaxerr .links").html("<a class='btn btn-default ajaxerr-close'>"
+ gettext("Close message") + "</a>");
$("body").addClass("ajaxerr");
},
hide: function () {
"use strict";
$("body").removeClass("ajaxerr");
}
};
var apiGET = function (url, callback) { var apiGET = function (url, callback) {
$.getJSON(url, function (data) { $.getJSON(url, function (data) {
callback(data); callback(data);

View File

@@ -3,6 +3,7 @@
@import "../../bootstrap/scss/_bootstrap.scss"; @import "../../bootstrap/scss/_bootstrap.scss";
@import "../../fontawesome/scss/font-awesome.scss"; @import "../../fontawesome/scss/font-awesome.scss";
@import "../../pretixbase/scss/_theme.scss"; @import "../../pretixbase/scss/_theme.scss";
@import "../../pretixbase/scss/_dialogs.scss";
@import "../../typeahead/typeahead.scss"; @import "../../typeahead/typeahead.scss";
@import "../../charts/morris.scss"; @import "../../charts/morris.scss";
@import "../../cropper/cropper.scss"; @import "../../cropper/cropper.scss";
@@ -234,91 +235,6 @@ p.bigger {
} }
} }
body.loading #wrapper {
-webkit-filter: blur(2px);
-moz-filter: blur(2px);
-ms-filter: blur(2px);
-o-filter: blur(2px);
filter: blur(2px);
}
#loadingmodal, #ajaxerr {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(255, 255, 255, .7);
opacity: 0;
z-index: 900000;
visibility: hidden;
padding: 10px;
.big-icon {
margin-top: 50px;
font-size: 200px;
color: $brand-primary;
}
.modal-card {
margin: 50px auto 0;
top: 50px;
width: 90%;
max-width: 600px;
background: white;
border-radius: $border-radius-large;
box-shadow: 0 7px 14px 0 rgba(78, 50, 92, 0.1),0 3px 6px 0 rgba(0,0,0,.07);
padding: 20px;
min-height: 160px;
.modal-card-icon {
float: left;
width: 150px;
text-align: center;
.big-rotating-icon {
font-size: 120px;
margin: 0;
}
}
.modal-card-content {
margin-left: 160px;
text-align: left;
h3 {
margin-top: 0;
}
}
}
}
@media (max-width: 700px) {
#loadingmodal, #ajaxerr {
.modal-card {
.modal-card-icon {
float: none;
width: 100%;
}
.modal-card-content {
text-align: center;
margin-left: 0;
margin-right: 0;
margin-top: 10px;
}
}
}
}
#ajaxerr {
background: rgba(236, 236, 236, .9);
}
.loading #loadingmodal, .ajaxerr #ajaxerr {
opacity: 1;
visibility: visible;
transition: opacity .5s ease-in-out;
-moz-transition: opacity .5s ease-in-out;
-webkit-transition: opacity .5s ease-in-out;
}
.big-rotating-icon { .big-rotating-icon {
margin-top: 50px; margin-top: 50px;
-webkit-animation: fa-spin 8s infinite linear; -webkit-animation: fa-spin 8s infinite linear;

View File

@@ -1,9 +1,11 @@
/*global $,gettext,ngettext */ /*global $,gettext,ngettext */
var cart = { var cart = {
_deadline: null, _deadline: null,
_deadline_interval: null, _deadline_timeout: null,
_deadline_call: 0, _deadline_call: 0,
_time_offset: 0, _time_offset: 0,
_prev_diff_minutes: 0,
_renewed_message: "",
_get_now: function () { _get_now: function () {
return moment().add(cart._time_offset, 'ms'); return moment().add(cart._time_offset, 'ms');
@@ -20,7 +22,12 @@ var cart = {
cart._time_offset = server_time - client_time; cart._time_offset = server_time - client_time;
}, },
draw_deadline: function () { show_expiry_notification: function () {
document.getElementById("dialog-cart-extend").showModal();
cart._expiry_notified = true;
},
draw_deadline: function (renewed="") {
function pad(n, width, z) { function pad(n, width, z) {
z = z || '0'; z = z || '0';
n = n + ''; n = n + '';
@@ -33,36 +40,73 @@ var cart = {
return; return;
} }
var now = cart._get_now(); var now = cart._get_now();
var diff_minutes = Math.floor(cart._deadline.diff(now) / 1000 / 60); var diff_total_seconds = cart._deadline.diff(now) / 1000;
var diff_seconds = Math.floor(cart._deadline.diff(now) / 1000 % 60); var diff_minutes = Math.floor(diff_total_seconds / 60);
var diff_seconds = Math.floor(diff_total_seconds % 60);
if (diff_minutes < 2 || diff_minutes == 5) $("#cart-deadline").get(0).setAttribute("aria-live", "polite");
else $("#cart-deadline").get(0).removeAttribute("aria-live");
if (diff_minutes < 0) { if (diff_minutes < 0) {
$("#cart-deadline").text(gettext("The items in your cart are no longer reserved for you. You can still complete your order as long as theyre available.")); $("#cart-deadline").text(gettext("The items in your cart are no longer reserved for you. You can still complete your order as long as theyre available."));
$("#cart-deadline-short").text( $("#cart-deadline-short").text(
gettext("Cart expired") gettext("Cart expired")
); );
window.clearInterval(cart._deadline_interval); if (!cart._deadline_timeout) {
// no timeout => first time draw_deadline is invoked, but cart already expired => do not show dialog
cart._expiry_notified = true;
}
} else { } else {
$("#cart-deadline").text(ngettext( if (diff_minutes !== cart._prev_diff_minutes) {
if (diff_minutes == 0) {
$("#cart-deadline").text(gettext("Your cart is about to expire. If you want to continue, please click here:"))
} else {
$("#cart-deadline").text(
cart._renewed_message + " " +
ngettext(
"The items in your cart are reserved for you for one minute.", "The items in your cart are reserved for you for one minute.",
"The items in your cart are reserved for you for {num} minutes.", "The items in your cart are reserved for you for {num} minutes.",
diff_minutes diff_minutes
).replace(/\{num\}/g, diff_minutes)); ).replace(/\{num\}/g, diff_minutes)
);
}
cart._prev_diff_minutes = diff_minutes;
}
$("#cart-deadline-short").text( $("#cart-deadline-short").text(
pad(diff_minutes.toString(), 2) + ':' + pad(diff_seconds.toString(), 2) pad(diff_minutes.toString(), 2) + ':' + pad(diff_seconds.toString(), 2)
); );
cart._renewed_message = "";
cart._deadline_timeout = window.setTimeout(cart.draw_deadline, 500);
}
var already_expired = diff_total_seconds <= 0;
var can_extend_cart = diff_minutes < 3 && (already_expired || cart._deadline < cart._max_extend);
$("#cart-extend-button").toggle(can_extend_cart);
if (can_extend_cart && diff_total_seconds < 45) {
if (!cart._expiry_notified) cart.show_expiry_notification();
$("#dialog-cart-extend-description").text(already_expired
? gettext("Your cart has expired. If you want to continue, please click here:")
: gettext("Your cart is about to expire. If you want to continue, please click here:"));
} }
$("#cart-extend-button").toggle(diff_minutes < 3);
}, },
init: function () { init: function () {
"use strict"; "use strict";
cart._deadline = moment($("#cart-deadline").attr("data-expires"));
cart._deadline_interval = window.setInterval(cart.draw_deadline, 500);
cart._calc_offset(); cart._calc_offset();
cart.set_deadline(
$("#cart-deadline").attr("data-expires"),
$("#cart-deadline").attr("data-max-expiry-extend")
);
},
set_deadline: function (expiry, max_extend, renewed_message) {
"use strict";
cart._expiry_notified = false;
cart._deadline = moment(expiry);
if (cart._deadline_timeout) {
window.clearTimeout(cart._deadline_timeout);
}
cart._deadline_timeout = null;
cart._max_extend = moment(max_extend);
cart._renewed_message = renewed_message || "";
cart.draw_deadline(); cart.draw_deadline();
} }
}; };
@@ -74,6 +118,22 @@ $(function () {
cart.init(); cart.init();
} }
$("#cart-extend-form").on("pretix:async-task-success", function(e, data) {
if (data.success) {
cart.set_deadline(data.expiry, data.max_expiry_extend, data.message);
var cart_panel_heading = $(this).closest(".panel").find(".panel-heading").get(0);
if (cart_panel_heading) {
cart_panel_heading.focus();
}
} else {
alert(data.message);
}
});
$("#dialog-cart-extend form").submit(function() {
$("#cart-extend-form").submit();
});
$(".toggle-container").each(function() { $(".toggle-container").each(function() {
var summary = $(".toggle-summary", this); var summary = $(".toggle-summary", this);
var content = $("> :not(.toggle-summary)", this); var content = $("> :not(.toggle-summary)", this);

View File

@@ -407,8 +407,6 @@ $(function () {
$("#voucher-toggle").slideUp(); $("#voucher-toggle").slideUp();
}); });
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
// Handlers for "Copy answers from above" buttons // Handlers for "Copy answers from above" buttons
$(".js-copy-answers").click(function (e) { $(".js-copy-answers").click(function (e) {
e.preventDefault(); e.preventDefault();

View File

@@ -4,8 +4,6 @@ $(function () {
var popup_window = null var popup_window = null
var popup_check_interval = null var popup_check_interval = null
$("#popupmodal").removeAttr("hidden");
$("a[data-open-in-popup-window]").on("click", function (e) { $("a[data-open-in-popup-window]").on("click", function (e) {
e.preventDefault() e.preventDefault()
@@ -22,11 +20,13 @@ $(function () {
"presale-popup", "presale-popup",
"scrollbars=yes,resizable=yes,status=yes,location=yes,toolbar=no,menubar=no,width=940,height=620,left=50,top=50" "scrollbars=yes,resizable=yes,status=yes,location=yes,toolbar=no,menubar=no,width=940,height=620,left=50,top=50"
) )
$("body").addClass("has-popup") $("body").addClass("has-popup has-modal-dialog")
$("#popupmodal").removeAttr("hidden");
popup_check_interval = window.setInterval(function () { popup_check_interval = window.setInterval(function () {
if (popup_window.closed) { if (popup_window.closed) {
$("body").removeClass("has-popup") $("body").removeClass("has-popup has-modal-dialog")
$("#popupmodal").attr("hidden", true);
window.clearInterval(popup_check_interval) window.clearInterval(popup_check_interval)
} }
}, 250) }, 250)

View File

@@ -44,6 +44,7 @@ $headings-small-color: $text-muted;
@import "../../bootstrap/scss/_bootstrap_reduced.scss"; @import "../../bootstrap/scss/_bootstrap_reduced.scss";
@import "../../pretixbase/scss/_theme.scss"; @import "../../pretixbase/scss/_theme.scss";
@import "../../pretixbase/scss/_dialogs.scss";
@import "../../lightbox/css/lightbox.scss"; @import "../../lightbox/css/lightbox.scss";
@import "../../cropper/cropper.scss"; @import "../../cropper/cropper.scss";
@import "../../datetimepicker/_bootstrap-datetimepicker.scss"; @import "../../datetimepicker/_bootstrap-datetimepicker.scss";
@@ -277,119 +278,12 @@ a:hover .panel-primary > .panel-heading {
margin-right: auto; margin-right: auto;
} }
body.loading .container {
-webkit-filter: blur(2px);
-moz-filter: blur(2px);
-ms-filter: blur(2px);
-o-filter: blur(2px);
filter: blur(2px);
}
.big-rotating-icon { .big-rotating-icon {
-webkit-animation: fa-spin 8s infinite linear; -webkit-animation: fa-spin 8s infinite linear;
animation: fa-spin 8s infinite linear; animation: fa-spin 8s infinite linear;
font-size: 120px; font-size: 120px;
color: $brand-primary; color: $brand-primary;
} }
#loadingmodal, #ajaxerr, #popupmodal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(255, 255, 255, .7);
opacity: 0;
z-index: 900000;
visibility: hidden;
padding: 10px;
.big-icon {
margin-top: 50px;
font-size: 200px;
color: $brand-primary;
}
&#popupmodal .big-icon {
margin-top: 10px;
font-size: 100px;
color: $brand-primary;
}
.modal-card {
margin: 50px auto 0;
width: 90%;
max-width: 600px;
max-height: calc(100vh - 100px);
overflow-y: auto;
background: white;
border-radius: $border-radius-large;
box-shadow: 0 7px 14px 0 rgba(78, 50, 92, 0.1),0 3px 6px 0 rgba(0,0,0,.07);
padding: 20px;
min-height: 160px;
.modal-card-icon {
float: left;
width: 150px;
text-align: center;
}
.modal-card-content {
margin-left: 160px;
text-align: left;
h3 {
margin-top: 0;
}
}
}
&#cookie-consent-modal {
background: rgba(255, 255, 255, .5);
opacity: 1;
visibility: visible;
display: none;
.modal-card-content {
margin-left: 0;
}
details {
& > summary {
list-style: none;
}
& > summary::-webkit-details-marker,
& > summary::marker {
display: none;
}
margin-bottom: 10px;
}
}
}
@media (max-width: 700px) {
#loadingmodal, #ajaxerr, #popupmodal {
.modal-card {
margin: 25px auto 0;
max-height: calc(100vh - 50px - 20px);
.modal-card-icon {
float: none;
width: 100%;
}
.modal-card-content {
text-align: center;
margin-left: 0;
margin-right: 0;
margin-top: 10px;
}
}
}
}
#ajaxerr {
background: rgba(236, 236, 236, .9);
}
.loading #loadingmodal, .ajaxerr #ajaxerr, .has-popup #popupmodal {
opacity: 1;
visibility: visible;
transition: opacity .5s ease-in-out;
-moz-transition: opacity .5s ease-in-out;
-webkit-transition: opacity .5s ease-in-out;
}
.typo-alert span[data-typosuggest] { .typo-alert span[data-typosuggest] {
text-decoration: underline; text-decoration: underline;