diff --git a/admin/views/settings-general.php b/admin/views/settings-general.php index 04a1cb2..abcc442 100644 --- a/admin/views/settings-general.php +++ b/admin/views/settings-general.php @@ -101,6 +101,42 @@ if ( ! defined( 'ABSPATH' ) ) { + + + + + + +

+ +

+ + + + + + + + + +

+ +

+ + + diff --git a/includes/class-admin.php b/includes/class-admin.php index 553c033..7d605bc 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -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['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['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; case 'appearance': diff --git a/includes/class-att-consent.php b/includes/class-att-consent.php index d4da47d..519884a 100644 --- a/includes/class-att-consent.php +++ b/includes/class-att-consent.php @@ -29,8 +29,10 @@ class ATT_Consent { 'ga4_measurement_id' => '', 'gtm_container_id' => '', 'consent_mode' => 'advanced', - 'banner_position' => 'bottom', - 'consent_expiry' => 365, + 'banner_position' => 'bottom', + 'consent_expiry' => 365, + 'floating_widget' => 'bottom-right', + 'floating_widget_label' => 'Cookie Settings', // Appearance. 'banner_bg_color' => '#1a1a2e', diff --git a/includes/class-frontend.php b/includes/class-frontend.php index 7f4a76a..fb0ee83 100644 --- a/includes/class-frontend.php +++ b/includes/class-frontend.php @@ -284,7 +284,14 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= '; - $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 = ''; + } + + $html = apply_filters( 'att_consent_banner_html', $banner_html . $modal_html . $widget_html, $s ); echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above. do_action( 'att_consent_after_banner' ); diff --git a/public/css/banner.css b/public/css/banner.css index a5e0f11..af3fbac 100644 --- a/public/css/banner.css +++ b/public/css/banner.css @@ -248,6 +248,55 @@ body.att-cc-modal-active { 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 --- */ @media (max-width: 600px) { diff --git a/public/js/banner.js b/public/js/banner.js index 38e797f..5ef858a 100644 --- a/public/js/banner.js +++ b/public/js/banner.js @@ -1,7 +1,8 @@ /** * 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. */ (function() { @@ -9,43 +10,12 @@ var banner = document.getElementById('att-cc-banner'); var modal = document.getElementById('att-cc-modal'); + var widget = document.getElementById('att-cc-widget'); if (!banner || !modal) { 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 --- var saveBtn = modal.querySelector('[data-att-cc="save-preferences"]'); @@ -62,6 +32,7 @@ AttConsent.update(categories); closeModal(); 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 --- function hideBanner() { @@ -84,6 +108,12 @@ banner.classList.add('att-cc-hidden'); } + function showWidget() { + if (widget) { + widget.style.display = ''; + } + } + function openModal() { // Pre-fill checkboxes with existing consent if available. var existing = AttConsent.getConsent(); @@ -104,9 +134,14 @@ modal.classList.remove('att-cc-modal--open'); document.body.classList.remove('att-cc-modal-active'); - // Return focus to the preferences button. - if (prefsBtn) { - prefsBtn.focus(); + // Return focus to the widget if visible, otherwise the banner prefs button. + if (widget && widget.style.display !== 'none') { + widget.focus(); + } else { + var prefsBtn = banner.querySelector('[data-att-cc="preferences"]'); + if (prefsBtn) { + prefsBtn.focus(); + } } } @@ -162,13 +197,4 @@ 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). - } - }); })();