1/* AIM Transparency â front-end badge injector. 2 * Renders a visible badge into the nearest positioned ancestor for every 3 * element marked server-side with data-aicl. <img> elements cannot host a 4 * pseudo-element, so they are wrapped in a positioned span. Background-image 5 * containers receive the badge directly. Re-runs on AJAX/DOM changes so 6 * Enfold "load more" thumbnails are covered too. */ 7( function () { 8 'use strict'; 9 10 const POS = ( window.AICL && window.AICL.position ) || 'top-left'; 11 const POS_CLASS = { 'top-left': 'aicl-tl', 'top-right': 'aicl-tr', 'bottom-left': 'aicl-bl', 'bottom-right': 'aicl-br' }; 12 const MAP = ( window.AICL && window.AICL.scanAll && window.AICL.map ) || null; 13 const STYLE = ( window.AICL && window.AICL.style ) || 'eu'; 14 const EU_BADGE = { 15 basicAI: '', 16 trainedAlgorithmicMedia: 'Generated', 17 compositeWithTrainedAlgorithmicMedia: 'Modified' 18 }; 19 const ICON_BASE = ( window.AICL && window.AICL.iconBase ) || ''; 20 const ICON_VARIANT = ( window.AICL && window.AICL.iconVariant ) === 'white' ? 'white' : 'black'; 21 const EU_ICON = { 22 basicAI: 'basic', 23 trainedAlgorithmicMedia: 'generated', 24 compositeWithTrainedAlgorithmicMedia: 'modified' 25 }; 26 27 /** 28 * Resolve the official EU icon file for a type and style. 29 * 30 * @param {string} type - The AI content type token. 31 * @param {string} style - The badge style ('eu' full, or 'eu_basic' compact). 32 * @return {string} The icon filename within ICON_BASE, or '' when none applies. 33 */ 34 function euIconFile( type, style ) { 35 const name = ( style === 'eu_basic' ) ? 'basic' : ( EU_ICON[ type ] || '' ); 36 37 return name ? ( name + '-' + ICON_VARIANT + '.png' ) : ''; 38 } 39 40 const SHAPE = ( window.AICL && window.AICL.shape ) || 'pill'; 41 const MODAL = !!( window.AICL && window.AICL.modal ); 42 const MODAL_TITLE = ( window.AICL && window.AICL.modalTitle ) || ''; 43 const MODAL_TEXT = ( window.AICL && window.AICL.modalText ) || ''; 44 // Custom logo. Applies to the built-in styles only ('aim' / 'custom'); never to the 45 // official EU icon ('eu' / 'eu_basic'), whose mark is fixed by the disclosure standard. 46 const LOGO = ( window.AICL && window.AICL.logo ) || ''; 47 // Translated on the server (badge_config); the literals below are fallbacks. 48 const TYPE_HEADING = ( window.AICL && window.AICL.typeHeadings ) || { 49 trainedAlgorithmicMedia: 'AI-generated image', 50 compositeWithTrainedAlgorithmicMedia: 'AI-edited image', 51 basicAI: 'AI content', 52 algorithmicMedia: 'Algorithmically generated image', 53 digitalCapture: 'Camera photograph', 54 digitalCreation: 'Human-created image', 55 composite: 'Composite image' 56 }; 57 const TYPE_BODY = ( window.AICL && window.AICL.typeBodies ) || { 58 trainedAlgorithmicMedia: 'This image was generated by an artificial-intelligence model.', 59 compositeWithTrainedAlgorithmicMedia: 'This is a real image that was modified using artificial intelligence.', 60 basicAI: 'Artificial intelligence was involved in creating this image.', 61 algorithmicMedia: 'This image was produced by an algorithm (not AI).', 62 digitalCapture: 'This photograph was captured from real life with a camera. No AI was involved.', 63 digitalCreation: 'This image was created by a human using digital tools, without generative AI.', 64 composite: 'This image is a composite of non-AI elements, such as photographs and 3D renders.' 65 }; 66 const ARIA_AI = ( window.AICL && window.AICL.ariaAi ) || 'artificially generated'; 67 const ARIA_NON_AI = ( window.AICL && window.AICL.ariaNonAi ) || 'content disclosure'; 68 const CLOSE_LABEL = ( window.AICL && window.AICL.closeLabel ) || 'Close'; 69 const MODAL_FOOTER = ( window.AICL && window.AICL.modalFooter ) || 'Disclosed under the EU AI Act (Article 50), labelled by the site owner.'; 70 const NON_AI = { algorithmicMedia: true, digitalCapture: true, digitalCreation: true, composite: true }; 71 let lastFocus = null; 72 73 /** 74 * Extracts the type token from a MAP entry. 75 * 76 * @param {(string|Array)} e - A MAP entry, either a legacy string or a [token, label] tuple. 77 * @return {string} The type token, or an empty string for legacy string entries. 78 */ 79 function mapType( e ) { return ( typeof e === 'string' ) ? '' : e[ 0 ]; } 80 81 /** 82 * Extracts the label from a MAP entry. 83 * 84 * @param {(string|Array)} e - A MAP entry, either a legacy string or a [token, label] tuple. 85 * @return {string} The label text. 86 */ 87 function mapLabel( e ) { return ( typeof e === 'string' ) ? e : e[ 1 ]; } 88 89 /** 90 * Normalizes an image URL to its base filename (strip query, hash and -WxH size suffix). 91 * 92 * @param {string} url - The image URL to normalize. 93 * @return {string} The base filename without query string, hash or size suffix. 94 */ 95 function baseName( url ) { 96 if ( !url ) { return ''; } 97 98 url = url.split( '?' )[ 0 ].split( '#' )[ 0 ]; 99 const f = url.substring( url.lastIndexOf( '/' ) + 1 ); 100 101 return f.replace( /-\d+x\d+(\.[a-z0-9]+)$/i, '$1' ); 102 } 103 104 /** 105 * Looks a URL up in the map, preferring the most specific key the URL can supply. 106 * 107 * The map is keyed by the path relative to the uploads directory, which is what distinguishes 108 * two images that share a filename in different month folders. Hand-written markup does not 109 * always carry a resolvable path, so the bare filename is tried second, and the server drops any 110 * filename claimed by more than one attachment rather than pick between them. 111 * 112 * @param {string} url - The image URL from the markup. 113 * @return {?(string|Array)} The MAP entry, or null when nothing matches. 114 */ 115 function mapLookup( url ) { 116 if ( !MAP || !url ) { return null; } 117 118 const clean = url.split( '?' )[ 0 ].split( '#' )[ 0 ]; 119 const full = clean.replace( /-\d+x\d+(\.[a-z0-9]+)$/i, '$1' ); 120 const at = full.indexOf( '/uploads/' ); 121 122 if ( at !== -1 ) { 123 const rel = full.substring( at + 9 ); 124 125 if ( MAP[ rel ] ) { return MAP[ rel ]; } 126 } 127 128 return MAP[ baseName( url ) ] || null; 129 } 130 131 /** 132 * Builds a badge element for the given content type and label. 133 * 134 * The official EU icons are AI-only, so non-AI (algorithmic/procedural) types 135 * yield no badge; 'eu_basic' forces the compact mark on every AI image. 136 * 137 * @param {string} type - The AI content type token used to pick the EU badge word. 138 * @param {string} label - The fallback label text used for the non-EU badge style. 139 * @return {?HTMLSpanElement} The badge element, or null when the type warrants no badge. 140 */ 141 function makeBadge( type, label ) { 142 let badge; 143 144 if ( STYLE === 'eu' || STYLE === 'eu_basic' || STYLE === 'aim' ) { 145 if ( !EU_BADGE.hasOwnProperty( type ) ) { return null; } 146 147 // 'aim' = built-in CSS badge (word is customisable per type via the label, minus a 148 // leading "AI "); 'eu'/'eu_basic' = the official PNG icon, whose text is fixed. 149 const word = ( STYLE === 'eu_basic' ) 150 ? '' 151 : ( STYLE === 'aim' ) 152 ? ( type === 'basicAI' ? '' : ( ( label || '' ).replace( /^ai\s+/i, '' ).trim() || EU_BADGE[ type ] ) ) 153 : EU_BADGE[ type ]; 154 const file = ( STYLE === 'aim' ) ? '' : euIconFile( type, STYLE ); 155 badge = document.createElement( 'span' ); 156 badge.className = 'aicl-badge aicl-eu-svg ' + ( POS_CLASS[ POS ] || 'aicl-tl' ); 157 badge.setAttribute( 'role', 'note' ); 158 badge.setAttribute( 'aria-label', word ? ( 'AI ' + word.toLowerCase() ) : ( 'AI (' + ARIA_AI + ')' ) ); 159 160 if ( ICON_BASE && file ) { 161 const img = document.createElement( 'img' ); 162 img.src = ICON_BASE + file; 163 img.alt = word ? ( 'AI ' + word ) : 'AI'; 164 img.className = 'aicl-eu-img'; 165 badge.appendChild( img ); 166 } else { 167 // Fallback to the built-in mark if the icon asset is unavailable. 168 badge.className = 'aicl-badge aicl-eu ' + ( POS_CLASS[ POS ] || 'aicl-tl' ) + ( word ? '' : ' aicl-eu-basic' ); 169 170 const circle = document.createElement( 'span' ); 171 circle.className = 'aicl-eu-ai'; 172 circle.textContent = 'AI'; 173 badge.appendChild( circle ); 174 175 if ( word ) { 176 const w = document.createElement( 'span' ); 177 w.className = 'aicl-eu-word'; 178 w.textContent = word; 179 badge.appendChild( w ); 180 } 181 } 182 } else { 183 badge = document.createElement( 'span' ); 184 badge.className = 'aicl-badge aicl-shape-' + SHAPE + ' ' + ( POS_CLASS[ POS ] || 'aicl-tl' ); 185 badge.setAttribute( 'role', 'note' ); 186 badge.textContent = label || 'AI'; 187 badge.setAttribute( 'aria-label', ( label || 'AI' ) + ' (' + ( NON_AI[ type ] ? ARIA_NON_AI : ARIA_AI ) + ')' ); 188 } 189 190 // Prepend the deployer's custom logo on the built-in styles (never the official EU icon). 191 if ( LOGO && STYLE !== 'eu' && STYLE !== 'eu_basic' ) { 192 const logo = document.createElement( 'img' ); 193 logo.src = LOGO; 194 logo.alt = ''; 195 logo.loading = 'lazy'; 196 logo.className = 'aicl-badge-logo';
197 // The badge draws a small dot as its mark. A logo is a mark, so showing both puts two 198 // of them side by side, which reads as clutter rather than as a label. The class lets 199 // the stylesheet drop the dot without needing :has(), which keeps older browsers right. 200 badge.classList.add( 'aicl-has-logo' ); 201 badge.insertBefore( logo, badge.firstChild ); 202 } 203 204 return decorateBadge( badge, type, label ); 205 } 206 207 /** 208 * Store disclosure data on a badge and, when the click-to-disclose modal is enabled, 209 * make it an accessible button that opens the disclosure. 210 * 211 * @param {HTMLSpanElement} badge - The badge element. 212 * @param {string} type - The source-type token. 213 * @param {string} label - The disclosure label. 214 * @return {HTMLSpanElement} The decorated badge. 215 */ 216 function decorateBadge( badge, type, label ) { 217 badge.setAttribute( 'data-aicl-type', type || '' ); 218 badge.setAttribute( 'data-aicl-label', label || '' ); 219 badge.setAttribute( 'title', label || 'AI' ); 220 221 if ( MODAL ) { 222 badge.classList.add( 'aicl-clickable' ); 223 badge.setAttribute( 'role', 'button' ); 224 badge.setAttribute( 'tabindex', '0' ); 225 badge.setAttribute( 'aria-haspopup', 'dialog' ); 226 badge.addEventListener( 'click', onBadgeActivate ); 227 badge.addEventListener( 'keydown', onBadgeKey ); 228 } 229 230 return badge; 231 } 232 233 /** 234 * Open the disclosure modal for the activated badge. 235 * 236 * @param {Event} e - The triggering event. 237 * @return {void} 238 */ 239 function onBadgeActivate( e ) { 240 if ( e ) { e.preventDefault(); e.stopPropagation(); } 241 242 openModal( this.getAttribute( 'data-aicl-type' ) || '', this.getAttribute( 'data-aicl-label' ) || 'AI' ); 243 } 244 245 /** 246 * Activate the badge on Enter or Space for keyboard users. 247 * 248 * @param {KeyboardEvent} e - The key event. 249 * @return {void} 250 */ 251 function onBadgeKey( e ) { 252 if ( 'Enter' === e.key || ' ' === e.key ) { 253 onBadgeActivate.call( this, e ); 254 } 255 } 256 257 /** 258 * Render the accessible click-to-disclose modal for an image's AI provenance. 259 * 260 * @param {string} type - The source-type token. 261 * @param {string} label - The disclosure label. 262 * @return {void} 263 */ 264 function openModal( type, label ) { 265 closeModal(); 266 267 lastFocus = document.activeElement; 268 269 const overlay = document.createElement( 'div' ); 270 overlay.className = 'aicl-modal-overlay'; 271 overlay.id = 'aicl-modal'; 272 273 const dialog = document.createElement( 'div' ); 274 dialog.className = 'aicl-modal'; 275 dialog.setAttribute( 'role', 'dialog' ); 276 dialog.setAttribute( 'aria-modal', 'true' ); 277 dialog.setAttribute( 'aria-labelledby', 'aicl-modal-title' ); 278 279 const close = document.createElement( 'button' ); 280 close.type = 'button'; 281 close.className = 'aicl-modal-x'; 282 close.setAttribute( 'aria-label', CLOSE_LABEL ); 283 close.textContent = 'Ã'; 284 285 const mark = document.createElement( 'div' ); 286 mark.className = 'aicl-modal-mark'; 287 mark.textContent = 'AI'; 288 289 const title = document.createElement( 'h2' ); 290 title.className = 'aicl-modal-title'; 291 title.id = 'aicl-modal-title'; 292 title.textContent = MODAL_TITLE || TYPE_HEADING[ type ] || 'AI content'; 293 294 const body = document.createElement( 'div' ); 295 body.className = 'aicl-modal-body'; 296 297 if ( MODAL_TEXT ) { 298 // Server-sanitised with wp_kses (tight allowlist), safe to render as HTML. 299 body.innerHTML = MODAL_TEXT; 300 } else { 301 body.textContent = TYPE_BODY[ type ] || ( ( label || 'AI' ) + ' â created or altered with artificial intelligence.' ); 302 } 303 304 const foot = document.createElement( 'p' ); 305 foot.className = 'aicl-modal-foot'; 306 foot.textContent = MODAL_FOOTER; 307 308 dialog.appendChild( close ); 309 dialog.appendChild( mark ); 310 dialog.appendChild( title ); 311 dialog.appendChild( body ); 312 dialog.appendChild( foot ); 313 overlay.appendChild( dialog ); 314 document.body.appendChild( overlay ); 315 316 overlay.addEventListener( 'click', function ( e ) { if ( e.target === overlay ) { closeModal(); } } ); 317 close.addEventListener( 'click', closeModal ); 318 document.addEventListener( 'keydown', onModalKey ); 319 320 close.focus(); 321 } 322 323 /** 324 * Close the disclosure modal on Escape. 325 * 326 * @param {KeyboardEvent} e - The key event. 327 * @return {void} 328 */ 329 function onModalKey( e ) { 330 if ( 'Escape' === e.key ) { closeModal(); } 331 } 332
333 /** 334 * Remove the disclosure modal and restore focus to the badge. 335 * 336 * @return {void} 337 */ 338 function closeModal() { 339 const m = document.getElementById( 'aicl-modal' ); 340 341 if ( m ) { m.parentNode.removeChild( m ); } 342 343 document.removeEventListener( 'keydown', onModalKey ); 344 345 if ( lastFocus && lastFocus.focus ) { 346 lastFocus.focus(); 347 lastFocus = null; 348 } 349 } 350 351 /** 352 * Wraps an <img> in a positioned span and appends its badge. 353 * 354 * @param {HTMLImageElement} img - The image element to decorate. 355 * @return {void} 356 */ 357 function decorateImg( img ) { 358 if ( img.getAttribute( 'data-aicl-done' ) ) { return; } 359 img.setAttribute( 'data-aicl-done', '1' ); 360 361 rememberOriginal( img ); 362 363 const badge = makeBadge( img.getAttribute( 'data-aicl-type' ) || '', img.getAttribute( 'data-aicl-label' ) || 'AI' ); 364 if ( !badge ) { return; } 365 366 img.aiclBadge = badge; 367 368 const parent = img.parentNode; 369 370 if ( parent && parent.classList && parent.classList.contains( 'aicl-wrap' ) ) { 371 parent.appendChild( badge ); 372 return; 373 } 374 375 // object-fit images are sized/cropped by their container (aspect-ratio boxes, 376 // cover blocks, cropped thumbnails). Re-parenting them into a wrapper detaches 377 // them from that sizing and blows them up, so anchor the badge to the container. 378 const fit = window.getComputedStyle( img ).objectFit; 379 if ( ( fit === 'cover' || fit === 'contain' ) && parent && 1 === parent.nodeType ) { 380 if ( 'static' === window.getComputedStyle( parent ).position ) { 381 parent.style.position = 'relative'; 382 } 383 parent.appendChild( badge ); 384 return; 385 } 386 387 const wrap = document.createElement( 'span' ); 388 wrap.className = 'aicl-wrap'; 389 if ( parent ) { parent.insertBefore( wrap, img ); } 390 wrap.appendChild( img ); 391 wrap.appendChild( badge ); 392 } 393 394 /** 395 * Records the verdict an image was first rendered with, so clearing a 396 * WooCommerce variation can put it back. 397 * 398 * Called for every decorated image and, separately, for a variable product's 399 * gallery image even when that image carries no disclosure at all. An empty 400 * recorded type is meaningful: it restores "no badge". 401 * 402 * @param {HTMLImageElement} img - The image to remember. 403 * @return {void} 404 */ 405 function rememberOriginal( img ) { 406 if ( img.hasAttribute( 'data-aicl-otype' ) ) { return; } 407 408 img.setAttribute( 'data-aicl-otype', img.getAttribute( 'data-aicl-type' ) || '' ); 409 img.setAttribute( 'data-aicl-olabel', img.getAttribute( 'data-aicl-label' ) || '' ); 410 } 411 412 /** 413 * The gallery image WooCommerce swaps when a variation is chosen. 414 * 415 * Scoped to the product the form belongs to, so a page showing more than one 416 * variable product cannot resolve to a neighbour's picture. 417 * 418 * @param {HTMLFormElement} form - A WooCommerce variations form. 419 * @return {?HTMLImageElement} The main gallery image, or null. 420 */ 421 function galleryImage( form ) { 422 const scope = ( form.closest && form.closest( '.product' ) ) || document; 423 const wrap = scope.querySelector( '.woocommerce-product-gallery__image, .woocommerce-product-gallery__image--placeholder' ); 424 425 return wrap ? wrap.querySelector( '.wp-post-image' ) : null; 426 } 427 428 /** 429 * Replaces an image's disclosure with a new verdict, badge and all. 430 * 431 * @param {HTMLImageElement} img - The image to re-decide. 432 * @param {?Object} verdict - {type, label} to apply, or a falsy value for none. 433 * @return {void} 434 */ 435 function applyVerdict( img, verdict ) { 436 if ( !img ) { return; } 437 438 const existing = img.aiclBadge || ( img.parentNode && img.parentNode.querySelector( '.aicl-badge' ) ); 439 440 if ( existing && existing.parentNode ) { existing.parentNode.removeChild( existing ); } 441 442 img.aiclBadge = null; 443 img.removeAttribute( 'data-aicl-done' ); 444 img.removeAttribute( 'data-aicl' ); 445 img.removeAttribute( 'data-aicl-type' ); 446 img.removeAttribute( 'data-aicl-label' ); 447 448 if ( !verdict || !verdict.type ) { return; } 449 450 img.setAttribute( 'data-aicl', '1' ); 451 img.setAttribute( 'data-aicl-type', verdict.type ); 452 img.setAttribute( 'data-aicl-label', verdict.label || 'AI' ); 453 decorateImg( img ); 454 } 455 456 /** 457 * Keeps the disclosure honest across WooCommerce variation changes. 458 * 459 * Choosing a variation swaps the gallery picture by rewriting src on the 460 * existing element, so the markers the server wrote for the original 461 * attachment would otherwise stay attached to a different photograph: a real 462 * photo wearing an "AI Generated" badge, or an AI variation wearing none. 463 * 464 * This listens to WooCommerce's own `found_variation` and `reset_data` 465 * events rather than watching src. Those events carry the chosen variation, 466 * including the verdict AICL_Frontend::variation_disclosure() attached to it, 467 * and they fire in AJAX mode too, so products above the variation threshold 468 * (where WooCommerce prints no inline JSON) are covered as well. Watching src 469 * would also misfire on lazy-loading plugins, which swap a placeholder for 470 * the real file exactly the same way. 471 * 472 * @return {void} 473 */ 474 function watchVariations() { 475 const $ = window.jQuery; 476 if ( !$ ) { return; } 477 478 $( 'form.variations_form' ).each( function () { 479 const form = this; 480 if ( form.getAttribute( 'data-aicl-bound' ) ) { return; } 481 form.setAttribute( 'data-aicl-bound', '1' ); 482 483 const first = galleryImage( form ); 484 if ( first ) { rememberOriginal( first ); } 485 486 $( form ).on( 'found_variation', function ( event, variation ) { 487 const img = galleryImage( form ); 488 window.requestAnimationFrame( function () { 489 applyVerdict( img, variation && variation.aicl ); 490 } ); 491 } ); 492 493 $( form ).on( 'reset_data', function () { 494 const img = galleryImage( form ); 495 window.requestAnimationFrame( function () { 496 applyVerdict( img, img && { type: img.getAttribute( 'data-aicl-otype' ), label: img.getAttribute( 'data-aicl-olabel' ) } ); 497 } ); 498 } ); 499 } ); 500 } 501 502 /** 503 * Marks a background-image container and appends its badge. 504 * 505 * @param {HTMLElement} el - The background-image container element to decorate. 506 * @return {void} 507 */ 508 function decorateBg( el ) { 509 if ( el.getAttribute( 'data-aicl-done' ) ) { return; } 510 el.setAttribute( 'data-aicl-done', '1' ); 511 512 const badge = makeBadge( el.getAttribute( 'data-aicl-type' ) || '', el.getAttribute( 'data-aicl-label' ) || 'AI' ); 513 if ( !badge ) { return; } 514 515 const host = bgHost( el ); 516 517 host.classList.add( 'aicl-bg' ); 518 host.appendChild( badge ); 519 520 if ( host !== el ) { host.setAttribute( 'data-aicl-done', '1' ); } 521 } 522
523 /** 524 * Picks the element that should host a background badge. 525 * 526 * Enfold parallax sections paint the background in a transformed, oversized 527 * layer (.av-parallax-inner) that forms its own stacking context beneath the 528 * section colour overlay, so a badge placed there is pushed off-screen and 529 * hidden behind the overlay. Anchor the badge to the visible section instead. 530 * 531 * @param {HTMLElement} el - The element carrying the background image. 532 * @return {HTMLElement} The element the badge should be appended to. 533 */ 534 function bgHost( el ) { 535 if ( el.closest( '.av-parallax' ) ) { 536 const section = el.closest( '.avia-section' ); 537 if ( section ) { return section; } 538 } 539 540 return el; 541 } 542 543 const BG_SEL = 'div,section,a,article,aside,header,figure,li'; 544 let lastBgScan = 0; 545 546 /** 547 * Cheap pass: badge any <img> whose file matches a flagged attachment. 548 * 549 * @param {(Document|HTMLElement)} root - The subtree to scan for matching images. 550 * @return {void} 551 */ 552 function scanImgs( root ) { 553 const imgs = root.querySelectorAll( 'img:not([data-aicl-done]):not([data-aicl])' ); 554 555 for ( let i = 0; i < imgs.length; i++ ) { 556 const entry = mapLookup( imgs[ i ].getAttribute( 'src' ) ); 557 if ( entry ) { 558 imgs[ i ].setAttribute( 'data-aicl-type', mapType( entry ) ); 559 imgs[ i ].setAttribute( 'data-aicl-label', mapLabel( entry ) ); 560 decorateImg( imgs[ i ] ); 561 } 562 } 563 } 564 565 /** 566 * Heavier pass: read the computed background-image of candidate containers and 567 * badge those whose file matches a flagged attachment. Reading the computed 568 * style catches CSS-class / <style>-block backgrounds, not just inline ones; 569 * the read pass runs before any DOM writes to avoid layout thrash. 570 * 571 * @param {(Document|HTMLElement)} root - The subtree to scan for matching backgrounds. 572 * @return {void} 573 */ 574 function scanBackgrounds( root ) { 575 const els = root.querySelectorAll( BG_SEL + ':not([data-aicl-done]):not([data-aicl])' ); 576 const hits = []; 577 const max = Math.min( els.length, 4000 ); 578 579 for ( let j = 0; j < max; j++ ) { 580 const el = els[ j ]; 581 let bg = el.style.backgroundImage; 582 if ( !bg || bg.indexOf( 'url(' ) === -1 ) { 583 const cs = window.getComputedStyle( el ); 584 if ( cs.display === 'none' ) { continue; } 585 bg = cs.backgroundImage; 586 } 587 if ( !bg || bg.indexOf( 'url(' ) === -1 ) { continue; } 588 589 const m = bg.match( /url\((['"]?)([^'")]+)\1\)/ ); 590 if ( !m ) { continue; } 591 592 const bentry = mapLookup( m[ 2 ] ); 593 if ( bentry ) { hits.push( [ el, bentry ] ); } 594 } 595 596 for ( let k = 0; k < hits.length; k++ ) { 597 hits[ k ][ 0 ].setAttribute( 'data-aicl-type', mapType( hits[ k ][ 1 ] ) ); 598 hits[ k ][ 0 ].setAttribute( 'data-aicl-label', mapLabel( hits[ k ][ 1 ] ) ); 599 decorateBg( hits[ k ][ 0 ] ); 600 } 601 } 602 603 /** 604 * Universal scan: badge any <img>/background whose file matches a flagged attachment. 605 * 606 * @param {(Document|HTMLElement)} root - The subtree to scan. 607 * @return {void} 608 */ 609 function scanAll( root ) { 610 if ( !MAP ) { return; } 611 612 scanImgs( root ); 613 614 const now = Date.now(); 615 if ( now - lastBgScan > 800 ) { 616 lastBgScan = now; 617 scanBackgrounds( root ); 618 } 619 } 620 621 /** 622 * Decorates all server-flagged images and backgrounds in a subtree, then runs the universal scan. 623 * 624 * @param {(Document|HTMLElement)} [root] - The subtree to process; defaults to document. 625 * @return {void} 626 */ 627 function run( root ) { 628 root = root || document; 629 630 const imgs = root.querySelectorAll( 'img[data-aicl="1"]:not([data-aicl-done])' ); 631 for ( let i = 0; i < imgs.length; i++ ) { decorateImg( imgs[ i ] ); } 632 633 const bgs = root.querySelectorAll( '[data-aicl="1"]:not(img):not([data-aicl-done])' ); 634 for ( let j = 0; j < bgs.length; j++ ) { decorateBg( bgs[ j ] ); } 635 636 scanAll( root ); 637 } 638 639 if ( document.readyState === 'loading' ) { 640 document.addEventListener( 'DOMContentLoaded', function () { run(); watchVariations(); } ); 641 } else { 642 run(); 643 watchVariations(); 644 } 645 /* Bound again on load because jQuery, which WooCommerce's variation form 646 needs, may not have been present the first time. watchVariations() marks 647 each form it has bound, so this cannot double-subscribe. */ 648 window.addEventListener( 'load', function () { run(); watchVariations(); } ); 649 650 if ( 'MutationObserver' in window ) { 651 let pending = false;
652 const mo = new MutationObserver( function () { 653 if ( pending ) { return; } 654 pending = true; 655 window.requestAnimationFrame( function () { pending = false; run(); } ); 656 } ); 657 mo.observe( document.body || document.documentElement, { childList: true, subtree: true } ); 658 } 659}() );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.