@bebranded/bb-contents 1.0.2-beta → 1.0.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 +793 -559
  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.2-beta
4
+ * @version 1.0.2
5
5
  * @author BeBranded
6
6
  * @license MIT
7
7
  * @website https://www.bebranded.xyz
@@ -9,6 +9,11 @@
9
9
  (function() {
10
10
  'use strict';
11
11
 
12
+ // Créer l'objet temporaire pour la configuration si il n'existe pas
13
+ if (!window._bbContentsConfig) {
14
+ window._bbContentsConfig = {};
15
+ }
16
+
12
17
  // Protection contre le double chargement
13
18
  if (window.bbContents) {
14
19
  console.warn('BeBranded Contents est déjà chargé');
@@ -17,13 +22,24 @@
17
22
 
18
23
  // Configuration
19
24
  const config = {
20
- version: '1.0.2-beta',
21
- debug: window.location.hostname === 'localhost' || window.location.hostname.includes('webflow.io'),
25
+ version: '1.0.2',
26
+ debug: false, // Debug désactivé
22
27
  prefix: 'bb-', // utilisé pour générer les sélecteurs (data-bb-*)
28
+ youtubeEndpoint: null, // URL du worker YouTube (à définir par l'utilisateur)
23
29
  i18n: {
24
30
  copied: 'Lien copié !'
25
31
  }
26
32
  };
33
+
34
+ // Détecter la configuration YouTube définie avant le chargement
35
+ if (window.bbContents && window.bbContents.config && window.bbContents.config.youtubeEndpoint) {
36
+ config.youtubeEndpoint = window.bbContents.config.youtubeEndpoint;
37
+ }
38
+
39
+ // Détecter la configuration dans l'objet temporaire
40
+ if (window._bbContentsConfig && window._bbContentsConfig.youtubeEndpoint) {
41
+ config.youtubeEndpoint = window._bbContentsConfig.youtubeEndpoint;
42
+ }
27
43
 
28
44
  // Objet principal
29
45
  const bbContents = {
@@ -31,11 +47,14 @@
31
47
  modules: {},
32
48
  _observer: null,
33
49
  _reinitScheduled: false,
50
+ _initRetryCount: 0,
51
+ _maxInitRetries: 3,
52
+ _performanceBoostDetected: false,
34
53
 
35
54
  // Utilitaires
36
55
  utils: {
37
56
  log: function(...args) {
38
- if (config.debug) {
57
+ if (bbContents.config.debug) {
39
58
  console.log('[BB Contents]', ...args);
40
59
  }
41
60
  },
@@ -59,7 +78,7 @@
59
78
  }
60
79
  },
61
80
 
62
- // Helper: construire des sélecteurs dattributs selon le prefix
81
+ // Helper: construire des sélecteurs d'attributs selon le prefix
63
82
  _attrSelector: function(name) {
64
83
  const p = (this.config.prefix || 'bb-').replace(/-?$/, '-');
65
84
  const legacy = name.startsWith('bb-') ? name : (p + name);
@@ -76,8 +95,19 @@
76
95
 
77
96
  // Initialisation
78
97
  init: function() {
98
+ // Console simple et épurée
99
+ console.log('bb-contents | v' + this.config.version);
100
+
79
101
  this.utils.log('Initialisation v' + this.config.version);
80
102
 
103
+ // Debug environnement supprimé pour console propre
104
+
105
+ // Détection du bb-performance-boost
106
+ this._performanceBoostDetected = document.body.hasAttribute('bb-performance-boost');
107
+ if (this._performanceBoostDetected) {
108
+ // bb-performance-boost détecté - mode de compatibilité activé
109
+ }
110
+
81
111
  // Déterminer la portée
82
112
  const scope = document.querySelector('[data-bb-scope]') || document;
83
113
 
@@ -85,7 +115,7 @@
85
115
  Object.keys(this.modules).forEach(function(moduleName) {
86
116
  const module = bbContents.modules[moduleName];
87
117
  if (module.detect && module.detect(scope)) {
88
- bbContents.utils.log('Module détecté:', moduleName);
118
+ // Module détecté
89
119
  try {
90
120
  module.init(scope);
91
121
  } catch (error) {
@@ -97,530 +127,179 @@
97
127
 
98
128
  // Activer l'observer DOM pour contenu dynamique
99
129
  this.setupObserver();
130
+
131
+ // Vérifier et réinitialiser les éléments non initialisés
132
+ this.checkAndReinitFailedElements();
100
133
  },
101
-
102
- // Ré-initialiser une sous-arborescence DOM (pour contenus ajoutés dynamiquement)
103
- reinit: function(root) {
104
- const rootNode = root && root.nodeType ? root : document;
105
- // Ne pas traiter les sous-arbres marqués en disable
106
- if (rootNode.closest && rootNode.closest('[data-bb-disable]')) return;
134
+
135
+ // Nouvelle méthode pour vérifier et réinitialiser les éléments échoués
136
+ checkAndReinitFailedElements: function() {
137
+ const scope = document.querySelector('[data-bb-scope]') || document;
138
+ let needsReinit = false;
107
139
 
108
- // Éviter les ré-initialisations multiples sur le même scope
109
- if (rootNode === document && this._lastReinitTime && (Date.now() - this._lastReinitTime) < 1000) {
110
- return; // Éviter les reinit trop fréquents sur document
140
+ // Vérifier les marquees non initialisés
141
+ const marqueeElements = scope.querySelectorAll('[bb-marquee]:not([data-bb-marquee-processed])');
142
+ if (marqueeElements.length > 0) {
143
+ // Marquees non initialisés détectés
144
+ needsReinit = true;
111
145
  }
112
- this._lastReinitTime = Date.now();
113
146
 
147
+ // Vérifier les autres modules si nécessaire
114
148
  Object.keys(this.modules).forEach(function(moduleName) {
115
149
  const module = bbContents.modules[moduleName];
116
- try {
117
- module.init(rootNode);
118
- } catch (error) {
119
- console.error('[BB Contents] Erreur reinit dans le module', moduleName, error);
120
- }
121
- });
122
- },
123
-
124
- // Mise en place d'un MutationObserver avec debounce
125
- setupObserver: function() {
126
- if (!('MutationObserver' in window) || this._observer) return;
127
- const self = this;
128
- this._observer = new MutationObserver(function(mutations) {
129
- let hasRelevantChanges = false;
130
- for (let i = 0; i < mutations.length; i++) {
131
- const mutation = mutations[i];
132
- // Vérifier si les changements concernent des éléments avec nos attributs
133
- if (mutation.addedNodes && mutation.addedNodes.length > 0) {
134
- for (let j = 0; j < mutation.addedNodes.length; j++) {
135
- const node = mutation.addedNodes[j];
136
- if (node.nodeType === 1) { // Element node
137
- if (node.querySelector && (
138
- node.querySelector('[bb-], [data-bb-]') ||
139
- node.matches && node.matches('[bb-], [data-bb-]')
140
- )) {
141
- hasRelevantChanges = true;
142
- break;
143
- }
144
- }
145
- }
146
- }
150
+ if (module.checkFailed && module.checkFailed(scope)) {
151
+ // Module a des éléments échoués
152
+ needsReinit = true;
147
153
  }
148
- if (!hasRelevantChanges) return;
149
- if (self._reinitScheduled) return;
150
- self._reinitScheduled = true;
151
- setTimeout(function() {
152
- try {
153
- self.reinit(document);
154
- } finally {
155
- self._reinitScheduled = false;
156
- }
157
- }, 200); // Augmenté à 200ms pour réduire la fréquence
158
154
  });
159
- try {
160
- this._observer.observe(document.body, { childList: true, subtree: true });
161
- this.utils.log('MutationObserver actif');
162
- } catch (e) {
163
- // No-op si document.body indisponible
164
- }
165
- }
166
- };
167
-
168
- // ========================================
169
- // MODULE: SHARE (Partage Social)
170
- // ========================================
171
- bbContents.modules.share = {
172
- // Configuration des réseaux
173
- networks: {
174
- twitter: function(data) {
175
- return 'https://twitter.com/intent/tweet?url=' +
176
- encodeURIComponent(data.url) +
177
- '&text=' + encodeURIComponent(data.text);
178
- },
179
- facebook: function(data) {
180
- return 'https://facebook.com/sharer/sharer.php?u=' +
181
- encodeURIComponent(data.url);
182
- },
183
- linkedin: function(data) {
184
- // LinkedIn - URL de partage officielle (2024+)
185
- return 'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(data.url);
186
- },
187
- whatsapp: function(data) {
188
- return 'https://wa.me/?text=' +
189
- encodeURIComponent(data.text + ' ' + data.url);
190
- },
191
- telegram: function(data) {
192
- return 'https://t.me/share/url?url=' +
193
- encodeURIComponent(data.url) +
194
- '&text=' + encodeURIComponent(data.text);
195
- },
196
- email: function(data) {
197
- return 'mailto:?subject=' +
198
- encodeURIComponent(data.text) +
199
- '&body=' + encodeURIComponent(data.text + ' ' + data.url);
200
- },
201
- copy: function(data) {
202
- return 'copy:' + data.url;
203
- },
204
- native: function(data) {
205
- return 'native:' + JSON.stringify(data);
206
- }
207
- },
208
-
209
- // Détection
210
- detect: function(scope) {
211
- const s = scope || document;
212
- return s.querySelector(bbContents._attrSelector('share')) !== null;
213
- },
214
-
215
- // Initialisation
216
- init: function(root) {
217
- const scope = root || document;
218
- if (scope.closest && scope.closest('[data-bb-disable]')) return;
219
- const elements = scope.querySelectorAll(bbContents._attrSelector('share'));
220
155
 
221
- elements.forEach(function(element) {
222
- // Vérifier si déjà traité
223
- if (element.bbProcessed) return;
224
- element.bbProcessed = true;
225
-
226
- // Récupérer les données
227
- const network = bbContents._getAttr(element, 'bb-share');
228
- const customUrl = bbContents._getAttr(element, 'bb-url');
229
- const customText = bbContents._getAttr(element, 'bb-text');
230
-
231
- // Valeurs par défaut sécurisées
232
- const data = {
233
- url: bbContents.utils.isValidUrl(customUrl) ? customUrl : window.location.href,
234
- text: bbContents.utils.sanitize(customText || document.title || 'Découvrez ce site')
235
- };
156
+ // Réinitialiser si nécessaire et si on n'a pas dépassé le nombre max de tentatives
157
+ if (needsReinit && this._initRetryCount < this._maxInitRetries) {
158
+ this._initRetryCount++;
159
+ // Tentative de réinitialisation
236
160
 
237
- // Gestionnaire de clic
238
- element.addEventListener('click', function(e) {
239
- e.preventDefault();
240
- bbContents.modules.share.share(network, data, element);
241
- });
242
-
243
- // Accessibilité
244
- if (element.tagName !== 'BUTTON' && element.tagName !== 'A') {
245
- element.setAttribute('role', 'button');
246
- element.setAttribute('tabindex', '0');
247
-
248
- // Support clavier
249
- element.addEventListener('keydown', function(e) {
250
- if (e.key === 'Enter' || e.key === ' ') {
251
- e.preventDefault();
252
- bbContents.modules.share.share(network, data, element);
253
- }
254
- });
255
- }
256
-
257
- element.style.cursor = 'pointer';
258
- });
259
-
260
- bbContents.utils.log('Module Share initialisé:', elements.length, 'éléments');
261
- },
262
-
263
- // Fonction de partage
264
- share: function(network, data, element) {
265
- const networkFunc = this.networks[network];
266
-
267
- if (!networkFunc) {
268
- console.error('[BB Contents] Réseau non supporté:', network);
269
- return;
270
- }
271
-
272
- const shareUrl = networkFunc(data);
273
-
274
- // Cas spécial : copier le lien
275
- if (shareUrl.startsWith('copy:')) {
276
- const url = shareUrl.substring(5);
277
- // Copie silencieuse (pas de feedback visuel)
278
- this.copyToClipboard(url, element, true);
279
- return;
280
- }
281
-
282
- // Cas spécial : partage natif (Web Share API)
283
- if (shareUrl.startsWith('native:')) {
284
- const shareData = JSON.parse(shareUrl.substring(7));
285
- this.nativeShare(shareData, element);
286
- return;
161
+ const delay = this._performanceBoostDetected ? 1000 * this._initRetryCount : 500 * this._initRetryCount;
162
+ setTimeout(() => {
163
+ this.init();
164
+ }, delay); // Délai progressif adaptatif
287
165
  }
288
-
289
- // Ouvrir popup de partage
290
- const width = 600;
291
- const height = 400;
292
- const left = (window.innerWidth - width) / 2;
293
- const top = (window.innerHeight - height) / 2;
294
-
295
- window.open(
296
- shareUrl,
297
- 'bbshare',
298
- 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',noopener,noreferrer'
299
- );
300
-
301
- bbContents.utils.log('Partage sur', network, data);
302
166
  },
303
167
 
304
- // Copier dans le presse-papier
305
- copyToClipboard: function(text, element, silent) {
306
- const isSilent = !!silent;
307
- // Méthode moderne
308
- if (navigator.clipboard && navigator.clipboard.writeText) {
309
- navigator.clipboard.writeText(text).then(function() {
310
- if (!isSilent) {
311
- bbContents.modules.share.showFeedback(element, '✓ ' + (bbContents.config.i18n.copied || 'Lien copié !'));
312
- }
313
- }).catch(function() {
314
- bbContents.modules.share.fallbackCopy(text, element, isSilent);
315
- });
316
- } else {
317
- // Fallback pour environnements sans Clipboard API
318
- this.fallbackCopy(text, element, isSilent);
319
- }
168
+ // Méthode publique pour forcer la réinitialisation
169
+ reinit: function() {
170
+ this._initRetryCount = 0;
171
+ this.init();
320
172
  },
321
173
 
322
- // Fallback copie
323
- fallbackCopy: function(text, element, silent) {
324
- const isSilent = !!silent;
325
- // Pas de UI si silencieux (exigence produit)
326
- if (isSilent) return;
327
- try {
328
- // Afficher un prompt natif pour permettre à l'utilisateur de copier manuellement
329
- // (solution universelle sans execCommand)
330
- window.prompt('Copiez le lien ci-dessous (Ctrl/Cmd+C) :', text);
331
- } catch (err) {
332
- // Dernier recours: ne rien faire
174
+ // Méthode pour détecter la configuration YouTube définie après le chargement
175
+ checkYouTubeConfig: function() {
176
+ // Vérifier si la configuration a été définie après le chargement
177
+ if (this.config.youtubeEndpoint) {
178
+ return true;
333
179
  }
334
- },
335
-
336
- // Partage natif (Web Share API)
337
- nativeShare: function(data, element) {
338
- // Vérifier si Web Share API est disponible
339
- if (navigator.share) {
340
- navigator.share({
341
- title: data.text,
342
- url: data.url
343
- }).then(function() {
344
- bbContents.utils.log('Partage natif réussi');
345
- }).catch(function(error) {
346
- if (error.name !== 'AbortError') {
347
- console.error('[BB Contents] Erreur partage natif:', error);
348
- // Fallback vers copie si échec
349
- bbContents.modules.share.copyToClipboard(data.url, element, false);
350
- }
351
- });
352
- } else {
353
- // Fallback si Web Share API non disponible
354
- bbContents.utils.log('Web Share API non disponible, fallback vers copie');
355
- this.copyToClipboard(data.url, element, false);
180
+
181
+ // Vérifier dans l'objet temporaire
182
+ if (window._bbContentsConfig && window._bbContentsConfig.youtubeEndpoint) {
183
+ this.config.youtubeEndpoint = window._bbContentsConfig.youtubeEndpoint;
184
+ return true;
356
185
  }
357
- },
358
-
359
- // Feedback visuel
360
- showFeedback: function(element, message) {
361
- const originalText = element.textContent;
362
- element.textContent = message;
363
- element.style.pointerEvents = 'none';
364
186
 
365
- setTimeout(function() {
366
- element.textContent = originalText;
367
- element.style.pointerEvents = '';
368
- }, 2000);
369
- }
370
- };
371
-
372
- // ========================================
373
- // MODULE: CURRENT YEAR (Année courante)
374
- // ========================================
375
- bbContents.modules.currentYear = {
376
- detect: function(scope) {
377
- const s = scope || document;
378
- return s.querySelector(bbContents._attrSelector('current-year')) !== null;
379
- },
380
- init: function(root) {
381
- const scope = root || document;
382
- if (scope.closest && scope.closest('[data-bb-disable]')) return;
383
- const elements = scope.querySelectorAll(bbContents._attrSelector('current-year'));
384
-
385
- const year = String(new Date().getFullYear());
386
- elements.forEach(function(element) {
387
- if (element.bbProcessed) return;
388
- element.bbProcessed = true;
389
-
390
- const customFormat = bbContents._getAttr(element, 'bb-current-year-format');
391
- const prefix = bbContents._getAttr(element, 'bb-current-year-prefix');
392
- const suffix = bbContents._getAttr(element, 'bb-current-year-suffix');
393
-
394
- if (customFormat && customFormat.includes('{year}')) {
395
- element.textContent = customFormat.replace('{year}', year);
396
- } else if (prefix || suffix) {
397
- element.textContent = prefix + year + suffix;
398
- } else {
399
- element.textContent = year;
400
- }
401
- });
402
-
403
- bbContents.utils.log('Module CurrentYear initialisé:', elements.length, 'éléments');
404
- }
405
- };
406
-
407
-
408
-
409
- // ========================================
410
- // MODULE: READING TIME (Temps de lecture)
411
- // ========================================
412
- bbContents.modules.readingTime = {
413
- detect: function(scope) {
414
- const s = scope || document;
415
- return s.querySelector(bbContents._attrSelector('reading-time')) !== null;
187
+ return false;
416
188
  },
417
- init: function(root) {
418
- const scope = root || document;
419
- if (scope.closest && scope.closest('[data-bb-disable]')) return;
420
- const elements = scope.querySelectorAll(bbContents._attrSelector('reading-time'));
421
-
422
- elements.forEach(function(element) {
423
- if (element.bbProcessed) return;
424
- element.bbProcessed = true;
425
189
 
426
- const targetSelector = bbContents._getAttr(element, 'bb-reading-time-target');
427
- const speedAttr = bbContents._getAttr(element, 'bb-reading-time-speed');
428
- const imageSpeedAttr = bbContents._getAttr(element, 'bb-reading-time-image-speed');
429
- const format = bbContents._getAttr(element, 'bb-reading-time-format') || '{minutes} min';
190
+ // Observer DOM pour contenu dynamique
191
+ setupObserver: function() {
192
+ if (this._observer) {
193
+ this._observer.disconnect();
194
+ }
430
195
 
431
- const wordsPerMinute = Number(speedAttr) > 0 ? Number(speedAttr) : 230;
432
- const secondsPerImage = Number(imageSpeedAttr) > 0 ? Number(imageSpeedAttr) : 12;
196
+ this._observer = new MutationObserver((mutations) => {
197
+ let shouldReinit = false;
433
198
 
434
- // Validation des valeurs
435
- if (isNaN(wordsPerMinute) || wordsPerMinute <= 0) {
436
- bbContents.utils.log('Vitesse de lecture invalide, utilisation de la valeur par défaut (230)');
437
- }
438
- if (isNaN(secondsPerImage) || secondsPerImage < 0) {
439
- bbContents.utils.log('Temps par image invalide, utilisation de la valeur par défaut (12)');
440
- }
199
+ mutations.forEach((mutation) => {
200
+ if (mutation.type === 'childList') {
201
+ mutation.addedNodes.forEach((node) => {
202
+ if (node.nodeType === 1) { // Element node
203
+ // Vérifier si le nouveau nœud ou ses enfants ont des attributs bb-*
204
+ if (node.querySelector && (
205
+ node.querySelector('[bb-]') ||
206
+ node.querySelector('[data-bb-]') ||
207
+ node.matches && (node.matches('[bb-]') || node.matches('[data-bb-]'))
208
+ )) {
209
+ shouldReinit = true;
210
+ }
211
+ }
212
+ });
213
+ }
214
+ });
441
215
 
442
- let sourceNode = element;
443
- if (targetSelector) {
444
- const found = document.querySelector(targetSelector);
445
- if (found) sourceNode = found;
216
+ if (shouldReinit && !this._reinitScheduled) {
217
+ this._reinitScheduled = true;
218
+ const delay = this._performanceBoostDetected ? 200 : 100;
219
+ setTimeout(() => {
220
+ this.init();
221
+ this._reinitScheduled = false;
222
+ }, delay);
446
223
  }
224
+ });
447
225
 
448
- const text = (sourceNode.textContent || '').trim();
449
- const wordCount = text ? (text.match(/\b\w+\b/g) || []).length : 0;
450
-
451
- // Compter les images dans le contenu ciblé
452
- const images = sourceNode.querySelectorAll('img');
453
- const imageCount = images.length;
454
- const imageTimeInMinutes = (imageCount * secondsPerImage) / 60;
455
-
456
- let minutesFloat = (wordCount / wordsPerMinute) + imageTimeInMinutes;
457
- let minutes = Math.ceil(minutesFloat);
458
-
459
- if ((wordCount > 0 || imageCount > 0) && minutes < 1) minutes = 1; // affichage minimal 1 min si contenu non vide
460
- if (wordCount === 0 && imageCount === 0) minutes = 0;
461
-
462
- const output = format.replace('{minutes}', String(minutes));
463
- element.textContent = output;
226
+ this._observer.observe(document.body, {
227
+ childList: true,
228
+ subtree: true
464
229
  });
465
230
 
466
- bbContents.utils.log('Module ReadingTime initialisé:', elements.length, 'éléments');
231
+ this.utils.log('MutationObserver actif');
467
232
  }
468
233
  };
469
234
 
470
- // ========================================
471
- // MODULE: FAVICON (Favicon Dynamique)
472
- // ========================================
473
- bbContents.modules.favicon = {
474
- originalFavicon: null,
475
-
476
- // Détection
235
+ // Modules
236
+ bbContents.modules = {
237
+ // Module Marquee - Version live 1.0.41-beta avec modules parasites supprimés
238
+ marquee: {
477
239
  detect: function(scope) {
478
240
  const s = scope || document;
479
- return s.querySelector(bbContents._attrSelector('favicon')) !== null;
480
- },
481
-
482
- // Initialisation
483
- init: function(root) {
484
- const scope = root || document;
485
- if (scope.closest && scope.closest('[data-bb-disable]')) return;
486
-
487
- // Chercher les éléments avec bb-favicon ou bb-favicon-dark
488
- const elements = scope.querySelectorAll(bbContents._attrSelector('favicon') + ', ' + bbContents._attrSelector('favicon-dark'));
489
- if (elements.length === 0) return;
490
-
491
- // Sauvegarder le favicon original
492
- const existingLink = document.querySelector("link[rel*='icon']");
493
- if (existingLink) {
494
- this.originalFavicon = existingLink.href;
495
- }
496
-
497
- // Collecter les URLs depuis tous les éléments
498
- let faviconUrl = null;
499
- let darkUrl = null;
500
-
501
- elements.forEach(function(element) {
502
- const light = bbContents._getAttr(element, 'bb-favicon') || bbContents._getAttr(element, 'favicon');
503
- const dark = bbContents._getAttr(element, 'bb-favicon-dark') || bbContents._getAttr(element, 'favicon-dark');
504
-
505
- if (light) faviconUrl = light;
506
- if (dark) darkUrl = dark;
507
- });
508
-
509
- // Appliquer la logique
510
- if (faviconUrl && darkUrl) {
511
- this.setupDarkMode(faviconUrl, darkUrl);
512
- } else if (faviconUrl) {
513
- this.setFavicon(faviconUrl);
514
- bbContents.utils.log('Favicon changé:', faviconUrl);
515
- }
516
- },
517
-
518
- // Helper: Récupérer ou créer un élément favicon
519
- getFaviconElement: function() {
520
- let favicon = document.querySelector('link[rel="icon"]') ||
521
- document.querySelector('link[rel="shortcut icon"]');
522
- if (!favicon) {
523
- favicon = document.createElement('link');
524
- favicon.rel = 'icon';
525
- document.head.appendChild(favicon);
526
- }
527
- return favicon;
528
- },
529
-
530
- // Changer le favicon
531
- setFavicon: function(url) {
532
- if (!url) return;
533
-
534
- // Ajouter un timestamp pour forcer le rafraîchissement du cache
535
- const cacheBuster = '?v=' + Date.now();
536
- const urlWithCacheBuster = url + cacheBuster;
537
-
538
- const favicon = this.getFaviconElement();
539
- favicon.href = urlWithCacheBuster;
540
- },
541
-
542
- // Support dark mode (méthode simplifiée et directe)
543
- setupDarkMode: function(lightUrl, darkUrl) {
544
- // Fonction pour mettre à jour le favicon selon le mode sombre
545
- const updateFavicon = function(e) {
546
- const darkModeOn = e ? e.matches : window.matchMedia('(prefers-color-scheme: dark)').matches;
547
- const selectedUrl = darkModeOn ? darkUrl : lightUrl;
548
- bbContents.modules.favicon.setFavicon(selectedUrl);
549
- };
550
-
551
- // Initialiser le favicon au chargement de la page
552
- updateFavicon();
241
+ return s.querySelector(bbContents._attrSelector('marquee')) !== null;
242
+ },
553
243
 
554
- // Écouter les changements du mode sombre
555
- const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
556
- if (typeof darkModeMediaQuery.addEventListener === 'function') {
557
- darkModeMediaQuery.addEventListener('change', updateFavicon);
558
- } else if (typeof darkModeMediaQuery.addListener === 'function') {
559
- darkModeMediaQuery.addListener(updateFavicon);
560
- }
561
- }
562
- };
563
-
564
- // ========================================
565
- // MODULE: MARQUEE (Défilement Infini)
566
- // ========================================
567
- bbContents.modules.marquee = {
568
- // Détection
569
- detect: function(scope) {
244
+ // Nouvelle méthode pour vérifier les éléments échoués
245
+ checkFailed: function(scope) {
570
246
  const s = scope || document;
571
- return s.querySelector(bbContents._attrSelector('marquee')) !== null;
247
+ const failedElements = s.querySelectorAll('[bb-marquee]:not([data-bb-marquee-processed])');
248
+ return failedElements.length > 0;
572
249
  },
573
250
 
574
- // Initialisation
575
251
  init: function(root) {
576
252
  const scope = root || document;
577
253
  if (scope.closest && scope.closest('[data-bb-disable]')) return;
578
254
  const elements = scope.querySelectorAll(bbContents._attrSelector('marquee'));
579
255
 
580
256
  elements.forEach(function(element) {
581
- if (element.bbProcessed) return;
257
+ // Vérifier si l'élément a déjà été traité par un autre module
258
+ if (element.bbProcessed || element.hasAttribute('data-bb-youtube-processed')) {
259
+ // Élément marquee déjà traité par un autre module, ignoré
260
+ return;
261
+ }
582
262
  element.bbProcessed = true;
583
263
 
584
264
  // Récupérer les options
585
- const speed = bbContents._getAttr(element, 'bb-marquee-speed') || '100';
586
- const direction = bbContents._getAttr(element, 'bb-marquee-direction') || 'left';
587
- const pauseOnHover = bbContents._getAttr(element, 'bb-marquee-pause') || 'true';
588
- const gap = bbContents._getAttr(element, 'bb-marquee-gap') || '50';
589
- const orientation = bbContents._getAttr(element, 'bb-marquee-orientation') || 'horizontal';
590
- const height = bbContents._getAttr(element, 'bb-marquee-height') || '300';
591
- const isVertical = orientation === 'vertical';
592
- const minHeight = bbContents._getAttr(element, 'bb-marquee-min-height') || (isVertical ? '100px' : 'auto');
265
+ const speed = bbContents._getAttr(element, 'bb-marquee-speed') || '100';
266
+ const direction = bbContents._getAttr(element, 'bb-marquee-direction') || 'left';
267
+ const pauseOnHover = bbContents._getAttr(element, 'bb-marquee-pause');
268
+ const gap = bbContents._getAttr(element, 'bb-marquee-gap') || '50';
269
+ const orientation = bbContents._getAttr(element, 'bb-marquee-orientation') || 'horizontal';
270
+ const height = bbContents._getAttr(element, 'bb-marquee-height') || '300';
271
+ const minHeight = bbContents._getAttr(element, 'bb-marquee-min-height');
593
272
 
594
273
  // Sauvegarder le contenu original
595
274
  const originalHTML = element.innerHTML;
596
275
 
597
276
  // Créer le conteneur principal
598
277
  const mainContainer = document.createElement('div');
599
- // Pour le marquee horizontal, on va détecter automatiquement la hauteur des logos
600
- const autoHeight = !isVertical && !bbContents._getAttr(element, 'bb-marquee-height');
601
-
278
+ const isVertical = orientation === 'vertical';
279
+ const useAutoHeight = isVertical && height === 'auto';
280
+
602
281
  mainContainer.style.cssText = `
603
282
  position: relative;
604
283
  width: 100%;
605
- height: ${isVertical ? height + 'px' : (autoHeight ? 'auto' : height + 'px')};
284
+ height: ${isVertical ? (height === 'auto' ? 'auto' : height + 'px') : 'auto'};
606
285
  overflow: hidden;
607
- min-height: ${minHeight};
286
+ min-height: ${isVertical ? '100px' : '50px'};
287
+ ${minHeight ? `min-height: ${minHeight};` : ''}
608
288
  `;
609
289
 
610
290
  // Créer le conteneur de défilement
611
291
  const scrollContainer = document.createElement('div');
612
292
  scrollContainer.style.cssText = `
613
- position: absolute;
293
+ ${useAutoHeight ? 'position: relative;' : 'position: absolute;'}
614
294
  will-change: transform;
615
- height: 100%;
616
- top: 0px;
617
- left: 0px;
295
+ ${useAutoHeight ? '' : 'height: 100%; top: 0px; left: 0px;'}
618
296
  display: flex;
619
297
  ${isVertical ? 'flex-direction: column;' : ''}
620
298
  align-items: center;
621
299
  gap: ${gap}px;
622
300
  ${isVertical ? '' : 'white-space: nowrap;'}
623
301
  flex-shrink: 0;
302
+ transition: transform 0.1s ease-out;
624
303
  `;
625
304
 
626
305
  // Créer le bloc de contenu principal
@@ -652,152 +331,686 @@
652
331
  element.innerHTML = '';
653
332
  element.appendChild(mainContainer);
654
333
 
655
- // Fonction pour initialiser l'animation
656
- const initAnimation = () => {
657
- // Attendre que le contenu soit dans le DOM
658
- requestAnimationFrame(() => {
659
- const contentWidth = mainBlock.offsetWidth;
660
- const contentHeight = mainBlock.offsetHeight;
334
+ // Marquer l'élément comme traité par le module marquee
335
+ element.setAttribute('data-bb-marquee-processed', 'true');
336
+
337
+ // Fonction pour initialiser l'animation avec vérification robuste des dimensions
338
+ const initAnimation = (retryCount = 0) => {
339
+ // Vérifier que les images sont chargées
340
+ const images = mainBlock.querySelectorAll('img');
341
+ const imagesLoaded = Array.from(images).every(img => img.complete && img.naturalHeight > 0);
661
342
 
662
- // Si auto-height est activé, ajuster la hauteur du conteneur
663
- if (autoHeight && !isVertical) {
664
- const logoElements = mainBlock.querySelectorAll('.bb-marquee_logo, img, svg');
665
- let maxHeight = 0;
343
+ // Attendre que le contenu soit dans le DOM et que les images soient chargées
344
+ requestAnimationFrame(() => {
345
+ // Calcul plus robuste des dimensions
346
+ const rect = mainBlock.getBoundingClientRect();
347
+ const contentWidth = rect.width || mainBlock.offsetWidth;
348
+ const contentHeight = rect.height || mainBlock.offsetHeight;
666
349
 
667
- logoElements.forEach(logo => {
668
- const logoHeight = logo.offsetHeight || logo.getBoundingClientRect().height;
669
- if (logoHeight > maxHeight) {
670
- maxHeight = logoHeight;
671
- }
672
- });
350
+ // Pour les marquees verticaux, utiliser la largeur du parent si nécessaire
351
+ let finalWidth = contentWidth;
352
+ let finalHeight = contentHeight;
673
353
 
674
- if (maxHeight > 0) {
675
- mainContainer.style.height = maxHeight + 'px';
676
- bbContents.utils.log('Auto-height détecté:', maxHeight + 'px');
354
+ if (isVertical && contentWidth < 10) {
355
+ // Si largeur trop petite, utiliser la largeur du parent
356
+ const parentRect = mainBlock.parentElement.getBoundingClientRect();
357
+ finalWidth = parentRect.width || mainBlock.parentElement.offsetWidth;
358
+ // Largeur corrigée pour marquee vertical
677
359
  }
678
- }
679
-
680
- // Debug
681
- bbContents.utils.log('Debug - Largeur du contenu:', contentWidth, 'px', 'Hauteur:', contentHeight, 'px', 'Enfants:', mainBlock.children.length, 'Vertical:', isVertical, 'Direction:', direction);
682
-
683
- // Si pas de contenu, réessayer
684
- if ((isVertical && contentHeight === 0) || (!isVertical && contentWidth === 0)) {
685
- bbContents.utils.log('Contenu non prêt, nouvelle tentative dans 200ms');
686
- setTimeout(initAnimation, 200);
687
- return;
688
- }
689
-
690
- // Pour le vertical, s'assurer qu'on a une hauteur minimale
691
- if (isVertical && contentHeight < 50) {
692
- bbContents.utils.log('Hauteur insuffisante pour le marquee vertical (' + contentHeight + 'px), nouvelle tentative dans 200ms');
693
- setTimeout(initAnimation, 200);
360
+
361
+ // Debug supprimé pour console propre
362
+
363
+ // Vérifications robustes avant initialisation
364
+ const hasValidDimensions = (isVertical && finalHeight > 50) || (!isVertical && finalWidth > 50);
365
+ const maxRetries = 8; // Plus de tentatives pour attendre les images
366
+
367
+ // Si pas de contenu valide ou images pas chargées, réessayer
368
+ if (!hasValidDimensions || !imagesLoaded) {
369
+ if (retryCount < maxRetries) {
370
+ const delay = 300 + retryCount * 200; // Délais plus longs pour attendre les images
371
+ // Contenu/images non prêts, nouvelle tentative
372
+ setTimeout(() => initAnimation(retryCount + 1), delay);
373
+ return;
374
+ } else {
375
+ // Échec d'initialisation après plusieurs tentatives
694
376
  return;
377
+ }
695
378
  }
696
379
 
697
380
  if (isVertical) {
698
381
  // Animation JavaScript pour le vertical
699
- const contentSize = contentHeight;
382
+ const contentSize = finalHeight;
700
383
  const totalSize = contentSize * 4 + parseInt(gap) * 3; // 4 copies au lieu de 3
384
+
385
+ // Ajuster la hauteur du scrollContainer seulement si pas en mode auto
386
+ if (!useAutoHeight) {
701
387
  scrollContainer.style.height = totalSize + 'px';
388
+ }
702
389
 
703
390
  let currentPosition = direction === 'bottom' ? -contentSize - parseInt(gap) : 0;
704
- const step = (parseFloat(speed) * 2) / 60; // Vitesse différente
391
+ const baseStep = (parseFloat(speed) * 2) / 60; // Vitesse de base
392
+ let currentStep = baseStep;
705
393
  let isPaused = false;
706
-
707
- // Fonction d'animation JavaScript
708
- const animate = () => {
709
- if (!isPaused) {
394
+ let animationId = null;
395
+ let lastTime = 0;
396
+
397
+ // Fonction d'animation JavaScript optimisée
398
+ const animate = (currentTime) => {
399
+ if (!lastTime) lastTime = currentTime;
400
+ const deltaTime = currentTime - lastTime;
401
+ lastTime = currentTime;
402
+
710
403
  if (direction === 'bottom') {
711
- currentPosition += step;
404
+ currentPosition += currentStep * (deltaTime / 16.67); // Normaliser à 60fps
712
405
  if (currentPosition >= 0) {
713
406
  currentPosition = -contentSize - parseInt(gap);
714
407
  }
715
408
  } else {
716
- currentPosition -= step;
409
+ currentPosition -= currentStep * (deltaTime / 16.67);
717
410
  if (currentPosition <= -contentSize - parseInt(gap)) {
718
411
  currentPosition = 0;
719
412
  }
720
413
  }
721
414
 
722
415
  scrollContainer.style.transform = `translate3d(0px, ${currentPosition}px, 0px)`;
723
- }
724
- requestAnimationFrame(animate);
416
+ animationId = requestAnimationFrame(animate);
725
417
  };
726
418
 
727
419
  // Démarrer l'animation
728
- animate();
420
+ animationId = requestAnimationFrame(animate);
729
421
 
730
- bbContents.utils.log('Marquee vertical créé avec animation JS - direction:', direction, 'taille:', contentSize + 'px', 'total:', totalSize + 'px', 'hauteur-wrapper:', height + 'px');
422
+ // Marquee vertical créé avec animation JS
731
423
 
732
- // Pause au survol
424
+ // Pause au survol avec transition fluide CSS + JS
733
425
  if (pauseOnHover === 'true') {
426
+ // Transition fluide avec easing naturel
427
+ const transitionSpeed = (targetSpeed, duration = 300) => {
428
+ const startSpeed = currentStep;
429
+ const speedDiff = targetSpeed - startSpeed;
430
+ const startTime = performance.now();
431
+
432
+ // Easing naturel
433
+ const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
434
+ const easeInCubic = (t) => t * t * t;
435
+
436
+ const animateTransition = (currentTime) => {
437
+ const elapsed = currentTime - startTime;
438
+ const progress = Math.min(elapsed / duration, 1);
439
+
440
+ // Easing différent selon la direction
441
+ const easedProgress = targetSpeed === 0 ?
442
+ easeOutCubic(progress) : easeInCubic(progress);
443
+
444
+ currentStep = startSpeed + speedDiff * easedProgress;
445
+
446
+ if (progress < 1) {
447
+ requestAnimationFrame(animateTransition);
448
+ } else {
449
+ currentStep = targetSpeed;
450
+ }
451
+ };
452
+
453
+ requestAnimationFrame(animateTransition);
454
+ };
455
+
734
456
  element.addEventListener('mouseenter', function() {
735
- isPaused = true;
457
+ transitionSpeed(0); // Ralentir jusqu'à 0
736
458
  });
737
459
  element.addEventListener('mouseleave', function() {
738
- isPaused = false;
460
+ transitionSpeed(baseStep); // Revenir à la vitesse normale
739
461
  });
740
462
  }
741
463
  } else {
742
- // Animation CSS pour l'horizontal (modifiée)
743
- const contentSize = contentWidth;
744
- const totalSize = contentSize * 4 + parseInt(gap) * 3; // 4 copies au lieu de 3
464
+ // Animation JavaScript pour l'horizontal (comme le vertical pour éviter les saccades)
465
+ const contentSize = finalWidth;
466
+ const totalSize = contentSize * 4 + parseInt(gap) * 3;
745
467
  scrollContainer.style.width = totalSize + 'px';
746
468
 
747
- // Créer l'animation CSS optimisée
748
- const animationName = 'bb-scroll-' + Math.random().toString(36).substr(2, 9);
749
- const animationDuration = (totalSize / (parseFloat(speed) * 1.5)).toFixed(2) + 's'; // Vitesse différente
750
-
751
- // Animation avec translate3d pour hardware acceleration
752
- let keyframes;
469
+ let currentPosition = direction === 'right' ? -contentSize - parseInt(gap) : 0;
470
+ const baseStep = (parseFloat(speed) * 0.5) / 60; // Vitesse de base
471
+ let currentStep = baseStep;
472
+ let isPaused = false;
473
+ let animationId = null;
474
+ let lastTime = 0;
475
+
476
+ // Fonction d'animation JavaScript optimisée
477
+ const animate = (currentTime) => {
478
+ if (!lastTime) lastTime = currentTime;
479
+ const deltaTime = currentTime - lastTime;
480
+ lastTime = currentTime;
481
+
753
482
  if (direction === 'right') {
754
- keyframes = `@keyframes ${animationName} {
755
- 0% { transform: translate3d(-${contentSize + parseInt(gap)}px, 0px, 0px); }
756
- 100% { transform: translate3d(0px, 0px, 0px); }
757
- }`;
483
+ currentPosition += currentStep * (deltaTime / 16.67); // Normaliser à 60fps
484
+ if (currentPosition >= 0) {
485
+ currentPosition = -contentSize - parseInt(gap);
486
+ }
758
487
  } else {
759
- // Direction 'left' par défaut
760
- keyframes = `@keyframes ${animationName} {
761
- 0% { transform: translate3d(0px, 0px, 0px); }
762
- 100% { transform: translate3d(-${contentSize + parseInt(gap)}px, 0px, 0px); }
763
- }`;
764
- }
765
-
766
- // Ajouter les styles
767
- const style = document.createElement('style');
768
- style.textContent = keyframes;
769
- document.head.appendChild(style);
770
-
771
- // Appliquer l'animation
772
- scrollContainer.style.animation = `${animationName} ${animationDuration} linear infinite`;
773
-
774
- bbContents.utils.log('Marquee horizontal créé:', animationName, 'durée:', animationDuration + 's', 'direction:', direction, 'taille:', contentSize + 'px', 'total:', totalSize + 'px');
775
-
776
- // Pause au survol
488
+ currentPosition -= currentStep * (deltaTime / 16.67);
489
+ if (currentPosition <= -contentSize - parseInt(gap)) {
490
+ currentPosition = 0;
491
+ }
492
+ }
493
+
494
+ scrollContainer.style.transform = `translate3d(${currentPosition}px, 0px, 0px)`;
495
+ animationId = requestAnimationFrame(animate);
496
+ };
497
+
498
+ // Démarrer l'animation
499
+ animationId = requestAnimationFrame(animate);
500
+
501
+ // Marquee horizontal créé avec animation JS
502
+
503
+ // Pause au survol avec transition fluide CSS + JS
777
504
  if (pauseOnHover === 'true') {
505
+ // Transition fluide avec easing naturel
506
+ const transitionSpeed = (targetSpeed, duration = 300) => {
507
+ const startSpeed = currentStep;
508
+ const speedDiff = targetSpeed - startSpeed;
509
+ const startTime = performance.now();
510
+
511
+ // Easing naturel
512
+ const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
513
+ const easeInCubic = (t) => t * t * t;
514
+
515
+ const animateTransition = (currentTime) => {
516
+ const elapsed = currentTime - startTime;
517
+ const progress = Math.min(elapsed / duration, 1);
518
+
519
+ // Easing différent selon la direction
520
+ const easedProgress = targetSpeed === 0 ?
521
+ easeOutCubic(progress) : easeInCubic(progress);
522
+
523
+ currentStep = startSpeed + speedDiff * easedProgress;
524
+
525
+ if (progress < 1) {
526
+ requestAnimationFrame(animateTransition);
527
+ } else {
528
+ currentStep = targetSpeed;
529
+ }
530
+ };
531
+
532
+ requestAnimationFrame(animateTransition);
533
+ };
534
+
778
535
  element.addEventListener('mouseenter', function() {
779
- scrollContainer.style.animationPlayState = 'paused';
536
+ transitionSpeed(0); // Ralentir jusqu'à 0
780
537
  });
781
538
  element.addEventListener('mouseleave', function() {
782
- scrollContainer.style.animationPlayState = 'running';
539
+ transitionSpeed(baseStep); // Revenir à la vitesse normale
783
540
  });
784
541
  }
785
542
  }
786
543
  });
787
544
  };
788
545
 
789
- // Démarrer l'initialisation
790
- setTimeout(initAnimation, isVertical ? 300 : 100);
791
- });
546
+ // Démarrer l'initialisation avec délai adaptatif - Option 1: Attendre que tout soit prêt
547
+ let initDelay = isVertical ? 500 : 200; // Délais plus longs par défaut
548
+ if (bbContents._performanceBoostDetected) {
549
+ initDelay = isVertical ? 800 : 500; // Délais encore plus longs avec bb-performance-boost
550
+ }
551
+
552
+ // Attendre window.load si pas encore déclenché
553
+ if (document.readyState !== 'complete') {
554
+ // Attente de window.load pour initialiser le marquee
555
+ window.addEventListener('load', () => {
556
+ setTimeout(() => initAnimation(0), initDelay);
557
+ });
558
+ } else {
559
+ // window.load déjà déclenché, initialiser directement
560
+ setTimeout(() => initAnimation(0), initDelay);
561
+ }
562
+ });
563
+
564
+ // Module Marquee initialisé
565
+ }
566
+ },
792
567
 
793
- bbContents.utils.log('Module Marquee initialisé:', elements.length, 'éléments');
568
+ // Module YouTube Feed
569
+ youtube: {
570
+ // Détection des bots pour éviter les appels API inutiles
571
+ isBot: function() {
572
+ const userAgent = navigator.userAgent.toLowerCase();
573
+ const botPatterns = [
574
+ 'bot', 'crawler', 'spider', 'scraper', 'googlebot', 'bingbot', 'slurp',
575
+ 'duckduckbot', 'baiduspider', 'yandexbot', 'facebookexternalhit', 'twitterbot',
576
+ 'linkedinbot', 'whatsapp', 'telegrambot', 'discordbot', 'slackbot'
577
+ ];
578
+
579
+ return botPatterns.some(pattern => userAgent.includes(pattern)) ||
580
+ navigator.webdriver ||
581
+ !navigator.userAgent;
582
+ },
583
+
584
+ // Gestion du cache localStorage
585
+ cache: {
586
+ get: function(key) {
587
+ try {
588
+ const cached = localStorage.getItem(key);
589
+ if (!cached) return null;
590
+
591
+ const data = JSON.parse(cached);
592
+ const now = Date.now();
593
+
594
+ // Cache expiré après 24h
595
+ if (now - data.timestamp > 24 * 60 * 60 * 1000) {
596
+ localStorage.removeItem(key);
597
+ return null;
598
+ }
599
+
600
+ return data.value;
601
+ } catch (e) {
602
+ return null;
603
+ }
604
+ },
605
+
606
+ set: function(key, value) {
607
+ try {
608
+ const data = {
609
+ value: value,
610
+ timestamp: Date.now()
611
+ };
612
+ localStorage.setItem(key, JSON.stringify(data));
613
+ } catch (e) {
614
+ // Ignorer les erreurs de localStorage
615
+ }
616
+ }
617
+ },
618
+
619
+ detect: function(scope) {
620
+ return scope.querySelector('[bb-youtube-channel]') !== null;
621
+ },
622
+
623
+ init: function(scope) {
624
+ // Vérifier si c'est un bot - pas d'appel API
625
+ if (this.isBot()) {
626
+ // Bot détecté, pas de chargement YouTube (économie API)
627
+ return;
628
+ }
629
+
630
+ // Nettoyer le cache expiré au démarrage
631
+ this.cleanCache();
632
+
633
+ const elements = scope.querySelectorAll('[bb-youtube-channel]');
634
+ if (elements.length === 0) return;
635
+
636
+ // Module détecté: youtube
637
+
638
+ elements.forEach(element => {
639
+ // Vérifier si l'élément a déjà été traité par un autre module
640
+ if (element.bbProcessed || element.hasAttribute('data-bb-marquee-processed')) {
641
+ // Élément youtube déjà traité par un autre module, ignoré
642
+ return;
643
+ }
644
+ element.bbProcessed = true;
645
+
646
+ // Utiliser la nouvelle fonction initElement
647
+ this.initElement(element);
648
+ });
649
+ },
650
+
651
+ // Fonction pour initialiser un seul élément YouTube
652
+ initElement: function(element) {
653
+ // Vérifier si c'est un bot - pas d'appel API
654
+ if (this.isBot()) {
655
+ return;
656
+ }
657
+
658
+ const channelId = bbContents._getAttr(element, 'bb-youtube-channel');
659
+ const videoCount = bbContents._getAttr(element, 'bb-youtube-video-count') || '10';
660
+ const allowShorts = bbContents._getAttr(element, 'bb-youtube-allow-shorts') === 'true';
661
+ const language = bbContents._getAttr(element, 'bb-youtube-language') || 'fr';
662
+
663
+ // Vérifier la configuration au moment de l'initialisation
664
+ const endpoint = bbContents.checkYouTubeConfig() ? bbContents.config.youtubeEndpoint : null;
665
+
666
+
667
+ if (!channelId) {
668
+ return;
669
+ }
670
+
671
+ if (!endpoint) {
672
+ // Attendre que la configuration soit définie (max 5 secondes)
673
+ const retryCount = element.getAttribute('data-youtube-retry-count') || '0';
674
+ const retries = parseInt(retryCount);
675
+
676
+ if (retries < 50) { // 50 * 100ms = 5 secondes max
677
+ element.innerHTML = '<div style="padding: 20px; text-align: center; color: #6b7280;">Configuration YouTube en cours...</div>';
678
+ element.setAttribute('data-youtube-retry-count', (retries + 1).toString());
679
+
680
+ // Réessayer dans 100ms
681
+ setTimeout(() => {
682
+ this.initElement(element);
683
+ }, 100);
684
+ return;
685
+ } else {
686
+ // Timeout après 5 secondes
687
+ 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>';
688
+ return;
689
+ }
690
+ }
691
+
692
+ // Chercher le template pour une vidéo (directement dans l'élément ou dans un conteneur)
693
+ let template = element.querySelector('[bb-youtube-item]');
694
+ let container = element;
695
+
696
+ // Si pas de template direct, chercher dans un conteneur
697
+ if (!template) {
698
+ const containerElement = element.querySelector('[bb-youtube-container]');
699
+ if (containerElement) {
700
+ container = containerElement;
701
+ template = containerElement.querySelector('[bb-youtube-item]');
702
+ }
703
+ }
704
+
705
+ if (!template) {
706
+ 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>';
707
+ return;
708
+ }
709
+
710
+ // Cacher le template original
711
+ template.style.display = 'none';
712
+
713
+ // Marquer l'élément comme traité par le module YouTube
714
+ element.setAttribute('data-bb-youtube-processed', 'true');
715
+
716
+ // Vérifier le cache d'abord
717
+ const cacheKey = `youtube_${channelId}_${videoCount}_${allowShorts}_${language}`;
718
+ const cachedData = this.cache.get(cacheKey);
719
+
720
+ if (cachedData && cachedData.value) {
721
+ // Données YouTube récupérées du cache (économie API)
722
+ this.generateYouTubeFeed(container, template, cachedData.value, allowShorts, language);
723
+ return;
724
+ }
725
+
726
+ // Vérifier si un appel API est déjà en cours pour cette clé
727
+ const loadingKey = `loading_${cacheKey}`;
728
+ if (window[loadingKey]) {
729
+ // Attendre que l'autre appel se termine
730
+ const checkLoading = () => {
731
+ if (!window[loadingKey]) {
732
+ // L'autre appel est terminé, vérifier le cache
733
+ const newCachedData = this.cache.get(cacheKey);
734
+ if (newCachedData && newCachedData.value) {
735
+ this.generateYouTubeFeed(container, template, newCachedData.value, allowShorts, language);
736
+ } else {
737
+ container.innerHTML = '<div style="padding: 20px; text-align: center; color: #6b7280;">Erreur de chargement</div>';
738
+ }
739
+ } else {
740
+ setTimeout(checkLoading, 100);
741
+ }
742
+ };
743
+ checkLoading();
744
+ return;
745
+ }
746
+
747
+ // Marquer qu'un appel API est en cours
748
+ window[loadingKey] = true;
749
+
750
+ // Afficher un loader
751
+ container.innerHTML = '<div style="padding: 20px; text-align: center; color: #6b7280;">Chargement des vidéos YouTube...</div>';
752
+
753
+ // Appeler l'API via le Worker
754
+ fetch(`${endpoint}?channelId=${channelId}&maxResults=${videoCount}&allowShorts=${allowShorts}`)
755
+ .then(response => {
756
+ if (!response.ok) {
757
+ throw new Error(`HTTP ${response.status}`);
758
+ }
759
+ return response.json();
760
+ })
761
+ .then(data => {
762
+ if (data.error) {
763
+ throw new Error(data.error.message || 'Erreur API YouTube');
764
+ }
765
+
766
+ // Sauvegarder en cache pour 24h
767
+ this.cache.set(cacheKey, data);
768
+ // Données YouTube mises en cache pour 24h (économie API)
769
+
770
+ this.generateYouTubeFeed(container, template, data, allowShorts, language);
771
+
772
+ // Libérer le verrou
773
+ window[loadingKey] = false;
774
+ })
775
+ .catch(error => {
776
+ console.error('Erreur API YouTube:', error);
777
+ // Erreur dans le module youtube
778
+
779
+ // Libérer le verrou en cas d'erreur
780
+ window[loadingKey] = false;
781
+
782
+ // En cas d'erreur, essayer de récupérer du cache même expiré
783
+ const expiredCache = localStorage.getItem(cacheKey);
784
+ if (expiredCache) {
785
+ try {
786
+ const cachedData = JSON.parse(expiredCache);
787
+ // Utilisation du cache expiré en cas d'erreur API
788
+ this.generateYouTubeFeed(container, template, cachedData.value, allowShorts, language);
789
+ return;
790
+ } catch (e) {
791
+ // Ignorer les erreurs de parsing
792
+ }
793
+ }
794
+
795
+ container.innerHTML = `<div style="padding: 20px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; color: #dc2626;"><strong>Erreur de chargement</strong><br>${error.message}</div>`;
796
+ });
797
+ },
798
+
799
+ generateYouTubeFeed: function(container, template, data, allowShorts, language = 'fr') {
800
+ if (!data || !data.items || data.items.length === 0) {
801
+ container.innerHTML = '<div style="padding: 20px; text-align: center; color: #6b7280;">Aucune vidéo trouvée</div>';
802
+ return;
803
+ }
804
+
805
+ // Les vidéos sont déjà filtrées par l'API YouTube selon allowShorts
806
+ let videos = data.items;
807
+ // Vidéos reçues de l'API
808
+
809
+ // Vider le conteneur (en préservant les éléments marquee)
810
+ const marqueeElements = container.querySelectorAll('[data-bb-marquee-processed]');
811
+ container.innerHTML = '';
812
+
813
+ // Restaurer les éléments marquee si présents
814
+ marqueeElements.forEach(marqueeEl => {
815
+ container.appendChild(marqueeEl);
816
+ });
817
+
818
+ // Cloner le template pour chaque vidéo
819
+ videos.forEach(item => {
820
+ const videoId = item.id.videoId;
821
+ const snippet = item.snippet;
822
+
823
+ // Cloner le template
824
+ const clone = template.cloneNode(true);
825
+ clone.style.display = ''; // Rendre visible
826
+
827
+ // Remplir les données
828
+ this.fillVideoData(clone, videoId, snippet, language);
829
+
830
+ // Ajouter au conteneur
831
+ container.appendChild(clone);
832
+ });
833
+
834
+ // YouTube Feed généré
835
+ },
836
+
837
+ fillVideoData: function(element, videoId, snippet, language = 'fr') {
838
+ // Remplir le lien directement sur l'élément (link block)
839
+ if (element.tagName === 'A' || element.hasAttribute('bb-youtube-item')) {
840
+ element.href = `https://www.youtube.com/watch?v=${videoId}`;
841
+ element.target = '_blank';
842
+ element.rel = 'noopener noreferrer';
843
+ }
844
+
845
+ // Remplir la thumbnail (qualité optimisée)
846
+ const thumbnail = element.querySelector('[bb-youtube-thumbnail]');
847
+ if (thumbnail) {
848
+ // Logique optimisée pour la meilleure qualité disponible
849
+ let bestThumbnailUrl = null;
850
+ let bestQuality = 'unknown';
851
+
852
+ // Priorité 1: maxres (1280x720) - qualité maximale
853
+ if (snippet.thumbnails.maxres?.url) {
854
+ bestThumbnailUrl = snippet.thumbnails.maxres.url;
855
+ bestQuality = 'maxres (1280x720)';
856
+ }
857
+ // Priorité 2: high (480x360) - bonne qualité pour l'affichage
858
+ else if (snippet.thumbnails.high?.url) {
859
+ bestThumbnailUrl = snippet.thumbnails.high.url;
860
+ bestQuality = 'high (480x360)';
861
+ }
862
+ // Priorité 3: medium (320x180) - qualité acceptable en dernier recours
863
+ else if (snippet.thumbnails.medium?.url) {
864
+ bestThumbnailUrl = snippet.thumbnails.medium.url;
865
+ bestQuality = 'medium (320x180)';
866
+ }
867
+ // Fallback: default (120x90) - seulement si rien d'autre
868
+ else if (snippet.thumbnails.default?.url) {
869
+ bestThumbnailUrl = snippet.thumbnails.default.url;
870
+ bestQuality = 'default (120x90)';
871
+ }
872
+
873
+ // Appliquer la meilleure thumbnail trouvée
874
+ if (bestThumbnailUrl) {
875
+ thumbnail.src = bestThumbnailUrl;
876
+ thumbnail.alt = snippet.title;
877
+
878
+ // Debug: logger la qualité utilisée (en mode debug seulement)
879
+ if (bbContents.config.debug) {
880
+ // Thumbnail optimisée
881
+ }
882
+ } else {
883
+ // Aucune thumbnail disponible
884
+ }
885
+ }
886
+
887
+ // Remplir le titre (avec décodage HTML)
888
+ const title = element.querySelector('[bb-youtube-title]');
889
+ if (title) {
890
+ title.textContent = this.decodeHtmlEntities(snippet.title);
891
+ }
892
+
893
+ // Remplir la description (avec décodage HTML)
894
+ const description = element.querySelector('[bb-youtube-description]');
895
+ if (description) {
896
+ description.textContent = this.decodeHtmlEntities(snippet.description);
897
+ }
898
+
899
+ // Remplir la date
900
+ const date = element.querySelector('[bb-youtube-date]');
901
+ if (date) {
902
+ date.textContent = this.formatDate(snippet.publishedAt, language);
903
+ }
904
+
905
+ // Remplir le nom de la chaîne
906
+ const channel = element.querySelector('[bb-youtube-channel]');
907
+ if (channel) {
908
+ channel.textContent = snippet.channelTitle;
909
+ }
910
+ },
911
+
912
+ formatDate: function(dateString, language = 'fr') {
913
+ const date = new Date(dateString);
914
+ const now = new Date();
915
+ const diffTime = Math.abs(now - date);
916
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
917
+
918
+ // Traductions
919
+ const translations = {
920
+ fr: {
921
+ day: 'jour',
922
+ days: 'jours',
923
+ week: 'semaine',
924
+ weeks: 'semaines',
925
+ month: 'mois',
926
+ months: 'mois',
927
+ year: 'an',
928
+ years: 'ans',
929
+ ago: 'Il y a'
930
+ },
931
+ en: {
932
+ day: 'day',
933
+ days: 'days',
934
+ week: 'week',
935
+ weeks: 'weeks',
936
+ month: 'month',
937
+ months: 'months',
938
+ year: 'year',
939
+ years: 'years',
940
+ ago: 'ago'
941
+ }
942
+ };
943
+
944
+ const t = translations[language] || translations.fr;
945
+
946
+ if (diffDays === 1) return `${t.ago} 1 ${t.day}`;
947
+ if (diffDays < 7) return `${t.ago} ${diffDays} ${t.days}`;
948
+
949
+ const weeks = Math.floor(diffDays / 7);
950
+ if (weeks === 1) return `${t.ago} 1 ${t.week}`;
951
+ if (diffDays < 30) return `${t.ago} ${weeks} ${t.weeks}`;
952
+
953
+ const months = Math.floor(diffDays / 30);
954
+ if (months === 1) return `${t.ago} 1 ${t.month}`;
955
+ if (diffDays < 365) return `${t.ago} ${months} ${t.months}`;
956
+
957
+ const years = Math.floor(diffDays / 365);
958
+ if (years === 1) return `${t.ago} 1 ${t.year}`;
959
+ return `${t.ago} ${years} ${t.years}`;
960
+ },
961
+
962
+ // Fonction pour décoder les entités HTML
963
+ decodeHtmlEntities: function(text) {
964
+ if (!text) return '';
965
+ const textarea = document.createElement('textarea');
966
+ textarea.innerHTML = text;
967
+ return textarea.value;
968
+ },
969
+
970
+ // Nettoyer le cache expiré
971
+ cleanCache: function() {
972
+ try {
973
+ const keys = Object.keys(localStorage);
974
+ const now = Date.now();
975
+ let cleaned = 0;
976
+
977
+ keys.forEach(key => {
978
+ if (key.startsWith('youtube_')) {
979
+ try {
980
+ const cached = JSON.parse(localStorage.getItem(key));
981
+ if (now - cached.timestamp > 24 * 60 * 60 * 1000) {
982
+ localStorage.removeItem(key);
983
+ cleaned++;
984
+ }
985
+ } catch (e) {
986
+ // Supprimer les clés corrompues
987
+ localStorage.removeItem(key);
988
+ cleaned++;
989
+ }
990
+ }
991
+ });
992
+
993
+ if (cleaned > 0) {
994
+ // Cache YouTube nettoyé
995
+ }
996
+ } catch (e) {
997
+ // Ignorer les erreurs de nettoyage
998
+ }
999
+ }
794
1000
  }
795
1001
  };
796
1002
 
797
-
798
-
799
1003
  // Exposer globalement
800
1004
  window.bbContents = bbContents;
1005
+
1006
+ // Méthode globale pour configurer YouTube après le chargement
1007
+ window.configureYouTube = function(endpoint) {
1008
+ if (bbContents) {
1009
+ bbContents.config.youtubeEndpoint = endpoint;
1010
+ // Réinitialiser les modules YouTube
1011
+ bbContents.reinit();
1012
+ }
1013
+ };
801
1014
 
802
1015
  // Initialisation automatique avec délai pour éviter le blocage
803
1016
  function initBBContents() {
@@ -805,24 +1018,45 @@
805
1018
  if (document.readyState === 'loading') {
806
1019
  document.addEventListener('DOMContentLoaded', function() {
807
1020
  // Délai pour éviter le blocage du rendu
1021
+ const delay = document.body.hasAttribute('bb-performance-boost') ? 300 : 100;
808
1022
  setTimeout(function() {
809
1023
  bbContents.init();
810
- }, 100);
1024
+ }, delay);
811
1025
  });
812
1026
  } else {
813
1027
  // Délai pour éviter le blocage du rendu
1028
+ const delay = document.body.hasAttribute('bb-performance-boost') ? 300 : 100;
814
1029
  setTimeout(function() {
815
1030
  bbContents.init();
816
- }, 100);
1031
+ }, delay);
817
1032
  }
1033
+
1034
+ // Initialisation différée supplémentaire pour les cas difficiles - Option 1: Attendre que tout soit vraiment prêt
1035
+ window.addEventListener('load', function() {
1036
+ const loadDelay = document.body.hasAttribute('bb-performance-boost') ? 3000 : 1500; // Délais plus longs
1037
+ setTimeout(function() {
1038
+ // Vérifier s'il y a des éléments non initialisés
1039
+ const unprocessedMarquees = document.querySelectorAll('[bb-marquee]:not([data-bb-marquee-processed])');
1040
+ if (unprocessedMarquees.length > 0) {
1041
+ // Éléments marquee non initialisés détectés après load, réinitialisation
1042
+ bbContents.reinit();
1043
+ }
1044
+
1045
+ // Vérification supplémentaire des images chargées
1046
+ const allImages = document.querySelectorAll('img');
1047
+ const unloadedImages = Array.from(allImages).filter(img => !img.complete || img.naturalHeight === 0);
1048
+ if (unloadedImages.length > 0) {
1049
+ // Images non chargées détectées, attente supplémentaire
1050
+ setTimeout(() => {
1051
+ bbContents.reinit();
1052
+ }, 1000);
1053
+ }
1054
+ }, loadDelay);
1055
+ });
818
1056
  }
819
1057
 
820
1058
  // Initialisation
821
1059
  initBBContents();
822
1060
 
823
- // Message de confirmation
824
- console.log(
825
- '%cBeBranded Contents v' + config.version + ' chargé avec succès !',
826
- 'color: #422eff; font-weight: bold; font-size: 14px;'
827
- );
1061
+ // Message de confirmation supprimé pour une console plus propre
828
1062
  })();