@bebranded/bb-contents 1.0.0

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 (3) hide show
  1. package/README.md +36 -0
  2. package/bb-contents.js +806 -0
  3. package/package.json +24 -0
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # BeBranded Contents
2
+
3
+ Contenus additionnels pour Webflow. Une bibliothèque JavaScript open source qui ajoute des fonctionnalités prêtes à l’emploi via de simples attributs HTML (`data-bb-*`). Aucune configuration complexe, un seul script à inclure.
4
+
5
+ ## Getting started
6
+
7
+ Merci de suivre la documentation sur notre site pour l’intégration détaillée et les exemples:
8
+ - https://www.bebranded.xyz/contents
9
+
10
+ ## Installation rapide
11
+
12
+ ### CDN (recommandé)
13
+ ```html
14
+ <script src="https://cdn.jsdelivr.net/npm/bb-contents@1/bb-contents.js"></script>
15
+ ```
16
+
17
+ ### NPM
18
+ ```bash
19
+ npm install bb-contents@^1
20
+ ```
21
+
22
+ ## Questions
23
+
24
+ Besoin d’aide ou d’un conseil d’intégration ?
25
+ - Email: hello@bebranded.xyz
26
+ - Issues GitHub: bugs et demandes de fonctionnalités uniquement
27
+
28
+ ## Contribution
29
+
30
+ Les contributions sont bienvenues. Ouvrez une issue pour discuter d’une amélioration, ou proposez directement une pull request.
31
+
32
+ ## Licence
33
+
34
+ MIT
35
+
36
+ © 2025 BeBranded
package/bb-contents.js ADDED
@@ -0,0 +1,806 @@
1
+ /**
2
+ * BeBranded Contents
3
+ * Contenus additionnels français pour Webflow
4
+ * @version 1.0.0
5
+ * @author BeBranded
6
+ * @license MIT
7
+ * @website https://www.bebranded.xyz
8
+ */
9
+ (function() {
10
+ 'use strict';
11
+
12
+ // Protection contre le double chargement
13
+ if (window.bbContents) {
14
+ console.warn('BeBranded Contents est déjà chargé');
15
+ return;
16
+ }
17
+
18
+ // Configuration
19
+ const config = {
20
+ version: '1.0.0',
21
+ debug: window.location.hostname === 'localhost' || window.location.hostname.includes('webflow.io'),
22
+ prefix: 'bb-', // utilisé pour générer les sélecteurs (data-bb-*)
23
+ i18n: {
24
+ copied: 'Lien copié !'
25
+ }
26
+ };
27
+
28
+ // Objet principal
29
+ const bbContents = {
30
+ config: config,
31
+ modules: {},
32
+ _observer: null,
33
+ _reinitScheduled: false,
34
+
35
+ // Utilitaires
36
+ utils: {
37
+ log: function(...args) {
38
+ if (config.debug) {
39
+ console.log('[BB Contents]', ...args);
40
+ }
41
+ },
42
+
43
+ // Protection XSS
44
+ sanitize: function(str) {
45
+ if (typeof str !== 'string') return '';
46
+ const div = document.createElement('div');
47
+ div.textContent = str;
48
+ return div.innerHTML;
49
+ },
50
+
51
+ // Validation des URLs
52
+ isValidUrl: function(string) {
53
+ try {
54
+ new URL(string);
55
+ return true;
56
+ } catch (_) {
57
+ return false;
58
+ }
59
+ }
60
+ },
61
+
62
+ // Helper: construire des sélecteurs d’attributs selon le prefix
63
+ _attrSelector: function(name) {
64
+ const p = (this.config.prefix || 'bb-').replace(/-?$/, '-');
65
+ const legacy = name.startsWith('bb-') ? name : (p + name);
66
+ const dataName = 'data-' + legacy.replace(/^bb-/, 'bb-');
67
+ return '[' + legacy + '], [' + dataName + ']';
68
+ },
69
+
70
+ // Helper: lire un attribut avec compat data-bb-*
71
+ _getAttr: function(element, name) {
72
+ const p = (this.config.prefix || 'bb-').replace(/-?$/, '-');
73
+ const legacy = name.startsWith('bb-') ? name : (p + name);
74
+ return element.getAttribute(legacy) || element.getAttribute('data-' + legacy);
75
+ },
76
+
77
+ // Initialisation
78
+ init: function() {
79
+ this.utils.log('Initialisation v' + this.config.version);
80
+
81
+ // Déterminer la portée
82
+ const scope = document.querySelector('[data-bb-scope]') || document;
83
+
84
+ // Initialiser seulement les modules qui ont des attributs sur la page courante
85
+ Object.keys(this.modules).forEach(function(moduleName) {
86
+ const module = bbContents.modules[moduleName];
87
+ if (module.detect && module.detect(scope)) {
88
+ bbContents.utils.log('Module détecté:', moduleName);
89
+ try {
90
+ module.init(scope);
91
+ } catch (error) {
92
+ console.error('[BB Contents] Erreur dans le module', moduleName, error);
93
+ // Continuer avec les autres modules même si un échoue
94
+ }
95
+ }
96
+ });
97
+
98
+ // Activer l'observer DOM pour contenu dynamique
99
+ this.setupObserver();
100
+ },
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;
107
+
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
111
+ }
112
+ this._lastReinitTime = Date.now();
113
+
114
+ Object.keys(this.modules).forEach(function(moduleName) {
115
+ 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
+ }
147
+ }
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
+ });
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
+
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
+ };
236
+
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;
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é !'));
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
+ }
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);
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);
356
+ }
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
+
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;
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;
433
+
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;
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;
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;
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();
553
+
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) {
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;
583
+
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
+
592
+ // Sauvegarder le contenu original
593
+ const originalHTML = element.innerHTML;
594
+
595
+ // Créer le conteneur principal
596
+ const mainContainer = document.createElement('div');
597
+ const isVertical = orientation === 'vertical';
598
+ mainContainer.style.cssText = `
599
+ position: relative;
600
+ width: 100%;
601
+ height: ${isVertical ? height + 'px' : 'auto'};
602
+ overflow: hidden;
603
+ min-height: ${isVertical ? '100px' : '50px'};
604
+ `;
605
+
606
+ // Créer le conteneur de défilement
607
+ const scrollContainer = document.createElement('div');
608
+ scrollContainer.style.cssText = `
609
+ position: absolute;
610
+ will-change: transform;
611
+ height: 100%;
612
+ top: 0px;
613
+ left: 0px;
614
+ display: flex;
615
+ ${isVertical ? 'flex-direction: column;' : ''}
616
+ align-items: center;
617
+ gap: ${gap}px;
618
+ ${isVertical ? '' : 'white-space: nowrap;'}
619
+ flex-shrink: 0;
620
+ `;
621
+
622
+ // Créer le bloc de contenu principal
623
+ const mainBlock = document.createElement('div');
624
+ mainBlock.innerHTML = originalHTML;
625
+ mainBlock.style.cssText = `
626
+ display: flex;
627
+ ${isVertical ? 'flex-direction: column;' : ''}
628
+ align-items: center;
629
+ gap: ${gap}px;
630
+ ${isVertical ? '' : 'white-space: nowrap;'}
631
+ flex-shrink: 0;
632
+ ${isVertical ? 'min-height: 100px;' : ''}
633
+ `;
634
+
635
+ // Créer plusieurs répétitions pour un défilement continu
636
+ const repeatBlock1 = mainBlock.cloneNode(true);
637
+ const repeatBlock2 = mainBlock.cloneNode(true);
638
+ const repeatBlock3 = mainBlock.cloneNode(true);
639
+
640
+ // Assembler la structure
641
+ scrollContainer.appendChild(mainBlock);
642
+ scrollContainer.appendChild(repeatBlock1);
643
+ scrollContainer.appendChild(repeatBlock2);
644
+ scrollContainer.appendChild(repeatBlock3);
645
+ mainContainer.appendChild(scrollContainer);
646
+
647
+ // Vider et remplacer le contenu original
648
+ element.innerHTML = '';
649
+ element.appendChild(mainContainer);
650
+
651
+ // Fonction pour initialiser l'animation
652
+ const initAnimation = () => {
653
+ // Attendre que le contenu soit dans le DOM
654
+ requestAnimationFrame(() => {
655
+ const contentWidth = mainBlock.offsetWidth;
656
+ const contentHeight = mainBlock.offsetHeight;
657
+
658
+ // Debug
659
+ bbContents.utils.log('Debug - Largeur du contenu:', contentWidth, 'px', 'Hauteur:', contentHeight, 'px', 'Enfants:', mainBlock.children.length, 'Vertical:', isVertical, 'Direction:', direction);
660
+
661
+ // Si pas de contenu, réessayer
662
+ if ((isVertical && contentHeight === 0) || (!isVertical && contentWidth === 0)) {
663
+ bbContents.utils.log('Contenu non prêt, nouvelle tentative dans 200ms');
664
+ setTimeout(initAnimation, 200);
665
+ return;
666
+ }
667
+
668
+ // Pour le vertical, s'assurer qu'on a une hauteur minimale
669
+ if (isVertical && contentHeight < 50) {
670
+ bbContents.utils.log('Hauteur insuffisante pour le marquee vertical (' + contentHeight + 'px), nouvelle tentative dans 200ms');
671
+ setTimeout(initAnimation, 200);
672
+ return;
673
+ }
674
+
675
+ if (isVertical) {
676
+ // Animation JavaScript pour le vertical
677
+ const contentSize = contentHeight;
678
+ const totalSize = contentSize * 4 + parseInt(gap) * 3; // 4 copies au lieu de 3
679
+ scrollContainer.style.height = totalSize + 'px';
680
+
681
+ let currentPosition = direction === 'bottom' ? -contentSize - parseInt(gap) : 0;
682
+ const step = (parseFloat(speed) * 2) / 60; // Vitesse différente
683
+ let isPaused = false;
684
+
685
+ // Fonction d'animation JavaScript
686
+ const animate = () => {
687
+ if (!isPaused) {
688
+ if (direction === 'bottom') {
689
+ currentPosition += step;
690
+ if (currentPosition >= 0) {
691
+ currentPosition = -contentSize - parseInt(gap);
692
+ }
693
+ } else {
694
+ currentPosition -= step;
695
+ if (currentPosition <= -contentSize - parseInt(gap)) {
696
+ currentPosition = 0;
697
+ }
698
+ }
699
+
700
+ scrollContainer.style.transform = `translate3d(0px, ${currentPosition}px, 0px)`;
701
+ }
702
+ requestAnimationFrame(animate);
703
+ };
704
+
705
+ // Démarrer l'animation
706
+ animate();
707
+
708
+ bbContents.utils.log('Marquee vertical créé avec animation JS - direction:', direction, 'taille:', contentSize + 'px', 'total:', totalSize + 'px', 'hauteur-wrapper:', height + 'px');
709
+
710
+ // Pause au survol
711
+ if (pauseOnHover === 'true') {
712
+ element.addEventListener('mouseenter', function() {
713
+ isPaused = true;
714
+ });
715
+ element.addEventListener('mouseleave', function() {
716
+ isPaused = false;
717
+ });
718
+ }
719
+ } else {
720
+ // Animation CSS pour l'horizontal (modifiée)
721
+ const contentSize = contentWidth;
722
+ const totalSize = contentSize * 4 + parseInt(gap) * 3; // 4 copies au lieu de 3
723
+ scrollContainer.style.width = totalSize + 'px';
724
+
725
+ // Créer l'animation CSS optimisée
726
+ const animationName = 'bb-scroll-' + Math.random().toString(36).substr(2, 9);
727
+ const animationDuration = (totalSize / (parseFloat(speed) * 1.5)).toFixed(2) + 's'; // Vitesse différente
728
+
729
+ // Animation avec translate3d pour hardware acceleration
730
+ let keyframes;
731
+ if (direction === 'right') {
732
+ keyframes = `@keyframes ${animationName} {
733
+ 0% { transform: translate3d(-${contentSize + parseInt(gap)}px, 0px, 0px); }
734
+ 100% { transform: translate3d(0px, 0px, 0px); }
735
+ }`;
736
+ } else {
737
+ // Direction 'left' par défaut
738
+ keyframes = `@keyframes ${animationName} {
739
+ 0% { transform: translate3d(0px, 0px, 0px); }
740
+ 100% { transform: translate3d(-${contentSize + parseInt(gap)}px, 0px, 0px); }
741
+ }`;
742
+ }
743
+
744
+ // Ajouter les styles
745
+ const style = document.createElement('style');
746
+ style.textContent = keyframes;
747
+ document.head.appendChild(style);
748
+
749
+ // Appliquer l'animation
750
+ scrollContainer.style.animation = `${animationName} ${animationDuration} linear infinite`;
751
+
752
+ bbContents.utils.log('Marquee horizontal créé:', animationName, 'durée:', animationDuration + 's', 'direction:', direction, 'taille:', contentSize + 'px', 'total:', totalSize + 'px');
753
+
754
+ // Pause au survol
755
+ if (pauseOnHover === 'true') {
756
+ element.addEventListener('mouseenter', function() {
757
+ scrollContainer.style.animationPlayState = 'paused';
758
+ });
759
+ element.addEventListener('mouseleave', function() {
760
+ scrollContainer.style.animationPlayState = 'running';
761
+ });
762
+ }
763
+ }
764
+ });
765
+ };
766
+
767
+ // Démarrer l'initialisation
768
+ setTimeout(initAnimation, isVertical ? 300 : 100);
769
+ });
770
+
771
+ bbContents.utils.log('Module Marquee initialisé:', elements.length, 'éléments');
772
+ }
773
+ };
774
+
775
+
776
+
777
+ // Exposer globalement
778
+ window.bbContents = bbContents;
779
+
780
+ // Initialisation automatique avec délai pour éviter le blocage
781
+ function initBBContents() {
782
+ // Attendre que la page soit prête
783
+ if (document.readyState === 'loading') {
784
+ document.addEventListener('DOMContentLoaded', function() {
785
+ // Délai pour éviter le blocage du rendu
786
+ setTimeout(function() {
787
+ bbContents.init();
788
+ }, 100);
789
+ });
790
+ } else {
791
+ // Délai pour éviter le blocage du rendu
792
+ setTimeout(function() {
793
+ bbContents.init();
794
+ }, 100);
795
+ }
796
+ }
797
+
798
+ // Initialisation
799
+ initBBContents();
800
+
801
+ // Message de confirmation
802
+ console.log(
803
+ '%cBeBranded Contents v' + config.version + ' chargé avec succès !',
804
+ 'color: #422eff; font-weight: bold; font-size: 14px;'
805
+ );
806
+ })();
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@bebranded/bb-contents",
3
+ "version": "1.0.0",
4
+ "description": "Contenus additionnels français pour Webflow",
5
+ "main": "bb-contents.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [],
10
+ "author": "BeBranded <hello@bebranded.xyz>",
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/BeBranded-xyz/bb-contents"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/BeBranded-xyz/bb-contents/issues"
18
+ },
19
+ "homepage": "https://www.bebranded.xyz/contents",
20
+ "files": [
21
+ "bb-contents.js",
22
+ "README.md"
23
+ ]
24
+ }