@bebranded/bb-contents 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bb-contents.js +360 -310
  2. package/package.json +1 -1
package/bb-contents.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * BeBranded Contents
3
3
  * Contenus additionnels français pour Webflow
4
- * @version 1.1.0
4
+ * @version 1.1.2
5
5
  * @author BeBranded
6
6
  * @license MIT
7
7
  * @website https://www.bebranded.xyz
@@ -9,6 +9,9 @@
9
9
  (function() {
10
10
  'use strict';
11
11
 
12
+ // Version du script
13
+ const BB_CONTENTS_VERSION = '1.1.2';
14
+
12
15
  // Créer l'objet temporaire pour la configuration si il n'existe pas
13
16
  if (!window._bbContentsConfig) {
14
17
  window._bbContentsConfig = {};
@@ -31,12 +34,12 @@
31
34
  }
32
35
  window._bbContentsInitialized = true;
33
36
 
34
- // Log de démarrage simple (une seule fois)
35
- console.log('bb-contents | v1.1.0');
37
+ // Log de démarrage
38
+ console.log('bb-contents | v' + BB_CONTENTS_VERSION);
36
39
 
37
40
  // Configuration
38
41
  const config = {
39
- version: '1.1.0',
42
+ version: BB_CONTENTS_VERSION,
40
43
  debug: false, // Debug désactivé pour rendu propre
41
44
  prefix: 'bb-', // utilisé pour générer les sélecteurs (data-bb-*)
42
45
  youtubeEndpoint: null, // URL du worker YouTube (à définir par l'utilisateur)
@@ -178,7 +181,7 @@
178
181
  this.checkAndReinitFailedElements();
179
182
  },
180
183
 
181
- // Nouvelle méthode pour vérifier et réinitialiser les éléments échoués
184
+ // Vérifier et réinitialiser les éléments échoués
182
185
  checkAndReinitFailedElements: function() {
183
186
  const scope = document.querySelector('[data-bb-scope]') || document;
184
187
  let needsReinit = false;
@@ -280,14 +283,14 @@
280
283
 
281
284
  // Modules
282
285
  bbContents.modules = {
283
- // Module Marquee - Version simplifiée et robuste
286
+ // Module Marquee
284
287
  marquee: {
285
- detect: function(scope) {
286
- const s = scope || document;
288
+ detect: function(scope) {
289
+ const s = scope || document;
287
290
  return s.querySelector(bbContents._attrSelector('marquee')) !== null;
288
- },
289
-
290
- init: function(root) {
291
+ },
292
+
293
+ init: function(root) {
291
294
  const scope = root || document;
292
295
  if (scope.closest && scope.closest('[data-bb-disable]')) return;
293
296
  const elements = scope.querySelectorAll(bbContents._attrSelector('marquee'));
@@ -320,20 +323,15 @@
320
323
  const isVertical = orientation === 'vertical';
321
324
  const useAutoHeight = isVertical && height === 'auto';
322
325
 
323
- // Vérifier le overflow du parent pour respecter overflow: visible
324
- // Si le parent a overflow: visible, on laisse passer
325
- // Sinon (hidden, clip, auto, scroll), on contient les logos avec overflow: hidden
326
+ // Respecter overflow du parent
326
327
  const parentComputedStyle = getComputedStyle(element);
327
328
  const parentOverflow = parentComputedStyle.overflow;
328
329
  const parentOverflowX = parentComputedStyle.overflowX;
329
330
  const parentOverflowY = parentComputedStyle.overflowY;
330
331
 
331
- // Vérifier si le parent a explicitement overflow: visible (ou les deux axes)
332
332
  const isParentOverflowVisible = (parentOverflow === 'visible' || parentOverflow === '') &&
333
333
  (parentOverflowX === 'visible' || parentOverflowX === '') &&
334
334
  (parentOverflowY === 'visible' || parentOverflowY === '');
335
-
336
- // Si le parent a overflow: visible, on laisse passer, sinon on contient avec hidden
337
335
  const mainContainerOverflow = isParentOverflowVisible ? 'visible' : 'hidden';
338
336
 
339
337
  mainContainer.style.cssText = `
@@ -346,7 +344,7 @@
346
344
  `;
347
345
 
348
346
  const scrollContainer = document.createElement('div');
349
- // Pour horizontal, utiliser position relative au lieu de absolute pour éviter les problèmes de calcul
347
+ // Position relative pour horizontal
350
348
  const useRelativeForHorizontal = !isVertical;
351
349
  scrollContainer.style.cssText = `
352
350
  ${useAutoHeight || useRelativeForHorizontal ? 'position: relative;' : 'position: absolute;'}
@@ -363,8 +361,7 @@
363
361
  const mainBlock = document.createElement('div');
364
362
  mainBlock.innerHTML = originalHTML;
365
363
 
366
- // IMPORTANT: Forcer le chargement de TOUTES les images dans mainBlock AVANT de cloner
367
- // Cela garantit que les images sont dans le cache du navigateur avant le clonage
364
+ // Précharger toutes les images avant clonage
368
365
  const preloadAllImagesFirst = function(block) {
369
366
  return new Promise(function(resolve) {
370
367
  const images = block.querySelectorAll('img');
@@ -451,14 +448,12 @@
451
448
  });
452
449
  };
453
450
 
454
- // Permettre le retour à la ligne pour le texte dans les items du marquee
455
- // Le white-space: nowrap sur le conteneur flex empêche les items de se retourner,
456
- // mais ne doit pas empêcher le texte à l'intérieur des items de faire plusieurs lignes
451
+ // Permettre le retour à la ligne du texte dans les items
457
452
  if (!isVertical) {
458
453
  setTimeout(() => {
459
454
  const marqueeItems = mainBlock.querySelectorAll('.bb-marquee_item, [role="listitem"]');
460
455
  marqueeItems.forEach(item => {
461
- // Préserver la largeur de l'item définie dans Webflow
456
+ // Préserver la largeur de l'item
462
457
  const computedStyle = getComputedStyle(item);
463
458
  const itemWidth = computedStyle.width;
464
459
  if (itemWidth && itemWidth !== 'auto' && itemWidth !== '0px') {
@@ -466,11 +461,10 @@
466
461
  item.style.width = itemWidth;
467
462
  }
468
463
 
469
- // Permettre le retour à la ligne pour les conteneurs de texte
470
- // Ne pas toucher aux éléments qui doivent garder leur taille auto (comme .tag-m)
464
+ // Permettre le retour à la ligne du texte
471
465
  const textContainers = item.querySelectorAll('.use-case_client, .testimonial_client-info, [class*="text"], p, span');
472
466
  textContainers.forEach(container => {
473
- // Exclure les éléments qui doivent garder leur taille auto (tags, badges, etc.)
467
+ // Exclure les tags/badges (taille auto)
474
468
  const containerStyle = container.getAttribute('style');
475
469
  const shouldPreserveAuto = container.classList.contains('tag-m') ||
476
470
  container.classList.contains('tag') ||
@@ -478,33 +472,23 @@
478
472
  (containerStyle && containerStyle.includes('width'));
479
473
 
480
474
  if (shouldPreserveAuto) {
481
- // Ne pas toucher à ces éléments, ils gardent leur taille auto
482
475
  return;
483
476
  }
484
477
 
485
478
  const containerComputed = getComputedStyle(container);
486
- // Vérifier si l'élément a un style inline width défini
487
479
  const hasInlineWidth = container.style.width && container.style.width !== '';
488
480
 
489
- // Si l'élément a une largeur inline définie, la préserver
490
481
  if (hasInlineWidth) {
491
- // Garder la largeur inline
492
482
  return;
493
483
  }
494
-
495
- // Si l'élément a une largeur calculée qui n'est pas auto, vérifier si c'est une valeur fixe
496
- // Sinon, appliquer width: 100% seulement aux conteneurs de texte qui doivent wrapper
497
484
  const isTextContainer = container.classList.contains('use-case_client') ||
498
485
  container.classList.contains('testimonial_client-info') ||
499
486
  container.tagName === 'P' && !container.classList.contains('tag');
500
487
 
501
488
  if (isTextContainer) {
502
- // Pour les conteneurs de texte principaux, permettre le wrapping
503
- // Ne pas forcer width si déjà défini
504
489
  if (!containerComputed.width || containerComputed.width === 'auto' || containerComputed.width === '0px') {
505
490
  container.style.width = '100%';
506
491
  }
507
- // Forcer le retour à la ligne
508
492
  container.style.whiteSpace = 'normal';
509
493
  container.style.wordWrap = 'break-word';
510
494
  container.style.overflowWrap = 'break-word';
@@ -524,27 +508,22 @@
524
508
  ${isVertical ? 'min-height: 100px;' : ''}
525
509
  `;
526
510
 
527
- // NOUVELLE APPROCHE: Attendre que TOUTES les images du mainBlock soient chargées AVANT de cloner
528
- // Cela garantit que les copies héritent d'images déjà dans le cache du navigateur
529
511
  preloadAllImagesFirst(mainBlock).then(function() {
530
- // Maintenant créer les copies - les images sont déjà en cache
531
512
  const repeatBlock1 = mainBlock.cloneNode(true);
532
513
  const repeatBlock2 = mainBlock.cloneNode(true);
533
514
 
534
- // Forcer l'affichage immédiat des images dans les copies (elles sont en cache)
515
+ // Forcer l'affichage des images dans les copies
535
516
  const forceImagesDisplay = function(block) {
536
517
  const images = block.querySelectorAll('img');
537
518
  images.forEach(function(img) {
538
519
  if (img.dataset.src && !img.src) {
539
520
  img.src = img.dataset.src;
540
521
  }
541
- // Forcer le chargement et l'affichage
542
- if (img.src) {
543
- img.src = img.src;
544
- img.style.opacity = '1';
545
- img.style.visibility = 'visible';
546
- // Forcer un reflow pour s'assurer que l'image est rendue
547
- void img.offsetHeight;
522
+ if (img.src) {
523
+ img.src = img.src;
524
+ img.style.opacity = '1';
525
+ img.style.visibility = 'visible';
526
+ void img.offsetHeight;
548
527
  }
549
528
  });
550
529
  };
@@ -552,15 +531,14 @@
552
531
  forceImagesDisplay(repeatBlock1);
553
532
  forceImagesDisplay(repeatBlock2);
554
533
 
555
- // NOUVELLE APPROCHE: Ajouter temporairement les copies au DOM (hors écran)
556
- // pour forcer le navigateur à les rendre complètement avant l'animation
534
+ // Pré-rendre les copies hors écran
557
535
  const tempContainer = document.createElement('div');
558
536
  tempContainer.style.cssText = 'position: absolute; left: -9999px; top: -9999px; visibility: hidden;';
559
537
  tempContainer.appendChild(repeatBlock1);
560
538
  tempContainer.appendChild(repeatBlock2);
561
539
  document.body.appendChild(tempContainer);
562
540
 
563
- // Forcer le rendu en vérifiant que toutes les images sont vraiment chargées et rendues
541
+ // Vérifier que toutes les images sont rendues
564
542
  const waitForImagesRender = function(block) {
565
543
  return new Promise(function(resolve) {
566
544
  const images = block.querySelectorAll('img');
@@ -579,18 +557,14 @@
579
557
  };
580
558
 
581
559
  images.forEach(function(img) {
582
- // Vérifier que l'image est vraiment rendue (naturalWidth > 0 ET dans le DOM)
583
560
  const checkImage = function() {
584
561
  if (img.complete && img.naturalWidth > 0 && img.naturalHeight > 0 && img.offsetWidth > 0) {
585
562
  renderedCount++;
586
563
  checkRendered();
587
564
  } else {
588
- // Réessayer après un court délai
589
565
  setTimeout(checkImage, 10);
590
566
  }
591
567
  };
592
-
593
- // Forcer le chargement si nécessaire
594
568
  if (img.dataset.src && !img.src) {
595
569
  img.src = img.dataset.src;
596
570
  }
@@ -606,7 +580,7 @@
606
580
  }
607
581
  });
608
582
 
609
- // Timeout de sécurité
583
+ // Timeout
610
584
  setTimeout(function() {
611
585
  if (renderedCount < totalImages) {
612
586
  renderedCount = totalImages;
@@ -616,11 +590,7 @@
616
590
  });
617
591
  };
618
592
 
619
- // NOUVEAU: Forcer le rendu complet en déplaçant temporairement le conteneur
620
- // pour que toutes les parties soient visibles (même brièvement)
621
- // Cela force le navigateur à rendre même les parties très larges sur grands écrans
622
- // Pour "left", on force le rendu de la partie DROITE (où les copies apparaîtront)
623
- // Pour "right", on force le rendu de la partie GAUCHE
593
+ // Forcer le rendu complet (grands écrans)
624
594
  const forceFullRender = function() {
625
595
  return new Promise(function(resolve) {
626
596
  // Calculer la largeur totale des copies
@@ -630,29 +600,19 @@
630
600
  );
631
601
 
632
602
  if (totalWidth > 0 && totalWidth > window.innerWidth) {
633
- // Déplacer temporairement le conteneur pour forcer le rendu
634
603
  tempContainer.style.left = '0px';
635
604
  tempContainer.style.width = totalWidth + 'px';
636
605
  tempContainer.style.overflow = 'visible';
637
-
638
- // Forcer un reflow pour que le navigateur calcule les dimensions
639
606
  void tempContainer.offsetWidth;
640
-
641
- // NOUVEAU: Pour "left", déplacer pour que la FIN soit visible (partie droite)
642
- // Pour "right", déplacer pour que le DÉBUT soit visible (partie gauche)
643
- // On va faire les deux pour être sûr que tout est rendu
644
607
  const translateXEnd = Math.max(0, totalWidth - window.innerWidth);
645
608
  const translateXStart = 0;
646
609
 
647
- // D'abord rendre la fin (pour "left" - où les copies apparaîtront)
648
610
  tempContainer.style.transform = 'translateX(-' + translateXEnd + 'px)';
649
611
  void tempContainer.offsetWidth;
650
612
  requestAnimationFrame(function() {
651
- // Ensuite rendre le début (pour "right" - où les copies apparaîtront)
652
613
  tempContainer.style.transform = 'translateX(-' + translateXStart + 'px)';
653
614
  void tempContainer.offsetWidth;
654
615
  requestAnimationFrame(function() {
655
- // Revenir à la position initiale
656
616
  tempContainer.style.transform = '';
657
617
  tempContainer.style.left = '-9999px';
658
618
  tempContainer.style.width = 'auto';
@@ -663,7 +623,6 @@
663
623
  });
664
624
  });
665
625
  } else {
666
- // Si pas besoin de déplacement, juste attendre un frame
667
626
  requestAnimationFrame(function() {
668
627
  requestAnimationFrame(resolve);
669
628
  });
@@ -675,14 +634,13 @@
675
634
  Promise.all([
676
635
  waitForImagesRender(repeatBlock1),
677
636
  waitForImagesRender(repeatBlock2),
678
- forceFullRender() // NOUVEAU: Forcer le rendu complet
637
+ forceFullRender()
679
638
  ]).then(function() {
680
639
  // Retirer les copies du conteneur temporaire (si toujours présent)
681
640
  if (tempContainer && tempContainer.parentNode === document.body) {
682
641
  document.body.removeChild(tempContainer);
683
642
  }
684
643
 
685
- // Maintenant ajouter les copies au scrollContainer
686
644
  // Les images sont maintenant complètement rendues
687
645
  if (!isVertical) {
688
646
  scrollContainer.appendChild(mainBlock);
@@ -690,10 +648,9 @@
690
648
  scrollContainer.appendChild(repeatBlock2);
691
649
  mainContainer.appendChild(scrollContainer);
692
650
 
693
- // Calculer la hauteur maximale des items après ajout au DOM
694
651
  requestAnimationFrame(() => {
695
652
  requestAnimationFrame(() => {
696
- const items = mainBlock.querySelectorAll('.bb-marquee_item, [role="listitem"], > *');
653
+ const items = mainBlock.querySelectorAll('.bb-marquee_item, [role="listitem"]');
697
654
  let maxHeight = 0;
698
655
  items.forEach(function(item) {
699
656
  const itemHeight = item.offsetHeight;
@@ -701,20 +658,15 @@
701
658
  maxHeight = itemHeight;
702
659
  }
703
660
  });
704
-
705
- // Si aucun item trouvé, essayer de prendre la hauteur du scrollContainer
706
661
  if (maxHeight === 0) {
707
662
  maxHeight = scrollContainer.offsetHeight;
708
663
  }
709
-
710
- // Appliquer la hauteur calculée au mainContainer si elle est valide
711
664
  if (maxHeight > 0) {
712
665
  mainContainer.style.height = maxHeight + 'px';
713
666
  }
714
667
  });
715
668
  });
716
669
  } else {
717
- // Pour vertical, garder le comportement actuel
718
670
  scrollContainer.appendChild(mainBlock);
719
671
  scrollContainer.appendChild(repeatBlock1);
720
672
  scrollContainer.appendChild(repeatBlock2);
@@ -725,11 +677,8 @@
725
677
  element.appendChild(mainContainer);
726
678
  element.setAttribute('data-bb-marquee-processed', 'true');
727
679
 
728
- // Attendre un peu pour s'assurer que le rendu est complet
729
- // Les images sont maintenant complètement rendues
730
680
  requestAnimationFrame(() => {
731
681
  requestAnimationFrame(() => {
732
- // Maintenant démarrer l'animation
733
682
  const initDelay = isVertical ? 500 : 100;
734
683
  setTimeout(() => {
735
684
  this.initAnimation(element, scrollContainer, mainBlock, {
@@ -740,12 +689,9 @@
740
689
  });
741
690
  }.bind(this));
742
691
  }.bind(this)).catch(function() {
743
- // En cas d'erreur, nettoyer le tempContainer s'il existe
744
692
  if (tempContainer && tempContainer.parentNode === document.body) {
745
693
  document.body.removeChild(tempContainer);
746
694
  }
747
-
748
- // Créer les copies quand même et démarrer
749
695
  const repeatBlock1 = mainBlock.cloneNode(true);
750
696
  const repeatBlock2 = mainBlock.cloneNode(true);
751
697
 
@@ -779,11 +725,9 @@
779
725
  const { speed, direction, pauseOnHover, gap, isVertical, useAutoHeight } = options;
780
726
 
781
727
  // Calculer les dimensions
782
- // Maintenant que scrollContainer est en position relative pour horizontal, offsetWidth devrait fonctionner
783
728
  const contentSize = isVertical ? mainBlock.offsetHeight : mainBlock.offsetWidth;
784
729
 
785
730
  if (contentSize === 0) {
786
- // Si toujours 0, réessayer après un délai
787
731
  setTimeout(() => this.initAnimation(element, scrollContainer, mainBlock, options), 200);
788
732
  return;
789
733
  }
@@ -796,33 +740,26 @@
796
740
  let isPaused = false;
797
741
 
798
742
  if (isSafari) {
799
- // Solution Safari : Animation CSS avec keyframes
800
743
  this.initSafariAnimation(element, scrollContainer, mainBlock, {
801
744
  speed, direction, gap, isVertical, useAutoHeight, contentSize, gapSize
802
745
  });
803
746
  } else {
804
- // Solution standard : créer les copies seulement si elles n'existent pas déjà
805
- // (elles ont peut-être été créées pour le calcul de hauteur en horizontal)
806
- // Utiliser children.length au lieu de querySelectorAll pour compter uniquement les enfants directs
807
- const hasCopies = scrollContainer.children.length >= 3; // mainBlock + 2 copies
747
+ const hasCopies = scrollContainer.children.length >= 3;
808
748
 
809
749
  if (!hasCopies) {
810
- // Créer les copies maintenant (les navigateurs non-Safari gèrent mieux)
811
750
  const repeatBlock1 = mainBlock.cloneNode(true);
812
751
  const repeatBlock2 = mainBlock.cloneNode(true);
813
752
 
814
- // Forcer le chargement COMPLET des images dans les copies
753
+ // Précharger les images dans les copies
815
754
  const preloadImagesInBlockSync = function(block) {
816
755
  const images = block.querySelectorAll('img');
817
756
  images.forEach(function(img) {
818
757
  if (img.dataset.src && !img.src) {
819
758
  img.src = img.dataset.src;
820
759
  }
821
- // Précharger avec new Image() pour forcer le cache
822
760
  if (img.src) {
823
761
  const preloadImg = new Image();
824
762
  preloadImg.src = img.src;
825
- // Forcer aussi le chargement dans l'image du DOM
826
763
  if (!img.complete) {
827
764
  img.src = img.src;
828
765
  }
@@ -836,8 +773,6 @@
836
773
  scrollContainer.appendChild(repeatBlock1);
837
774
  scrollContainer.appendChild(repeatBlock2);
838
775
  }
839
-
840
- // Solution standard pour autres navigateurs
841
776
  this.initStandardAnimation(element, scrollContainer, mainBlock, {
842
777
  speed, direction, pauseOnHover, gap, isVertical, useAutoHeight, contentSize, gapSize, step
843
778
  });
@@ -847,8 +782,7 @@
847
782
  initSafariAnimation: function(element, scrollContainer, mainBlock, options) {
848
783
  const { speed, direction, gap, isVertical, useAutoHeight, contentSize, gapSize } = options;
849
784
 
850
-
851
- // SOLUTION SAFARI : Forcer le chargement des images avant animation
785
+ // Précharger les images
852
786
  const images = mainBlock.querySelectorAll('img');
853
787
  let imagesLoaded = 0;
854
788
  const totalImages = images.length;
@@ -857,9 +791,7 @@
857
791
  const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
858
792
  // Détecter spécifiquement Safari (pas Chrome mobile)
859
793
  const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) || /iPhone|iPad|iPod/.test(navigator.userAgent);
860
-
861
- // OPTIMISATION: Charger les images et appliquer les styles SVG AVANT le clonage
862
- // pour éviter les reflows qui causent la saccade de l'animation
794
+ // Appliquer les styles avant clonage
863
795
  images.forEach(img => {
864
796
  if (img.dataset.src && !img.src) {
865
797
  img.src = img.dataset.src;
@@ -868,39 +800,27 @@
868
800
 
869
801
  // Détecter si c'est un SVG (par l'extension du src ou le type)
870
802
  const isSVG = img.src && (img.src.toLowerCase().endsWith('.svg') || img.src.includes('data:image/svg+xml'));
871
-
872
- // OPTIMISATION: Préserver les styles CSS existants (object-fit, etc.)
803
+ // Préserver les styles existants
873
804
  const originalObjectFit = img.style.objectFit || getComputedStyle(img).objectFit;
874
805
  const originalObjectPosition = img.style.objectPosition || getComputedStyle(img).objectPosition;
875
806
  const originalWidth = img.style.width;
876
807
  const originalHeight = img.style.height;
877
808
 
878
809
  img.onload = () => {
879
- // SOLUTION SAFARI : Pour les SVG sur Safari (desktop et mobile), utiliser contain avec optimisations
880
810
  if (isSVG && isSafari) {
881
- // SUR SAFARI : Utiliser contain MAIS avec des optimisations pour éviter le flou
882
811
  img.style.objectFit = 'contain';
883
812
  img.style.objectPosition = 'center';
884
-
885
- // Contraindre les dimensions sans forcer (max-width/max-height au lieu de width/height 100%)
886
813
  img.style.maxWidth = '100%';
887
814
  img.style.maxHeight = '100%';
888
815
  img.style.boxSizing = 'border-box';
889
-
890
- // Optimisations pour améliorer le rendu des SVG avec contain
891
- // Utiliser auto au lieu de crisp-edges pour contain
892
816
  img.style.imageRendering = 'auto';
893
817
  img.style.webkitBackfaceVisibility = 'hidden';
894
818
  img.style.backfaceVisibility = 'hidden';
895
-
896
- // Forcer le GPU rendering AVANT d'appliquer contain
897
819
  img.style.webkitTransform = 'translateZ(0)';
898
820
  img.style.transform = 'translateZ(0)';
899
821
 
900
- // Conteneur parent pour contraindre et centrer (sans forcer les dimensions)
901
822
  const parent = img.parentElement;
902
823
  if (parent) {
903
- // Vérifier si le parent a déjà des dimensions définies
904
824
  const parentStyles = getComputedStyle(parent);
905
825
  const hasParentWidth = parentStyles.width && parentStyles.width !== 'auto' && parentStyles.width !== '0px';
906
826
  const hasParentHeight = parentStyles.height && parentStyles.height !== 'auto' && parentStyles.height !== '0px';
@@ -911,12 +831,10 @@
911
831
  parent.style.overflow = 'hidden';
912
832
  parent.style.boxSizing = 'border-box';
913
833
 
914
- // Ne forcer les dimensions que si le parent n'en a pas déjà
915
834
  if (!hasParentWidth && !parent.style.width) parent.style.width = '100%';
916
835
  if (!hasParentHeight && !parent.style.height) parent.style.height = '100%';
917
836
  }
918
837
  } else if (isSVG && isMobile) {
919
- // Pour Chrome mobile, utiliser contain normalement
920
838
  img.style.objectFit = 'contain';
921
839
  img.style.objectPosition = 'center';
922
840
  img.style.maxWidth = '100%';
@@ -934,8 +852,6 @@
934
852
  parent.style.boxSizing = 'border-box';
935
853
  }
936
854
  } else if (isSafari) {
937
- // SUR SAFARI : Optimisations GPU et dimensions pour toutes les images
938
- // Restaurer les styles CSS après chargement pour les non-SVG
939
855
  if (originalObjectFit && originalObjectFit !== 'none') {
940
856
  img.style.objectFit = originalObjectFit;
941
857
  }
@@ -950,29 +866,22 @@
950
866
  if (!originalHeight || originalHeight === '') {
951
867
  img.style.height = 'auto';
952
868
  }
953
-
954
- // Optimisations GPU pour Safari (desktop et mobile)
955
869
  img.style.webkitBackfaceVisibility = 'hidden';
956
870
  img.style.backfaceVisibility = 'hidden';
957
871
  img.style.webkitTransform = 'translateZ(0)';
958
872
  img.style.transform = 'translateZ(0)';
959
-
960
- // Conteneur parent pour contraindre
961
873
  const parent = img.parentElement;
962
874
  if (parent) {
963
875
  parent.style.overflow = 'hidden';
964
876
  parent.style.boxSizing = 'border-box';
965
877
  }
966
878
  } else {
967
- // OPTIMISATION: Restaurer les styles CSS après chargement pour les non-SVG (autres navigateurs)
968
879
  if (originalObjectFit && originalObjectFit !== 'none') {
969
880
  img.style.objectFit = originalObjectFit;
970
881
  }
971
882
  if (originalObjectPosition && originalObjectPosition !== 'initial') {
972
883
  img.style.objectPosition = originalObjectPosition;
973
884
  }
974
-
975
- // OPTIMISATION: Préserver les dimensions naturelles des images
976
885
  if (!originalWidth || originalWidth === '') {
977
886
  img.style.width = 'auto';
978
887
  }
@@ -1051,9 +960,7 @@
1051
960
  });
1052
961
  }
1053
962
 
1054
- // Les styles sont maintenant appliqués AVANT le clonage (dans img.onload)
1055
- // Cela évite les reflows qui causaient la saccade de l'animation
1056
- // Les copies héritent automatiquement des styles des images originales
963
+ // Styles appliqués avant clonage
1057
964
 
1058
965
  // Recalculer la taille après chargement des images
1059
966
  const newContentSize = isVertical ? mainBlock.offsetHeight : mainBlock.offsetWidth;
@@ -1072,12 +979,11 @@
1072
979
  }
1073
980
  }
1074
981
 
1075
- // Solution Safari simplifiée
1076
982
  const totalSize = finalContentSize * 3 + gapSize * 2;
1077
983
  const step = (parseFloat(speed) * (isVertical ? 1.5 : 0.8)) / 60;
1078
984
  let isPaused = false;
1079
985
 
1080
- // OPTIMISATION SAFARI MOBILE : Ajouter will-change pour améliorer la fluidité
986
+ // Optimisations Safari mobile
1081
987
  if (isSafari && isMobile) {
1082
988
  scrollContainer.style.willChange = 'transform';
1083
989
  scrollContainer.style.webkitBackfaceVisibility = 'hidden';
@@ -1092,7 +998,7 @@
1092
998
  }
1093
999
 
1094
1000
  // Position initiale optimisée pour Safari
1095
- // Pour direction left, commencer à -(finalContentSize + gapSize) pour que repeatBlock1 soit déjà visible
1001
+ // Position initiale pour éviter saccade
1096
1002
  let currentPosition;
1097
1003
  if (direction === (isVertical ? 'bottom' : 'right')) {
1098
1004
  currentPosition = -(finalContentSize + gapSize);
@@ -1107,7 +1013,7 @@
1107
1013
  : `translate3d(${currentPosition}px, 0, 0)`;
1108
1014
  scrollContainer.style.transform = initialTransform;
1109
1015
 
1110
- // OPTIMISATION SAFARI MOBILE : Forcer un reflow avant de démarrer l'animation
1016
+ // Forcer reflow Safari mobile
1111
1017
  if (isSafari && isMobile) {
1112
1018
  void scrollContainer.offsetHeight;
1113
1019
  }
@@ -1116,7 +1022,7 @@
1116
1022
  let lastTime = performance.now();
1117
1023
  const animate = (currentTime) => {
1118
1024
  if (!isPaused) {
1119
- // OPTIMISATION SAFARI MOBILE : Utiliser le temps réel pour une animation plus fluide
1025
+ // Animation basée sur le temps
1120
1026
  const deltaTime = isSafari && isMobile ? (currentTime - lastTime) / 16.67 : 1;
1121
1027
  lastTime = currentTime;
1122
1028
 
@@ -1183,8 +1089,7 @@
1183
1089
  let isPaused = false;
1184
1090
 
1185
1091
  // Position initiale
1186
- // Pour direction left, commencer à -(contentSize + gapSize) pour que repeatBlock1 soit déjà visible
1187
- // Cela évite la saccade au premier cycle
1092
+ // Position initiale pour éviter saccade
1188
1093
  let currentPosition;
1189
1094
  if (direction === (isVertical ? 'bottom' : 'right')) {
1190
1095
  currentPosition = -(contentSize + gapSize);
@@ -1215,7 +1120,6 @@
1215
1120
  currentPosition += step * clampedDelta;
1216
1121
  // Reset BEAUCOUP PLUS TÔT pour "right" aussi (comme pour "left")
1217
1122
  // Reset à 80% du chemin au lieu d'attendre 100% pour avoir une marge de sécurité
1218
- // Cela garantit que la copie suivante est toujours visible avant le reset
1219
1123
  const resetThreshold = -(0.2 * (contentSize + gapSize)); // 80% du chemin (on est à -20%)
1220
1124
  if (currentPosition >= resetThreshold) {
1221
1125
  // Reset en gardant la position relative pour éviter le saut visible
@@ -1225,7 +1129,6 @@
1225
1129
  currentPosition -= step * clampedDelta;
1226
1130
  // Reset BEAUCOUP PLUS TÔT pour éviter toute saccade visible
1227
1131
  // Reset à 80% du chemin au lieu d'attendre 100% pour avoir une marge de sécurité
1228
- // Cela garantit que la copie suivante est toujours visible avant le reset
1229
1132
  const resetThreshold = -(1.8 * (contentSize + gapSize));
1230
1133
  if (currentPosition <= resetThreshold) {
1231
1134
  // Reset en gardant la position relative pour éviter le saut visible
@@ -1258,8 +1161,8 @@
1258
1161
 
1259
1162
  // Module Share (Partage Social)
1260
1163
  share: {
1261
- // Configuration des réseaux
1262
- networks: {
1164
+ // Configuration des réseaux
1165
+ networks: {
1263
1166
  twitter: function(data) {
1264
1167
  return 'https://twitter.com/intent/tweet?url=' +
1265
1168
  encodeURIComponent(data.url) +
@@ -1295,14 +1198,14 @@
1295
1198
  }
1296
1199
  },
1297
1200
 
1298
- // Détection
1299
- detect: function(scope) {
1300
- const s = scope || document;
1301
- return s.querySelector(bbContents._attrSelector('share')) !== null;
1302
- },
1303
-
1304
- // Initialisation
1305
- init: function(root) {
1201
+ // Détection
1202
+ detect: function(scope) {
1203
+ const s = scope || document;
1204
+ return s.querySelector(bbContents._attrSelector('share')) !== null;
1205
+ },
1206
+
1207
+ // Initialisation
1208
+ init: function(root) {
1306
1209
  const scope = root || document;
1307
1210
  if (scope.closest && scope.closest('[data-bb-disable]')) return;
1308
1211
  const elements = scope.querySelectorAll(bbContents._attrSelector('share'));
@@ -1349,8 +1252,8 @@
1349
1252
  bbContents.utils.log('Module Share initialisé:', elements.length, 'éléments');
1350
1253
  },
1351
1254
 
1352
- // Fonction de partage
1353
- share: function(network, data, element) {
1255
+ // Fonction de partage
1256
+ share: function(network, data, element) {
1354
1257
  const networkFunc = this.networks[network];
1355
1258
 
1356
1259
  if (!networkFunc) {
@@ -1389,8 +1292,8 @@
1389
1292
  bbContents.utils.log('Partage sur', network, data);
1390
1293
  },
1391
1294
 
1392
- // Copier dans le presse-papier
1393
- copyToClipboard: function(text, element, silent) {
1295
+ // Copier dans le presse-papier
1296
+ copyToClipboard: function(text, element, silent) {
1394
1297
  const isSilent = !!silent;
1395
1298
  // Méthode moderne
1396
1299
  if (navigator.clipboard && navigator.clipboard.writeText) {
@@ -1407,8 +1310,8 @@
1407
1310
  }
1408
1311
  },
1409
1312
 
1410
- // Fallback copie
1411
- fallbackCopy: function(text, element, silent) {
1313
+ // Fallback copie
1314
+ fallbackCopy: function(text, element, silent) {
1412
1315
  const isSilent = !!silent;
1413
1316
  // Pas de UI si silencieux (exigence produit)
1414
1317
  if (isSilent) return;
@@ -1421,8 +1324,8 @@
1421
1324
  }
1422
1325
  },
1423
1326
 
1424
- // Partage natif (Web Share API)
1425
- nativeShare: function(data, element) {
1327
+ // Partage natif (Web Share API)
1328
+ nativeShare: function(data, element) {
1426
1329
  // Vérifier si Web Share API est disponible
1427
1330
  if (navigator.share) {
1428
1331
  navigator.share({
@@ -1443,8 +1346,8 @@
1443
1346
  }
1444
1347
  },
1445
1348
 
1446
- // Feedback visuel
1447
- showFeedback: function(element, message) {
1349
+ // Feedback visuel
1350
+ showFeedback: function(element, message) {
1448
1351
  const originalText = element.textContent;
1449
1352
  element.textContent = message;
1450
1353
  element.style.pointerEvents = 'none';
@@ -1458,11 +1361,12 @@
1458
1361
 
1459
1362
  // Module Current Year (Année courante)
1460
1363
  currentYear: {
1461
- detect: function(scope) {
1462
- const s = scope || document;
1463
- return s.querySelector(bbContents._attrSelector('current-year')) !== null;
1464
- },
1465
- init: function(root) {
1364
+ detect: function(scope) {
1365
+ const s = scope || document;
1366
+ return s.querySelector(bbContents._attrSelector('current-year')) !== null;
1367
+ },
1368
+
1369
+ init: function(root) {
1466
1370
  const scope = root || document;
1467
1371
  if (scope.closest && scope.closest('[data-bb-disable]')) return;
1468
1372
  const elements = scope.querySelectorAll(bbContents._attrSelector('current-year'));
@@ -1491,13 +1395,13 @@
1491
1395
 
1492
1396
  // Module Reading Time (Temps de lecture)
1493
1397
  readingTime: {
1494
- detect: function(scope) {
1495
- const s = scope || document;
1496
- return s.querySelector(bbContents._attrSelector('reading-time')) !== null;
1497
- },
1498
-
1499
- // Fonction pour extraire le texte et les images depuis une URL
1500
- fetchContentFromUrl: function(url, targetSelector) {
1398
+ detect: function(scope) {
1399
+ const s = scope || document;
1400
+ return s.querySelector(bbContents._attrSelector('reading-time')) !== null;
1401
+ },
1402
+
1403
+ // Fonction pour extraire le texte et les images depuis une URL
1404
+ fetchContentFromUrl: function(url, targetSelector) {
1501
1405
  return fetch(url)
1502
1406
  .then(function(response) {
1503
1407
  if (!response.ok) {
@@ -1554,10 +1458,10 @@
1554
1458
 
1555
1459
  return { text: text, images: images };
1556
1460
  });
1557
- },
1558
-
1559
- // Fonction pour calculer le temps de lecture
1560
- calculateReadingTime: function(text, images, wordsPerMinute, secondsPerImage) {
1461
+ },
1462
+
1463
+ // Fonction pour calculer le temps de lecture
1464
+ calculateReadingTime: function(text, images, wordsPerMinute, secondsPerImage) {
1561
1465
  // Utiliser split(/\s+/) pour un comptage plus fiable (comme le code de référence)
1562
1466
  const wordCount = text ? text.trim().split(/\s+/).filter(function(word) { return word.length > 0; }).length : 0;
1563
1467
  const imageCount = images ? images.length : 0;
@@ -2196,13 +2100,13 @@
2196
2100
 
2197
2101
  // Module Favicon (Favicon Dynamique)
2198
2102
  favicon: {
2199
- originalFavicon: null,
2200
-
2201
- // Détection
2202
- detect: function(scope) {
2203
- const s = scope || document;
2204
- return s.querySelector(bbContents._attrSelector('favicon')) !== null;
2205
- },
2103
+ originalFavicon: null,
2104
+
2105
+ // Détection
2106
+ detect: function(scope) {
2107
+ const s = scope || document;
2108
+ return s.querySelector(bbContents._attrSelector('favicon')) !== null;
2109
+ },
2206
2110
 
2207
2111
  // Initialisation
2208
2112
  init: function(root) {
@@ -2240,8 +2144,8 @@
2240
2144
  }
2241
2145
  },
2242
2146
 
2243
- // Helper: Récupérer ou créer un élément favicon
2244
- getFaviconElement: function() {
2147
+ // Helper: Récupérer ou créer un élément favicon
2148
+ getFaviconElement: function() {
2245
2149
  let favicon = document.querySelector('link[rel="icon"]') ||
2246
2150
  document.querySelector('link[rel="shortcut icon"]');
2247
2151
  if (!favicon) {
@@ -2252,8 +2156,8 @@
2252
2156
  return favicon;
2253
2157
  },
2254
2158
 
2255
- // Changer le favicon
2256
- setFavicon: function(url) {
2159
+ // Changer le favicon
2160
+ setFavicon: function(url) {
2257
2161
  if (!url) return;
2258
2162
 
2259
2163
  // Ajouter un timestamp pour forcer le rafraîchissement du cache
@@ -2264,8 +2168,8 @@
2264
2168
  favicon.href = urlWithCacheBuster;
2265
2169
  },
2266
2170
 
2267
- // Support dark mode (méthode simplifiée et directe)
2268
- setupDarkMode: function(lightUrl, darkUrl) {
2171
+ // Support dark mode (méthode simplifiée et directe)
2172
+ setupDarkMode: function(lightUrl, darkUrl) {
2269
2173
  // Fonction pour mettre à jour le favicon selon le mode sombre
2270
2174
  const updateFavicon = function(e) {
2271
2175
  const darkModeOn = e ? e.matches : window.matchMedia('(prefers-color-scheme: dark)').matches;
@@ -2288,7 +2192,7 @@
2288
2192
 
2289
2193
  // Module YouTube Feed
2290
2194
  youtube: {
2291
- // OPTIMISATION: Détection améliorée des bots pour éviter les appels API inutiles
2195
+ // Détection des bots
2292
2196
  isBot: function() {
2293
2197
  const userAgent = navigator.userAgent.toLowerCase();
2294
2198
  const botPatterns = [
@@ -2317,7 +2221,7 @@
2317
2221
  return isBot;
2318
2222
  },
2319
2223
 
2320
- // OPTIMISATION: Cache amélioré avec protection contre les appels multiples
2224
+ // Cache avec protection appels multiples
2321
2225
  cache: {
2322
2226
  get: function(key) {
2323
2227
  try {
@@ -2327,7 +2231,7 @@
2327
2231
  const data = JSON.parse(cached);
2328
2232
  const now = Date.now();
2329
2233
 
2330
- // OPTIMISATION: Cache plus long (24h maintenu)
2234
+ // Cache 24h
2331
2235
  if (now - data.timestamp > 24 * 60 * 60 * 1000) {
2332
2236
  localStorage.removeItem(key);
2333
2237
  return null;
@@ -2352,7 +2256,7 @@
2352
2256
  }
2353
2257
  },
2354
2258
 
2355
- // OPTIMISATION: Protection globale contre les appels multiples
2259
+ // Protection globale contre les appels multiples
2356
2260
  _activeRequests: new Set(),
2357
2261
 
2358
2262
  isRequestActive: function(cacheKey) {
@@ -2386,6 +2290,9 @@
2386
2290
 
2387
2291
  // Module détecté: youtube
2388
2292
 
2293
+ // OPTIMISATION: Grouper les éléments par configuration unique pour partager les requêtes API
2294
+ const elementsByConfig = {};
2295
+
2389
2296
  elements.forEach(element => {
2390
2297
  // Vérifier si l'élément a déjà été traité par un autre module
2391
2298
  if (element.bbProcessed || element.hasAttribute('data-bb-marquee-processed')) {
@@ -2394,57 +2301,123 @@
2394
2301
  }
2395
2302
  element.bbProcessed = true;
2396
2303
 
2397
- // Utiliser la nouvelle fonction initElement
2398
- this.initElement(element);
2304
+ const channelIdsRaw = bbContents._getAttr(element, 'bb-youtube-channel');
2305
+ if (!channelIdsRaw) return;
2306
+
2307
+ // Parser les channelIds (peuvent être séparés par virgules)
2308
+ const channelIds = channelIdsRaw.split(',').map(id => id.trim()).filter(id => id);
2309
+ if (channelIds.length === 0) return;
2310
+
2311
+ // Normaliser et trier les channelIds pour créer une clé unique
2312
+ const normalizedChannelIds = channelIds.sort().join(',');
2313
+
2314
+ const allowShorts = bbContents._getAttr(element, 'bb-youtube-allow-shorts') === 'true';
2315
+ const language = bbContents._getAttr(element, 'bb-youtube-language') || 'fr';
2316
+ const videoCount = parseInt(bbContents._getAttr(element, 'bb-youtube-video-count') || '10', 10);
2317
+ const skip = parseInt(bbContents._getAttr(element, 'bb-youtube-skip') || '0', 10);
2318
+
2319
+ // Créer une clé unique par configuration (channelIds + allowShorts + language)
2320
+ const configKey = `${normalizedChannelIds}_${allowShorts}_${language}`;
2321
+
2322
+ if (!elementsByConfig[configKey]) {
2323
+ elementsByConfig[configKey] = {
2324
+ elements: [],
2325
+ maxVideoCount: 0,
2326
+ maxSkip: 0,
2327
+ channelIds: normalizedChannelIds,
2328
+ allowShorts: allowShorts,
2329
+ language: language
2330
+ };
2331
+ }
2332
+
2333
+ elementsByConfig[configKey].elements.push(element);
2334
+ // Garder le max videoCount et maxSkip pour ce groupe
2335
+ elementsByConfig[configKey].maxVideoCount = Math.max(
2336
+ elementsByConfig[configKey].maxVideoCount,
2337
+ videoCount
2338
+ );
2339
+ elementsByConfig[configKey].maxSkip = Math.max(
2340
+ elementsByConfig[configKey].maxSkip,
2341
+ skip
2342
+ );
2343
+ });
2344
+
2345
+ // Initialiser chaque groupe d'éléments
2346
+ Object.keys(elementsByConfig).forEach(configKey => {
2347
+ const group = elementsByConfig[configKey];
2348
+
2349
+ // Initialiser tous les éléments de ce groupe
2350
+ group.elements.forEach(element => {
2351
+ const videoCount = parseInt(bbContents._getAttr(element, 'bb-youtube-video-count') || '10', 10);
2352
+ const skip = parseInt(bbContents._getAttr(element, 'bb-youtube-skip') || '0', 10);
2353
+ this.initElement(element, group, videoCount, skip);
2354
+ });
2399
2355
  });
2400
2356
  },
2401
2357
 
2402
2358
  // Fonction pour initialiser un seul élément YouTube
2403
- initElement: function(element) {
2359
+ initElement: function(element, groupConfig, videoCount, skip) {
2404
2360
  // Vérifier si c'est un bot - pas d'appel API
2405
2361
  if (this.isBot()) {
2406
2362
  return;
2407
2363
  }
2408
2364
 
2409
- const channelId = bbContents._getAttr(element, 'bb-youtube-channel');
2410
- const videoCount = bbContents._getAttr(element, 'bb-youtube-video-count') || '10';
2411
- const allowShorts = bbContents._getAttr(element, 'bb-youtube-allow-shorts') === 'true';
2412
- const language = bbContents._getAttr(element, 'bb-youtube-language') || 'fr';
2365
+ // Si les paramètres ne sont pas fournis, les récupérer depuis l'élément
2366
+ if (!groupConfig) {
2367
+ const channelIdsRaw = bbContents._getAttr(element, 'bb-youtube-channel');
2368
+ if (!channelIdsRaw) return;
2369
+
2370
+ const channelIds = channelIdsRaw.split(',').map(id => id.trim()).filter(id => id);
2371
+ if (channelIds.length === 0) return;
2372
+
2373
+ const normalizedChannelIds = channelIds.sort().join(',');
2374
+ const allowShorts = bbContents._getAttr(element, 'bb-youtube-allow-shorts') === 'true';
2375
+ const language = bbContents._getAttr(element, 'bb-youtube-language') || 'fr';
2376
+
2377
+ groupConfig = {
2378
+ channelIds: normalizedChannelIds,
2379
+ allowShorts: allowShorts,
2380
+ language: language,
2381
+ maxVideoCount: parseInt(bbContents._getAttr(element, 'bb-youtube-video-count') || '10', 10),
2382
+ maxSkip: parseInt(bbContents._getAttr(element, 'bb-youtube-skip') || '0', 10)
2383
+ };
2384
+ videoCount = groupConfig.maxVideoCount;
2385
+ skip = groupConfig.maxSkip;
2386
+ }
2387
+
2388
+ if (!videoCount) {
2389
+ videoCount = parseInt(bbContents._getAttr(element, 'bb-youtube-video-count') || '10', 10);
2390
+ }
2391
+ if (skip === undefined || skip === null) {
2392
+ skip = parseInt(bbContents._getAttr(element, 'bb-youtube-skip') || '0', 10);
2393
+ }
2413
2394
 
2414
2395
  // Vérifier la configuration au moment de l'initialisation
2415
2396
  const endpoint = bbContents.checkYouTubeConfig() ? bbContents.config.youtubeEndpoint : null;
2416
2397
 
2417
-
2418
- if (!channelId) {
2419
- return;
2420
- }
2421
-
2422
2398
  if (!endpoint) {
2423
- // OPTIMISATION: Réduire drastiquement les retries (de 50 à 10)
2399
+ // Limiter les retries
2424
2400
  const retryCount = element.getAttribute('data-youtube-retry-count') || '0';
2425
2401
  const retries = parseInt(retryCount);
2426
2402
 
2427
- if (retries < 10) { // 10 * 500ms = 5 secondes max (plus espacé)
2403
+ if (retries < 10) {
2428
2404
  element.innerHTML = '<div style="padding: 20px; text-align: center; color: #6b7280;">Configuration YouTube en cours...</div>';
2429
2405
  element.setAttribute('data-youtube-retry-count', (retries + 1).toString());
2430
2406
 
2431
- // OPTIMISATION: Espacer les retries (500ms au lieu de 100ms)
2432
2407
  setTimeout(() => {
2433
2408
  this.initElement(element);
2434
2409
  }, 500);
2435
2410
  return;
2436
2411
  } else {
2437
- // Timeout après 5 secondes
2438
2412
  element.innerHTML = '<div style="padding: 20px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; color: #dc2626;"><strong>Configuration YouTube manquante</strong><br>Ajoutez dans le &lt;head&gt; :<br><code style="display: block; background: #f3f4f6; padding: 10px; margin: 10px 0; border-radius: 4px; font-family: monospace;">&lt;script&gt;<br>bbContents.config.youtubeEndpoint = \'votre-worker-url\';<br>&lt;/script&gt;</code></div>';
2439
- return;
2413
+ return;
2440
2414
  }
2441
2415
  }
2442
2416
 
2443
- // Chercher le template pour une vidéo (directement dans l'élément ou dans un conteneur)
2417
+ // Chercher le template pour une vidéo
2444
2418
  let template = element.querySelector('[bb-youtube-item]');
2445
2419
  let container = element;
2446
2420
 
2447
- // Si pas de template direct, chercher dans un conteneur
2448
2421
  if (!template) {
2449
2422
  const containerElement = element.querySelector('[bb-youtube-container]');
2450
2423
  if (containerElement) {
@@ -2455,110 +2428,213 @@
2455
2428
 
2456
2429
  if (!template) {
2457
2430
  element.innerHTML = '<div style="padding: 20px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; color: #dc2626;"><strong>Template manquant</strong><br>Ajoutez un élément avec l\'attribut bb-youtube-item</div>';
2458
- return;
2459
- }
2460
-
2431
+ return;
2432
+ }
2433
+
2461
2434
  // Cacher le template original
2462
2435
  template.style.display = 'none';
2463
2436
 
2464
2437
  // Marquer l'élément comme traité par le module YouTube
2465
2438
  element.setAttribute('data-bb-youtube-processed', 'true');
2466
2439
 
2467
- // Vérifier le cache d'abord
2468
- const cacheKey = `youtube_${channelId}_${videoCount}_${allowShorts}_${language}`;
2469
- const cachedData = this.cache.get(cacheKey);
2440
+ // OPTIMISATION: Cache partagé par configuration (sans videoCount ni skip)
2441
+ const baseCacheKey = `youtube_${groupConfig.channelIds}_${groupConfig.allowShorts}_${groupConfig.language}`;
2442
+ const cachedData = this.cache.get(baseCacheKey);
2470
2443
 
2471
2444
  if (cachedData && cachedData.value) {
2472
2445
  // Données YouTube récupérées du cache (économie API)
2473
- this.generateYouTubeFeed(container, template, cachedData.value, allowShorts, language);
2446
+ // Appliquer skip puis limiter par videoCount
2447
+ const limitedData = this.applySkipAndLimit(cachedData.value, skip, videoCount);
2448
+ this.generateYouTubeFeed(container, template, limitedData, groupConfig.allowShorts, groupConfig.language);
2474
2449
  return;
2475
2450
  }
2476
2451
 
2477
- // OPTIMISATION: Protection globale contre les appels multiples
2478
- if (this.isRequestActive(cacheKey)) {
2479
- // Un appel est déjà en cours pour cette clé, attendre
2452
+ if (this.isRequestActive(baseCacheKey)) {
2480
2453
  const checkActive = () => {
2481
- if (!this.isRequestActive(cacheKey)) {
2454
+ if (!this.isRequestActive(baseCacheKey)) {
2482
2455
  // L'autre appel est terminé, vérifier le cache
2483
- const newCachedData = this.cache.get(cacheKey);
2456
+ const newCachedData = this.cache.get(baseCacheKey);
2484
2457
  if (newCachedData && newCachedData.value) {
2485
- this.generateYouTubeFeed(container, template, newCachedData.value, allowShorts, language);
2486
- } else {
2458
+ const limitedData = this.applySkipAndLimit(newCachedData.value, skip, videoCount);
2459
+ this.generateYouTubeFeed(container, template, limitedData, groupConfig.allowShorts, groupConfig.language);
2460
+ } else {
2487
2461
  container.innerHTML = '<div style="padding: 20px; text-align: center; color: #6b7280;">Erreur de chargement</div>';
2488
2462
  }
2489
2463
  } else {
2490
- setTimeout(checkActive, 200); // Vérifier moins souvent
2464
+ setTimeout(checkActive, 200);
2491
2465
  }
2492
2466
  };
2493
2467
  checkActive();
2494
2468
  return;
2495
2469
  }
2496
2470
 
2497
- // Marquer qu'un appel API est en cours
2498
- this.markRequestActive(cacheKey);
2471
+ // Marquer qu'un appel API est en cours (utiliser baseCacheKey)
2472
+ this.markRequestActive(baseCacheKey);
2499
2473
 
2500
2474
  // Afficher un loader
2501
2475
  container.innerHTML = '<div style="padding: 20px; text-align: center; color: #6b7280;">Chargement des vidéos YouTube...</div>';
2502
2476
 
2503
- // Appeler l'API via le Worker
2504
- // Valider l'endpoint et le channelId avant fetch
2477
+ // OPTIMISATION: Utiliser le maxVideoCount et maxSkip du groupe pour la requête API
2478
+ const apiVideoCount = groupConfig.maxVideoCount + groupConfig.maxSkip;
2479
+
2480
+ // Valider l'endpoint
2505
2481
  if (!endpoint || typeof endpoint !== 'string') {
2506
2482
  throw new Error('Endpoint YouTube invalide');
2507
2483
  }
2508
- // Vérifier que l'endpoint correspond à la configuration
2509
2484
  if (bbContents.config.youtubeEndpoint && !endpoint.startsWith(bbContents.config.youtubeEndpoint)) {
2510
2485
  throw new Error('Endpoint YouTube non autorisé');
2511
2486
  }
2512
- // Valider le format de channelId (alphanumérique, tirets, underscores)
2513
- if (!channelId || !/^[a-zA-Z0-9_-]+$/.test(channelId)) {
2514
- throw new Error('Channel ID invalide');
2487
+
2488
+ // Parser les channelIds
2489
+ const channelIds = groupConfig.channelIds.split(',');
2490
+
2491
+ // Valider les channelIds
2492
+ channelIds.forEach(channelId => {
2493
+ if (!channelId || !/^[a-zA-Z0-9_-]+$/.test(channelId)) {
2494
+ throw new Error('Channel ID invalide: ' + channelId);
2495
+ }
2496
+ });
2497
+
2498
+ const safeAllowShorts = groupConfig.allowShorts === true || groupConfig.allowShorts === 'true';
2499
+
2500
+ // OPTIMISATION: Si plusieurs channelIds, récupérer depuis plusieurs chaînes
2501
+ if (channelIds.length > 1) {
2502
+ this.fetchMultipleChannels(endpoint, channelIds, apiVideoCount, safeAllowShorts)
2503
+ .then(data => {
2504
+ if (data.error) {
2505
+ throw new Error(data.error.message || 'Erreur API YouTube');
2506
+ }
2507
+
2508
+ // Stocker dans le cache avec la clé de base (sans videoCount ni skip)
2509
+ this.cache.set(baseCacheKey, data);
2510
+
2511
+ // Appliquer skip puis limiter par videoCount pour cet élément
2512
+ const limitedData = this.applySkipAndLimit(data, skip, videoCount);
2513
+
2514
+ this.generateYouTubeFeed(container, template, limitedData, groupConfig.allowShorts, groupConfig.language);
2515
+
2516
+ this.markRequestComplete(baseCacheKey);
2517
+ })
2518
+ .catch(error => {
2519
+ this.markRequestComplete(baseCacheKey);
2520
+ this.handleFetchError(error, container, baseCacheKey, skip, videoCount, template, groupConfig);
2521
+ });
2522
+ } else {
2523
+ // Un seul channelId, requête simple
2524
+ fetch(`${endpoint}?channelId=${encodeURIComponent(channelIds[0])}&maxResults=${apiVideoCount}&allowShorts=${safeAllowShorts}`)
2525
+ .then(response => {
2526
+ if (!response.ok) {
2527
+ throw new Error(`HTTP ${response.status}`);
2528
+ }
2529
+ return response.json();
2530
+ })
2531
+ .then(data => {
2532
+ if (data.error) {
2533
+ throw new Error(data.error.message || 'Erreur API YouTube');
2534
+ }
2535
+
2536
+ // Stocker dans le cache avec la clé de base (sans videoCount ni skip)
2537
+ this.cache.set(baseCacheKey, data);
2538
+
2539
+ // Appliquer skip puis limiter par videoCount pour cet élément
2540
+ const limitedData = this.applySkipAndLimit(data, skip, videoCount);
2541
+
2542
+ this.generateYouTubeFeed(container, template, limitedData, groupConfig.allowShorts, groupConfig.language);
2543
+
2544
+ this.markRequestComplete(baseCacheKey);
2545
+ })
2546
+ .catch(error => {
2547
+ this.markRequestComplete(baseCacheKey);
2548
+ this.handleFetchError(error, container, baseCacheKey, skip, videoCount, template, groupConfig);
2549
+ });
2515
2550
  }
2516
- // Valider videoCount et allowShorts
2517
- const safeVideoCount = parseInt(videoCount, 10);
2518
- const safeAllowShorts = allowShorts === true || allowShorts === 'true';
2519
-
2520
- fetch(`${endpoint}?channelId=${encodeURIComponent(channelId)}&maxResults=${safeVideoCount}&allowShorts=${safeAllowShorts}`)
2521
- .then(response => {
2522
- if (!response.ok) {
2523
- throw new Error(`HTTP ${response.status}`);
2524
- }
2525
- return response.json();
2526
- })
2527
- .then(data => {
2528
- if (data.error) {
2529
- throw new Error(data.error.message || 'Erreur API YouTube');
2530
- }
2531
-
2532
- // OPTIMISATION: Sauvegarder en cache pour 24h
2533
- this.cache.set(cacheKey, data);
2534
- // Données YouTube mises en cache pour 24h (économie API)
2535
-
2536
- this.generateYouTubeFeed(container, template, data, allowShorts, language);
2537
-
2538
- // OPTIMISATION: Libérer le verrou avec la nouvelle méthode
2539
- this.markRequestComplete(cacheKey);
2540
- })
2541
- .catch(error => {
2542
- // Erreur dans le module youtube
2543
-
2544
- // OPTIMISATION: Libérer le verrou en cas d'erreur
2545
- this.markRequestComplete(cacheKey);
2546
-
2547
- // En cas d'erreur, essayer de récupérer du cache même expiré
2548
- const expiredCache = localStorage.getItem(cacheKey);
2549
- if (expiredCache) {
2550
- try {
2551
- const cachedData = JSON.parse(expiredCache);
2552
- // Utilisation du cache expiré en cas d'erreur API
2553
- this.generateYouTubeFeed(container, template, cachedData.value, allowShorts, language);
2554
- return;
2555
- } catch (e) {
2556
- // Ignorer les erreurs de parsing
2551
+ },
2552
+
2553
+ // Fonction pour appliquer skip puis limiter par videoCount
2554
+ applySkipAndLimit: function(data, skip, videoCount) {
2555
+ if (!data || !data.items) {
2556
+ return data;
2557
+ }
2558
+
2559
+ // Appliquer skip
2560
+ const afterSkip = skip > 0 ? data.items.slice(skip) : data.items;
2561
+
2562
+ // Limiter par videoCount
2563
+ const limited = afterSkip.slice(0, videoCount);
2564
+
2565
+ return {
2566
+ ...data,
2567
+ items: limited
2568
+ };
2569
+ },
2570
+
2571
+ // Fonction pour récupérer les vidéos de plusieurs chaînes
2572
+ fetchMultipleChannels: function(endpoint, channelIds, maxResults, allowShorts) {
2573
+ // Limiter le nombre de channelIds pour éviter les abus
2574
+ if (channelIds.length > 10) {
2575
+ throw new Error('Maximum 10 channelIds allowed');
2576
+ }
2577
+
2578
+ // Faire une requête par channelId unique
2579
+ const promises = channelIds.map(channelId => {
2580
+ return fetch(`${endpoint}?channelId=${encodeURIComponent(channelId)}&maxResults=${maxResults}&allowShorts=${allowShorts}`)
2581
+ .then(response => {
2582
+ if (!response.ok) {
2583
+ throw new Error(`HTTP ${response.status} for channel ${channelId}`);
2557
2584
  }
2558
- }
2559
-
2560
- container.innerHTML = `<div style="padding: 20px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; color: #dc2626;"><strong>Erreur de chargement</strong><br>${bbContents.utils.sanitize(error.message || 'Erreur inconnue')}</div>`;
2585
+ return response.json();
2586
+ })
2587
+ .then(data => {
2588
+ if (data.error) {
2589
+ throw new Error(data.error.message || 'Erreur API YouTube');
2590
+ }
2591
+ return data.items || [];
2592
+ })
2593
+ .catch(error => {
2594
+ // En cas d'erreur pour une chaîne, retourner un tableau vide
2595
+ // Les autres chaînes continueront à fonctionner
2596
+ return [];
2597
+ });
2598
+ });
2599
+
2600
+ return Promise.all(promises).then(allItems => {
2601
+ // Fusionner tous les items de toutes les chaînes
2602
+ const mergedItems = [].concat(...allItems);
2603
+
2604
+ // Trier par date (publishedAt) - du plus récent au plus ancien
2605
+ mergedItems.sort((a, b) => {
2606
+ const dateA = new Date(a.snippet.publishedAt);
2607
+ const dateB = new Date(b.snippet.publishedAt);
2608
+ return dateB - dateA;
2561
2609
  });
2610
+
2611
+ // Retourner dans le format attendu
2612
+ return {
2613
+ items: mergedItems,
2614
+ pageInfo: {
2615
+ totalResults: mergedItems.length,
2616
+ resultsPerPage: mergedItems.length
2617
+ }
2618
+ };
2619
+ });
2620
+ },
2621
+
2622
+ // Fonction pour gérer les erreurs de fetch
2623
+ handleFetchError: function(error, container, cacheKey, skip, videoCount, template, groupConfig) {
2624
+ // En cas d'erreur, essayer de récupérer du cache même expiré
2625
+ const expiredCache = localStorage.getItem(cacheKey);
2626
+ if (expiredCache) {
2627
+ try {
2628
+ const cachedData = JSON.parse(expiredCache);
2629
+ const limitedData = this.applySkipAndLimit(cachedData.value, skip, videoCount);
2630
+ this.generateYouTubeFeed(container, template, limitedData, groupConfig.allowShorts, groupConfig.language);
2631
+ return;
2632
+ } catch (e) {
2633
+ // Ignorer les erreurs de parsing
2634
+ }
2635
+ }
2636
+
2637
+ container.innerHTML = `<div style="padding: 20px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; color: #dc2626;"><strong>Erreur de chargement</strong><br>${bbContents.utils.sanitize(error.message || 'Erreur inconnue')}</div>`;
2562
2638
  },
2563
2639
 
2564
2640
  generateYouTubeFeed: function(container, template, data, allowShorts, language = 'fr') {
@@ -2732,7 +2808,7 @@
2732
2808
  return textarea.value;
2733
2809
  },
2734
2810
 
2735
- // OPTIMISATION: Nettoyer le cache expiré (48h)
2811
+ // Nettoyer le cache expiré
2736
2812
  cleanCache: function() {
2737
2813
  try {
2738
2814
  const keys = Object.keys(localStorage);
@@ -2743,7 +2819,7 @@
2743
2819
  if (key.startsWith('youtube_')) {
2744
2820
  try {
2745
2821
  const cached = JSON.parse(localStorage.getItem(key));
2746
- // OPTIMISATION: Cache 24h maintenu
2822
+ // Cache 24h
2747
2823
  if (now - cached.timestamp > 24 * 60 * 60 * 1000) {
2748
2824
  localStorage.removeItem(key);
2749
2825
  cleaned++;
@@ -2778,51 +2854,25 @@
2778
2854
  }
2779
2855
  };
2780
2856
 
2781
- // Initialisation automatique avec délai pour éviter le blocage
2857
+ // Initialisation automatique avec délai
2782
2858
  function initBBContents() {
2783
- // Attendre que la page soit prête
2784
2859
  if (document.readyState === 'loading') {
2785
2860
  document.addEventListener('DOMContentLoaded', function() {
2786
- // Délai pour éviter le blocage du rendu
2787
2861
  const delay = document.body.hasAttribute('bb-performance-boost') ? 300 : 100;
2788
2862
  setTimeout(function() {
2789
2863
  bbContents.init();
2790
2864
  }, delay);
2791
2865
  });
2792
2866
  } else {
2793
- // Délai pour éviter le blocage du rendu
2794
2867
  const delay = document.body.hasAttribute('bb-performance-boost') ? 300 : 100;
2795
2868
  setTimeout(function() {
2796
2869
  bbContents.init();
2797
2870
  }, delay);
2798
2871
  }
2799
-
2800
- // Initialisation différée supplémentaire pour les cas difficiles - Solution cache optimisée
2801
- window.addEventListener('load', function() {
2802
- const loadDelay = document.body.hasAttribute('bb-performance-boost') ? 4000 : 3000; // Délais plus longs pour le cache
2803
- setTimeout(function() {
2804
- // Vérifier s'il y a des éléments non initialisés
2805
- const unprocessedMarquees = document.querySelectorAll('[bb-marquee]:not([data-bb-marquee-processed])');
2806
- if (unprocessedMarquees.length > 0) {
2807
- // Éléments marquee non initialisés détectés après load, réinitialisation
2808
- bbContents.reinit();
2809
- }
2810
-
2811
- // Vérification supplémentaire des images chargées - Solution cache optimisée
2812
- const allImages = document.querySelectorAll('img');
2813
- const unloadedImages = Array.from(allImages).filter(img => !img.complete || img.naturalHeight === 0);
2814
- if (unloadedImages.length > 0) {
2815
- // Images non chargées détectées, attente supplémentaire plus longue
2816
- setTimeout(() => {
2817
- bbContents.reinit();
2818
- }, 2000); // 2 secondes au lieu de 1 seconde
2819
- }
2820
- }, loadDelay);
2821
- });
2822
2872
  }
2823
2873
 
2824
2874
  // Initialisation
2825
2875
  initBBContents();
2826
2876
 
2827
- // Message de confirmation supprimé pour une console plus propre
2877
+ // Message de confirmation supprimé
2828
2878
  })();