Add consent reopener: floating widget + anchor link trigger
Allow visitors to reopen the cookie preferences modal after making their choice via a configurable floating button (bottom-right or right-side vertical) or any anchor link with href="#att-cc-preferences". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,42 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="floating_widget"><?php esc_html_e( 'Floating Widget', 'att-consent' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select name="floating_widget" id="floating_widget">
|
||||||
|
<option value="bottom-right" <?php selected( $settings['floating_widget'], 'bottom-right' ); ?>>
|
||||||
|
<?php esc_html_e( 'Bottom Right (recommended)', 'att-consent' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="right" <?php selected( $settings['floating_widget'], 'right' ); ?>>
|
||||||
|
<?php esc_html_e( 'Right Side (vertical)', 'att-consent' ); ?>
|
||||||
|
</option>
|
||||||
|
<option value="none" <?php selected( $settings['floating_widget'], 'none' ); ?>>
|
||||||
|
<?php esc_html_e( 'Disabled', 'att-consent' ); ?>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'A small floating button that allows visitors to reopen the cookie preferences modal after they\'ve made their choice.', 'att-consent' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="floating_widget_label"><?php esc_html_e( 'Floating Widget Label', 'att-consent' ); ?></label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="floating_widget_label" id="floating_widget_label"
|
||||||
|
value="<?php echo esc_attr( $settings['floating_widget_label'] ); ?>"
|
||||||
|
class="regular-text">
|
||||||
|
<p class="description">
|
||||||
|
<?php esc_html_e( 'Text shown on the floating widget button.', 'att-consent' ); ?>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<label for="consent_expiry"><?php esc_html_e( 'Consent Expiry (days)', 'att-consent' ); ?></label>
|
<label for="consent_expiry"><?php esc_html_e( 'Consent Expiry (days)', 'att-consent' ); ?></label>
|
||||||
|
|||||||
@@ -164,6 +164,12 @@ class ATT_Consent_Admin {
|
|||||||
$settings['consent_mode'] = in_array( $_POST['consent_mode'] ?? '', array( 'advanced', 'basic' ), true ) ? $_POST['consent_mode'] : 'advanced';
|
$settings['consent_mode'] = in_array( $_POST['consent_mode'] ?? '', array( 'advanced', 'basic' ), true ) ? $_POST['consent_mode'] : 'advanced';
|
||||||
$settings['banner_position'] = in_array( $_POST['banner_position'] ?? '', array( 'bottom', 'top', 'center' ), true ) ? $_POST['banner_position'] : 'bottom';
|
$settings['banner_position'] = in_array( $_POST['banner_position'] ?? '', array( 'bottom', 'top', 'center' ), true ) ? $_POST['banner_position'] : 'bottom';
|
||||||
$settings['consent_expiry'] = absint( $_POST['consent_expiry'] ?? 365 );
|
$settings['consent_expiry'] = absint( $_POST['consent_expiry'] ?? 365 );
|
||||||
|
$settings['floating_widget'] = in_array(
|
||||||
|
$_POST['floating_widget'] ?? '',
|
||||||
|
array( 'bottom-right', 'right', 'none' ),
|
||||||
|
true
|
||||||
|
) ? $_POST['floating_widget'] : 'bottom-right';
|
||||||
|
$settings['floating_widget_label'] = sanitize_text_field( $_POST['floating_widget_label'] ?? '' );
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'appearance':
|
case 'appearance':
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ class ATT_Consent {
|
|||||||
'ga4_measurement_id' => '',
|
'ga4_measurement_id' => '',
|
||||||
'gtm_container_id' => '',
|
'gtm_container_id' => '',
|
||||||
'consent_mode' => 'advanced',
|
'consent_mode' => 'advanced',
|
||||||
'banner_position' => 'bottom',
|
'banner_position' => 'bottom',
|
||||||
'consent_expiry' => 365,
|
'consent_expiry' => 365,
|
||||||
|
'floating_widget' => 'bottom-right',
|
||||||
|
'floating_widget_label' => 'Cookie Settings',
|
||||||
|
|
||||||
// Appearance.
|
// Appearance.
|
||||||
'banner_bg_color' => '#1a1a2e',
|
'banner_bg_color' => '#1a1a2e',
|
||||||
|
|||||||
@@ -284,7 +284,14 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|||||||
</div>
|
</div>
|
||||||
</div>';
|
</div>';
|
||||||
|
|
||||||
$html = apply_filters( 'att_consent_banner_html', $banner_html . $modal_html, $s );
|
$widget_html = '';
|
||||||
|
if ( 'none' !== $s['floating_widget'] ) {
|
||||||
|
$widget_position = 'right' === $s['floating_widget'] ? 'att-cc-widget--right' : 'att-cc-widget--bottom-right';
|
||||||
|
$widget_label = ! empty( $s['floating_widget_label'] ) ? $s['floating_widget_label'] : 'Cookie Settings';
|
||||||
|
$widget_html = '<button type="button" id="att-cc-widget" class="att-cc-widget ' . esc_attr( $widget_position ) . '" aria-label="' . esc_attr( $widget_label ) . '" style="display:none;' . $style_vars . '"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8zm-1-13a1.5 1.5 0 1 1-1.5-1.5A1.5 1.5 0 0 1 11 7zm4 2a1 1 0 1 1-1-1 1 1 0 0 1 1 1zm-7 3a1 1 0 1 1-1-1 1 1 0 0 1 1 1zm3 4a1.5 1.5 0 1 1-1.5-1.5A1.5 1.5 0 0 1 11 16zm5-1a1 1 0 1 1-1-1 1 1 0 0 1 1 1z"/></svg> ' . esc_html( $widget_label ) . '</button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = apply_filters( 'att_consent_banner_html', $banner_html . $modal_html . $widget_html, $s );
|
||||||
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above.
|
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above.
|
||||||
|
|
||||||
do_action( 'att_consent_after_banner' );
|
do_action( 'att_consent_after_banner' );
|
||||||
|
|||||||
@@ -248,6 +248,55 @@ body.att-cc-modal-active {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Floating Widget --- */
|
||||||
|
|
||||||
|
.att-cc-widget {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999998;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--att-cc-bg, #1a1a2e);
|
||||||
|
color: var(--att-cc-text, #fff);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.att-cc-widget:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.att-cc-widget:focus-visible {
|
||||||
|
outline: 2px solid #fff;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.att-cc-widget--bottom-right {
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.att-cc-widget--right {
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: rotate(-90deg) translateX(50%);
|
||||||
|
transform-origin: right center;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.att-cc-widget svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Responsive --- */
|
/* --- Responsive --- */
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* ATT Consent Banner UI
|
* ATT Consent Banner UI
|
||||||
*
|
*
|
||||||
* Handles banner display, preferences modal, focus trapping, and user interactions.
|
* Handles banner display, preferences modal, floating widget, anchor link
|
||||||
|
* triggers, focus trapping, and user interactions.
|
||||||
* Depends on AttConsent (consent-manager.js) for consent state management.
|
* Depends on AttConsent (consent-manager.js) for consent state management.
|
||||||
*/
|
*/
|
||||||
(function() {
|
(function() {
|
||||||
@@ -9,43 +10,12 @@
|
|||||||
|
|
||||||
var banner = document.getElementById('att-cc-banner');
|
var banner = document.getElementById('att-cc-banner');
|
||||||
var modal = document.getElementById('att-cc-modal');
|
var modal = document.getElementById('att-cc-modal');
|
||||||
|
var widget = document.getElementById('att-cc-widget');
|
||||||
|
|
||||||
if (!banner || !modal) {
|
if (!banner || !modal) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If consent already exists, hide the banner immediately.
|
|
||||||
if (AttConsent.hasConsent()) {
|
|
||||||
hideBanner();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Banner buttons ---
|
|
||||||
|
|
||||||
var acceptBtn = banner.querySelector('[data-att-cc="accept-all"]');
|
|
||||||
var rejectBtn = banner.querySelector('[data-att-cc="reject-all"]');
|
|
||||||
var prefsBtn = banner.querySelector('[data-att-cc="preferences"]');
|
|
||||||
|
|
||||||
if (acceptBtn) {
|
|
||||||
acceptBtn.addEventListener('click', function() {
|
|
||||||
AttConsent.update({ functional: true, analytics: true, marketing: true });
|
|
||||||
hideBanner();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rejectBtn) {
|
|
||||||
rejectBtn.addEventListener('click', function() {
|
|
||||||
AttConsent.update({ functional: false, analytics: false, marketing: false });
|
|
||||||
hideBanner();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prefsBtn) {
|
|
||||||
prefsBtn.addEventListener('click', function() {
|
|
||||||
openModal();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Modal buttons ---
|
// --- Modal buttons ---
|
||||||
|
|
||||||
var saveBtn = modal.querySelector('[data-att-cc="save-preferences"]');
|
var saveBtn = modal.querySelector('[data-att-cc="save-preferences"]');
|
||||||
@@ -62,6 +32,7 @@
|
|||||||
AttConsent.update(categories);
|
AttConsent.update(categories);
|
||||||
closeModal();
|
closeModal();
|
||||||
hideBanner();
|
hideBanner();
|
||||||
|
showWidget();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +48,59 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Floating widget click handler ---
|
||||||
|
|
||||||
|
if (widget) {
|
||||||
|
widget.addEventListener('click', function() {
|
||||||
|
openModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Anchor link listener ---
|
||||||
|
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var link = e.target.closest('a[href="#att-cc-preferences"]');
|
||||||
|
if (link) {
|
||||||
|
e.preventDefault();
|
||||||
|
openModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Initial state ---
|
||||||
|
|
||||||
|
if (AttConsent.hasConsent()) {
|
||||||
|
hideBanner();
|
||||||
|
showWidget();
|
||||||
|
} else {
|
||||||
|
// --- Banner buttons (only needed for first-time visitors) ---
|
||||||
|
|
||||||
|
var acceptBtn = banner.querySelector('[data-att-cc="accept-all"]');
|
||||||
|
var rejectBtn = banner.querySelector('[data-att-cc="reject-all"]');
|
||||||
|
var prefsBtn = banner.querySelector('[data-att-cc="preferences"]');
|
||||||
|
|
||||||
|
if (acceptBtn) {
|
||||||
|
acceptBtn.addEventListener('click', function() {
|
||||||
|
AttConsent.update({ functional: true, analytics: true, marketing: true });
|
||||||
|
hideBanner();
|
||||||
|
showWidget();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rejectBtn) {
|
||||||
|
rejectBtn.addEventListener('click', function() {
|
||||||
|
AttConsent.update({ functional: false, analytics: false, marketing: false });
|
||||||
|
hideBanner();
|
||||||
|
showWidget();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefsBtn) {
|
||||||
|
prefsBtn.addEventListener('click', function() {
|
||||||
|
openModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Helper functions ---
|
// --- Helper functions ---
|
||||||
|
|
||||||
function hideBanner() {
|
function hideBanner() {
|
||||||
@@ -84,6 +108,12 @@
|
|||||||
banner.classList.add('att-cc-hidden');
|
banner.classList.add('att-cc-hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showWidget() {
|
||||||
|
if (widget) {
|
||||||
|
widget.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openModal() {
|
function openModal() {
|
||||||
// Pre-fill checkboxes with existing consent if available.
|
// Pre-fill checkboxes with existing consent if available.
|
||||||
var existing = AttConsent.getConsent();
|
var existing = AttConsent.getConsent();
|
||||||
@@ -104,9 +134,14 @@
|
|||||||
modal.classList.remove('att-cc-modal--open');
|
modal.classList.remove('att-cc-modal--open');
|
||||||
document.body.classList.remove('att-cc-modal-active');
|
document.body.classList.remove('att-cc-modal-active');
|
||||||
|
|
||||||
// Return focus to the preferences button.
|
// Return focus to the widget if visible, otherwise the banner prefs button.
|
||||||
if (prefsBtn) {
|
if (widget && widget.style.display !== 'none') {
|
||||||
prefsBtn.focus();
|
widget.focus();
|
||||||
|
} else {
|
||||||
|
var prefsBtn = banner.querySelector('[data-att-cc="preferences"]');
|
||||||
|
if (prefsBtn) {
|
||||||
|
prefsBtn.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,13 +197,4 @@
|
|||||||
|
|
||||||
element.addEventListener('keydown', handleKeydown);
|
element.addEventListener('keydown', handleKeydown);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for programmatic consent revocation to re-show banner.
|
|
||||||
document.addEventListener('att_consent_update', function(e) {
|
|
||||||
var cats = e.detail;
|
|
||||||
if (!cats.functional && !cats.analytics && !cats.marketing) {
|
|
||||||
// All denied - consent was effectively revoked, but don't re-show
|
|
||||||
// banner immediately (user just clicked reject).
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user