Compare commits

...

18 Commits

Author SHA1 Message Date
Richard Schreiber
0368d548a0 remove wrong position relative on dialog 2025-05-21 14:51:47 +02:00
Richard Schreiber
1c2df6eaae allow Escape, save as „do not consent“ to unknown consents 2025-05-21 12:41:32 +02:00
Richard Schreiber
fddeff9530 change consent icon to shield 2025-05-21 12:18:16 +02:00
Richard Schreiber
f96e10011a improve dialog animation 2025-05-21 09:58:56 +02:00
Richard Schreiber
a7c80872cd improve open-transition 2025-05-21 07:50:58 +02:00
Richard Schreiber
587c3f8c8b disable escaping cookie-consent the first time shown 2025-05-21 07:41:55 +02:00
Richard Schreiber
9bc9fd2eb5 Add cookieconsent-modal 2025-05-21 07:41:24 +02:00
Richard Schreiber
44ddeac4d1 fix licenseheaders 2025-05-20 11:04:20 +02:00
Richard Schreiber
fdb2c0e313 fix code-style 2025-05-20 10:59:04 +02:00
Richard Schreiber
1a68c30413 improve style 2025-05-20 10:54:23 +02:00
Richard Schreiber
68222b2fe3 rename templatetag to dialog 2025-05-20 10:53:59 +02:00
Richard Schreiber
d17777da9b improve dialog 2025-05-20 10:20:55 +02:00
Richard Schreiber
498e8a52af update text and icon 2025-05-20 10:18:42 +02:00
Richard Schreiber
c7db1eb1d7 add styling 2025-05-20 10:18:34 +02:00
Richard Schreiber
6d56ed6e46 remove default buttons 2025-05-20 10:18:18 +02:00
Richard Schreiber
80a5c4ac5b fix buttons 2025-05-20 10:08:21 +02:00
Richard Schreiber
51d1d1fbc1 add dialog-templatetag 2025-05-20 09:28:32 +02:00
Richard Schreiber
37a9ecc61f [A11y] show dialog when empty add-to-cart instead of disabling the button 2025-05-19 22:54:29 +02:00
7 changed files with 263 additions and 124 deletions

View File

@@ -0,0 +1,60 @@
#
# 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/>.
#
from django import template
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ # NOQA
register = template.Library()
@register.simple_tag
def dialog(html_id, label, description, *args, **kwargs):
format_kwargs = {
"id": html_id,
"label": label,
"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 "",
"alert": mark_safe('role="alertdialog"') if kwargs.get("alert", "False") != "False" else "",
}
result = """
<dialog {alert}
id="{id}"
aria-labelledby="{id}-label"
aria-describedby="{id}-description">
<form method="dialog" class="modal-card form-horizontal">
{icon}
<div class="modal-card-content">
<h2 id="{id}-label">{label}</h2>
<p id="{id}-description">{description}</p>
"""
return format_html(result, **format_kwargs)
@register.simple_tag
def enddialog(*args, **kwargs):
return mark_safe("""
</div>
</form>
</dialog>
""")

View File

@@ -7,6 +7,8 @@
{% load thumb %}
{% load eventsignal %}
{% load rich_text %}
{% load icon %}
{% load dialog %}
{% block title %}
{% if "year" in request.GET %}
@@ -240,6 +242,13 @@
</div>
{% endif %}
</form>
{% if ev.presale_is_running and display_add_to_cart %}
{% trans "You didnt select any ticket." as label_nothing_to_add %}
{% trans "Please tick a checkbox or enter a quantity for one of the ticket types to add to the cart." as description_nothing_to_add %}
{% dialog "dialog-nothing-to-add" label_nothing_to_add description_nothing_to_add icon="exclamation-circle" %}
<p class="modal-card-confirm"><button class="btn btn-primary">{% trans "OK" %}</button></p>
{% enddialog %}
{% endif %}
{% endif %}
{% endif %}
</main>

View File

@@ -2,6 +2,8 @@
{% load rich_text %}
{% load safelink %}
{% load escapejson %}
{% load icon %}
{% load dialog %}
<div id="ajaxerr">
</div>
<div id="popupmodal" hidden aria-live="polite">
@@ -50,93 +52,74 @@
{{ cookie_consent_from_widget|json_script:"cookie-consent-from-widget" }}
{% endif %}
{% if cookie_providers %}
<div id="cookie-consent-modal" aria-live="polite">
<div class="modal-card">
<div class="modal-card-content">
<h3 id="cookie-consent-modal-label"></h3>
<div id="cookie-consent-modal-description">
<form class="form-horizontal">
{% with request.event|default:request.organizer as sh %}
<h3>{{ sh.settings.cookie_consent_dialog_title }}</h3>
{{ sh.settings.cookie_consent_dialog_text|rich_text }}
{% if sh.settings.cookie_consent_dialog_text_secondary %}
<div class="text-muted">
{{ sh.settings.cookie_consent_dialog_text_secondary|rich_text }}
</div>
{% endif %}
<details id="cookie-consent-details">
<summary>
<span class="fa fa-fw chevron"></span>
{% trans "Adjust settings in detail" %}
</summary>
<div class="checkbox">
<label>
<input type="checkbox" disabled checked="" aira-describedby="cookie-consent-checkbox-required-description">
{% trans "Required cookies" %}
</label>
</div>
<div class="help-block" id="cookie-consent-checkbox-required-description">
<p>{% trans "Functional cookies (e.g. shopping cart, login, payment, language preference) and technical cookies (e.g. security purposes)" %}</p>
</div>
{% for cp in cookie_providers %}
<div class="checkbox">
<label>
<input type="checkbox" name="{{ cp.identifier }}" aira-describedby="cookie-consent-checkbox-{{ cp.identifier }}-description">
{{ cp.provider_name }}
</label>
</div>
<div class="help-block" id="cookie-consent-checkbox-{{ cp.identifier }}-description">
<p>
{% for c in cp.usage_classes %}
{% if forloop.counter0 > 0 %}&middot; {% endif %}
{% if c.value == 1 %}
{% trans "Functionality" context "cookie_usage" %}
{% elif c.value == 2 %}
{% trans "Analytics" context "cookie_usage" %}
{% elif c.value == 3 %}
{% trans "Marketing" context "cookie_usage" %}
{% elif c.value == 4 %}
{% trans "Social features" context "cookie_usage" %}
{% endif %}
{% endfor %}
{% if cp.privacy_url %}
&middot;
<a href="{% safelink cp.privacy_url %}" target="_blank">
{% trans "Privacy policy" %}
</a>
{% endif %}
</p>
</div>
{% endfor %}
</details>
<div class="row">
<div class="col-xs-12 col-md-6">
<p>
<button type="button" class="btn btn-lg btn-block btn-primary" id="cookie-consent-button-no"
data-summary-text="{{ sh.settings.cookie_consent_dialog_button_no }}"
data-detail-text="{% trans "Save selection" %}">
{{ sh.settings.cookie_consent_dialog_button_no }}
</button>
</p>
</div>
<div class="col-xs-12 col-md-6">
<p>
<button type="button" class="btn btn-lg btn-block btn-primary" id="cookie-consent-button-yes">
{{ sh.settings.cookie_consent_dialog_button_yes }}
</button>
</p>
</div>
</div>
{% if sh.settings.privacy_url %}
<p class="text-center">
<a href="{% safelink sh.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a>
</p>
{% endif %}
{% endwith %}
<form>
{% with request.event|default:request.organizer as sh %}
{% dialog "cookie-consent-modal" sh.settings.cookie_consent_dialog_title sh.settings.cookie_consent_dialog_text|rich_text icon="shield" %}
{% if sh.settings.cookie_consent_dialog_text_secondary %}
<div class="text-muted">
{{ sh.settings.cookie_consent_dialog_text_secondary|rich_text }}
</div>
</div>
</div>
</div>
{% endif %}
<details id="cookie-consent-details">
<summary>
<span class="fa fa-fw chevron"></span>
{% trans "Adjust settings in detail" %}
</summary>
<div class="checkbox">
<label>
<input type="checkbox" disabled checked="" aira-describedby="cookie-consent-checkbox-required-description">
{% trans "Required cookies" %}
</label>
</div>
<div class="help-block" id="cookie-consent-checkbox-required-description">
<p>{% trans "Functional cookies (e.g. shopping cart, login, payment, language preference) and technical cookies (e.g. security purposes)" %}</p>
</div>
{% for cp in cookie_providers %}
<div class="checkbox">
<label>
<input type="checkbox" name="{{ cp.identifier }}" aira-describedby="cookie-consent-checkbox-{{ cp.identifier }}-description">
{{ cp.provider_name }}
</label>
</div>
<div class="help-block" id="cookie-consent-checkbox-{{ cp.identifier }}-description">
<p>
{% for c in cp.usage_classes %}
{% if forloop.counter0 > 0 %}&middot; {% endif %}
{% if c.value == 1 %}
{% trans "Functionality" context "cookie_usage" %}
{% elif c.value == 2 %}
{% trans "Analytics" context "cookie_usage" %}
{% elif c.value == 3 %}
{% trans "Marketing" context "cookie_usage" %}
{% elif c.value == 4 %}
{% trans "Social features" context "cookie_usage" %}
{% endif %}
{% endfor %}
{% if cp.privacy_url %}
&middot;
<a href="{% safelink cp.privacy_url %}" target="_blank">
{% trans "Privacy policy" %}
</a>
{% endif %}
</p>
</div>
{% endfor %}
</details>
<p class="modal-card-confirm modal-card-confirm-spread">
<button class="btn btn-lg btn-default" id="cookie-consent-button-no" value="no" autofocus="true"
data-summary-text="{{ sh.settings.cookie_consent_dialog_button_no }}"
data-detail-text="{% trans "Save selection" %}">
{{ sh.settings.cookie_consent_dialog_button_no }}
</button>
<button class="btn btn-lg btn-primary" id="cookie-consent-button-yes" value="yes">
{{ sh.settings.cookie_consent_dialog_button_yes }}
</button>
</p>
{% if sh.settings.privacy_url %}
<p class="text-center">
<small><a href="{% safelink sh.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></small>
</p>
{% endif %}
{% enddialog %}
{% endwith %}
{% endif %}
{% endif %}

View File

@@ -262,3 +262,95 @@ svg.svg-icon {
@include table-row-variant('info', var(--pretix-brand-info-success-lighten-30), var(--pretix-brand-info-success-lighten-25));
@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));
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); }
}

View File

@@ -6,7 +6,7 @@ $(function () {
var storage_key = $("#cookie-consent-storage-key").text();
var widget_consent = $("#cookie-consent-from-widget").text();
var consent_checkboxes = $("#cookie-consent-details input[type=checkbox][name]");
var consent_modal = $("#cookie-consent-modal");
var consent_modal = document.getElementById("cookie-consent-modal");
function update_consent(consent, sessionOnly) {
if (storage_key && window.sessionStorage && sessionOnly) {
@@ -108,25 +108,32 @@ $(function () {
_set_button_text();
if (show_dialog) {
// We use .css() instead of .show() because of some weird issue that only occurs in Firefox
// and only within the widget.
consent_modal.css("display", "block");
consent_modal.showModal();
consent_modal.addEventListener("cancel", function() {
// Dialog was initially shown, interpret Escape as „do not consent to new providers“
var consent = {};
consent_checkboxes.each(function () {
consent[this.name] = storage_val[this.name] || false;
});
update_consent(consent, false);
}, {once : true});
}
$("#cookie-consent-button-yes, #cookie-consent-button-no").on("click", function () {
consent_modal.hide();
consent_modal.addEventListener("close", function () {
if (!consent_modal.returnValue) {// ESC, do not save
return;
}
var consent = {};
var consent_all = this.id == "cookie-consent-button-yes";
var consent_all = consent_modal.returnValue == "yes";
consent_checkboxes.each(function () {
consent[this.name] = this.checked = consent_all || this.checked;
});
if (consent_all) _set_button_text();
// Always save explicit consent to permanent storage
update_consent(consent, false);
});
consent_checkboxes.on("change", _set_button_text);
$("#cookie-consent-reopen").on("click", function (e) {
consent_modal.show()
consent_modal.showModal()
e.preventDefault()
return true
})

View File

@@ -478,33 +478,21 @@ $(function () {
sessionStorage.setItem('scrollpos', window.scrollY);
});
}
var update_cart_form = function () {
var is_enabled = $(".product-row input[type=checkbox]:checked, .variations input[type=checkbox]:checked, .product-row input[type=radio]:checked, .variations input[type=radio]:checked").length;
if (!is_enabled) {
$(".input-item-count").each(function () {
if ($(this).val() && $(this).val() !== "0") {
is_enabled = true;
}
});
$(".input-seat-selection option").each(function() {
if ($(this).val() && $(this).val() !== "" && $(this).prop('selected')) {
is_enabled = true;
}
});
$("form:has(#btn-add-to-cart)").on("submit", function(e) {
if (
(this.classList.contains("has-seating") && this.querySelector("pretix-seating-checkout-button button")) ||
this.querySelector("input[type=checkbox]:checked") ||
[...this.querySelectorAll(".input-item-count[type=text]")].some(input => input.value && input.value !== "0") // TODO: seating hat noch einen seating-dummy-item-count, das ist Mist!
) {
// okay, let the submit-event bubble to async-task
return;
}
if (!is_enabled && (!$(".has-seating").length || $("#seating-dummy-item-count").length)) {
$("#btn-add-to-cart").prop("disabled", !is_enabled).popover({
'content': function () { return gettext("Please enter a quantity for one of the ticket types.") },
'placement': 'top',
'trigger': 'hover focus'
});
} else {
$("#btn-add-to-cart").prop("disabled", false).popover("destroy")
}
};
update_cart_form();
$(".product-row input[type=checkbox], .variations input[type=checkbox], .product-row input[type=radio], .variations input[type=radio], .input-item-count, .input-seat-selection")
.on("change mouseup keyup", update_cart_form);
e.preventDefault();
e.stopPropagation();
document.querySelector("#dialog-nothing-to-add").showModal();
});
$(".table-calendar td.has-events").click(function () {
var $grid = $(this).closest("[role='grid']");

View File

@@ -289,7 +289,7 @@ body.loading .container {
font-size: 120px;
color: $brand-primary;
}
#loadingmodal, #ajaxerr, #cookie-consent-modal, #popupmodal {
#loadingmodal, #ajaxerr, #popupmodal {
position: fixed;
top: 0;
left: 0;
@@ -359,7 +359,7 @@ body.loading .container {
}
}
@media (max-width: 700px) {
#loadingmodal, #ajaxerr, #cookie-consent-modal, #popupmodal {
#loadingmodal, #ajaxerr, #popupmodal {
.modal-card {
margin: 25px auto 0;
max-height: calc(100vh - 50px - 20px);