/** * 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: "interner-name — Anbietername" ── */ function syncTitle( box ) { const id = ( $( '.cb-input-id', box ).val() || '' ).trim(); const name = ( $( '.cb-input-name', box ).val() || '' ).trim(); const head = ( id || ( cbAdmin.newServiceLbl || 'neu' ) ) + ( name ? ' — ' + name : '' ); $( '.cb-service-title', box ).first().text( head ); } function bindNameSync( box ) { $( '.cb-input-name, .cb-input-id', box ).on( 'input', function () { syncTitle( 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 ); 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 = $( '

' ).text( ( i18n.scannedPages || 'Scanned:' ) + ' ' ); data.scanned.forEach( function ( s, i ) { if ( i > 0 ) $p.append( ', ' ); $p.append( $( '' ).text( s.url + ( s.error ? ' (!)' : '' ) ) ); } ); $out.append( $p ); } if ( ! findings.length ) { $out.append( $( '

' ).text( i18n.noFindings || 'No external resources found.' ) ); return; } const $table = $( '
' ); const $head = $( '' ); [ 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( $( '' ).text( h ) ); } ); $table.append( $( '' ).append( $head ) ); const $body = $( '' ); findings.forEach( function ( f ) { const $tr = $( '' ); // Host (+ party badge) const $host = $( '' ); $host.append( $( '' ).text( f.host ) ); $host.append( document.createElement( 'br' ) ); $host.append( $( '' ) .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( $( '' ).text( ( f.types || [] ).join( ', ' ) ) ); // Count $tr.append( $( '' ).text( f.count ) ); // Found on which pages const $pages = $( '' ).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( $( '' ) .attr( { href: pageUrl, target: '_blank', rel: 'noopener noreferrer', title: pageUrl } ) .text( label ) ); } ); $tr.append( $pages ); // Example URL const $ex = $( '' ); const sample = ( f.sample_urls || [] )[ 0 ] || ''; $ex.append( $( '' ).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 = $( '' ); if ( f.covered ) { $st.append( $( '' ).css( { color: '#1a7f37', fontWeight: '600' } ).text( '✓ ' + ( i18n.covered || 'covered' ) ) ); } else if ( preset ) { $st.append( $( '' ).css( { color: '#2043B7', fontWeight: '600' } ).text( '★ ' + ( i18n.templateAvail || 'Vorlage verfügbar' ) ) ); } $tr.append( $st ); // Action: take over as a new service (only useful for uncovered third parties) const $act = $( '' ); 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 }; $( '' ) .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-id' ).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 ); $( '.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 );