nodebb-plugin-ezoic-infinite 1.4.0 → 1.4.3

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 +152 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.4.0",
3
+ "version": "1.4.3",
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
@@ -27,8 +27,16 @@
27
27
  usedPosts: new Set(),
28
28
  usedCategories: new Set(),
29
29
 
30
+ // wrappers currently on page (FIFO for recycling)
31
+ liveBetween: [],
32
+ liveMessage: [],
33
+ liveCategory: [],
34
+ usedCategories: new Set(),
35
+
30
36
  lastShowById: new Map(),
31
37
  pendingById: new Set(),
38
+ retryById: new Map(),
39
+ retryTimer: null,
32
40
 
33
41
  scheduled: false,
34
42
  timer: null,
@@ -103,6 +111,60 @@
103
111
  });
104
112
  }
105
113
 
114
+
115
+ function safeRect(el) {
116
+ try { return el.getBoundingClientRect(); } catch (e) { return null; }
117
+ }
118
+
119
+ function destroyPlaceholderIds(ids) {
120
+ if (!ids || !ids.length) return;
121
+ const call = () => {
122
+ try {
123
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
124
+ window.ezstandalone.destroyPlaceholders(ids);
125
+ }
126
+ } catch (e) {}
127
+ };
128
+ try {
129
+ window.ezstandalone = window.ezstandalone || {};
130
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
131
+ if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
132
+ else window.ezstandalone.cmd.push(call);
133
+ } catch (e) {}
134
+ }
135
+
136
+ function getRecyclable(liveArr) {
137
+ const margin = 1800;
138
+ for (let i = 0; i < liveArr.length; i++) {
139
+ const entry = liveArr[i];
140
+ if (!entry || !entry.wrap || !entry.wrap.isConnected) { liveArr.splice(i, 1); i--; continue; }
141
+ const r = safeRect(entry.wrap);
142
+ if (r && r.bottom < -margin) {
143
+ liveArr.splice(i, 1);
144
+ return entry;
145
+ }
146
+ }
147
+ return null;
148
+ }
149
+
150
+ function moveWrapAfter(wrap, target, kindClass, afterPos) {
151
+ try {
152
+ wrap.className = `${WRAP_CLASS} ${kindClass}`;
153
+ wrap.setAttribute('data-ezoic-after', String(afterPos));
154
+ target.insertAdjacentElement('afterend', wrap);
155
+ return true;
156
+ } catch (e) {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ function pickId(pool, liveArr) {
162
+ if (pool.length) return { id: pool.shift(), recycled: null };
163
+ const recycled = getRecyclable(liveArr);
164
+ if (recycled) return { id: recycled.id, recycled };
165
+ return { id: null, recycled: null };
166
+ }
167
+
106
168
  function buildWrap(id, kindClass, afterPos) {
107
169
  const wrap = document.createElement('div');
108
170
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
@@ -132,16 +194,10 @@
132
194
  try {
133
195
  state.usedTopics.forEach((id) => ids.push(id));
134
196
  state.usedPosts.forEach((id) => ids.push(id));
135
- state.usedCategories.forEach((id) => ids.push(id));
197
+ state.usedCategories && state.usedCategories.forEach((id) => ids.push(id));
136
198
  } catch (e) {}
137
-
138
- if (!ids.length) return;
139
-
140
- const call = () => {
141
- try {
142
- if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
143
- window.ezstandalone.destroyPlaceholders(ids);
144
- }
199
+ destroyPlaceholderIds(ids);
200
+ }
145
201
  } catch (e) {}
146
202
  };
147
203
 
@@ -180,6 +236,50 @@
180
236
  } catch (e) {}
181
237
  }
182
238
 
239
+
240
+ function isPlaceholderFilled(id) {
241
+ const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
242
+ if (!ph || !ph.isConnected) return false;
243
+ if (ph.childNodes && ph.childNodes.length > 0) return true;
244
+ const wrap = ph.parentElement;
245
+ if (wrap && wrap.querySelector && wrap.querySelector('iframe, ins, [id^="ezslot_"], [class*="ez"]')) return true;
246
+ return false;
247
+ }
248
+
249
+ function scheduleRefill(delay = 350) {
250
+ clearTimeout(state.retryTimer);
251
+ state.retryTimer = setTimeout(refillUnfilled, delay);
252
+ }
253
+
254
+ function refillUnfilled() {
255
+ const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}`));
256
+ let scheduledAny = false;
257
+
258
+ for (const wrap of wraps) {
259
+ const ph = wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
260
+ if (!ph) continue;
261
+ const id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
262
+ if (!Number.isFinite(id) || id <= 0) continue;
263
+
264
+ if (isPlaceholderFilled(id)) {
265
+ state.retryById.delete(id);
266
+ continue;
267
+ }
268
+
269
+ const tries = (state.retryById.get(id) || 0);
270
+ if (tries >= 8) continue;
271
+
272
+ const r = safeRect(wrap);
273
+ if (r && (r.top > window.innerHeight + 1200 || r.bottom < -1200)) continue;
274
+
275
+ state.retryById.set(id, tries + 1);
276
+ callShowAdsWhenReady(id);
277
+ scheduledAny = true;
278
+ }
279
+
280
+ if (scheduledAny) scheduleRefill(700);
281
+ }
282
+
183
283
  function callShowAdsWhenReady(id) {
184
284
  if (!id) return;
185
285
 
@@ -239,9 +339,9 @@
239
339
  })();
240
340
  }
241
341
 
242
- function nextId(kind) {
243
- const pool = (kind === 'between') ? state.poolTopics : (kind === 'message') ? state.poolPosts : state.poolCategories;
244
- if (pool.length) return pool.shift();
342
+ function nextId(pool) {
343
+ // backward compatible: the injector passes the pool array
344
+ if (Array.isArray(pool) && pool.length) return pool.shift();
245
345
  return null;
246
346
  }
247
347
 
@@ -298,13 +398,26 @@
298
398
 
299
399
  if (findWrap(kindClass, afterPos)) continue;
300
400
 
301
- const id = nextId(kindPool);
401
+ const liveArr = (kindClass === 'ezoic-ad-between') ? state.liveBetween
402
+ : (kindClass === 'ezoic-ad-message') ? state.liveMessage
403
+ : state.liveCategory;
404
+
405
+ const pick = pickId(kindPool, liveArr);
406
+ const id = pick.id;
302
407
  if (!id) break;
303
408
 
304
- usedSet.add(id);
305
- const wrap = insertAfter(el, id, kindClass, afterPos);
306
- if (!wrap) continue;
409
+ let wrap = null;
410
+ if (pick.recycled && pick.recycled.wrap) {
411
+ destroyPlaceholderIds([id]);
412
+ wrap = pick.recycled.wrap;
413
+ if (!moveWrapAfter(wrap, el, kindClass, afterPos)) continue;
414
+ } else {
415
+ usedSet.add(id);
416
+ wrap = insertAfter(el, id, kindClass, afterPos);
417
+ if (!wrap) continue;
418
+ }
307
419
 
420
+ liveArr.push({ id, wrap });
308
421
  callShowAdsWhenReady(id);
309
422
  inserted += 1;
310
423
  }
@@ -331,8 +444,13 @@
331
444
  state.poolTopics = [];
332
445
  state.poolPosts = [];
333
446
  state.poolCategories = [];
447
+ state.poolCategories = [];
334
448
  state.usedTopics.clear();
335
449
  state.usedPosts.clear();
450
+ state.usedCategories && state.usedCategories.clear();
451
+ state.liveBetween = [];
452
+ state.liveMessage = [];
453
+ state.liveCategory = [];
336
454
  state.usedCategories.clear();
337
455
 
338
456
  state.lastShowById = new Map();
@@ -393,6 +511,7 @@
393
511
  }
394
512
 
395
513
  enforceNoAdjacentAds();
514
+ scheduleRefill(250);
396
515
 
397
516
  // If nothing inserted and list isn't in DOM yet (first click), retry a bit
398
517
  let count = 0;
@@ -468,4 +587,20 @@
468
587
  state.pageKey = getPageKey();
469
588
  scheduleRun();
470
589
  setTimeout(scheduleRun, 250);
471
- })();
590
+ })()
591
+ function bindScroll() {
592
+ if (state.__scrollBound) return;
593
+ state.__scrollBound = true;
594
+ let ticking = false;
595
+ window.addEventListener('scroll', () => {
596
+ if (ticking) return;
597
+ ticking = true;
598
+ window.requestAnimationFrame(() => {
599
+ ticking = false;
600
+ enforceNoAdjacentAds();
601
+ scheduleRefill(200);
602
+ });
603
+ }, { passive: true });
604
+ }
605
+
606
+ ;