- Aufklapp-Pfeil deutlich groesser (26px). - Tabs: kein Fokus-Kasten mehr, nur untere Linie markiert den aktiven Tab. - CB_VERSION + Header auf 1.1.0 -> bricht gecachte alte admin.js/frontend.js (Ursache, dass ein Dienst auf einer Seite nicht aufklappbar war). - Aufklappen via Event-Delegation (robust gegen Load-Order/dynamische Zeilen). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
285 lines
9.9 KiB
JavaScript
285 lines
9.9 KiB
JavaScript
/**
|
||
* 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 = $( '<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: take over as a new service (only useful for uncovered third parties)
|
||
const $act = $( '<td></td>' );
|
||
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-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 );
|