Files
GDPR-Content-Blocker/gdpr-content-blocker/assets/admin.js
s4luorth c796400492 feat: dienst-ID im Dienste-tab anzeigen + shortcode-hilfe fuer text/style (v1.4.9)
- Unter jedem dienstnamen wird jetzt die auto-erzeugte ID angezeigt
  (z. B. "ID: eigenes-matomo") - kopierbar, fuer den consent-shortcode.
  Bei neuen diensten live-vorschau aus dem namen (slugify, spiegelt
  sanitize_title); bei gespeicherten die stabile ID.
- Shortcode-tab: [content_blocker_consent] erklaert jetzt id/text/style einzeln
  mit beispielen (button vs. link).

+ i18n DE/EN ergaenzt (131 strings). version 1.4.9.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 14:00:22 +02:00

314 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Content Blocker – admin.js
* Color-picker init + service repeater (add / remove rows).
*/
( function ( $ ) {
'use strict';
/* ── Color pickers ── */
function initColorPickers( ctx ) {
$( '.cb-color-picker', ctx ).wpColorPicker();
}
/* ── Live header update: provider name ── */
function syncTitle( box ) {
const name = ( $( '.cb-input-name', box ).val() || '' ).trim();
$( '.cb-service-title', box ).first().text( name || ( cbAdmin.newServiceLbl || 'Neuer Dienst' ) );
}
/* ── Slugify a name to an id (mirrors PHP sanitize_title closely enough). ── */
function slugify( s ) {
return ( s || '' ).toString().trim().toLowerCase()
.replace( /ä/g, 'a' ).replace( /ö/g, 'o' ).replace( /ü/g, 'u' ).replace( /ß/g, 'ss' )
.replace( /[^a-z0-9]+/g, '-' )
.replace( /^-+|-+$/g, '' );
}
/* ── Show the service id (stable for saved services, live preview for new). ── */
function updateId( box ) {
let id = ( $( '.cb-input-id', box ).val() || '' ).trim();
if ( ! id ) {
id = slugify( $( '.cb-input-name', box ).val() );
}
$( '.cb-service-id', box ).text( id );
$( '.cb-service-idline', box ).toggle( !! id );
}
function bindNameSync( box ) {
$( '.cb-input-name', box ).on( 'input', function () {
syncTitle( box );
updateId( box );
} );
}
/* ── Expand / collapse a service box ── */
function setExpanded( box, open ) {
$( '.cb-service-grid', box ).toggle( open );
const $btn = $( '.cb-service-toggle', box );
$btn.attr( 'aria-expanded', open ? 'true' : 'false' ).text( open ? '▾' : '▸' );
}
/* Delegated once on document → works for existing AND dynamically added rows,
* and is resilient to load-order issues. */
$( document ).on( 'click', '.cb-service-toggle, .cb-service-title', function () {
const box = $( this ).closest( '.cb-service-box' );
const open = $( '.cb-service-toggle', box ).attr( 'aria-expanded' ) !== 'true';
setExpanded( box, open );
} );
function bindToggle( box ) {
// No-op kept for call sites; toggling is handled by delegation above.
}
/* ── Re-index a row's field names after a remove ── */
function reindexRows() {
$( '#cb-services-list .cb-service-box' ).each( function ( i ) {
$( this ).attr( 'data-cb-index', i );
$( this ).find( '[name]' ).each( function () {
const n = $( this ).attr( 'name' );
$( this ).attr( 'name', n.replace( /cb_services\[\d+\]/, 'cb_services[' + i + ']' ) );
} );
} );
}
/* ── Add a new (optionally prefilled) service row ── */
function addServiceRow( preset ) {
const index = $( '#cb-services-list .cb-service-box' ).length;
const template = $( '#cb-service-template' ).html();
const newHtml = template.replace( /__INDEX__/g, index );
const $box = $( newHtml );
$( '#cb-services-list' ).append( $box );
bindNameSync( $box );
bindToggle( $box );
$box.find( '.cb-remove-service' ).on( 'click', handleRemove );
if ( preset ) {
fillPreset( $box, preset );
}
setExpanded( $box, true ); // new rows start expanded for editing
syncTitle( $box );
updateId( $box );
return $box;
}
/* ── Fill a row from a preset object ── */
function fillPreset( $box, p ) {
const setText = function ( field, val ) {
$box.find( '[name$="[' + field + ']"]' ).val( val != null ? val : '' );
};
const setBool = function ( field, val ) {
$box.find( '[name$="[' + field + ']"]' ).prop( 'checked', !! val );
};
setText( 'id', p.id );
setText( 'name', p.name );
setText( 'match_pattern', p.match_pattern );
setText( 'recipient', p.recipient );
setText( 'privacy_url', p.privacy_url );
setText( 'purpose', p.purpose );
setBool( 'third_country', p.third_country );
setBool( 'sets_cookie', p.sets_cookie );
setBool( 'loads_script', p.loads_script );
$box.find( '.cb-service-title' ).text( p.name || cbAdmin.newServiceLbl );
}
/* ── Add empty service ── */
$( '#cb-add-service' ).on( 'click', function () {
addServiceRow( null );
} );
/* ── Insert preset ── */
$( '#cb-preset-select' ).on( 'change', function () {
const key = $( this ).val();
if ( ! key || ! cbAdmin.presets || ! cbAdmin.presets[ key ] ) {
return;
}
addServiceRow( cbAdmin.presets[ key ] );
$( this ).val( '' ); // reset so the same preset can be added again
} );
/* ── Remove service ── */
function handleRemove() {
if ( ! window.confirm( cbAdmin.confirmRemove ) ) {
return;
}
$( this ).closest( '.cb-service-box' ).remove();
reindexRows();
}
/* ── Tab switching ── */
$( '[data-cb-tab]' ).on( 'click', function ( e ) {
e.preventDefault();
activateTab( $( this ).data( 'cb-tab' ) );
} );
/* ── Website scan ── */
$( '#cb-scan-btn' ).on( 'click', function () {
const $btn = $( this ).prop( 'disabled', true );
const i18n = cbAdmin.i18n || {};
$( '#cb-scan-status' ).text( i18n.scanning || 'Scanning…' );
$( '#cb-scan-results' ).empty();
$.post( cbAdmin.ajaxUrl, { action: 'cb_scan', nonce: cbAdmin.scanNonce } )
.done( function ( resp ) {
if ( ! resp || ! resp.success ) {
const msg = resp && resp.data && resp.data.message ? resp.data.message : 'Error';
$( '#cb-scan-status' ).text( ( i18n.scanError || 'Scan failed:' ) + ' ' + msg );
return;
}
$( '#cb-scan-status' ).text( '' );
renderScan( resp.data );
} )
.fail( function ( xhr ) {
$( '#cb-scan-status' ).text( ( i18n.scanError || 'Scan failed:' ) + ' ' + xhr.status );
} )
.always( function () {
$btn.prop( 'disabled', false );
} );
} );
function renderScan( data ) {
const i18n = cbAdmin.i18n || {};
const findings = ( data && data.findings ) || [];
const $out = $( '#cb-scan-results' ).empty();
// Scanned pages summary
if ( data && data.scanned && data.scanned.length ) {
const $p = $( '<p class="description"></p>' ).text( ( i18n.scannedPages || 'Scanned:' ) + ' ' );
data.scanned.forEach( function ( s, i ) {
if ( i > 0 ) $p.append( ', ' );
$p.append( $( '<code></code>' ).text( s.url + ( s.error ? ' (!)' : '' ) ) );
} );
$out.append( $p );
}
if ( ! findings.length ) {
$out.append( $( '<p></p>' ).text( i18n.noFindings || 'No external resources found.' ) );
return;
}
const $table = $( '<table class="widefat striped"></table>' );
const $head = $( '<tr></tr>' );
[ i18n.host || 'Host', i18n.type || 'Type', i18n.count || 'Count', i18n.foundOn || 'Found on', i18n.example || 'Example', i18n.status || 'Status', i18n.action || 'Action' ]
.forEach( function ( h ) { $head.append( $( '<th></th>' ).text( h ) ); } );
$table.append( $( '<thead></thead>' ).append( $head ) );
const $body = $( '<tbody></tbody>' );
findings.forEach( function ( f ) {
const $tr = $( '<tr></tr>' );
// Host (+ party badge)
const $host = $( '<td></td>' );
$host.append( $( '<strong></strong>' ).text( f.host ) );
$host.append( document.createElement( 'br' ) );
$host.append( $( '<span></span>' )
.css( { fontSize: '11px', color: f.third_party ? '#b32d2e' : '#1a7f37' } )
.text( f.third_party ? ( i18n.thirdParty || 'third-party' ) : ( i18n.firstParty || 'first-party' ) ) );
$tr.append( $host );
// Types
$tr.append( $( '<td></td>' ).text( ( f.types || [] ).join( ', ' ) ) );
// Count
$tr.append( $( '<td></td>' ).text( f.count ) );
// Found on which pages
const $pages = $( '<td></td>' ).css( { fontSize: '11px' } );
( f.pages || [] ).forEach( function ( pageUrl, i ) {
if ( i > 0 ) $pages.append( document.createElement( 'br' ) );
let label = pageUrl;
try { label = new URL( pageUrl ).pathname || pageUrl; } catch ( e ) {}
$pages.append( $( '<a></a>' )
.attr( { href: pageUrl, target: '_blank', rel: 'noopener noreferrer', title: pageUrl } )
.text( label ) );
} );
$tr.append( $pages );
// Example URL
const $ex = $( '<td></td>' );
const sample = ( f.sample_urls || [] )[ 0 ] || '';
$ex.append( $( '<code></code>' ).css( { fontSize: '11px', wordBreak: 'break-all' } ).text( sample ) );
$tr.append( $ex );
// Does a ready-made template exist for this finding?
const preset = ( f.preset && cbAdmin.presets && cbAdmin.presets[ f.preset ] )
? cbAdmin.presets[ f.preset ]
: null;
// Status
const $st = $( '<td></td>' );
if ( f.covered ) {
$st.append( $( '<span></span>' ).css( { color: '#1a7f37', fontWeight: '600' } ).text( '✓ ' + ( i18n.covered || 'covered' ) ) );
} else if ( preset ) {
$st.append( $( '<span></span>' ).css( { color: '#2043B7', fontWeight: '600' } ).text( '★ ' + ( i18n.templateAvail || 'Vorlage verfügbar' ) ) );
}
$tr.append( $st );
// Action column.
const $act = $( '<td></td>' );
const host = ( f.host || '' ).toLowerCase();
const isGoogleFont = host === 'fonts.googleapis.com' || host === 'fonts.gstatic.com';
if ( isGoogleFont ) {
// Google Fonts can't be consent-gated meaningfully — the right fix is
// hosting them locally. Show a non-clickable hint, no button.
$act.append( $( '<span></span>' )
.css( { color: '#8a6d3b', fontStyle: 'italic' } )
.text( i18n.fontHint || 'Host this font locally for GDPR-compliant operation.' ) );
} else if ( f.third_party && ! f.covered ) {
// Prefill from the matching preset (full data) if there is one,
// otherwise just seed host + pattern.
const data = preset
? preset
: { name: f.host, match_pattern: f.suggested_pattern || f.host, third_country: true };
$( '<button type="button" class="button button-small"></button>' )
.text( preset ? ( i18n.useTemplate || 'Vorlage übernehmen' ) : ( i18n.addService || 'Add as service' ) )
.on( 'click', function () {
activateTab( 'services' );
const $box = addServiceRow( data );
if ( $box && $box.length ) {
$box[ 0 ].scrollIntoView( { behavior: 'smooth', block: 'center' } );
$box.find( '.cb-input-name' ).trigger( 'focus' );
}
} )
.appendTo( $act );
}
$tr.append( $act );
$body.append( $tr );
} );
$table.append( $body );
$out.append( $table );
}
/* ── Activate a tab by key ── */
function activateTab( tab ) {
$( '.cb-tab-content' ).hide();
$( '#cb-tab-' + tab ).show();
$( '[data-cb-tab]' ).removeClass( 'nav-tab-active' );
$( '[data-cb-tab="' + tab + '"]' ).addClass( 'nav-tab-active' );
}
/* ── Bootstrap ── */
$( document ).ready( function () {
initColorPickers( document );
$( '#cb-services-list .cb-service-box' ).each( function () {
bindNameSync( this );
bindToggle( this );
updateId( this );
$( '.cb-remove-service', this ).on( 'click', handleRemove );
} );
// Honor ?cb_tab=… so license redirects land on the right tab.
const params = new URLSearchParams( window.location.search );
const tab = params.get( 'cb_tab' );
if ( tab && $( '#cb-tab-' + tab ).length ) {
activateTab( tab );
}
} );
// Expose strings from wp_localize_script if needed.
window.cbAdmin = window.cbAdmin || { confirmRemove: 'Dienst wirklich entfernen?' };
} )( jQuery );