nodebb-plugin-ezoic-infinite 0.6.2 → 0.6.4

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/package.json +1 -1
  2. package/public/client.js +148 -87
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -5,13 +5,33 @@ window.ezoicInfiniteLoaded = true;
5
5
 
6
6
  let cachedConfig;
7
7
  let lastFetch = 0;
8
-
9
8
  let debounceTimer;
10
- let lastSignature = null;
11
9
 
12
10
  let inFlight = false;
13
11
  let rerunRequested = false;
14
12
 
13
+ // Per-page incremental state
14
+ let pageKey = null;
15
+ let injectedSlots = new Set(); // slot numbers already injected on this page
16
+ let usedIds = new Set(); // IDs currently present in DOM
17
+ let adsFIFO = []; // [{slot, id, selector}] in insertion order (oldest first)
18
+
19
+ function resetPageState() {
20
+ injectedSlots = new Set();
21
+ usedIds = new Set();
22
+ adsFIFO = [];
23
+ }
24
+
25
+ function currentPageKey() {
26
+ try {
27
+ if (ajaxify && ajaxify.data) {
28
+ if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
29
+ if (ajaxify.data.cid) return 'category:' + ajaxify.data.cid;
30
+ }
31
+ } catch (e) {}
32
+ return window.location.pathname;
33
+ }
34
+
15
35
  function parsePool(raw) {
16
36
  if (!raw) return [];
17
37
  return Array.from(new Set(
@@ -29,7 +49,6 @@ async function fetchConfig() {
29
49
  return cachedConfig;
30
50
  }
31
51
 
32
- // Robust DOM-based detection (no dependency on ajaxify template timing)
33
52
  function isTopicPage() {
34
53
  return $('[component="post/content"]').length > 0 || $('[component="post"][data-pid]').length > 0;
35
54
  }
@@ -42,7 +61,6 @@ function getTopicPosts() {
42
61
  const $primary = $('[component="post"][data-pid]');
43
62
  if ($primary.length) return $primary.not('.ezoic-ad-post');
44
63
 
45
- // Fallback: top-level data-pid containing post/content
46
64
  return $('[data-pid]').filter(function () {
47
65
  const $el = $(this);
48
66
  const hasContent = $el.find('[component="post/content"]').length > 0;
@@ -59,83 +77,26 @@ function tagName($el) {
59
77
  return ($el && $el.length ? (($el.prop('tagName') || '').toUpperCase()) : '');
60
78
  }
61
79
 
62
- function makeWrapperLike($target, classes, innerHtml) {
80
+ function makeWrapperLike($target, classes, innerHtml, attrs) {
63
81
  const t = tagName($target);
82
+ const attrStr = attrs ? ' ' + attrs : '';
64
83
  if (t === 'LI') {
65
- return '<li class="' + classes + ' list-unstyled" data-ezoic-ad="1">' + innerHtml + '</li>';
84
+ return '<li class="' + classes + ' list-unstyled"' + attrStr + '>' + innerHtml + '</li>';
66
85
  }
67
- return '<div class="' + classes + '" data-ezoic-ad="1">' + innerHtml + '</div>';
68
- }
69
-
70
- function computeWindowSlots(totalItems, interval, poolSize) {
71
- const slots = Math.floor(totalItems / interval);
72
- if (slots <= 0) return [];
73
- const start = Math.max(1, slots - poolSize + 1);
74
- const out = [];
75
- for (let s = start; s <= slots; s++) out.push(s);
76
- return out;
86
+ return '<div class="' + classes + '"' + attrStr + '>' + innerHtml + '</div>';
77
87
  }
78
88
 
79
- function removeOurWrappers() {
80
- $('.ezoic-ad-post').remove();
81
- $('.ezoic-ad-between').remove();
82
- $('.ezoic-ad-topic').remove();
89
+ function cleanupWrappersOnNav() {
90
+ $('.ezoic-ad-post, .ezoic-ad-between, .ezoic-ad-topic').remove();
83
91
  }
84
92
 
85
- function insertBetween($items, ids, interval, wrapperClass) {
86
- const total = $items.length;
87
- const slotsToRender = computeWindowSlots(total, interval, ids.length);
88
- if (!slotsToRender.length) return [];
89
-
90
- const activeIds = [];
91
- for (let i = 0; i < slotsToRender.length; i++) {
92
- const slotNumber = slotsToRender[i];
93
- const id = ids[i];
94
- const index = slotNumber * interval - 1;
95
- const $target = $items.eq(index);
96
- if (!$target.length) continue;
97
-
98
- const html = makeWrapperLike($target, wrapperClass, '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>');
99
- $target.after(html);
100
- activeIds.push(id);
93
+ function pickNextId(pool) {
94
+ for (const id of pool) {
95
+ if (!usedIds.has(id)) return id;
101
96
  }
102
- return activeIds;
97
+ return null;
103
98
  }
104
99
 
105
- function insertMessageAds($posts, ids, interval) {
106
- const total = $posts.length;
107
- const slotsToRender = computeWindowSlots(total, interval, ids.length);
108
- if (!slotsToRender.length) return [];
109
-
110
- const activeIds = [];
111
- for (let i = 0; i < slotsToRender.length; i++) {
112
- const slotNumber = slotsToRender[i];
113
- const id = ids[i];
114
- const index = slotNumber * interval - 1;
115
- const $target = $posts.eq(index);
116
- if (!$target.length) continue;
117
-
118
- const inner = '<div class="content"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
119
- const html = makeWrapperLike($target, 'post ezoic-ad-post', inner);
120
- $target.after(html);
121
- activeIds.push(id);
122
- }
123
- return activeIds;
124
- }
125
-
126
- function signatureFor($posts, $topicItems) {
127
- if ($posts.length) {
128
- const lastPid = $posts.last().attr('data-pid') || '';
129
- return 'topic:' + $posts.length + ':' + lastPid;
130
- }
131
- if ($topicItems.length) {
132
- const lastTid = $topicItems.last().attr('data-tid') || '';
133
- return 'category:' + $topicItems.length + ':' + lastTid;
134
- }
135
- return 'none';
136
- }
137
-
138
- // Call Ezoic when ready (retry a few times, non-destructive)
139
100
  function callEzoic(ids) {
140
101
  if (!ids || !ids.length) return;
141
102
 
@@ -152,10 +113,9 @@ function callEzoic(ids) {
152
113
  return false;
153
114
  };
154
115
 
155
- // Use cmd queue first (best if Ezoic loads later)
156
116
  window.ezstandalone.cmd.push(function () { run(); });
157
117
 
158
- // Also retry a few times in case cmd isn't flushed promptly
118
+ // Retry (Ezoic can load late)
159
119
  let tries = 0;
160
120
  const maxTries = 6;
161
121
  const timer = setInterval(function () {
@@ -164,7 +124,114 @@ function callEzoic(ids) {
164
124
  }, 800);
165
125
  }
166
126
 
127
+ function fifoEvictIfNeeded(poolSize) {
128
+ // Keep at most poolSize ads in DOM: remove oldest when we exceed pool capacity.
129
+ // This makes ads "follow" the scroll (new ads appear, oldest disappear) without jumping positions.
130
+ while (adsFIFO.length > poolSize) {
131
+ const old = adsFIFO.shift();
132
+ if (!old) break;
133
+
134
+ const $el = $(old.selector);
135
+ if ($el.length) $el.remove();
136
+
137
+ injectedSlots.delete(old.slot);
138
+ usedIds.delete(old.id);
139
+ }
140
+ }
141
+
142
+ function injectBetweenIncremental($items, pool, interval, wrapperClass) {
143
+ const total = $items.length;
144
+ const maxSlot = Math.floor(total / interval);
145
+ if (maxSlot <= 0) return [];
146
+
147
+ const newIds = [];
148
+
149
+ for (let slot = 1; slot <= maxSlot; slot++) {
150
+ if (injectedSlots.has(slot)) continue;
151
+
152
+ const index = slot * interval - 1;
153
+ const $target = $items.eq(index);
154
+ if (!$target.length) continue;
155
+
156
+ const id = pickNextId(pool);
157
+ if (!id) {
158
+ // No free IDs right now; stop. Oldest will be evicted once new ads can be created.
159
+ break;
160
+ }
161
+
162
+ const placeholder = '<div id="ezoic-pub-ad-placeholder-' + id + '"></div>';
163
+ const selector = '.ezoic-ad-topic[data-ezoic-slot="' + slot + '"][data-ezoic-id="' + id + '"]';
164
+ const html = makeWrapperLike(
165
+ $target,
166
+ wrapperClass,
167
+ placeholder,
168
+ 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"'
169
+ );
170
+
171
+ $target.after(html);
172
+
173
+ injectedSlots.add(slot);
174
+ usedIds.add(id);
175
+ adsFIFO.push({ slot: slot, id: id, selector: selector });
176
+
177
+ newIds.push(id);
178
+
179
+ // Enforce capacity immediately (prevents runaway and keeps ads near viewport)
180
+ fifoEvictIfNeeded(pool.length);
181
+ }
182
+
183
+ return newIds;
184
+ }
185
+
186
+ function injectMessageIncremental($posts, pool, interval) {
187
+ const total = $posts.length;
188
+ const maxSlot = Math.floor(total / interval);
189
+ if (maxSlot <= 0) return [];
190
+
191
+ const newIds = [];
192
+
193
+ for (let slot = 1; slot <= maxSlot; slot++) {
194
+ if (injectedSlots.has(slot)) continue;
195
+
196
+ const index = slot * interval - 1;
197
+ const $target = $posts.eq(index);
198
+ if (!$target.length) continue;
199
+
200
+ const id = pickNextId(pool);
201
+ if (!id) break;
202
+
203
+ const inner = '<div class="content"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
204
+ const selector = '.ezoic-ad-post[data-ezoic-slot="' + slot + '"][data-ezoic-id="' + id + '"]';
205
+ const html = makeWrapperLike(
206
+ $target,
207
+ 'post ezoic-ad-post',
208
+ inner,
209
+ 'data-ezoic-slot="' + slot + '" data-ezoic-id="' + id + '"'
210
+ );
211
+
212
+ $target.after(html);
213
+
214
+ injectedSlots.add(slot);
215
+ usedIds.add(id);
216
+ adsFIFO.push({ slot: slot, id: id, selector: selector });
217
+
218
+ newIds.push(id);
219
+
220
+ fifoEvictIfNeeded(pool.length);
221
+ }
222
+
223
+ return newIds;
224
+ }
225
+
167
226
  async function refreshAds() {
227
+ // Reset state on navigation
228
+ const key = currentPageKey();
229
+ if (pageKey !== key) {
230
+ pageKey = key;
231
+ resetPageState();
232
+ cleanupWrappersOnNav();
233
+ }
234
+
168
235
  if (inFlight) { rerunRequested = true; return; }
169
236
  inFlight = true;
170
237
 
@@ -186,45 +253,39 @@ async function refreshAds() {
186
253
 
187
254
  if (!$posts.length && !$topicItems.length) return;
188
255
 
189
- const sig = signatureFor($posts, $topicItems);
190
- if (sig === lastSignature) return;
191
- lastSignature = sig;
192
-
193
- removeOurWrappers();
194
-
195
- const activeIds = [];
256
+ const newIds = [];
196
257
 
197
258
  // Your rule:
198
259
  // - Category topic list: BETWEEN only
199
260
  // - Topic page: MESSAGE only
200
261
  if ($topicItems.length) {
201
262
  if (cfg.enableBetweenAds && betweenPool.length) {
202
- activeIds.push(...insertBetween($topicItems, betweenPool, betweenInterval, 'ezoic-ad-topic'));
263
+ newIds.push(...injectBetweenIncremental($topicItems, betweenPool, betweenInterval, 'ezoic-ad-topic'));
203
264
  }
204
- callEzoic(activeIds);
265
+ callEzoic(newIds);
205
266
  return;
206
267
  }
207
268
 
208
269
  if ($posts.length) {
209
270
  if (cfg.enableMessageAds && messagePool.length) {
210
- activeIds.push(...insertMessageAds($posts, messagePool, messageInterval));
271
+ newIds.push(...injectMessageIncremental($posts, messagePool, messageInterval));
211
272
  }
212
- callEzoic(activeIds);
273
+ callEzoic(newIds);
213
274
  }
214
275
  } finally {
215
276
  inFlight = false;
216
277
  if (rerunRequested) {
217
278
  rerunRequested = false;
218
- setTimeout(refreshAds, 80);
279
+ setTimeout(refreshAds, 120);
219
280
  }
220
281
  }
221
282
  }
222
283
 
223
284
  function debounceRefresh() {
224
285
  clearTimeout(debounceTimer);
225
- debounceTimer = setTimeout(refreshAds, 200);
286
+ debounceTimer = setTimeout(refreshAds, 180);
226
287
  }
227
288
 
228
289
  $(document).ready(debounceRefresh);
229
290
  $(window).on('action:ajaxify.end action:posts.loaded action:topic.loaded', debounceRefresh);
230
- setTimeout(debounceRefresh, 2000);
291
+ setTimeout(debounceRefresh, 1800);