1/** 2 * KI-Kennzeichnung für intax.de 3 * 4 * Unterstützt: 5 * 6 * 1. manuell gesetzte CSS-Klassen 7 * ki-generiert 8 * ki-bearbeitet 9 * ki-inhalt 10 * 11 * 2. automatische Erkennung über Dateinamen 12 * --ki-generiert 13 * --ki-bearbeitet 14 * --ki-inhalt 15 * 16 * 3. automatische Erkennung über Ordnernamen 17 * /_ki-generiert/ 18 * /_ki-bearbeitet/ 19 * /_ki-inhalt/ 20 * 21 * 4. Kennzeichnung in der Contao-Colorbox 22 * 23 * Bei automatisch erkannten Bildern wird ausschließlich das 24 * eigentliche Bild umschlossen. Bildunterschriften und Namen 25 * bleiben außerhalb der Positionierungsfläche. 26 */ 27 28(function (window, document) { 29 "use strict"; 30 31 32 /* ===================================================== 33 KONFIGURATION 34 ===================================================== */ 35 36 const markerClasses = [ 37 "ki-generiert", 38 "ki-bearbeitet", 39 "ki-inhalt" 40 ]; 41 42 const markerSelector = 43 ".ki-generiert, .ki-bearbeitet, .ki-inhalt"; 44 45 const markerRules = [ 46 { 47 className: "ki-generiert", 48 patterns: [ 49 "--ki-generiert", 50 "/_ki-generiert/", 51 "/ki-generiert/" 52 ] 53 }, 54 { 55 className: "ki-bearbeitet", 56 patterns: [ 57 "--ki-bearbeitet", 58 "/_ki-bearbeitet/", 59 "/ki-bearbeitet/" 60 ] 61 }, 62 { 63 className: "ki-inhalt", 64 patterns: [ 65 "--ki-inhalt", 66 "/_ki-inhalt/", 67 "/ki-inhalt/" 68 ] 69 } 70 ]; 71 72 const badgeFiles = { 73 "ki-generiert": 74 "https://intax.de/files/img/_ki/" + 75 "LABEL_AI%20GENERATED_white%20transparent.svg", 76 77 "ki-bearbeitet": 78 "https://intax.de/files/img/_ki/" + 79 "LABEL_AI%20MODIFIED_white%20transparent.svg", 80 81 "ki-inhalt": 82 "https://intax.de/files/img/_ki/" + 83 "LABEL_AI_white%20transparent.svg" 84 }; 85 86 let lightboxBadge = null; 87 88 89 /* ===================================================== 90 ALLGEMEINE HILFSFUNKTIONEN 91 ===================================================== */ 92 93 function normalizeValue(value) { 94 if (!value) { 95 return ""; 96 } 97 98 let normalizedValue = String(value); 99 100 try { 101 normalizedValue = decodeURIComponent( 102 normalizedValue 103 ); 104 } catch (error) { 105 /* 106 * Ungültig codierte URLs werden unverändert 107 * weiter geprüft. 108 */ 109 } 110 111 return normalizedValue 112 .replace(/\\/g, "/") 113 .toLowerCase(); 114 } 115 116 117 function detectMarkerFromText(value) { 118 const normalizedValue = normalizeValue(value); 119 120 if (!normalizedValue) { 121 return null; 122 } 123 124 const matchingRule = markerRules.find( 125 function (rule) { 126 return rule.patterns.some( 127 function (pattern) { 128 return normalizedValue.includes( 129 pattern 130 ); 131 } 132 ); 133 } 134 ); 135 136 return matchingRule 137 ? matchingRule.className 138 : null; 139 } 140 141 142 function detectMarkerFromClasses(element) { 143 if (!element || !element.closest) { 144 return null; 145 } 146 147 const markedElement = element.closest( 148 markerSelector 149 ); 150 151 if (!markedElement) { 152 return null; 153 } 154 155 return markerClasses.find( 156 function (className) { 157 return markedElement.classList.contains( 158 className 159 ); 160 } 161 ) || null; 162 } 163 164 165 function getImageValues(image) { 166 const values = []; 167 168 if (!image) { 169 return values; 170 } 171 172 values.push(image.currentSrc); 173 values.push(image.src); 174 values.push(image.getAttribute("src")); 175 values.push(image.getAttribute("srcset")); 176 177 const picture = image.closest("picture"); 178 179 if (picture) { 180 picture 181 .querySelectorAll("source") 182 .forEach(function (source) { 183 values.push( 184 source.getAttribute("src") 185 ); 186 187 values.push( 188 source.getAttribute("srcset") 189 ); 190 }); 191 } 192 193 const link = image.closest("a"); 194 195 if (link) { 196 values.push(link.href); 197 values.push(link.getAttribute("href")); 198 } 199
200 return values.filter(Boolean); 201 } 202 203 204 function detectMarkerFromImageUrl(image) { 205 const values = getImageValues(image); 206 207 for (const value of values) { 208 const markerClass = 209 detectMarkerFromText(value); 210 211 if (markerClass) { 212 return markerClass; 213 } 214 } 215 216 return null; 217 } 218 219 220 function removeMarkerClasses(element) { 221 if (!element) { 222 return; 223 } 224 225 markerClasses.forEach(function (className) { 226 element.classList.remove(className); 227 }); 228 } 229 230 231 /* ===================================================== 232 BILDER AUSSCHLIESSEN, DIE NICHT VERÄNDERT WERDEN DÜRFEN 233 ===================================================== */ 234 235 function shouldIgnoreImage(image) { 236 if (!(image instanceof HTMLImageElement)) { 237 return true; 238 } 239 240 /* 241 * Das Bild innerhalb der Colorbox darf nicht mit einer 242 * zusätzlichen Hülle versehen werden. 243 */ 244 245 if ( 246 image.closest( 247 "#colorbox, #cboxLoadedContent, " + 248 ".cboxPhoto, #ki-lightbox-badge" 249 ) 250 ) { 251 return true; 252 } 253 254 /* 255 * Auch die eigentlichen Kennzeichnungsdateien werden 256 * nicht als normale Inhaltsbilder verarbeitet. 257 */ 258 259 const source = normalizeValue( 260 image.currentSrc || 261 image.src || 262 image.getAttribute("src") 263 ); 264 265 if (source.includes("/files/img/_ki/")) { 266 return true; 267 } 268 269 return false; 270 } 271 272 273 /* ===================================================== 274 EIGENE HÜLLE NUR UM DAS BILD ERZEUGEN 275 ===================================================== */ 276 277 function getExistingImageStage(image) { 278 const stage = image.closest(".ki-image-stage"); 279 280 return stage || null; 281 } 282 283 284 function createImageStage(image) { 285 const existingStage = 286 getExistingImageStage(image); 287 288 if (existingStage) { 289 return existingStage; 290 } 291 292 /* 293 * Wenn das Bild innerhalb eines picture-Elements 294 * steht, wird das gesamte picture-Element umschlossen. 295 * Die source-Elemente bleiben dadurch unverändert. 296 */ 297 298 const picture = image.closest("picture"); 299 const mediaElement = picture || image; 300 const parent = mediaElement.parentNode; 301 302 if (!parent) { 303 return null; 304 } 305 306 const stage = document.createElement("span"); 307 308 stage.className = "ki-image-stage"; 309 stage.dataset.kiCreatedWrapper = "true"; 310 stage.setAttribute("aria-hidden", "true"); 311 312 parent.insertBefore(stage, mediaElement); 313 stage.appendChild(mediaElement); 314 315 return stage; 316 } 317 318 319 function removeAutomaticMarkerFromStage(stage) { 320 if (!stage) { 321 return; 322 } 323 324 removeMarkerClasses(stage); 325 326 delete stage.dataset.kiAutomaticMarker; 327 } 328 329 330 /* ===================================================== 331 AUTOMATISCHE KENNZEICHNUNG NORMALER BILDER 332 ===================================================== */ 333 334 function applyAutomaticMarkerToImage(image) { 335 if (shouldIgnoreImage(image)) { 336 return; 337 } 338 339 const existingStage = 340 getExistingImageStage(image); 341 342 /* 343 * Wenn bereits eine manuelle Klasse an einem äußeren 344 * Contao-Element sitzt, wird keine zweite automatische 345 * Kennzeichnung erzeugt. 346 */ 347 348 const nearestMarkedElement = 349 image.closest(markerSelector); 350 351 if ( 352 nearestMarkedElement && 353 nearestMarkedElement !== existingStage 354 ) { 355 removeAutomaticMarkerFromStage( 356 existingStage 357 ); 358 359 return; 360 } 361 362 const markerClass = 363 detectMarkerFromImageUrl(image); 364 365 if (!markerClass) { 366 removeAutomaticMarkerFromStage( 367 existingStage 368 ); 369 370 return; 371 } 372 373 const stage = existingStage || 374 createImageStage(image); 375 376 if (!stage) { 377 return; 378 } 379 380 removeMarkerClasses(stage); 381 382 stage.classList.add(markerClass); 383 stage.dataset.kiAutomaticMarker = 384 markerClass; 385 } 386 387 388 function scanImages(root) { 389 if (!root) { 390 return; 391 } 392 393 if (root instanceof HTMLImageElement) { 394 applyAutomaticMarkerToImage(root); 395 } 396 397 if ( 398 root.querySelectorAll && 399 typeof root.querySelectorAll === "function" 400 ) { 401 root 402 .querySelectorAll("img") 403 .forEach( 404 applyAutomaticMarkerToImage 405 ); 406 } 407 } 408 409 410 function startImageObserver() { 411 scanImages(document); 412 413 const observer = new MutationObserver( 414 function (mutations) { 415 mutations.forEach( 416 function (mutation) { 417 418 if ( 419 mutation.type === "childList" 420 ) {
421 mutation.addedNodes.forEach( 422 function (node) { 423 if ( 424 node.nodeType === 425 Node.ELEMENT_NODE 426 ) { 427 scanImages(node); 428 } 429 } 430 ); 431 } 432 433 if ( 434 mutation.type === "attributes" && 435 mutation.target instanceof 436 HTMLImageElement 437 ) { 438 applyAutomaticMarkerToImage( 439 mutation.target 440 ); 441 } 442 } 443 ); 444 } 445 ); 446 447 observer.observe( 448 document.documentElement, 449 { 450 childList: true, 451 subtree: true, 452 attributes: true, 453 attributeFilter: [ 454 "src", 455 "srcset" 456 ] 457 } 458 ); 459 } 460 461 462 /* ===================================================== 463 COLORBOX-GROSSANSICHT 464 ===================================================== */ 465 466 function getLightboxBadge() { 467 if (lightboxBadge) { 468 return lightboxBadge; 469 } 470 471 lightboxBadge = 472 document.createElement("div"); 473 474 lightboxBadge.id = 475 "ki-lightbox-badge"; 476 477 lightboxBadge.setAttribute( 478 "aria-hidden", 479 "true" 480 ); 481 482 document.body.appendChild( 483 lightboxBadge 484 ); 485 486 return lightboxBadge; 487 } 488 489 490 function hideLightboxBadge() { 491 if (!lightboxBadge) { 492 return; 493 } 494 495 lightboxBadge.style.display = "none"; 496 lightboxBadge.style.visibility = "hidden"; 497 lightboxBadge.style.left = "-9999px"; 498 lightboxBadge.style.top = "-9999px"; 499 lightboxBadge.style.backgroundImage = "none"; 500 } 501 502 503 function detectMarkerFromColorboxLink( 504 linkElement 505 ) { 506 if (!linkElement) { 507 return null; 508 } 509 510 /* 511 * Zuerst wird nach einer manuell oder automatisch 512 * gesetzten Klasse gesucht. 513 */ 514 515 const markerFromClasses = 516 detectMarkerFromClasses(linkElement); 517 518 if (markerFromClasses) { 519 return markerFromClasses; 520 } 521 522 /* 523 * Danach werden alle Bilder innerhalb des Links 524 * anhand ihrer Dateinamen geprüft. 525 */ 526 527 const images = 528 linkElement.querySelectorAll("img"); 529 530 for (const image of images) { 531 const markerFromImage = 532 detectMarkerFromImageUrl(image); 533 534 if (markerFromImage) { 535 return markerFromImage; 536 } 537 } 538 539 /* 540 * Zuletzt wird die href-Adresse des Links geprüft. 541 */ 542 543 return detectMarkerFromText( 544 linkElement.href || 545 linkElement.getAttribute("href") 546 ); 547 } 548 549 550 function positionLightboxBadge() { 551 const image = document.querySelector( 552 "#colorbox .cboxPhoto" 553 ); 554 555 if (!image) { 556 hideLightboxBadge(); 557 return; 558 } 559 560 const imageRect = 561 image.getBoundingClientRect(); 562 563 if ( 564 imageRect.width < 100 || 565 imageRect.height < 100 566 ) { 567 hideLightboxBadge(); 568 return; 569 } 570 571 const badge = getLightboxBadge(); 572 573 const gap = 574 window.innerWidth <= 600 575 ? 8 576 : 12; 577 578 badge.style.display = "block"; 579 badge.style.visibility = "hidden"; 580 581 const badgeWidth = 582 badge.offsetWidth; 583 584 const badgeHeight = 585 badge.offsetHeight; 586 587 let left = 588 imageRect.left + gap; 589 590 let top = 591 imageRect.bottom - 592 badgeHeight - 593 gap; 594 595 left = Math.max( 596 gap, 597 Math.min( 598 left, 599 window.innerWidth - 600 badgeWidth - 601 gap 602 ) 603 ); 604 605 top = Math.max( 606 gap, 607 Math.min( 608 top, 609 window.innerHeight - 610 badgeHeight - 611 gap 612 ) 613 ); 614 615 badge.style.left = 616 Math.round(left) + "px"; 617 618 badge.style.top = 619 Math.round(top) + "px"; 620 621 badge.style.visibility = "visible"; 622 } 623 624 625 function startColorboxSupport() { 626 const $ = window.jQuery; 627 628 if (!$ || !$.colorbox) { 629 return; 630 } 631 632 $(document).on( 633 "cbox_complete",
634 function () { 635 const currentLink = 636 $.colorbox.element(); 637 638 if ( 639 !currentLink || 640 !currentLink.length 641 ) { 642 hideLightboxBadge(); 643 return; 644 } 645 646 const markerClass = 647 detectMarkerFromColorboxLink( 648 currentLink[0] 649 ); 650 651 if ( 652 !markerClass || 653 !badgeFiles[markerClass] 654 ) { 655 hideLightboxBadge(); 656 return; 657 } 658 659 const badge = 660 getLightboxBadge(); 661 662 badge.style.backgroundImage = 663 'url("' + 664 badgeFiles[markerClass] + 665 '")'; 666 667 window.requestAnimationFrame( 668 function () { 669 window.requestAnimationFrame( 670 positionLightboxBadge 671 ); 672 } 673 ); 674 } 675 ); 676 677 $(document).on( 678 "cbox_cleanup cbox_closed", 679 hideLightboxBadge 680 ); 681 682 $(window).on( 683 "resize scroll", 684 positionLightboxBadge 685 ); 686 687 document.addEventListener( 688 "scroll", 689 positionLightboxBadge, 690 true 691 ); 692 } 693 694 695 /* ===================================================== 696 INITIALISIERUNG 697 ===================================================== */ 698 699 function initialize() { 700 startImageObserver(); 701 startColorboxSupport(); 702 } 703 704 705 if (document.readyState === "loading") { 706 document.addEventListener( 707 "DOMContentLoaded", 708 initialize, 709 { 710 once: true 711 } 712 ); 713 } else { 714 initialize(); 715 } 716 717})(window, document);
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.