@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.
- package/README.md +36 -0
- package/bb-contents.js +806 -0
- 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
|
+
}
|