@bebranded/bb-contents 1.0.1-beta → 1.0.1

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