/** * Content Blocker – frontend.js * Click-to-load: the real iframe is CREATED only after the user actively * consents per service. Consent is stored in localStorage per service id. */ ( function () { 'use strict'; const STORAGE_PREFIX = 'cb_consent_'; const CFG = window.cbConfig || { services: [], i18n: {} }; /* ───────────────────────── consent storage ───────────────────────── */ function hasConsent( serviceId ) { try { return localStorage.getItem( STORAGE_PREFIX + serviceId ) === '1'; } catch ( e ) { return false; } } function grantConsent( serviceId ) { try { localStorage.setItem( STORAGE_PREFIX + serviceId, '1' ); } catch ( e ) { // localStorage unavailable; allow the load for this session only. } } /* ───────────────────────── iframe loading ────────────────────────── */ /** Replace a .cb-blocker element with the real iframe. src comes from data-src. */ function loadContent( blockerEl ) { const src = blockerEl.dataset.src; if ( ! src ) { blockerEl.remove(); return; } const iframe = document.createElement( 'iframe' ); if ( blockerEl.dataset.width ) iframe.width = blockerEl.dataset.width; if ( blockerEl.dataset.height ) iframe.height = blockerEl.dataset.height; iframe.setAttribute( 'loading', 'lazy' ); iframe.setAttribute( 'allowfullscreen', '' ); iframe.setAttribute( 'referrerpolicy', 'no-referrer-when-downgrade' ); iframe.setAttribute( 'data-cb-loaded', '1' ); // mark as ours (observer skips) // Set src last — this is the moment the network request is made. iframe.src = src; blockerEl.parentNode.replaceChild( iframe, blockerEl ); } function loadPreConsented() { document.querySelectorAll( '.cb-blocker[data-cb-id]' ).forEach( function ( el ) { const id = el.dataset.cbId; if ( id && hasConsent( id ) ) { loadContent( el ); } } ); } /* ───────────────────────── consent buttons ───────────────────────── */ function attachButtons() { document.addEventListener( 'click', function ( e ) { const btn = e.target.closest( '.cb-blocker__button' ); if ( ! btn ) return; const serviceId = btn.dataset.cbId; if ( ! serviceId ) return; const wrapper = btn.closest( '.cb-blocker' ); // "Remember" checkbox (default checked): persist consent only when // ticked; otherwise load this embed once without storing consent. const remember = wrapper ? wrapper.querySelector( '.cb-blocker__remember-cb' ) : null; if ( ! remember || remember.checked ) { grantConsent( serviceId ); } if ( wrapper ) { loadContent( wrapper ); } } ); } /* ───────────────────────── revoke (Art. 7 (3)) ───────────────────── */ window.cbRevokeAll = function () { try { const keysToRemove = []; for ( let i = 0; i < localStorage.length; i++ ) { const key = localStorage.key( i ); if ( key && key.indexOf( STORAGE_PREFIX ) === 0 ) { keysToRemove.push( key ); } } keysToRemove.forEach( function ( k ) { localStorage.removeItem( k ); } ); } catch ( e ) { // Silently ignore if localStorage is unavailable. } window.location.reload(); }; /* ─────────────── client-side detection (JS-injected iframes) ─────────────── */ /** Find a configured service whose match pattern is contained in the URL. */ function findService( url ) { if ( ! url ) return null; const list = CFG.services || []; for ( let i = 0; i < list.length; i++ ) { if ( list[ i ].match && url.indexOf( list[ i ].match ) !== -1 ) { return list[ i ]; } } return null; } /** Convert a YouTube/Vimeo watch URL into its embeddable URL. */ function toEmbedUrl( url ) { let m = url.match( /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([\w-]+)/ ); if ( m ) return 'https://www.youtube.com/embed/' + m[ 1 ]; m = url.match( /(?:player\.)?vimeo\.com\/(?:video\/)?(\d+)/ ); if ( m ) return 'https://player.vimeo.com/video/' + m[ 1 ]; return url; } /** Build a consent placeholder element (mirrors PHP render_placeholder). */ function buildPlaceholder( svc, src, dims ) { const i18n = CFG.i18n || {}; dims = dims || {}; const wrap = document.createElement( 'div' ); wrap.className = 'cb-blocker'; wrap.setAttribute( 'data-cb-id', svc.id ); wrap.dataset.src = src; if ( dims.width ) wrap.dataset.width = dims.width; if ( dims.height ) wrap.dataset.height = dims.height; if ( dims.height && /^\d+$/.test( String( dims.height ) ) ) { wrap.style.minHeight = dims.height + 'px'; } const inner = document.createElement( 'div' ); inner.className = 'cb-blocker__inner'; const text = document.createElement( 'p' ); text.className = 'cb-blocker__text'; if ( svc.placeholder ) { text.textContent = svc.placeholder; } else { const tpl = i18n.defaultText || '%s'; const parts = tpl.split( '%s' ); text.appendChild( document.createTextNode( parts[ 0 ] || '' ) ); const strong = document.createElement( 'strong' ); strong.textContent = svc.name; text.appendChild( strong ); text.appendChild( document.createTextNode( parts[ 1 ] || '' ) ); } inner.appendChild( text ); const rec = document.createElement( 'p' ); rec.className = 'cb-blocker__recipient'; const recLabel = document.createElement( 'strong' ); recLabel.textContent = ( i18n.recipient || 'Empfänger:' ) + ' '; rec.appendChild( recLabel ); rec.appendChild( document.createTextNode( svc.recipient || '' ) ); if ( svc.third_country ) { rec.appendChild( document.createTextNode( ' — ' ) ); const tc = document.createElement( 'span' ); tc.className = 'cb-blocker__third-country'; tc.textContent = i18n.thirdCountry || ''; rec.appendChild( tc ); } inner.appendChild( rec ); const pur = document.createElement( 'p' ); pur.className = 'cb-blocker__purpose'; const purLabel = document.createElement( 'strong' ); purLabel.textContent = ( i18n.purpose || 'Zweck:' ) + ' '; pur.appendChild( purLabel ); pur.appendChild( document.createTextNode( svc.purpose || '' ) ); inner.appendChild( pur ); if ( svc.privacy_url ) { const p = document.createElement( 'p' ); const a = document.createElement( 'a' ); a.className = 'cb-blocker__privacy-link'; a.href = svc.privacy_url; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.textContent = i18n.privacy || 'Datenschutz'; p.appendChild( a ); inner.appendChild( p ); } const btn = document.createElement( 'button' ); btn.type = 'button'; btn.className = 'cb-blocker__button'; btn.setAttribute( 'data-cb-id', svc.id ); btn.textContent = ( i18n.load || '%s' ).replace( '%s', svc.name ); inner.appendChild( btn ); const label = document.createElement( 'label' ); label.className = 'cb-blocker__remember'; const cb = document.createElement( 'input' ); cb.type = 'checkbox'; cb.className = 'cb-blocker__remember-cb'; cb.checked = true; label.appendChild( cb ); label.appendChild( document.createTextNode( ' ' + ( i18n.remember || '' ) ) ); inner.appendChild( label ); wrap.appendChild( inner ); return wrap; } /** Replace a freshly seen iframe with a placeholder (or let it load if consented). */ function handleIframe( iframe ) { if ( iframe.dataset.cbLoaded ) return; // one we created if ( iframe.closest( '.cb-blocker' ) ) return; // inside a placeholder const rawSrc = iframe.getAttribute( 'src' ) || ''; const svc = findService( rawSrc ); if ( ! svc ) return; if ( hasConsent( svc.id ) ) return; // already consented → leave it iframe.setAttribute( 'src', 'about:blank' ); // stop the request ASAP const dims = { width: iframe.getAttribute( 'width' ) || '', height: iframe.getAttribute( 'height' ) || '', }; const placeholder = buildPlaceholder( svc, rawSrc, dims ); if ( iframe.parentNode ) { iframe.parentNode.replaceChild( placeholder, iframe ); } } /** Elementor video widgets store the URL in data-settings and build the * player via JS — handle them before that happens. */ function handleElementorVideos() { document.querySelectorAll( '.elementor-widget-video[data-settings]' ).forEach( function ( widget ) { let settings; try { settings = JSON.parse( widget.dataset.settings ); } catch ( e ) { return; } const raw = settings.youtube_url || settings.vimeo_url || settings.external_url || ''; if ( ! raw ) return; const embed = toEmbedUrl( raw ); const svc = findService( raw ) || findService( embed ); if ( ! svc ) return; if ( hasConsent( svc.id ) ) return; // consented → let Elementor build it // Neutralise Elementor so it won't build the player / load the overlay. widget.removeAttribute( 'data-settings' ); const container = widget.querySelector( '.elementor-widget-container' ) || widget; container.innerHTML = ''; container.appendChild( buildPlaceholder( svc, embed, {} ) ); } ); } function scanExistingIframes() { document.querySelectorAll( 'iframe[src]' ).forEach( handleIframe ); } function startObserver() { if ( typeof MutationObserver === 'undefined' ) return; const obs = new MutationObserver( function ( mutations ) { for ( const m of mutations ) { for ( const node of m.addedNodes ) { if ( node.nodeType !== 1 ) continue; if ( node.tagName === 'IFRAME' ) { handleIframe( node ); } else if ( node.querySelectorAll ) { node.querySelectorAll( 'iframe[src]' ).forEach( handleIframe ); } } } } ); obs.observe( document.documentElement, { childList: true, subtree: true } ); } /* ───────────────────────── bootstrap ─────────────────────────────── */ function init() { loadPreConsented(); attachButtons(); handleElementorVideos(); scanExistingIframes(); startObserver(); } if ( document.readyState === 'loading' ) { document.addEventListener( 'DOMContentLoaded', init ); } else { init(); } } )();