nodebb-plugin-ezoic-infinite 1.6.54 → 1.6.56

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.54",
3
+ "version": "1.6.56",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -1,32 +1,39 @@
1
1
  (function () {
2
2
  'use strict';
3
3
 
4
+ // --- 1. VARIABLES ET CONFIG ---
4
5
  const WRAP_CLASS = 'nodebb-ezoic-wrap';
5
6
  const PINNED_ATTR = 'data-ezoic-pinned';
6
- const PROCESSED_ATTR = 'data-ezoic-seen'; // Nouveau : marqueur de sujet
7
7
  const PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
8
-
8
+
9
9
  let config = null;
10
10
  let usedIds = new Set();
11
11
  let scheduleTimer = null;
12
12
 
13
+ // --- 2. SÉLECTEURS DE THÈMES ---
13
14
  function getTopicList() {
14
- return document.querySelector('[component="topic/list"], [component="category"], [component="categories"], .topic-list');
15
+ return document.querySelector('.category [component="category"], .categories [component="categories"], [component="topic/list"], .topic-list');
16
+ }
17
+
18
+ function getPostList() {
19
+ return document.querySelector('[component="topic"], [component="post/list"]');
15
20
  }
16
21
 
17
- function getNextId(pool) {
18
- if (!pool) return null;
19
- const ids = pool.split(/[\s,]+/).filter(Boolean);
22
+ // --- 3. LOGIQUE DES POOLS D'ID ---
23
+ function getNextId(poolStr) {
24
+ if (!poolStr) return null;
25
+ const ids = poolStr.split(/[\s,]+/).filter(Boolean);
20
26
  for (const id of ids) {
21
- const n = id.replace(/[^\d]/g, '');
22
- if (n && !usedIds.has(n)) {
23
- usedIds.add(n);
24
- return n;
27
+ const num = id.replace(/[^\d]/g, '');
28
+ if (num && !usedIds.has(num)) {
29
+ usedIds.add(num);
30
+ return num;
25
31
  }
26
32
  }
27
33
  return null;
28
34
  }
29
35
 
36
+ // --- 4. APPEL EZOIC (REFRESH & SHOW) ---
30
37
  function callEzoic(id) {
31
38
  if (window.ezstandalone) {
32
39
  try {
@@ -38,87 +45,119 @@
38
45
  } else {
39
46
  window.ezstandalone.refresh();
40
47
  }
41
- } catch (e) { console.warn('[ezoic] err', e); }
48
+ } catch (e) { console.warn('[ezoic] refresh error', id, e); }
42
49
  }
43
50
  }
44
51
 
52
+ // --- 5. INJECTION SÉCURISÉE (ANTI-PILEUP) ---
45
53
  function inject() {
46
54
  if (!config || config.excluded) return;
47
- const ul = getTopicList();
48
- if (!ul) return;
49
55
 
50
- // 1. On récupère tous les LI qui ne sont pas des pubs
51
- const items = Array.from(ul.children).filter(c => c.tagName === 'LI' && !c.classList.contains(WRAP_CLASS));
52
- if (!items.length) return;
56
+ // A. LISTE DES SUJETS (Topics)
57
+ const ul = getTopicList();
58
+ if (ul && config.enableBetweenAds) {
59
+ // Nettoyage des pubs orphelines (celles qui n'ont plus de LI au dessus à cause du scroll up)
60
+ const wraps = ul.querySelectorAll('.' + WRAP_CLASS);
61
+ wraps.forEach(w => {
62
+ if (!w.previousElementSibling || w.previousElementSibling.tagName !== 'LI') w.remove();
63
+ });
64
+
65
+ const items = Array.from(ul.children).filter(c => c.tagName === 'LI' && !c.classList.contains(WRAP_CLASS));
66
+ if (items.length > 0) {
67
+ // Pub n°1 (Pinned)
68
+ if (config.showFirstTopicAd && !ul.querySelector(`[${PINNED_ATTR}="true"]`)) {
69
+ insertAd(items[0], getNextId(config.placeholderIds), true);
70
+ }
53
71
 
54
- // 2. PUB N°1 (Après le 1er sujet)
55
- if (config.showFirstTopicAd && !ul.querySelector(`[${PINNED_ATTR}="true"]`)) {
56
- items[0].setAttribute(PROCESSED_ATTR, 'true');
57
- insertAd(items[0], getNextId(config.placeholderIds), true);
72
+ // Pubs d'intervalles
73
+ const interval = parseInt(config.intervalPosts, 10) || 5;
74
+ items.forEach((li, idx) => {
75
+ const pos = idx + 1;
76
+ if (pos > 1 && pos % interval === 0) {
77
+ // Vérifie si ce LI a déjà une pub juste après lui pour éviter les doublons
78
+ const next = li.nextElementSibling;
79
+ if (!next || !next.classList.contains(WRAP_CLASS)) {
80
+ insertAd(li, getNextId(config.placeholderIds), false);
81
+ }
82
+ }
83
+ });
84
+ }
58
85
  }
59
86
 
60
- // 3. PUBS SUIVANTES (Intervalle)
61
- const interval = parseInt(config.intervalPosts, 10) || 5;
62
-
63
- // On ne traite que les éléments qui n'ont pas encore été "vus" par le script
64
- items.forEach((li, idx) => {
65
- const positionAbsolue = idx + 1;
87
+ // B. LISTE DES MESSAGES (Posts)
88
+ const postList = getPostList();
89
+ if (postList && config.enableMessageAds) {
90
+ const posts = Array.from(postList.querySelectorAll('[component="post"]'));
91
+ if (config.showFirstMessageAd && posts.length > 0 && !postList.querySelector('.ezoic-msg-first')) {
92
+ insertAd(posts[0], getNextId(config.messagePlaceholderIds), false, 'ezoic-msg-first');
93
+ }
66
94
 
67
- // Si on tombe sur un multiple de l'intervalle ET que le LI n'a pas déjà été marqué
68
- if (positionAbsolue > 1 && positionAbsolue % interval === 0) {
69
- // Vérification anti-doublon : est-ce que ce LI a déjà une pub juste après lui ?
70
- const hasAdAfter = li.nextElementSibling && li.nextElementSibling.classList.contains(WRAP_CLASS);
71
-
72
- if (!hasAdAfter) {
73
- insertAd(li, getNextId(config.placeholderIds), false);
74
- }
95
+ const msgInterval = parseInt(config.messageIntervalPosts, 10) || 5;
96
+ if (msgInterval > 0) {
97
+ posts.forEach((post, idx) => {
98
+ const pPos = idx + 1;
99
+ if (pPos > 1 && pPos % msgInterval === 0) {
100
+ if (!post.nextElementSibling || !post.nextElementSibling.classList.contains(WRAP_CLASS)) {
101
+ insertAd(post, getNextId(config.messagePlaceholderIds), false);
102
+ }
103
+ }
104
+ });
75
105
  }
76
- });
106
+ }
77
107
  }
78
108
 
79
- function insertAd(target, id, isPinned) {
80
- if (!id || !target) return;
81
-
109
+ function insertAd(target, id, isPinned, extraClass = '') {
110
+ if (!target || !id) return;
82
111
  const wrap = document.createElement('div');
83
- wrap.className = WRAP_CLASS + ' ezoic-ad-between';
112
+ wrap.className = `${WRAP_CLASS} ezoic-ad-between ${extraClass}`;
84
113
  if (isPinned) wrap.setAttribute(PINNED_ATTR, 'true');
85
114
 
86
115
  const placeholder = document.createElement('div');
87
116
  placeholder.id = PLACEHOLDER_PREFIX + id;
88
117
  wrap.appendChild(placeholder);
89
118
 
90
- // Insertion physique
91
119
  target.after(wrap);
92
120
 
93
- // On force un petit délai pour laisser Ezoic voir le nouveau div dans le DOM
121
+ // On laisse le DOM respirer 50ms avant d'appeler Ezoic
94
122
  setTimeout(() => callEzoic(id), 50);
95
123
  }
96
124
 
97
- async function init() {
125
+ // --- 6. INITIALISATION ET HOOKS ---
126
+ async function fetchConfig() {
98
127
  try {
99
128
  const res = await fetch('/api/plugins/ezoic-infinite/config');
100
129
  config = await res.json();
130
+ } catch (e) { console.error('[ezoic] config fetch failed'); }
131
+ }
101
132
 
102
- // Premier passage
103
- inject();
133
+ function schedule() {
134
+ if (scheduleTimer) clearTimeout(scheduleTimer);
135
+ scheduleTimer = setTimeout(inject, 250);
136
+ }
104
137
 
105
- // Observation du DOM (pour le scroll infini)
106
- const ul = getTopicList();
107
- if (ul && typeof MutationObserver !== 'undefined') {
108
- const mo = new MutationObserver(() => {
109
- if (scheduleTimer) clearTimeout(scheduleTimer);
110
- scheduleTimer = setTimeout(inject, 300);
111
- });
112
- mo.observe(ul, { childList: true });
113
- }
138
+ function init() {
139
+ fetchConfig().then(() => {
140
+ schedule();
141
+
142
+ // MutationObserver pour le scroll infini
143
+ const body = document.body;
144
+ const mo = new MutationObserver((mutations) => {
145
+ for (let m of mutations) {
146
+ if (m.addedNodes.length > 0) {
147
+ schedule();
148
+ break;
149
+ }
150
+ }
151
+ });
152
+ mo.observe(body, { childList: true, subtree: true });
153
+ });
114
154
 
115
- // Hooks NodeBB
116
- if (window.jQuery) {
117
- window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', () => {
118
- setTimeout(inject, 500);
119
- });
120
- }
121
- } catch (e) { console.error('[ezoic] init fail', e); }
155
+ // Compatibilité jQuery/NodeBB Events
156
+ if (window.jQuery) {
157
+ window.jQuery(window).on('action:ajaxify.end action:infiniteScroll.loaded', () => {
158
+ setTimeout(schedule, 500);
159
+ });
160
+ }
122
161
  }
123
162
 
124
163
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
package/public/style.css CHANGED
@@ -1,100 +1,67 @@
1
- /*
2
- Keep our NodeBB-inserted wrappers CLS-safe.
3
- NOTE: must not rely on `.ezoic-ad` because Ezoic uses that class internally.
4
- */
1
+ /* --- Conteneur principal de la publicité --- */
5
2
  .nodebb-ezoic-wrap {
6
- display: block;
7
- width: 100%;
8
- margin: 0 !important;
9
- padding: 0 !important;
10
- overflow: hidden;
3
+ display: block !important;
4
+ width: 100% !important;
5
+ clear: both !important;
6
+ margin: 20px 0 !important;
7
+ padding: 0 !important;
8
+ /* Empêche les débordements visuels lors du recyclage du DOM */
9
+ overflow: hidden;
10
+ /* Améliore les performances de rendu sur NodeBB 4.x */
11
+ contain: layout style;
11
12
  }
12
13
 
13
- .nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] {
14
- margin: 0 !important;
15
- padding: 0 !important;
16
- /* Keep the placeholder measurable (IO) but visually negligible */
17
- min-height: 1px;
14
+ /* --- Protection du premier emplacement (Pinned) --- */
15
+ .nodebb-ezoic-wrap[data-ezoic-pinned="true"] {
16
+ /* On réserve une hauteur minimale pour éviter que le contenu
17
+ ne saute violemment quand la pub charge au scroll up */
18
+ min-height: 120px;
19
+ border-bottom: 1px solid rgba(0,0,0,0.05);
20
+ margin-top: 0 !important;
18
21
  }
19
22
 
20
- /* If Ezoic wraps inside our wrapper, keep it tight */
21
- .nodebb-ezoic-wrap span.ezoic-ad,
22
- .nodebb-ezoic-wrap .ezoic-ad {
23
- margin: 0 !important;
24
- padding: 0 !important;
23
+ /* --- Le placeholder Ezoic interne --- */
24
+ [id^="ezoic-pub-ad-placeholder-"] {
25
+ margin: 0 auto !important;
26
+ padding: 0 !important;
27
+ text-align: center;
28
+ min-height: 1px;
29
+ line-height: 0;
25
30
  }
26
31
 
27
- /* Remove the classic "gap under iframe" (baseline/inline-block) */
28
- .nodebb-ezoic-wrap,
29
- .nodebb-ezoic-wrap * {
30
- line-height: 0 !important;
31
- font-size: 0 !important;
32
- }
33
-
34
- .nodebb-ezoic-wrap iframe,
35
- .nodebb-ezoic-wrap div[id$="__container__"] iframe {
36
- display: block !important;
37
- vertical-align: top !important;
38
- }
39
-
40
- .nodebb-ezoic-wrap div[id$="__container__"] {
41
- display: block !important;
42
- line-height: 0 !important;
43
- }
44
-
45
-
46
- /* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
47
- .nodebb-ezoic-wrap.is-empty {
48
- display: block !important;
49
- margin: 0 !important;
50
- padding: 0 !important;
51
- /* Don't fully collapse (can prevent fill / triggers "unused"), keep it at 1px */
52
- height: 1px !important;
53
- min-height: 1px !important;
54
- overflow: hidden !important;
32
+ /* --- Nettoyage des éléments Ezoic internes --- */
33
+ .nodebb-ezoic-wrap span.ezoic-ad,
34
+ .nodebb-ezoic-wrap .ezoic-ad {
35
+ display: block !important;
36
+ margin: 0 auto !important;
37
+ /* Force Ezoic à respecter la largeur du flux forum */
38
+ max-width: 100% !important;
55
39
  }
56
40
 
57
- /*
58
- Optional: also neutralize spacing on native Ezoic `.ezoic-ad` blocks.
59
- (Keeps your previous "CSS very good" behavior.)
60
- */
61
- .ezoic-ad {
62
- margin: 0 !important;
63
- padding: 0 !important;
64
- }
65
- /* Remove Ezoic's large reserved min-height inside our wrappers (topics/messages) */
66
- .nodebb-ezoic-wrap .ezoic-ad,
67
- .nodebb-ezoic-wrap span.ezoic-ad {
68
- min-height: 1px !important; /* kill 400px gaps */
69
- height: auto !important;
41
+ /* --- Gestion des blocs vides (Anti-Holes) --- */
42
+ /* Si Ezoic ne renvoie rien ou si le bloc est vide,
43
+ on réduit l'espace pour ne pas avoir de gros trous blancs */
44
+ .nodebb-ezoic-wrap:empty {
45
+ height: 0 !important;
46
+ min-height: 0 !important;
47
+ margin: 0 !important;
70
48
  }
71
49
 
72
- /* Ensure Ezoic reportline doesn't affect layout */
73
- .nodebb-ezoic-wrap .reportline{position:absolute!important;}
74
-
75
- /* Ezoic sometimes injects `position: sticky` inside placements. In long NodeBB topics,
76
- this can create "gliding" and sudden disappear/reappear effects while scrolling.
77
- We neutralize sticky positioning *inside our injected wrappers* only. */
78
- .nodebb-ezoic-wrap .ezads-sticky-intradiv {
79
- position: static !important;
80
- top: auto !important;
50
+ /* --- Adaptation spécifique pour les Messages (Sujets ouverts) --- */
51
+ .ezoic-msg-first {
52
+ margin-bottom: 30px !important;
53
+ border-bottom: 1px solid var(--border-color, #eee);
54
+ padding-bottom: 10px !important;
81
55
  }
82
56
 
83
-
84
- /* ===== V17 host styling ===== */
85
- li.nodebb-ezoic-host { list-style: none; width: 100%; display: block; }
86
- li.nodebb-ezoic-host > .nodebb-ezoic-wrap.ezoic-ad-between { width: 100%; display: block; }
87
- /* ===== /V17 ===== */
88
-
89
- .nodebb-ezoic-wrap {
90
- display: block;
91
- width: 100%;
92
- clear: both;
93
- margin: 20px 0 !important; /* Ajoute un peu d'espace pour le visuel */
94
- min-height: 50px; /* Évite que le bloc ne soit écrasé à 0px */
57
+ /* --- Support du mode sombre (NodeBB 4 Harmony) --- */
58
+ [data-theme="dark"] .nodebb-ezoic-wrap[data-ezoic-pinned="true"] {
59
+ border-bottom-color: rgba(255,255,255,0.1);
95
60
  }
96
61
 
97
- /* Si la pub est vide, on garde un petit espace pour éviter le saut au scroll up */
98
- .nodebb-ezoic-wrap:empty {
99
- min-height: 1px;
62
+ /* --- Neutralisation des marges forcées par Ezoic sur mobile --- */
63
+ @media (max-width: 767px) {
64
+ .nodebb-ezoic-wrap {
65
+ margin: 10px 0 !important;
66
+ }
100
67
  }