@bebranded/bb-contents 1.0.84-beta → 1.0.85

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 +386 -356
  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.0.84
4
+ * @version 1.0.85
5
5
  * @author BeBranded
6
6
  * @license MIT
7
7
  * @website https://www.bebranded.xyz
@@ -32,11 +32,11 @@
32
32
  window._bbContentsInitialized = true;
33
33
 
34
34
  // Log de démarrage simple (une seule fois)
35
- console.log('bb-contents | v1.0.84');
35
+ console.log('bb-contents | v1.0.85');
36
36
 
37
37
  // Configuration
38
38
  const config = {
39
- version: '1.0.84',
39
+ version: '1.0.85',
40
40
  debug: false, // Debug désactivé pour rendu propre
41
41
  prefix: 'bb-', // utilisé pour générer les sélecteurs (data-bb-*)
42
42
  youtubeEndpoint: null, // URL du worker YouTube (à définir par l'utilisateur)
@@ -589,259 +589,259 @@
589
589
 
590
590
  // Module Share (Partage Social)
591
591
  share: {
592
- // Configuration des réseaux
593
- networks: {
594
- twitter: function(data) {
595
- return 'https://twitter.com/intent/tweet?url=' +
596
- encodeURIComponent(data.url) +
597
- '&text=' + encodeURIComponent(data.text);
598
- },
599
- facebook: function(data) {
600
- return 'https://facebook.com/sharer/sharer.php?u=' +
601
- encodeURIComponent(data.url);
602
- },
603
- linkedin: function(data) {
604
- // LinkedIn - URL de partage officielle (2024+)
605
- return 'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(data.url);
606
- },
607
- whatsapp: function(data) {
608
- return 'https://wa.me/?text=' +
609
- encodeURIComponent(data.text + ' ' + data.url);
610
- },
611
- telegram: function(data) {
612
- return 'https://t.me/share/url?url=' +
613
- encodeURIComponent(data.url) +
614
- '&text=' + encodeURIComponent(data.text);
615
- },
616
- email: function(data) {
617
- return 'mailto:?subject=' +
618
- encodeURIComponent(data.text) +
619
- '&body=' + encodeURIComponent(data.text + ' ' + data.url);
620
- },
621
- copy: function(data) {
622
- return 'copy:' + data.url;
623
- },
624
- native: function(data) {
625
- return 'native:' + JSON.stringify(data);
626
- }
592
+ // Configuration des réseaux
593
+ networks: {
594
+ twitter: function(data) {
595
+ return 'https://twitter.com/intent/tweet?url=' +
596
+ encodeURIComponent(data.url) +
597
+ '&text=' + encodeURIComponent(data.text);
627
598
  },
628
-
629
- // Détection
630
- detect: function(scope) {
631
- const s = scope || document;
632
- return s.querySelector(bbContents._attrSelector('share')) !== null;
599
+ facebook: function(data) {
600
+ return 'https://facebook.com/sharer/sharer.php?u=' +
601
+ encodeURIComponent(data.url);
633
602
  },
634
-
635
- // Initialisation
636
- init: function(root) {
637
- const scope = root || document;
638
- if (scope.closest && scope.closest('[data-bb-disable]')) return;
639
- const elements = scope.querySelectorAll(bbContents._attrSelector('share'));
640
-
641
- elements.forEach(function(element) {
642
- // Vérifier si déjà traité
643
- if (element.bbProcessed) return;
644
- element.bbProcessed = true;
645
-
646
- // Récupérer les données
647
- const network = bbContents._getAttr(element, 'bb-share');
648
- const customUrl = bbContents._getAttr(element, 'bb-url');
649
- const customText = bbContents._getAttr(element, 'bb-text');
650
-
651
- // Valeurs par défaut sécurisées
652
- const data = {
653
- url: bbContents.utils.isValidUrl(customUrl) ? customUrl : window.location.href,
654
- text: bbContents.utils.sanitize(customText || document.title || 'Découvrez ce site')
655
- };
656
-
657
- // Gestionnaire de clic
658
- element.addEventListener('click', function(e) {
659
- e.preventDefault();
660
- bbContents.modules.share.share(network, data, element);
661
- });
662
-
663
- // Accessibilité
664
- if (element.tagName !== 'BUTTON' && element.tagName !== 'A') {
665
- element.setAttribute('role', 'button');
666
- element.setAttribute('tabindex', '0');
667
-
668
- // Support clavier
669
- element.addEventListener('keydown', function(e) {
670
- if (e.key === 'Enter' || e.key === ' ') {
671
- e.preventDefault();
672
- bbContents.modules.share.share(network, data, element);
673
- }
674
- });
675
- }
676
-
677
- element.style.cursor = 'pointer';
678
- });
679
-
680
- bbContents.utils.log('Module Share initialisé:', elements.length, 'éléments');
603
+ linkedin: function(data) {
604
+ // LinkedIn - URL de partage officielle (2024+)
605
+ return 'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(data.url);
681
606
  },
607
+ whatsapp: function(data) {
608
+ return 'https://wa.me/?text=' +
609
+ encodeURIComponent(data.text + ' ' + data.url);
610
+ },
611
+ telegram: function(data) {
612
+ return 'https://t.me/share/url?url=' +
613
+ encodeURIComponent(data.url) +
614
+ '&text=' + encodeURIComponent(data.text);
615
+ },
616
+ email: function(data) {
617
+ return 'mailto:?subject=' +
618
+ encodeURIComponent(data.text) +
619
+ '&body=' + encodeURIComponent(data.text + ' ' + data.url);
620
+ },
621
+ copy: function(data) {
622
+ return 'copy:' + data.url;
623
+ },
624
+ native: function(data) {
625
+ return 'native:' + JSON.stringify(data);
626
+ }
627
+ },
628
+
629
+ // Détection
630
+ detect: function(scope) {
631
+ const s = scope || document;
632
+ return s.querySelector(bbContents._attrSelector('share')) !== null;
633
+ },
634
+
635
+ // Initialisation
636
+ init: function(root) {
637
+ const scope = root || document;
638
+ if (scope.closest && scope.closest('[data-bb-disable]')) return;
639
+ const elements = scope.querySelectorAll(bbContents._attrSelector('share'));
682
640
 
683
- // Fonction de partage
684
- share: function(network, data, element) {
685
- const networkFunc = this.networks[network];
686
-
687
- if (!networkFunc) {
688
- return;
689
- }
690
-
691
- const shareUrl = networkFunc(data);
692
-
693
- // Cas spécial : copier le lien
694
- if (shareUrl.startsWith('copy:')) {
695
- const url = shareUrl.substring(5);
696
- // Copie silencieuse (pas de feedback visuel)
697
- this.copyToClipboard(url, element, true);
698
- return;
699
- }
641
+ elements.forEach(function(element) {
642
+ // Vérifier si déjà traité
643
+ if (element.bbProcessed) return;
644
+ element.bbProcessed = true;
700
645
 
701
- // Cas spécial : partage natif (Web Share API)
702
- if (shareUrl.startsWith('native:')) {
703
- const shareData = JSON.parse(shareUrl.substring(7));
704
- this.nativeShare(shareData, element);
705
- return;
706
- }
646
+ // Récupérer les données
647
+ const network = bbContents._getAttr(element, 'bb-share');
648
+ const customUrl = bbContents._getAttr(element, 'bb-url');
649
+ const customText = bbContents._getAttr(element, 'bb-text');
707
650
 
708
- // Ouvrir popup de partage
709
- const width = 600;
710
- const height = 400;
711
- const left = (window.innerWidth - width) / 2;
712
- const top = (window.innerHeight - height) / 2;
651
+ // Valeurs par défaut sécurisées
652
+ const data = {
653
+ url: bbContents.utils.isValidUrl(customUrl) ? customUrl : window.location.href,
654
+ text: bbContents.utils.sanitize(customText || document.title || 'Découvrez ce site')
655
+ };
713
656
 
714
- window.open(
715
- shareUrl,
716
- 'bbshare',
717
- 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',noopener,noreferrer'
718
- );
657
+ // Gestionnaire de clic
658
+ element.addEventListener('click', function(e) {
659
+ e.preventDefault();
660
+ bbContents.modules.share.share(network, data, element);
661
+ });
719
662
 
720
- bbContents.utils.log('Partage sur', network, data);
721
- },
722
-
723
- // Copier dans le presse-papier
724
- copyToClipboard: function(text, element, silent) {
725
- const isSilent = !!silent;
726
- // Méthode moderne
727
- if (navigator.clipboard && navigator.clipboard.writeText) {
728
- navigator.clipboard.writeText(text).then(function() {
729
- if (!isSilent) {
730
- bbContents.modules.share.showFeedback(element, '✓ ' + (bbContents.config.i18n.copied || 'Lien copié !'));
663
+ // Accessibilité
664
+ if (element.tagName !== 'BUTTON' && element.tagName !== 'A') {
665
+ element.setAttribute('role', 'button');
666
+ element.setAttribute('tabindex', '0');
667
+
668
+ // Support clavier
669
+ element.addEventListener('keydown', function(e) {
670
+ if (e.key === 'Enter' || e.key === ' ') {
671
+ e.preventDefault();
672
+ bbContents.modules.share.share(network, data, element);
731
673
  }
732
- }).catch(function() {
733
- bbContents.modules.share.fallbackCopy(text, element, isSilent);
734
674
  });
735
- } else {
736
- // Fallback pour environnements sans Clipboard API
737
- this.fallbackCopy(text, element, isSilent);
738
675
  }
739
- },
676
+
677
+ element.style.cursor = 'pointer';
678
+ });
740
679
 
741
- // Fallback copie
742
- fallbackCopy: function(text, element, silent) {
743
- const isSilent = !!silent;
744
- // Pas de UI si silencieux (exigence produit)
745
- if (isSilent) return;
746
- try {
747
- // Afficher un prompt natif pour permettre à l'utilisateur de copier manuellement
748
- // (solution universelle sans execCommand)
749
- window.prompt('Copiez le lien ci-dessous (Ctrl/Cmd+C) :', text);
750
- } catch (err) {
751
- // Dernier recours: ne rien faire
752
- }
753
- },
680
+ bbContents.utils.log('Module Share initialisé:', elements.length, 'éléments');
681
+ },
682
+
683
+ // Fonction de partage
684
+ share: function(network, data, element) {
685
+ const networkFunc = this.networks[network];
754
686
 
755
- // Partage natif (Web Share API)
756
- nativeShare: function(data, element) {
757
- // Vérifier si Web Share API est disponible
758
- if (navigator.share) {
759
- navigator.share({
760
- title: data.text,
761
- url: data.url
762
- }).then(function() {
763
- bbContents.utils.log('Partage natif réussi');
764
- }).catch(function(error) {
765
- if (error.name !== 'AbortError') {
766
- // Fallback vers copie si échec
767
- bbContents.modules.share.copyToClipboard(data.url, element, false);
768
- }
769
- });
770
- } else {
771
- // Fallback si Web Share API non disponible
772
- bbContents.utils.log('Web Share API non disponible, fallback vers copie');
773
- this.copyToClipboard(data.url, element, false);
774
- }
775
- },
687
+ if (!networkFunc) {
688
+ return;
689
+ }
776
690
 
777
- // Feedback visuel
778
- showFeedback: function(element, message) {
779
- const originalText = element.textContent;
780
- element.textContent = message;
781
- element.style.pointerEvents = 'none';
782
-
783
- setTimeout(function() {
784
- element.textContent = originalText;
785
- element.style.pointerEvents = '';
786
- }, 2000);
691
+ const shareUrl = networkFunc(data);
692
+
693
+ // Cas spécial : copier le lien
694
+ if (shareUrl.startsWith('copy:')) {
695
+ const url = shareUrl.substring(5);
696
+ // Copie silencieuse (pas de feedback visuel)
697
+ this.copyToClipboard(url, element, true);
698
+ return;
787
699
  }
700
+
701
+ // Cas spécial : partage natif (Web Share API)
702
+ if (shareUrl.startsWith('native:')) {
703
+ const shareData = JSON.parse(shareUrl.substring(7));
704
+ this.nativeShare(shareData, element);
705
+ return;
706
+ }
707
+
708
+ // Ouvrir popup de partage
709
+ const width = 600;
710
+ const height = 400;
711
+ const left = (window.innerWidth - width) / 2;
712
+ const top = (window.innerHeight - height) / 2;
713
+
714
+ window.open(
715
+ shareUrl,
716
+ 'bbshare',
717
+ 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',noopener,noreferrer'
718
+ );
719
+
720
+ bbContents.utils.log('Partage sur', network, data);
721
+ },
722
+
723
+ // Copier dans le presse-papier
724
+ copyToClipboard: function(text, element, silent) {
725
+ const isSilent = !!silent;
726
+ // Méthode moderne
727
+ if (navigator.clipboard && navigator.clipboard.writeText) {
728
+ navigator.clipboard.writeText(text).then(function() {
729
+ if (!isSilent) {
730
+ bbContents.modules.share.showFeedback(element, '✓ ' + (bbContents.config.i18n.copied || 'Lien copié !'));
731
+ }
732
+ }).catch(function() {
733
+ bbContents.modules.share.fallbackCopy(text, element, isSilent);
734
+ });
735
+ } else {
736
+ // Fallback pour environnements sans Clipboard API
737
+ this.fallbackCopy(text, element, isSilent);
738
+ }
739
+ },
740
+
741
+ // Fallback copie
742
+ fallbackCopy: function(text, element, silent) {
743
+ const isSilent = !!silent;
744
+ // Pas de UI si silencieux (exigence produit)
745
+ if (isSilent) return;
746
+ try {
747
+ // Afficher un prompt natif pour permettre à l'utilisateur de copier manuellement
748
+ // (solution universelle sans execCommand)
749
+ window.prompt('Copiez le lien ci-dessous (Ctrl/Cmd+C) :', text);
750
+ } catch (err) {
751
+ // Dernier recours: ne rien faire
752
+ }
753
+ },
754
+
755
+ // Partage natif (Web Share API)
756
+ nativeShare: function(data, element) {
757
+ // Vérifier si Web Share API est disponible
758
+ if (navigator.share) {
759
+ navigator.share({
760
+ title: data.text,
761
+ url: data.url
762
+ }).then(function() {
763
+ bbContents.utils.log('Partage natif réussi');
764
+ }).catch(function(error) {
765
+ if (error.name !== 'AbortError') {
766
+ // Fallback vers copie si échec
767
+ bbContents.modules.share.copyToClipboard(data.url, element, false);
768
+ }
769
+ });
770
+ } else {
771
+ // Fallback si Web Share API non disponible
772
+ bbContents.utils.log('Web Share API non disponible, fallback vers copie');
773
+ this.copyToClipboard(data.url, element, false);
774
+ }
775
+ },
776
+
777
+ // Feedback visuel
778
+ showFeedback: function(element, message) {
779
+ const originalText = element.textContent;
780
+ element.textContent = message;
781
+ element.style.pointerEvents = 'none';
782
+
783
+ setTimeout(function() {
784
+ element.textContent = originalText;
785
+ element.style.pointerEvents = '';
786
+ }, 2000);
787
+ }
788
788
  },
789
789
 
790
790
  // Module Current Year (Année courante)
791
791
  currentYear: {
792
- detect: function(scope) {
793
- const s = scope || document;
794
- return s.querySelector(bbContents._attrSelector('current-year')) !== null;
795
- },
796
- init: function(root) {
797
- const scope = root || document;
798
- if (scope.closest && scope.closest('[data-bb-disable]')) return;
799
- const elements = scope.querySelectorAll(bbContents._attrSelector('current-year'));
792
+ detect: function(scope) {
793
+ const s = scope || document;
794
+ return s.querySelector(bbContents._attrSelector('current-year')) !== null;
795
+ },
796
+ init: function(root) {
797
+ const scope = root || document;
798
+ if (scope.closest && scope.closest('[data-bb-disable]')) return;
799
+ const elements = scope.querySelectorAll(bbContents._attrSelector('current-year'));
800
800
 
801
- const year = String(new Date().getFullYear());
802
- elements.forEach(function(element) {
803
- if (element.bbProcessed) return;
804
- element.bbProcessed = true;
801
+ const year = String(new Date().getFullYear());
802
+ elements.forEach(function(element) {
803
+ if (element.bbProcessed) return;
804
+ element.bbProcessed = true;
805
805
 
806
806
  const customFormat = bbContents._getAttr(element, 'bb-current-year-format');
807
807
  const prefix = bbContents._getAttr(element, 'bb-current-year-prefix');
808
808
  const suffix = bbContents._getAttr(element, 'bb-current-year-suffix');
809
809
 
810
- if (customFormat && customFormat.includes('{year}')) {
811
- element.textContent = customFormat.replace('{year}', year);
812
- } else if (prefix || suffix) {
813
- element.textContent = prefix + year + suffix;
814
- } else {
815
- element.textContent = year;
816
- }
817
- });
810
+ if (customFormat && customFormat.includes('{year}')) {
811
+ element.textContent = customFormat.replace('{year}', year);
812
+ } else if (prefix || suffix) {
813
+ element.textContent = prefix + year + suffix;
814
+ } else {
815
+ element.textContent = year;
816
+ }
817
+ });
818
818
 
819
- bbContents.utils.log('Module CurrentYear initialisé:', elements.length, 'éléments');
820
- }
819
+ bbContents.utils.log('Module CurrentYear initialisé:', elements.length, 'éléments');
820
+ }
821
821
  },
822
822
 
823
823
  // Module Reading Time (Temps de lecture)
824
824
  readingTime: {
825
- detect: function(scope) {
826
- const s = scope || document;
827
- return s.querySelector(bbContents._attrSelector('reading-time')) !== null;
828
- },
829
- init: function(root) {
830
- const scope = root || document;
831
- if (scope.closest && scope.closest('[data-bb-disable]')) return;
832
- const elements = scope.querySelectorAll(bbContents._attrSelector('reading-time'));
825
+ detect: function(scope) {
826
+ const s = scope || document;
827
+ return s.querySelector(bbContents._attrSelector('reading-time')) !== null;
828
+ },
829
+ init: function(root) {
830
+ const scope = root || document;
831
+ if (scope.closest && scope.closest('[data-bb-disable]')) return;
832
+ const elements = scope.querySelectorAll(bbContents._attrSelector('reading-time'));
833
833
 
834
- elements.forEach(function(element) {
835
- if (element.bbProcessed) return;
836
- element.bbProcessed = true;
834
+ elements.forEach(function(element) {
835
+ if (element.bbProcessed) return;
836
+ element.bbProcessed = true;
837
837
 
838
838
  const targetSelector = bbContents._getAttr(element, 'bb-reading-time-target');
839
- const speedAttr = bbContents._getAttr(element, 'bb-reading-time-speed');
840
- const imageSpeedAttr = bbContents._getAttr(element, 'bb-reading-time-image-speed');
841
- const format = bbContents._getAttr(element, 'bb-reading-time-format') || '{minutes} min';
839
+ const speedAttr = bbContents._getAttr(element, 'bb-reading-time-speed');
840
+ const imageSpeedAttr = bbContents._getAttr(element, 'bb-reading-time-image-speed');
841
+ const format = bbContents._getAttr(element, 'bb-reading-time-format') || '{minutes} min';
842
842
 
843
- const wordsPerMinute = Number(speedAttr) > 0 ? Number(speedAttr) : 230;
844
- const secondsPerImage = Number(imageSpeedAttr) > 0 ? Number(imageSpeedAttr) : 12;
843
+ const wordsPerMinute = Number(speedAttr) > 0 ? Number(speedAttr) : 230;
844
+ const secondsPerImage = Number(imageSpeedAttr) > 0 ? Number(imageSpeedAttr) : 12;
845
845
 
846
846
  // Validation des valeurs
847
847
  if (isNaN(wordsPerMinute) || wordsPerMinute <= 0) {
@@ -851,143 +851,158 @@
851
851
  bbContents.utils.log('Temps par image invalide, utilisation de la valeur par défaut (12)');
852
852
  }
853
853
 
854
- let sourceNode = element;
855
- if (targetSelector) {
856
- const found = document.querySelector(targetSelector);
857
- if (found) sourceNode = found;
858
- }
854
+ let sourceNode = element;
855
+ if (targetSelector) {
856
+ const found = document.querySelector(targetSelector);
857
+ if (found) sourceNode = found;
858
+ }
859
859
 
860
- const text = (sourceNode.textContent || '').trim();
861
- const wordCount = text ? (text.match(/\b\w+\b/g) || []).length : 0;
862
-
863
- // Compter les images dans le contenu ciblé
864
- const images = sourceNode.querySelectorAll('img');
865
- const imageCount = images.length;
866
- const imageTimeInMinutes = (imageCount * secondsPerImage) / 60;
867
-
868
- let minutesFloat = (wordCount / wordsPerMinute) + imageTimeInMinutes;
869
- let minutes = Math.ceil(minutesFloat);
860
+ const text = (sourceNode.textContent || '').trim();
861
+ const wordCount = text ? (text.match(/\b\w+\b/g) || []).length : 0;
862
+
863
+ // Compter les images dans le contenu ciblé
864
+ const images = sourceNode.querySelectorAll('img');
865
+ const imageCount = images.length;
866
+ const imageTimeInMinutes = (imageCount * secondsPerImage) / 60;
867
+
868
+ let minutesFloat = (wordCount / wordsPerMinute) + imageTimeInMinutes;
869
+ let minutes = Math.ceil(minutesFloat);
870
870
 
871
- if ((wordCount > 0 || imageCount > 0) && minutes < 1) minutes = 1; // affichage minimal 1 min si contenu non vide
872
- if (wordCount === 0 && imageCount === 0) minutes = 0;
871
+ if ((wordCount > 0 || imageCount > 0) && minutes < 1) minutes = 1; // affichage minimal 1 min si contenu non vide
872
+ if (wordCount === 0 && imageCount === 0) minutes = 0;
873
873
 
874
- const output = format.replace('{minutes}', String(minutes));
875
- element.textContent = output;
876
- });
874
+ const output = format.replace('{minutes}', String(minutes));
875
+ element.textContent = output;
876
+ });
877
877
 
878
- bbContents.utils.log('Module ReadingTime initialisé:', elements.length, 'éléments');
879
- }
878
+ bbContents.utils.log('Module ReadingTime initialisé:', elements.length, 'éléments');
879
+ }
880
880
  },
881
881
 
882
882
  // Module Favicon (Favicon Dynamique)
883
883
  favicon: {
884
- originalFavicon: null,
885
-
886
- // Détection
887
- detect: function(scope) {
888
- const s = scope || document;
889
- return s.querySelector(bbContents._attrSelector('favicon')) !== null;
890
- },
884
+ originalFavicon: null,
885
+
886
+ // Détection
887
+ detect: function(scope) {
888
+ const s = scope || document;
889
+ return s.querySelector(bbContents._attrSelector('favicon')) !== null;
890
+ },
891
+
892
+ // Initialisation
893
+ init: function(root) {
894
+ const scope = root || document;
895
+ if (scope.closest && scope.closest('[data-bb-disable]')) return;
891
896
 
892
- // Initialisation
893
- init: function(root) {
894
- const scope = root || document;
895
- if (scope.closest && scope.closest('[data-bb-disable]')) return;
896
-
897
- // Chercher les éléments avec bb-favicon ou bb-favicon-dark
897
+ // Chercher les éléments avec bb-favicon ou bb-favicon-dark
898
898
  const elements = scope.querySelectorAll(bbContents._attrSelector('favicon') + ', ' + bbContents._attrSelector('favicon-dark'));
899
- if (elements.length === 0) return;
900
-
901
- // Sauvegarder le favicon original
902
- const existingLink = document.querySelector("link[rel*='icon']");
903
- if (existingLink) {
904
- this.originalFavicon = existingLink.href;
905
- }
906
-
907
- // Collecter les URLs depuis tous les éléments
908
- let faviconUrl = null;
909
- let darkUrl = null;
910
-
911
- elements.forEach(function(element) {
912
- const light = bbContents._getAttr(element, 'bb-favicon') || bbContents._getAttr(element, 'favicon');
913
- const dark = bbContents._getAttr(element, 'bb-favicon-dark') || bbContents._getAttr(element, 'favicon-dark');
914
-
915
- if (light) faviconUrl = light;
916
- if (dark) darkUrl = dark;
917
- });
918
-
919
- // Appliquer la logique
920
- if (faviconUrl && darkUrl) {
921
- this.setupDarkMode(faviconUrl, darkUrl);
922
- } else if (faviconUrl) {
923
- this.setFavicon(faviconUrl);
924
- bbContents.utils.log('Favicon changé:', faviconUrl);
925
- }
926
- },
899
+ if (elements.length === 0) return;
927
900
 
928
- // Helper: Récupérer ou créer un élément favicon
929
- getFaviconElement: function() {
930
- let favicon = document.querySelector('link[rel="icon"]') ||
931
- document.querySelector('link[rel="shortcut icon"]');
932
- if (!favicon) {
933
- favicon = document.createElement('link');
934
- favicon.rel = 'icon';
935
- document.head.appendChild(favicon);
936
- }
937
- return favicon;
938
- },
901
+ // Sauvegarder le favicon original
902
+ const existingLink = document.querySelector("link[rel*='icon']");
903
+ if (existingLink) {
904
+ this.originalFavicon = existingLink.href;
905
+ }
939
906
 
940
- // Changer le favicon
941
- setFavicon: function(url) {
942
- if (!url) return;
943
-
944
- // Ajouter un timestamp pour forcer le rafraîchissement du cache
945
- const cacheBuster = '?v=' + Date.now();
946
- const urlWithCacheBuster = url + cacheBuster;
947
-
948
- const favicon = this.getFaviconElement();
949
- favicon.href = urlWithCacheBuster;
950
- },
907
+ // Collecter les URLs depuis tous les éléments
908
+ let faviconUrl = null;
909
+ let darkUrl = null;
951
910
 
952
- // Support dark mode (méthode simplifiée et directe)
953
- setupDarkMode: function(lightUrl, darkUrl) {
954
- // Fonction pour mettre à jour le favicon selon le mode sombre
955
- const updateFavicon = function(e) {
956
- const darkModeOn = e ? e.matches : window.matchMedia('(prefers-color-scheme: dark)').matches;
957
- const selectedUrl = darkModeOn ? darkUrl : lightUrl;
958
- bbContents.modules.favicon.setFavicon(selectedUrl);
959
- };
960
-
961
- // Initialiser le favicon au chargement de la page
962
- updateFavicon();
911
+ elements.forEach(function(element) {
912
+ const light = bbContents._getAttr(element, 'bb-favicon') || bbContents._getAttr(element, 'favicon');
913
+ const dark = bbContents._getAttr(element, 'bb-favicon-dark') || bbContents._getAttr(element, 'favicon-dark');
963
914
 
964
- // Écouter les changements du mode sombre
965
- const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
966
- if (typeof darkModeMediaQuery.addEventListener === 'function') {
967
- darkModeMediaQuery.addEventListener('change', updateFavicon);
968
- } else if (typeof darkModeMediaQuery.addListener === 'function') {
969
- darkModeMediaQuery.addListener(updateFavicon);
970
- }
915
+ if (light) faviconUrl = light;
916
+ if (dark) darkUrl = dark;
917
+ });
918
+
919
+ // Appliquer la logique
920
+ if (faviconUrl && darkUrl) {
921
+ this.setupDarkMode(faviconUrl, darkUrl);
922
+ } else if (faviconUrl) {
923
+ this.setFavicon(faviconUrl);
924
+ bbContents.utils.log('Favicon changé:', faviconUrl);
925
+ }
926
+ },
927
+
928
+ // Helper: Récupérer ou créer un élément favicon
929
+ getFaviconElement: function() {
930
+ let favicon = document.querySelector('link[rel="icon"]') ||
931
+ document.querySelector('link[rel="shortcut icon"]');
932
+ if (!favicon) {
933
+ favicon = document.createElement('link');
934
+ favicon.rel = 'icon';
935
+ document.head.appendChild(favicon);
936
+ }
937
+ return favicon;
938
+ },
939
+
940
+ // Changer le favicon
941
+ setFavicon: function(url) {
942
+ if (!url) return;
943
+
944
+ // Ajouter un timestamp pour forcer le rafraîchissement du cache
945
+ const cacheBuster = '?v=' + Date.now();
946
+ const urlWithCacheBuster = url + cacheBuster;
947
+
948
+ const favicon = this.getFaviconElement();
949
+ favicon.href = urlWithCacheBuster;
950
+ },
951
+
952
+ // Support dark mode (méthode simplifiée et directe)
953
+ setupDarkMode: function(lightUrl, darkUrl) {
954
+ // Fonction pour mettre à jour le favicon selon le mode sombre
955
+ const updateFavicon = function(e) {
956
+ const darkModeOn = e ? e.matches : window.matchMedia('(prefers-color-scheme: dark)').matches;
957
+ const selectedUrl = darkModeOn ? darkUrl : lightUrl;
958
+ bbContents.modules.favicon.setFavicon(selectedUrl);
959
+ };
960
+
961
+ // Initialiser le favicon au chargement de la page
962
+ updateFavicon();
963
+
964
+ // Écouter les changements du mode sombre
965
+ const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
966
+ if (typeof darkModeMediaQuery.addEventListener === 'function') {
967
+ darkModeMediaQuery.addEventListener('change', updateFavicon);
968
+ } else if (typeof darkModeMediaQuery.addListener === 'function') {
969
+ darkModeMediaQuery.addListener(updateFavicon);
971
970
  }
971
+ }
972
972
  },
973
973
 
974
974
  // Module YouTube Feed
975
975
  youtube: {
976
- // Détection des bots pour éviter les appels API inutiles
976
+ // OPTIMISATION: Détection améliorée des bots pour éviter les appels API inutiles
977
977
  isBot: function() {
978
978
  const userAgent = navigator.userAgent.toLowerCase();
979
979
  const botPatterns = [
980
980
  'bot', 'crawler', 'spider', 'scraper', 'googlebot', 'bingbot', 'slurp',
981
981
  'duckduckbot', 'baiduspider', 'yandexbot', 'facebookexternalhit', 'twitterbot',
982
- 'linkedinbot', 'whatsapp', 'telegrambot', 'discordbot', 'slackbot'
982
+ 'linkedinbot', 'whatsapp', 'telegrambot', 'discordbot', 'slackbot', 'headless',
983
+ 'phantom', 'selenium', 'puppeteer', 'playwright', 'lighthouse', 'gtmetrix',
984
+ 'pagespeed', 'pingdom', 'uptime', 'monitor', 'check', 'test'
983
985
  ];
984
986
 
985
- return botPatterns.some(pattern => userAgent.includes(pattern)) ||
987
+ // Vérifications supplémentaires pour détecter plus de bots
988
+ const isBot = botPatterns.some(pattern => userAgent.includes(pattern)) ||
986
989
  navigator.webdriver ||
987
- !navigator.userAgent;
990
+ !navigator.userAgent ||
991
+ !window.chrome || // Détecte les navigateurs headless
992
+ navigator.userAgent.includes('HeadlessChrome') ||
993
+ window.navigator.plugins.length === 0; // Bots n'ont souvent pas de plugins
994
+
995
+ if (isBot) {
996
+ // Log pour debug (en mode debug seulement)
997
+ if (bbContents.config.debug) {
998
+ bbContents.utils.log('Bot détecté, pas d\'appel API YouTube');
999
+ }
1000
+ }
1001
+
1002
+ return isBot;
988
1003
  },
989
1004
 
990
- // Gestion du cache localStorage
1005
+ // OPTIMISATION: Cache amélioré avec protection contre les appels multiples
991
1006
  cache: {
992
1007
  get: function(key) {
993
1008
  try {
@@ -997,7 +1012,7 @@
997
1012
  const data = JSON.parse(cached);
998
1013
  const now = Date.now();
999
1014
 
1000
- // Cache expiré après 24h
1015
+ // OPTIMISATION: Cache plus long (24h maintenu)
1001
1016
  if (now - data.timestamp > 24 * 60 * 60 * 1000) {
1002
1017
  localStorage.removeItem(key);
1003
1018
  return null;
@@ -1022,7 +1037,22 @@
1022
1037
  }
1023
1038
  },
1024
1039
 
1025
- detect: function(scope) {
1040
+ // OPTIMISATION: Protection globale contre les appels multiples
1041
+ _activeRequests: new Set(),
1042
+
1043
+ isRequestActive: function(cacheKey) {
1044
+ return this._activeRequests.has(cacheKey);
1045
+ },
1046
+
1047
+ markRequestActive: function(cacheKey) {
1048
+ this._activeRequests.add(cacheKey);
1049
+ },
1050
+
1051
+ markRequestComplete: function(cacheKey) {
1052
+ this._activeRequests.delete(cacheKey);
1053
+ },
1054
+
1055
+ detect: function(scope) {
1026
1056
  return scope.querySelector('[bb-youtube-channel]') !== null;
1027
1057
  },
1028
1058
 
@@ -1075,23 +1105,23 @@
1075
1105
  }
1076
1106
 
1077
1107
  if (!endpoint) {
1078
- // Attendre que la configuration soit définie (max 5 secondes)
1108
+ // OPTIMISATION: Réduire drastiquement les retries (de 50 à 10)
1079
1109
  const retryCount = element.getAttribute('data-youtube-retry-count') || '0';
1080
1110
  const retries = parseInt(retryCount);
1081
1111
 
1082
- if (retries < 50) { // 50 * 100ms = 5 secondes max
1112
+ if (retries < 10) { // 10 * 500ms = 5 secondes max (plus espacé)
1083
1113
  element.innerHTML = '<div style="padding: 20px; text-align: center; color: #6b7280;">Configuration YouTube en cours...</div>';
1084
1114
  element.setAttribute('data-youtube-retry-count', (retries + 1).toString());
1085
1115
 
1086
- // Réessayer dans 100ms
1116
+ // OPTIMISATION: Espacer les retries (500ms au lieu de 100ms)
1087
1117
  setTimeout(() => {
1088
1118
  this.initElement(element);
1089
- }, 100);
1119
+ }, 500);
1090
1120
  return;
1091
1121
  } else {
1092
1122
  // Timeout après 5 secondes
1093
1123
  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>';
1094
- return;
1124
+ return;
1095
1125
  }
1096
1126
  }
1097
1127
 
@@ -1110,9 +1140,9 @@
1110
1140
 
1111
1141
  if (!template) {
1112
1142
  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>';
1113
- return;
1114
- }
1115
-
1143
+ return;
1144
+ }
1145
+
1116
1146
  // Cacher le template original
1117
1147
  template.style.display = 'none';
1118
1148
 
@@ -1129,12 +1159,11 @@
1129
1159
  return;
1130
1160
  }
1131
1161
 
1132
- // Vérifier si un appel API est déjà en cours pour cette clé
1133
- const loadingKey = `loading_${cacheKey}`;
1134
- if (window[loadingKey]) {
1135
- // Attendre que l'autre appel se termine
1136
- const checkLoading = () => {
1137
- if (!window[loadingKey]) {
1162
+ // OPTIMISATION: Protection globale contre les appels multiples
1163
+ if (this.isRequestActive(cacheKey)) {
1164
+ // Un appel est déjà en cours pour cette clé, attendre
1165
+ const checkActive = () => {
1166
+ if (!this.isRequestActive(cacheKey)) {
1138
1167
  // L'autre appel est terminé, vérifier le cache
1139
1168
  const newCachedData = this.cache.get(cacheKey);
1140
1169
  if (newCachedData && newCachedData.value) {
@@ -1143,15 +1172,15 @@
1143
1172
  container.innerHTML = '<div style="padding: 20px; text-align: center; color: #6b7280;">Erreur de chargement</div>';
1144
1173
  }
1145
1174
  } else {
1146
- setTimeout(checkLoading, 100);
1175
+ setTimeout(checkActive, 200); // Vérifier moins souvent
1147
1176
  }
1148
1177
  };
1149
- checkLoading();
1178
+ checkActive();
1150
1179
  return;
1151
1180
  }
1152
1181
 
1153
1182
  // Marquer qu'un appel API est en cours
1154
- window[loadingKey] = true;
1183
+ this.markRequestActive(cacheKey);
1155
1184
 
1156
1185
  // Afficher un loader
1157
1186
  container.innerHTML = '<div style="padding: 20px; text-align: center; color: #6b7280;">Chargement des vidéos YouTube...</div>';
@@ -1169,20 +1198,20 @@
1169
1198
  throw new Error(data.error.message || 'Erreur API YouTube');
1170
1199
  }
1171
1200
 
1172
- // Sauvegarder en cache pour 24h
1201
+ // OPTIMISATION: Sauvegarder en cache pour 24h
1173
1202
  this.cache.set(cacheKey, data);
1174
1203
  // Données YouTube mises en cache pour 24h (économie API)
1175
1204
 
1176
1205
  this.generateYouTubeFeed(container, template, data, allowShorts, language);
1177
1206
 
1178
- // Libérer le verrou
1179
- window[loadingKey] = false;
1207
+ // OPTIMISATION: Libérer le verrou avec la nouvelle méthode
1208
+ this.markRequestComplete(cacheKey);
1180
1209
  })
1181
1210
  .catch(error => {
1182
1211
  // Erreur dans le module youtube
1183
1212
 
1184
- // Libérer le verrou en cas d'erreur
1185
- window[loadingKey] = false;
1213
+ // OPTIMISATION: Libérer le verrou en cas d'erreur
1214
+ this.markRequestComplete(cacheKey);
1186
1215
 
1187
1216
  // En cas d'erreur, essayer de récupérer du cache même expiré
1188
1217
  const expiredCache = localStorage.getItem(cacheKey);
@@ -1284,7 +1313,7 @@
1284
1313
  if (bbContents.config.debug) {
1285
1314
  // Thumbnail optimisée
1286
1315
  }
1287
- } else {
1316
+ } else {
1288
1317
  // Aucune thumbnail disponible
1289
1318
  }
1290
1319
  }
@@ -1372,7 +1401,7 @@
1372
1401
  return textarea.value;
1373
1402
  },
1374
1403
 
1375
- // Nettoyer le cache expiré
1404
+ // OPTIMISATION: Nettoyer le cache expiré (48h)
1376
1405
  cleanCache: function() {
1377
1406
  try {
1378
1407
  const keys = Object.keys(localStorage);
@@ -1383,6 +1412,7 @@
1383
1412
  if (key.startsWith('youtube_')) {
1384
1413
  try {
1385
1414
  const cached = JSON.parse(localStorage.getItem(key));
1415
+ // OPTIMISATION: Cache 24h maintenu
1386
1416
  if (now - cached.timestamp > 24 * 60 * 60 * 1000) {
1387
1417
  localStorage.removeItem(key);
1388
1418
  cleaned++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bebranded/bb-contents",
3
- "version": "1.0.84-beta",
3
+ "version": "1.0.85",
4
4
  "description": "Contenus additionnels français pour Webflow",
5
5
  "main": "bb-contents.js",
6
6
  "scripts": {