- frontend.js erkennt JS-nachgeladene iframes (Slider etc.) per initialem Scan + MutationObserver und ersetzt sie durch den Platzhalter. - Elementor-Video-Widgets: data-settings (youtube_url/vimeo_url) wird gelesen, zu Embed-URL konvertiert, Widget neutralisiert und durch Platzhalter ersetzt. - Service-Daten + i18n werden dafuer ans Frontend lokalisiert (cbConfig). - readme: Erkennung + Grenzen (Elementor-Vorschaubild) dokumentiert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
310 lines
10 KiB
JavaScript
310 lines
10 KiB
JavaScript
/**
|
||
* 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();
|
||
}
|
||
} )();
|