Add central cookie consent mechanism (#2330)

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2021-11-30 17:12:17 +01:00
committed by GitHub
co-authored by Richard Schreiber
parent 832235411f
commit e767c6a68d
19 changed files with 627 additions and 8 deletions
+119
View File
@@ -0,0 +1,119 @@
.. highlight:: python
:linenothreshold: 5
.. _`cookieconsent`:
Handling cookie consent
=======================
pretix includes an optional feature to handle cookie consent explicitly to comply with EU regulations.
If your plugin sets non-essential cookies or includes a third-party service that does so, you should
integrate with this feature.
Server-side integration
-----------------------
First, you need to declare that you are using non-essential cookies by responding to the following
signal:
.. automodule:: pretix.presale.signals
:members: register_cookie_providers
You are expected to return a list of ``CookieProvider`` objects instantiated from the following class:
.. class:: pretix.presale.cookies.CookieProvider
.. py:attribute:: CookieProvider.identifier
A short and unique identifier used to distinguish this cookie provider form others (required).
.. py:attribute:: CookieProvider.provider_name
A human-readable name of the entity of feature responsible for setting the cookie (required).
.. py:attribute:: CookieProvider.usage_classes
A list of enum values from the ``pretix.presale.cookies.UsageClass`` enumeration class, such as
``UsageClass.ANALYTICS``, ``UsageClass.MARKETING``, or ``UsageClass.SOCIAL`` (required).
.. py:attribute:: CookieProvider.privacy_url
A link to a privacy policy (optional).
Here is an example of such a receiver:
.. code-block:: python
@receiver(register_cookie_providers)
def recv_cookie_providers(sender, request, **kwargs):
return [
CookieProvider(
identifier='google_analytics',
provider_name='Google Analytics',
usage_classes=[UsageClass.ANALYTICS],
)
]
JavaScript-side integration
---------------------------
The server-side integration only causes the cookie provider to show up in the cookie dialog. You still
need to care about actually enforcing the consent state.
You can access the consent state through the ``window.pretix.cookie_consent`` variable. Whenever the
value changes, a ``pretix:cookie-consent:change`` event is fired on the ``document`` object.
The variable will generally have one of the following states:
.. rst-class:: rest-resource-table
================================================================ =====================================================
State Interpretation
================================================================ =====================================================
``pretix === undefined || pretix.cookie_consent === undefined`` Your JavaScript has loaded before the cookie consent
script. Wait for the event to be fired, then try again,
do not yet set a cookie.
``pretix.cookie_consent === null`` The cookie consent mechanism has not been enabled. This
usually means that you can set cookies however you like.
``pretix.cookie_consent[identifier] === undefined`` The cookie consent mechanism is loaded, but has no data
on your cookie yet, wait for the event to be fired, do not
yet set a cookie.
``pretix.cookie_consent[identifier] === true`` The user has consented to your cookie.
``pretix.cookie_consent[identifier] === false`` The user has actively rejected your cookie.
================================================================ =====================================================
If you are integrating e.g. a tracking provider with native cookie consent support such
as Facebook's Pixel, you can integrate it like this:
.. code-block:: javascript
var consent = (window.pretix || {}).cookie_consent;
if (consent !== null && !(consent || {}).facebook) {
fbq('consent', 'revoke');
}
fbq('init', ...);
document.addEventListener('pretix:cookie-consent:change', function (e) {
fbq('consent', (e.detail || {}).facebook ? 'grant' : 'revoke');
})
If you have a JavaScript function that you only want to load if consent for a specific ``identifier``
is given, you can wrap it like this:
.. code-block:: javascript
var consent_identifier = "youridentifier";
var consent = (window.pretix || {}).cookie_consent;
if (consent === null || (consent || {})[consent_identifier] === true) {
// Cookie consent tool is either disabled or consent is given
addScriptElement(src);
return;
}
// Either cookie consent tool has not loaded yet or consent is not given
document.addEventListener('pretix:cookie-consent:change', function onChange(e) {
var consent = e.detail || {};
if (consent === null || consent[consent_identifier] === true) {
addScriptElement(src);
document.removeEventListener('pretix:cookie-consent:change', onChange);
}
})
+1
View File
@@ -17,6 +17,7 @@ Contents:
shredder
import
customview
cookieconsent
auth
general
quality
+4
View File
@@ -309,6 +309,10 @@ Currently, the following attributes are understood by pretix itself:
always be modified. Note that this is not a security feature and can easily be overridden by users, so do not rely
on this for authentication.
* If ``data-consent="…"`` is given, the cookie consent mechanism will be initialized with consent for the given cookie
providers. All other providers will be disabled, no consent dialog will be shown. This is useful if you already
asked the user for consent and don't want them to be asked again. Example: ``data-consent="facebook,google_analytics"``
Any configured pretix plugins might understand more data fields. For example, if the appropriate plugins on pretix
Hosted or pretix Enterprise are active, you can pass the following fields:
+8 -1
View File
@@ -296,7 +296,14 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'theme_round_borders',
'primary_font',
'organizer_logo_image_inherit',
'organizer_logo_image'
'organizer_logo_image',
'privacy_url',
'cookie_consent',
'cookie_consent_dialog_title',
'cookie_consent_dialog_text',
'cookie_consent_dialog_text_secondary',
'cookie_consent_dialog_button_yes',
'cookie_consent_dialog_button_no',
]
def __init__(self, *args, **kwargs):
+12 -1
View File
@@ -97,10 +97,21 @@ class Organizer(LoggedModel):
return self.name
def save(self, *args, **kwargs):
is_new = not self.pk
obj = super().save(*args, **kwargs)
self.get_cache().clear()
if is_new:
self.set_defaults()
else:
self.get_cache().clear()
return obj
def set_defaults(self):
"""
This will be called after organizer creation.
This way, we can use this to introduce new default settings to pretix that do not affect existing organizers.
"""
self.settings.cookie_consent = True
def get_cache(self):
"""
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
+82
View File
@@ -1512,6 +1512,17 @@ DEFAULTS = {
),
'serializer_class': serializers.URLField,
},
'privacy_url': {
'default': None,
'type': str,
'form_class': forms.URLField,
'form_kwargs': dict(
label=_("Privacy Policy URL"),
help_text=_("This should point e.g. to a part of your website that explains how you use data gathered in "
"your ticket shop."),
),
'serializer_class': serializers.URLField,
},
'confirm_texts': {
'default': LazyI18nStringList(),
'type': LazyI18nStringList,
@@ -2489,6 +2500,77 @@ Your {organizer} team"""))
'many years. If you keep it empty, gift cards do not have an explicit expiry date.'),
)
},
'cookie_consent': {
'default': 'False',
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Enable cookie consent management features"),
),
'type': bool,
},
'cookie_consent_dialog_text': {
'default': LazyI18nString.from_gettext(gettext_noop(
'By clicking "Accept all cookies", you agree to the storing of cookies and use of similar technologies on '
'your device.'
)),
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Dialog text"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '3', 'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
'cookie_consent_dialog_text_secondary': {
'default': LazyI18nString.from_gettext(gettext_noop(
'We use cookies and similar technologies to gather data that allows us to improve this website and our '
'offerings. If you do not agree, we will only use cookies if they are essential to providing the services '
'this website offers.'
)),
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Secondary dialog text"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '3', 'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
'cookie_consent_dialog_title': {
'default': LazyI18nString.from_gettext(gettext_noop('Privacy settings')),
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_('Dialog title'),
widget=I18nTextInput,
widget_kwargs={'attrs': {'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
'cookie_consent_dialog_button_yes': {
'default': LazyI18nString.from_gettext(gettext_noop('Accept all cookies')),
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_('"Accept" button description'),
widget=I18nTextInput,
widget_kwargs={'attrs': {'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
'cookie_consent_dialog_button_no': {
'default': LazyI18nString.from_gettext(gettext_noop('Required cookies only')),
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_('"Reject" button description'),
widget=I18nTextInput,
widget_kwargs={'attrs': {'data-display-dependency': '#id_settings-cookie_consent'}},
)
},
'seating_choice': {
'default': 'True',
'form_class': forms.BooleanField,
+8 -2
View File
@@ -307,8 +307,14 @@ class OrganizerSettingsForm(SettingsForm):
'theme_color_danger',
'theme_color_background',
'theme_round_borders',
'primary_font'
'primary_font',
'privacy_url',
'cookie_consent',
'cookie_consent_dialog_title',
'cookie_consent_dialog_text',
'cookie_consent_dialog_text_secondary',
'cookie_consent_dialog_button_yes',
'cookie_consent_dialog_button_no',
]
organizer_logo_image = ExtFileField(
@@ -82,6 +82,49 @@
{% bootstrap_field sform.giftcard_expiry_years layout="control" %}
{% bootstrap_field sform.giftcard_length layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Privacy" %}</legend>
{% bootstrap_field sform.privacy_url layout="control" %}
<div class="alert alert-legal">
<p>
{% blocktrans trimmed %}
Some jurisdictions, including the European Union, require user consent before you
are allowed to use cookies or similar technology for analytics, tracking, payment,
or similar purposes.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
pretix itself only ever sets cookies that are required to provide the service
requested by the user or to maintain an appropriate level of security. Therefore,
cookies set by pretix itself do not require consent in all jurisdictions that we
are aware of.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
Therefore, the settings on this page will <strong>only</strong> have an affect
if you use <strong>plugins</strong> that require additional cookies
<strong>and</strong> participate in our cookie consent mechanism.
{% endblocktrans %}
</p>
<p>
<strong>{% blocktrans trimmed %}
Ultimately, it is your responsibility to make sure you comply with all relevant
laws. We try to help by providing these settings, but we cannot assume liability
since we do not know the exact configuration of your pretix usage, the legal details
in your specific jurisdiction, or the agreements you have with third parties such as
payment or tracking providers.
{% endblocktrans %}</strong>
</p>
</div>
{% bootstrap_field sform.cookie_consent layout="control" %}
{% bootstrap_field sform.cookie_consent_dialog_title layout="control" %}
{% bootstrap_field sform.cookie_consent_dialog_text layout="control" %}
{% bootstrap_field sform.cookie_consent_dialog_text_secondary layout="control" %}
{% bootstrap_field sform.cookie_consent_dialog_button_yes layout="control" %}
{% bootstrap_field sform.cookie_consent_dialog_button_no layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Invoices" %}</legend>
{% bootstrap_field sform.invoice_regenerate_allowed layout="control" %}
+8 -1
View File
@@ -31,7 +31,6 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import logging
from django.conf import settings
@@ -47,10 +46,12 @@ from pretix.helpers.i18n import (
)
from ..base.i18n import get_language_without_region
from .cookies import get_cookie_providers
from .signals import (
footer_link, global_footer_link, global_html_footer, global_html_head,
global_html_page_header, html_footer, html_head, html_page_header,
)
from .views.cart import cart_session, get_or_create_cart_id
logger = logging.getLogger(__name__)
@@ -140,6 +141,12 @@ def _default_context(request):
ctx['event'] = request.event
ctx['languages'] = [get_language_info(code) for code in request.event.settings.locales]
ctx['cookie_providers'] = get_cookie_providers(request.event, request)
if get_or_create_cart_id(request, create=False):
c = cart_session(request)
if "widget_data" in c and c["widget_data"].get("consent"):
ctx['cookie_consent_from_widget'] = c["widget_data"].get("consent").split(",")
if request.resolver_match:
ctx['cart_namespace'] = request.resolver_match.kwargs.get('cart_namespace', '')
elif hasattr(request, 'organizer'):
+64
View File
@@ -0,0 +1,64 @@
#
# 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/>.
#
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
#
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
from enum import Enum
from typing import List
from pretix.presale.signals import register_cookie_providers
class UsageClass(Enum):
FUNCTIONAL = 1
ANALYTICS = 2
MARKETING = 3
SOCIAL = 4
class CookieProvider:
def __init__(self, identifier: str, usage_classes: List[UsageClass], provider_name: str, privacy_url: str = None, **kwargs):
self.identifier = identifier
self.usage_classes = usage_classes
self.provider_name = provider_name
self.privacy_url = privacy_url
def get_cookie_providers(event, request):
c = [
]
for receiver, response in register_cookie_providers.send(event, request=request):
if isinstance(response, list):
c += response
else:
c.append(response)
c.sort(key=lambda k: str(k.provider_name))
return c
+10
View File
@@ -401,3 +401,13 @@ This signal is sent out when the description of an item or variation is rendered
additional text to the description. You are passed the ``item`` and ``variation`` and expected to return
HTML.
"""
register_cookie_providers = EventPluginSignal()
"""
Arguments: ``request``
This signal is sent out to get all cookie providers that could set a cookie on this page, regardless of
consent state. Receivers should return a list of ``pretix.presale.cookies.CookieProvider`` objects.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -169,6 +169,12 @@
{% if request.event.settings.contact_mail %}
<li><a href="mailto:{{ request.event.settings.contact_mail }}">{% trans "Contact event organizer" %}</a></li>
{% endif %}
{% if request.event.settings.privacy_url %}
<li><a href="{% safelink request.event.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></li>
{% endif %}
{% if request.event.settings.cookie_consent and cookie_providers %}
<li><button type="button" class="btn btn-link" id="cookie-consent-reopen">{% trans "Cookie settings" %}</button></li>
{% endif %}
{% if request.event.settings.imprint_url %}
<li><a href="{% safelink request.event.settings.imprint_url %}" target="_blank" rel="noopener">{% trans "Imprint" %}</a></li>
{% endif %}
@@ -14,6 +14,7 @@
<script type="text/javascript" src="{% static "pretixpresale/js/widget/floatformat.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/questions.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/cookieconsent.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/cart.js" %}"></script>
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
@@ -1,4 +1,7 @@
{% load i18n %}
{% load rich_text %}
{% load safelink %}
{% load escapejson %}
<div id="ajaxerr">
</div>
<div id="loadingmodal" hidden aria-live="polite">
@@ -15,3 +18,95 @@
</div>
</div>
</div>
{% if request.organizer and request.organizer.settings.cookie_consent %}
<script type="text/plain" id="cookie-consent-storage-key">cookie-consent-{{ request.organizer.slug }}</script>
{% if cookie_consent_from_widget %}
{{ 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">
{% 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="">
{% trans "Required cookies" %}<br>
<span class="text-muted">
{% trans "Functional cookies (e.g. shopping cart, login, payment, language preference) and technical cookies (e.g. security purposes)" %}
</span>
</label>
</div>
{% for cp in cookie_providers %}
<div class="checkbox">
<label>
<input type="checkbox" name="{{ cp.identifier }}">
{{ cp.provider_name }}<br>
<span class="text-muted">
{% 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 %}
</span>
</label>
</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 %}
</div>
</div>
</div>
</div>
{% endif %}
{% endif %}
@@ -87,6 +87,12 @@
{% if not request.event and request.organizer.settings.contact_mail %}
<li><a href="mailto:{{ request.organizer.settings.contact_mail }}">{% trans "Contact event organizer" %}</a></li>
{% endif %}
{% if not request.event and request.organizer.settings.privacy_url %}
<li><a href="{% safelink request.organizer.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></li>
{% endif %}
{% if not request.event and request.organizer.settings.cookie_consent and cookie_providers %}
<li><button type="button" class="btn btn-link" id="cookie-consent-reopen">{% trans "Cookie settings" %}</button></li>
{% endif %}
{% if not request.event and request.organizer.settings.imprint_url %}
<li><a href="{% safelink request.organizer.settings.imprint_url %}" target="_blank" rel="noopener">{% trans "Imprint" %}</a></li>
{% endif %}
@@ -0,0 +1,86 @@
/*global $ */
$(function () {
window.pretix = window.pretix || {};
var storage_key = $("#cookie-consent-storage-key").text();
function update_consent(consent) {
if (storage_key) window.localStorage[storage_key] = JSON.stringify(consent);
window.pretix.cookie_consent = consent;
// Event() is not supported by IE11, see ployfill here:
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#polyfill
var e = document.createEvent('CustomEvent');
e.initCustomEvent('pretix:cookie-consent:change', true, true, consent);
document.dispatchEvent(e)
}
if (!storage_key) {
update_consent(null);
return;
}
var storage_val = window.localStorage[storage_key];
var show_dialog = !storage_val;
var consent_checkboxes = $("#cookie-consent-details input[type=checkbox][name]");
var consent_modal = $("#cookie-consent-modal");
if (storage_val) {
storage_val = JSON.parse(storage_val);
consent_checkboxes.each(function () {
if (typeof storage_val[this.name] === "undefined") {
// A new cookie type has been added that we haven't asked for yet
show_dialog = true;
} else if (storage_val[this.name]) {
this.checked = true;
}
})
} else {
storage_val = {}
var consented = $("#cookie-consent-from-widget").text();
if (consented) {
consented = JSON.parse(consented);
consent_checkboxes.each(function () {
this.checked = storage_val[this.name] = consented.indexOf(this.name) > -1;
})
show_dialog = false
}
}
update_consent(storage_val);
function _set_button_text () {
var btn = $("#cookie-consent-button-no");
btn.text(
consent_checkboxes.filter(":checked").length ?
btn.attr("data-detail-text") :
btn.attr("data-summary-text")
);
}
if (consent_checkboxes.filter(":checked").length) {
$("#cookie-consent-details").prop("open", true).find("> *:not(summary)").show();
}
_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");
}
$("#cookie-consent-button-yes, #cookie-consent-button-no").on("click", function () {
consent_modal.hide();
var consent = {};
var consent_all = this.id == "cookie-consent-button-yes";
consent_checkboxes.each(function () {
consent[this.name] = this.checked = consent_all || this.checked;
});
if (consent_all) _set_button_text();
update_consent(consent);
});
consent_checkboxes.on("change", _set_button_text);
$("#cookie-consent-reopen").on("click", function (e) {
consent_modal.show()
e.preventDefault()
return true
})
});
@@ -218,6 +218,16 @@ $(function () {
$(this).append(content);
});
$("[data-click-to-load]").on("click", function(e) {
var target = document.getElementById(this.getAttribute("data-click-to-load"));
target.src = this.href;
target.focus();
e.preventDefault();
});
$(".overlay-remove").on("click", function() {
$(this).closest(".contains-overlay").find(".overlay").fadeOut();
});
$("#voucher-box").hide();
$("#voucher-toggle").show();
$("#voucher-toggle a").click(function () {
@@ -132,6 +132,15 @@ a.btn, button.btn {
}
}
details {
summary .chevron::before {
content: $fa-var-caret-right;
}
&[open] .chevron::before {
content: $fa-var-caret-down;
}
}
@media(max-width: $screen-xs-max) {
.nameparts-form-group {
+55 -3
View File
@@ -75,6 +75,13 @@ footer nav li:not(:first-child):before {
width: 1.5em;
text-align: center;
}
footer nav .btn-link {
display: inline;
padding: 0;
margin: 0;
font-size: 11px;
vertical-align: baseline;
}
.js-only {
display: none;
@@ -122,6 +129,29 @@ a:hover .panel-primary > .panel-heading {
}
}
.contains-overlay {
position: relative;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $gray-lightest;
}
.overlay-centered {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.overlay-centered .overlay-content {
max-width: 35em;
margin-left: auto;
margin-right: auto;
}
body.loading .container {
-webkit-filter: blur(2px);
-moz-filter: blur(2px);
@@ -136,7 +166,7 @@ body.loading .container {
font-size: 120px;
color: $brand-primary;
}
#loadingmodal, #ajaxerr {
#loadingmodal, #ajaxerr, #cookie-consent-modal {
position: fixed;
top: 0;
left: 0;
@@ -156,9 +186,10 @@ body.loading .container {
.modal-card {
margin: 50px auto 0;
top: 50px;
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);
@@ -178,10 +209,31 @@ body.loading .container {
}
}
}
&#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: inherit;
}
& > summary::-webkit-details-marker {
display: inherit;
}
margin-bottom: 10px;
}
}
}
@media (max-width: 700px) {
#loadingmodal, #ajaxerr {
#loadingmodal, #ajaxerr, #cookie-consent-modal {
.modal-card {
margin: 25px auto 0;
max-height: calc(100vh - 50px - 20px);
.modal-card-icon {
float: none;
width: 100%;